fix(intent): two event-driven rules can no longer share one at-most-once guard - #6859
Conversation
The guard the templates emit is findAll(eq(<backReference>, sourceId)): it asks whether the source already has a row through that relation, and cannot tell which rule wrote it. So two event-driven `generates:` / `posts:` rules sharing a target AND a back-reference divided silently into a winner and a loser - whichever fired first claimed the source forever, and the other handed back that row instead of writing, for that source and every future one. Nothing surfaced it. It parsed, generated and compiled, and the loser read as a rule whose condition never matched. Disjoint `when:` guards did not save it either, which is the trap: the author writes two mutually exclusive conditions and gets two independent rules, but the collision is decided by the target's EXISTENCE, so the second no-ops on a source whose condition it matched perfectly. Both halves of the key are static in the model, so refuse it at parse time and name the ways out - separate back-references, separate targets, or `mode: append` on every one of them. Two decisions worth recording. The check spans both constructs rather than being one per collection, because a `posts:` row satisfies a `generates:` guard just as well. And `mode: append` is not simply exempt: two appending rules genuinely cannot collide, since neither reads the other's rows, but append PLUS guarded does - the appended rows carry the back-reference, and that is all the guarded rule's lookup needs to be satisfied forever. That pairing is the case eclipse-dirigible#6800 leaves open, and it has its own test.
Two separate races against one cold page load, both of which report a healthy Builder as broken. openBuilder() returns as soon as navigation does and the shell's scripts are deferred, so the probe for Alpine and the Builder's stores sampled a page that had not finished booting. That is the failure seen on this PR's smoke run and on eclipse-dirigible#6841 - a test with nothing to do with either change. It now polls the same condition instead; a mis-ordered or missing script still never registers the stores, so a real breakage still fails, just at the deadline rather than instantly. The second race had not been drawn yet. This class configures no Selenide timeout, so every shouldBe without an explicit Duration gets the library default of four seconds - and three of them wait on elements Alpine renders after those stores register, one straight after a page load. Four seconds there is a race against the boot, not a check on it. They get explicit deadlines, in line with every other wait in the class. Left alone: the waits that follow an already-satisfied wait on the same rendered region. The DOM is present by then, and inflating every timeout only makes a genuine breakage take minutes to surface instead of seconds.
One conflict, in the engine-intent guide's `generates` + `event:` bullet: master extended it with the sourceStatus flip ordering (the flip now runs BEFORE the target is saved) while this branch appended the guard-ownership paragraph. Both kept, master's text first. The parser halves auto-merged and both validators are wired: validateStatusWritesAgainstLifecycle from master, validateIdempotencyGuardOwnership from this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Merged current One conflict, in the engine-intent guide's The thing worth checking for a PR that adds a new parse-time rejection is whether master's newer fixtures still parse: #6824 added a pair of appending create-froms to
|
# Conflicts: # tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/IntentBuilderShellIT.java
What does this PR do?
Refuses, at parse time, two event-driven
generates:/posts:rules that share a target entity and a back-reference relation.The guard the templates emit is
findAll(eq(<backReference>, sourceId))— it asks whether the source already has a row through that relation, and cannot tell which rule wrote it. So two rules sharing both divided silently into a winner and a loser: whichever fired first claimed the source forever, and the other handed back that row instead of writing, for that source and every future one.Nothing surfaced it: it parsed, generated and compiled, and the loser read as a rule whose condition never matched. Disjoint
when:guards did not save it — which is the trap. The author writes two mutually exclusive conditions and expects two independent rules, but the collision is decided by the target's existence, not by the condition that led to it, so the second rule no-ops on a Fine whose condition it matched perfectly.Both halves of the key are static in the model, so this is now an authoring-time error naming the ways out: separate back-references (two to-one relations to the source), separate targets, or
mode: appendon every one of them.Two decisions worth recording
posts:row satisfies agenerates:guard just as well.mode: appendis exempt only in pairs. Two appending rules genuinely cannot collide — neither reads the other's rows, sinceappendis the absence of the guard. But append plus guarded does collide: the appended rows still carry the back-reference, and that is all the guarded rule's lookup needs to be satisfied forever. That pairing is the case generates.event: should support the process-step axis and an opt-in append cardinality (mode: once|append) #6800 leaves open, and it has its own test.A non-event-driven create-from is a button — a person decides when it runs and no guard is emitted — so two of those share nothing and are left alone.
Not addressed here: a
posts:entry whose glue yields an emptybackRefgets no idempotency guard at all. It is real, but it is a different defect from the collision, and turning a missingidempotentBy:into an error risks rejecting models that already ship. Worth its own issue.Verification
engine-intent760/760;IntentEngineIT54/54;IntentEmissionCoverageIT1/1 — the last two matter most here, since a new parse error could have rejected models the repo already ships.CollidingGuardIntentTestparse silently without the validator, which is the reported defect exactly.What issues does this PR fix or reference?
References generates.event: should support the process-step axis and an opt-in append cardinality (mode: once|append) #6800 (
mode: once|append), which removes the common accumulating-target case but leaves the shared-guard case silent.Release Notes
Two event-driven
generates:orposts:rules that wrote to the same target through the same back-reference used to collide silently: the first one to fire claimed the source permanently and the others never wrote again, with no error anywhere. This is now reported when the intent is parsed, with the available fixes named.Documentation
intent-assistant-guide.mdandcomponents/engine/engine-intent/CLAUDE.mdare updated in this PR. No matching docs-repo PR — the intent DSL reference lives in-tree.