Skip to content

intent: resolve a seeded status name in the glue event.when and refuse a guard that does not parse (#7289) - #7317

Merged
delchev merged 2 commits into
masterfrom
issue-7289-event-when-status
Sep 11, 2026
Merged

intent: resolve a seeded status name in the glue event.when and refuse a guard that does not parse (#7289)#7317
delchev merged 2 commits into
masterfrom
issue-7289-event-when-status

Conversation

@delchev

@delchev delchev commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

The gap

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 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, posting event.when, requiredWhen, generates event.when, report filter, schedules[].where since #7269) accepts the seeded name.

The other half: NotificationSupport.guard returned "true" for anything its one regex did not match, so Status = 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 own when.

The change

  • StatusSymbolResolver.rewriteGlue resolves 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 no model: 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 from validateEventBinding and therefore covering 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 (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 than true.
  • CheckSupport.condition becomes 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.requiredWhenGuard delegates to the shared compiler with no behaviour change; its own copy, and the now-dead toOneByName/relationKeyType in 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 test1221 green. New EventGuardIntentTest (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) and GlueEventGuardTest (the rendered expression for notification / integration / outbound, the list conjunction, a string field keeping its boxed equality). The event.when case joins StatusSymbolIntentTest's every-site fixture, with a mistyped name refused.
  • IntentEmissionCoverageITgreen. 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.
  • IntentEngineITgreen (77).
  • mvn formatter:validate with 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.md and .claude/docs/intent-layer.md.

Fixes #7289

🤖 Generated with Claude Code

delchev and others added 2 commits September 10, 2026 17:32
…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
@delchev
delchev merged commit 1b39f08 into master Sep 11, 2026
9 checks passed
@delchev
delchev deleted the issue-7289-event-when-status branch September 11, 2026 06:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

intent: notifications/integrations/outbound event.when never resolves a seeded status name and degrades an unparsable guard to true

1 participant