intent: a create-from is guarded by the source's status (#7068) - #7080
Merged
Conversation
A `generates:` was unconditional. With a `sourceStatus:` completion hook it flipped the source once the target existed - and then went on offering the same button on the flipped record and answering the same endpoint 200, so a second click minted a second document: a proforma already INVOICED produced a second invoice, in the customer's hands. The hook DECLARED what "already done" looks like; nothing consulted it, and both halves of the model read correctly, which is why the gap survived review. One rule, resolved once (`GeneratesGuardSupport`), feeds both halves of the action: the generated `run()` refuses with 409 before anything is created, and the contributed action descriptor carries the same guard so the shared `customActions` store stops OFFERING the click on a record it would refuse (`getActions(view, type, record)` takes the record the four entity-action views already have). Two shapes: `fromStatus: [...]` is the explicit allow-list - the `from:` of a `transitions:` entry, spelled differently only because `from:` on a create-from already names the source ENTITY - and absent it a declared `sourceStatus` implies the deny-list of exactly that status, so a model carrying the defect today is corrected with no authoring change. The guard is on the CLICK: an event-driven create-from keeps its own at-most-once back-reference guard and qualifies its moment with `event.when`, so `fromStatus` on an event-only rule is refused at parse rather than silently ignored - as are a `page` scope, a source with no `function: EntityStatus` relation, and an allow-list containing the `sourceStatus` the action itself writes. The statuses are symbolic like every other status site. Emission is gated on a new `hasStatusGuard` key, so a `.glue` written before it renders the unguarded `run()` it always did.
…s-from-status # Conflicts: # tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java
…s-from-status Both sides taught the same store not to offer an action a record's status cannot accept - #7073 for a transition's `from:`, this branch for a create-from's `fromStatus:` - so the two predicates now stand side by side and getActions applies both (`appliesTo` for the transition guard, `isAvailable` for the create-from guard), each still failing open on a missing guard, record or status value. The generated views take master's call shape, which passes the document page's stored `record` rather than the edit buffer.
…s-from-status Both sides added a rule to the same generates validation block: master refuses `unique:` on an on-demand create-from (#7070), this branch validates `fromStatus:`. They are independent checks and both now run.
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7068
The defect
A
generates:create-from was unconditional. With asourceStatus:completion hook it flipped the source once the target existed - and then went on offering the same button on the flipped record, and answering the same endpoint200. So a second click minted a second document: on sta, ProformaInvoice PF00000001 produced SalesInvoice 10, flipped to INVOICED, and a secondInvoiceFromProformaGenerate/run {id:1}produced SalesInvoice 11 for the same proforma - a duplicate invoice in the customer's hands.sourceStatus:declared what "already done" looks like for this action. Nothing consulted it, which is why the gap survives review: both halves of the model read correctly and the duplicate is an ordinary-looking document with a number of its own.The fix
One rule, resolved once (
GeneratesGuardSupport), feeding both halves of the action:run()refuses with 409 naming the action and the current status, before anything is created (a guard asked after the document exists is not a guard);customActionsstore stops offering the click on a record it would refuse -getActions(view, type, record)gained an optional record and the four entity-action views pass the one they already have (selected, and the documentform). The 409 stays the contract for every other caller.Two shapes:
fromStatus:is thefrom:of atransitions:entry - spelled differently only becausefrom:on a create-from already names the source entity, which is why the issue's suggestedfrom:could not be taken literally. Absent it, a declaredsourceStatusimplies the deny-list of exactly that status: the minimal refusal, derived from what the author already wrote, so a model carrying the defect today (base-timesheets'invoice-from-timesheetincluded) is corrected with no authoring change.The guard is on the click. An event-driven create-from already carries the at-most-once back-reference guard and qualifies its moment with
event.when, sofromStatuson an event-only rule is refused at parse rather than silently ignored - as are apagescope (no record whose status could be read), a source declaring nofunction: EntityStatusrelation, and an allow-list containing thesourceStatusthe action itself writes (it re-opens exactly the duplicate the guard removes). Statuses are symbolic like every other status site.Emission is gated on a new
hasStatusGuardkey, so a.gluewritten before it renders the unguardedrun()it always did.Tests
GeneratesIntentTest- the declared list parses; the four refusals.GlueGeneratesTest- the implied deny-list, the authored allow-list, and a create-from with no status at all keeping no guard.IntentEngineIT.generates_completion_hook_flips_the_source_via_targeted_update- the rendered 409 branch, its ordering before the create, and the descriptor's guard. Green locally (64 tests).formatter:validateclean; javadoc clean under-P release.Docs
CLAUDE.md,README.md, the assistant guide,.claude/docs/intent-layer.md