intent: resolve a seeded status name in the glue event.when and refuse a guard that does not parse (#7289) - #7317
Merged
Merged
Conversation
…e a guard that does not parse (#7289) The `event.when` guard of `notifications[]`, `integrations[]` and `outbound[]` was the one `when` in the DSL that neither resolved a seeded status name nor checked its own shape, and both halves failed silently, in opposite directions. notifications: - name: issued-mail event: { onUpdate: SalesInvoice, when: "Status == ISSUED" } generated `Objects.equals(entity.Status, "ISSUED")` - the integer status FK compared with a string, never true - so the mail never went out with parse, generation, compile and publish all green. And `NotificationSupport.guard` answered `"true"` for anything its one pattern did not match, so `Status = ISSUED`, `status == 'ISSUED' and channel == 'mail'` and every LIST guard (the notification path stringified the list into the scalar pattern) switched the guard off and fired the reaction on EVERY event - a guard nobody authored, and the degradation #7094 explicitly refused to accept for its own `when`. Three changes, one per layer: - `StatusSymbolResolver.rewriteGlue` resolves the guard on the nomenclature of the entity the bound event is about - the entity a lifecycle binding names, or the process's trigger entity for a step binding, which names a step and no record. The axis takes no `model:` and the parser refuses an unknown entity, so the nomenclature is always this file's and no cross-model fallback is needed. - `IntentParser.validateEventGuard`, in `validateEventBinding` and therefore on all three lists at once, holds the guard to `CheckSupport`'s grammar and type rule: one `<Property> ==|!= <literal>` over the record's own properties, or the list form meaning their AND (#6957). A guard that does not parse, names a property the record does not carry, or compares it with a literal of the wrong type is a parse ERROR, not `true`. - `CheckSupport.condition` is now the ONE renderer of a typed guard: `requiredWhen` (which it was written for) and the glue axis share it, so the grammar the parser refuses on, the type rule and the Java emitted cannot drift into three answers. A to-one's key is therefore compared numerically here too, because its width is not knowable from this file (#7237), and the glue sites pass the guard as authored rather than stringified, which is what makes the list form render at all. `EdmIntentGenerator.requiredWhenGuard` delegates to the shared compiler unchanged (its own copy, plus the now-dead `toOneByName`/`relationKeyType` in the parser, are removed). The property lookup is case-insensitive on both sides, as the renderer's already was, so a guard the generator compiles cannot be refused by the parser and vice versa. Verified: `mvn -pl components/engine/engine-intent test` green (1221), including the new `EventGuardIntentTest` (the resolution, each refusal, the accepted list form, the step-bound guard) and `GlueEventGuardTest` (the rendered expression for all three lists, the list conjunction, a string field keeping its boxed equality), and the `event.when` case added to `StatusSymbolIntentTest`'s every-site fixture. `IntentEmissionCoverageIT` green: its departure guard is now the LIST form and its runtime half creates two excluded records - with the old rendering (`true`) both would have departed. `IntentEngineIT` green (77). Formatter validated with the cache wiped. Fixes #7289 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…en-status # Conflicts: # components/engine/engine-intent/CLAUDE.md
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.
The gap
The
event.whenguard ofnotifications[],integrations[]andoutbound[]was the onewhenin the DSL that neither resolved a seeded status name nor checked its own shape — and both halves failed silently, in opposite directions.generated
if (!(java.util.Objects.equals(entity.Status, "ISSUED"))) return;— the integer status FK compared with a string, never true — so the mail never went out, with parse, generation, compile and publish all green. Every other guard site (transitions,abortOn.status,wait when,resolves event.when, postingevent.when,requiredWhen,generates event.when, reportfilter,schedules[].wheresince #7269) accepts the seeded name.The other half:
NotificationSupport.guardreturned"true"for anything its one regex did not match, soStatus = ISSUED,status == 'ISSUED' and channel == 'mail'— and every LIST guard, because the notification path stringified the list into the scalar pattern — switched the guard off and fired the reaction on EVERY event. That is a guard nobody authored, and the degradation #7094 explicitly refused to accept for its ownwhen.The change
StatusSymbolResolver.rewriteGlueresolves the guard on the nomenclature of the entity the bound event is about: the entity a lifecycle binding names, or — for a step binding, which names a step and no record — the trigger entity of that process. The axis takes nomodel:and the parser refuses an unknown entity, so the nomenclature is always this file's; no cross-model fallback is needed here.IntentParser.validateEventGuard, called fromvalidateEventBindingand therefore covering all three lists at once, holds the guard toCheckSupport's grammar and type rule: one<Property> ==|!= <literal>over the record's own properties, or the list form meaning their AND (Intent DSL: no way to guard a status consumer by HOW the status was reached - an automatic lookup and a manual task are indistinguishable #6957). A guard that does not parse, names a property the record does not carry, or compares it with a literal of the wrong type is a parse ERROR rather thantrue.CheckSupport.conditionbecomes the ONE renderer of a typed guard:requiredWhen(which it was written for) and the glue axis share it, so the grammar the parser refuses on, the type rule and the Java emitted cannot drift into three answers. A to-one's key is compared numerically here too — its width is not knowable from this file (intent: requiredWhen's when: guard NPEs the parser on a typeless field, refuses Integer/String spellings, and types a cross-model to-one as integer while its FK is Long #7237) — and the three glue sites now pass the guard as authored instead of stringified, which is what makes the list form render at all.EdmIntentGenerator.requiredWhenGuarddelegates to the shared compiler with no behaviour change; its own copy, and the now-deadtoOneByName/relationKeyTypein the parser, are removed. Property lookup is case-insensitive on both sides (as the renderer's already was), so a guard the generator compiles cannot be refused by the parser and vice versa.Verified
mvn -pl components/engine/engine-intent test— 1221 green. NewEventGuardIntentTest(the resolution; each refusal —=, a prose conjunction, an unknown property, a decimal, an empty list; the accepted list form; the step-bound guard resolving on the trigger entity) andGlueEventGuardTest(the rendered expression for notification / integration / outbound, the list conjunction, a string field keeping its boxed equality). Theevent.whencase joinsStatusSymbolIntentTest's every-site fixture, with a mistyped name refused.IntentEmissionCoverageIT— green. Its departure guard is now the LIST form and its runtime half creates two excluded records: with the old rendering (true) both would have departed on the queue. The generated publisher is asserted to carry both conditions ANDed.IntentEngineIT— green (77).mvn formatter:validatewith the formatter cache wiped — green.Not verified: no template changed, so no template-level check was needed beyond the two ITs that compile and run the generated listeners.
Documented in the assistant guide (the axis's guard, plus the "Statuses may be named, not numbered" site list), the module
CLAUDE.mdand.claude/docs/intent-layer.md.Fixes #7289
🤖 Generated with Claude Code