Skip to content

feat(intent): transitions - guarded on-demand status flips (void/cancel/close) - #6307

Merged
delchev merged 1 commit into
masterfrom
feat/intent-transitions
Jul 20, 2026
Merged

feat(intent): transitions - guarded on-demand status flips (void/cancel/close)#6307
delchev merged 1 commit into
masterfrom
feat/intent-transitions

Conversation

@delchev

@delchev delchev commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

A document whose create-time process has ended (an issued invoice, a posted entry) has no declarative affordance left to change its status: process triggers fire only on create/update/delete, and actions: only opens a custom page. The new top-level transitions: block adds one - a per-record button that moves the record into a designated EntityStatus, guarded server-side:

transitions:
  - name: VoidInvoice
    forEntity: Invoice          # must declare a function: EntityStatus relation
    from: [3, 4]                # allowed source status seed ids - 409 from any other status
    setStatus: 8                # the target status seed id
    when: "Paid == 0"           # optional <Field> ==|!= <number> guard (Calc semantics: null reads as 0)
    label: Void
    icon: ban

The classic consumers: void/cancel an issued document (анулиране keeps the number for the audit trail), cancel a confirmed order, close a case, reopen a ticket.

How

Two generated halves, mirroring generates:

  • Client: TransitionsIntentGenerator (@Order(470)) contributes <name>-transition-action.extension/.js to the app's <project>-custom-action point; the descriptor carries an endpoint (always per-record).
  • Server: GlueIntentGenerator.buildTransitions pre-renders everything (the allowed-statuses expression over an int currentStatus local; the when guard as a full Calc.eval(...).compareTo(...) expression) into the transitions glue collection, rendered by Transition.java.template into a @Controller at gen/events/<ClassName>Transition/run. The controller re-loads the record, returns 409 with the reason when a guard fails, flips ONLY the status column via the targeted updateProperty (a workflow-style system write - no -updated re-fire), and publishes -transitioned - the same channel the workflow setters and generates.sourceStatus publish, so postings: glue observes a manual void exactly like a workflow transition (the enabler for a follow-up red-storno reversal posting).

Also: ControllerInvoker no longer stamps text/plain over a content type a controller set explicitly on a String return (default unchanged when unset) - the transition controller returns JSON.

Docs: intent-assistant-guide.md gains the ### transitions section + quick-reference and allowed-values rows; engine-intent/CLAUDE.md gains the semantics bullet.

Testing

  • TransitionsIntentTest (parse + 6 validation rejections), GlueTransitionsTest (pre-rendered guards) - full engine-intent suite green.
  • ControllerInvokerBindingTest +2 content-type tests - full engine-java suite green.
  • IntentEmissionCoverageIT extended per the engine-intent testing contract (fixture transition + emission tokens + runtime: allowed cancel returns 200 with the status flipped and -transitioned observable; wrong-status re-cancel 409; when-guard 409 leaving the record untouched) - green locally.

🤖 Generated with Claude Code

…el/close)

A document whose create-time process has ended (invoice ISSUED, entry POSTED)
has no declarative affordance left to change its status: process triggers fire
only on create/update/delete, and actions: only opens a custom page. The new
top-level transitions: block adds one - a per-record button that moves the
record into a designated EntityStatus, guarded server-side:

  transitions:
    - name: VoidInvoice
      forEntity: Invoice          # must declare a function: EntityStatus relation
      from: [3, 4]                # allowed source status seed ids
      setStatus: 8                # target status seed id
      when: "Paid == 0"           # optional <Field> ==|!= <number> guard (Calc semantics)
      label: Void
      icon: ban

Two halves, the generates pattern: TransitionsIntentGenerator (@order(470))
contributes the per-record button to <project>-custom-action (descriptor
carries the endpoint); GlueIntentGenerator.buildTransitions pre-renders the
allowed-statuses expression and the Calc-backed when guard into the
transitions glue collection, rendered by Transition.java.template into a
@controller at gen/events/<ClassName>Transition/run. The controller re-loads
the record, returns 409 with the reason when a guard fails, flips ONLY the
status column via the targeted updateProperty (no -updated re-fire), and
publishes -transitioned - the same channel workflow setters publish, so
postings:/integrations observe a manual void exactly like a workflow
transition (the enabler for red-storno reversal postings).

ControllerInvoker: a CharSequence return no longer stamps text/plain over a
content type the controller set explicitly (the transition controller returns
JSON); default unchanged when unset.

Verified:
- unit: TransitionsIntentTest (7) + GlueTransitionsTest (2) + full
  engine-intent suite green; ControllerInvokerBindingTest +2 content-type
  tests, full engine-java suite green
- IntentEmissionCoverageIT extended (fixture transition + emission tokens +
  runtime: cancel 200 with the status flipped, wrong-status 409, when-guard
  409 leaving the record untouched) and green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@delchev
delchev merged commit a0c081d into master Jul 20, 2026
10 checks passed
@delchev
delchev deleted the feat/intent-transitions branch July 20, 2026 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant