You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The event-driven create-from's at-most-once guard queries row existence only - never the row's state. A voided/cancelled target still satisfies the guard, so a source's one-shot slot is consumed permanently at first creation and never released by anything that later happens to the target. "Void and reissue" - an ordinary business flow - is inexpressible for an event-driven create-from.
Found by static source review against origin/master (1dc87fb505).
Scope - what this does and does not affect
The guard is emitted ONLY for event-driven create-froms (#if($hasEvent), Generate.java.template:91-102); the template javadoc (32-38) states a button-only create-from deliberately keeps NO guard, so plain manual regeneration works today. This issue is about the event: form (including a button that shares an event - the javadoc's "or a click after the event").
mode: append (generates.event: should support the process-step axis and an opt-in append cardinality (mode: once|append) #6800) is NOT the fix for this and could be misread as one: someone hitting "I voided the document and can't regenerate" who reaches for append gets their new document - and also a new document on EVERY subsequent qualifying event, with no dedup whatsoever. append is the absence of a guard, not a state-aware guard. This issue is the other half of the same guard's semantics: once needs to understand "the existing target no longer counts".
Where
events/Generate.java.template:91-102 - findAll(eq(backRefProperty, sourceId)), no status predicate.
events/Transition.java.template:72-76 - a void flips exactly one column (status) via updateProperty; the target keeps existing and keeps back-referencing the source, so the guard keeps matching.
Same shape in events/Posts.java.template:53-58.
Example
An event mints Invoice INV-001 from Timesheet 12.
INV-001 is wrong; the transitions: Void moves it to VOIDED. It still exists, still references Timesheet 12.
The source re-qualifies (or the shared button is clicked): the guard finds VOIDED INV-001 and returns it. No new invoice; the UI toasts the voided document.
Proposed fix
Make the guard's predicate state-aware, opt-in:
supersedeWhen: "Status == VOIDED" on the rule - the guard query becomes "an existing target that is still live" - or reuse the lifecycle:/stage: classification to skip rows whose status stage is cancelled/void. Redelivery idempotence stays intact (a redelivered event finds the live row); a genuinely voided target stops blocking a replacement.
Alternative: have the void path clear the back-reference or move it to a supersededBy/replaces self-link, so the guard naturally stops matching - closer to how postings:reverses:/storno: already discriminates rows by a link column, and it keeps the audit trail of both documents.
Documentation ask regardless of the fix: the guide should state the at-most-once semantics precisely - today "idempotent under redelivery" reads as "no duplicates from the same event", not "one target per source forever, regardless of the target's fate".
Summary
The event-driven create-from's at-most-once guard queries row existence only - never the row's state. A voided/cancelled target still satisfies the guard, so a source's one-shot slot is consumed permanently at first creation and never released by anything that later happens to the target. "Void and reissue" - an ordinary business flow - is inexpressible for an event-driven create-from.
Found by static source review against
origin/master(1dc87fb505).Scope - what this does and does not affect
#if($hasEvent),Generate.java.template:91-102); the template javadoc (32-38) states a button-only create-from deliberately keeps NO guard, so plain manual regeneration works today. This issue is about theevent:form (including a button that shares an event - the javadoc's "or a click after the event").mode: append(generates.event: should support the process-step axis and an opt-in append cardinality (mode: once|append) #6800) is NOT the fix for this and could be misread as one: someone hitting "I voided the document and can't regenerate" who reaches forappendgets their new document - and also a new document on EVERY subsequent qualifying event, with no dedup whatsoever.appendis the absence of a guard, not a state-aware guard. This issue is the other half of the same guard's semantics:onceneeds to understand "the existing target no longer counts".Where
events/Generate.java.template:91-102-findAll(eq(backRefProperty, sourceId)), no status predicate.events/Transition.java.template:72-76- a void flips exactly one column (status) viaupdateProperty; the target keeps existing and keeps back-referencing the source, so the guard keeps matching.events/Posts.java.template:53-58.Example
transitions:Void moves it to VOIDED. It still exists, still references Timesheet 12.Proposed fix
Make the guard's predicate state-aware, opt-in:
supersedeWhen: "Status == VOIDED"on the rule - the guard query becomes "an existing target that is still live" - or reuse thelifecycle:/stage:classification to skip rows whose status stage is cancelled/void. Redelivery idempotence stays intact (a redelivered event finds the live row); a genuinely voided target stops blocking a replacement.supersededBy/replacesself-link, so the guard naturally stops matching - closer to howpostings:reverses:/storno:already discriminates rows by a link column, and it keeps the audit trail of both documents.Documentation ask regardless of the fix: the guide should state the at-most-once semantics precisely - today "idempotent under redelivery" reads as "no duplicates from the same event", not "one target per source forever, regardless of the target's fate".