Summary
Two or more event-driven generates: / posts: rules sharing a target entity AND a back-reference relation silently collide: the at-most-once guard is scoped per (target, back-reference relation), not per rule, so the first rule to fire claims the source permanently and every other rule no-ops forever. Nothing flags this at parse time - it parses, generates and compiles cleanly.
Found by static source review against origin/master (1dc87fb505).
Where
events/Generate.java.template:91-102 - the guard (inside #if($hasEvent)): findAll(eq(backRefProperty, sourceId)), return existing if non-empty. The query is indifferent to WHICH rule created the row and to its data.
events/Posts.java.template:53-58 - the identical guard (inside #if($backRef != "")). Note: a posts: entry whose glue yields an empty backRef gets NO idempotency guard at all - the opposite edge of the same coin.
GlueIntentGenerator.putGeneratesEvent derives backRefProperty from the map: entry copying the source pk - so two rules with map: { Fine: id } share one guard.
Example
generates:
- name: log-identification-failed
from: Fine
to: FineLog
event: { onTransition: Fine, when: "Status == UNRESOLVED" }
map: { Fine: id }
- name: log-declaration-created
from: Fine
to: FineLog
event: { onTransition: Fine, when: "Status == DECLARED" }
map: { Fine: id } # SAME back-reference
Rule 1 writes a row on failure; rule 2 later finds that row via the shared relation and returns it - it never writes anything, for this or any Fine that already has a row.
Relationship to #6800
#6800 (mode: once|append) gives the accumulating-target case a correct option and removes the COMMON collision case - but it mitigates rather than fixes this: two mode: once rules sharing a target + back-reference still silently no-op with no diagnostic.
Proposed fix
Parse-time detection (cheap, high value): error - or at minimum a generate warning - when two generates:/posts: rules share a target entity and a back-reference relation. Both are static in the model; this converts a silent runtime defect into an authoring-time message.
Deeper option: scope the guard per rule (match on a rule-owned discriminator - the rule name, or a declared kind column) so rules through one relation no longer share one guard while idempotence under redelivery is preserved.
Summary
Two or more event-driven
generates:/posts:rules sharing a target entity AND a back-reference relation silently collide: the at-most-once guard is scoped per (target, back-reference relation), not per rule, so the first rule to fire claims the source permanently and every other rule no-ops forever. Nothing flags this at parse time - it parses, generates and compiles cleanly.Found by static source review against
origin/master(1dc87fb505).Where
events/Generate.java.template:91-102- the guard (inside#if($hasEvent)):findAll(eq(backRefProperty, sourceId)), return existing if non-empty. The query is indifferent to WHICH rule created the row and to its data.events/Posts.java.template:53-58- the identical guard (inside#if($backRef != "")). Note: aposts:entry whose glue yields an empty backRef gets NO idempotency guard at all - the opposite edge of the same coin.GlueIntentGenerator.putGeneratesEventderivesbackRefPropertyfrom themap:entry copying the source pk - so two rules withmap: { Fine: id }share one guard.Example
Rule 1 writes a row on failure; rule 2 later finds that row via the shared relation and returns it - it never writes anything, for this or any Fine that already has a row.
Relationship to #6800
#6800 (
mode: once|append) gives the accumulating-target case a correct option and removes the COMMON collision case - but it mitigates rather than fixes this: twomode: oncerules sharing a target + back-reference still silently no-op with no diagnostic.Proposed fix
Parse-time detection (cheap, high value): error - or at minimum a generate warning - when two
generates:/posts:rules share a target entity and a back-reference relation. Both are static in the model; this converts a silent runtime defect into an authoring-time message.Deeper option: scope the guard per rule (match on a rule-owned discriminator - the rule name, or a declared kind column) so rules through one relation no longer share one guard while idempotence under redelivery is preserved.