Skip to content

docs(intent): correct the guard grammar and document the conditional checks - #7414

Merged
delchev merged 2 commits into
eclipse-dirigible:masterfrom
TIVMOF:fix/guide-guard-grammar-and-conditional-checks
Sep 17, 2026
Merged

delchev merged 2 commits into
eclipse-dirigible:masterfrom
TIVMOF:fix/guide-guard-grammar-and-conditional-checks

Conversation

@TIVMOF

@TIVMOF TIVMOF commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

The defect

#6996 (mine) added this sentence to the resolves: section:

A generates: / postings: / process trigger: guard compares a status and nothing else — when: cannot read the outcome: string

It is false, and it was false when it was written — #6957 had already made a when: an AND-ed list of equality terms over the record's own columns, status or not. Posting the counter-example to /services/ide/intent/validate:

issues: []
parsed event when: ["Status == 2", "resolution == found"]

A status term and a string term on an ordinary field, accepted.

Why it mattered

The sentence was not inert. An intent generated from a full process description split one lookup's two failure outcomes into two statuses because the guide said it had to — and therefore needed two otherwise identical fallback processes and two otherwise identical audit rules. It used resolution == found for its success guard a few lines below, so it had the capability in hand and still obeyed the prohibition.

Routing both failures to one status and separating the audit rows on the outcome removes a process, a create-from and a seeded status, and validates clean:

before after
processes 2 1
generates 7 6
fine statuses 4 3

What changed

The rule is restated as the choice it is — one status plus an outcome term when the two failures are handled the same way and differ only in what the trail records; separate statuses when they need different flows, since a process binds at most one trigger and a guard list is AND-ed, never OR-ed. One status and no outcome term remains the thing that is always wrong.

The grammar is now written down. The reason a wrong sentence about it was believable is that it was documented nowhere — the axis section showed when: "Status == 2" and left the rest to inference. Now stated: the operators; that the property is the record's own and why a dotted path is refused; that a status name is already its seed id; that only string/integer/boolean are guardable and why a decimal is refused rather than compared (a boxed cross-type comparison is silently always-false); that a list is AND-ed with no OR; and that onTransition requires a guard while the other axes do not.

requiredWhen (#7094) and forbidWhen (#7275) get real entries. They appeared only as a subordinate clause inside the compare bullet — the parser accepts seven check kinds and the guide taught four. Both now document the point that decides whether they work at all: without the status: gate a check runs in the controllers only, so a rule meant to gate a status a workflow sets needs the gate, because setField/setRelationField are repository writes that never reach a controller.

That is the check that expresses "this may be empty in a draft but not once it reaches STATUS" — which required: true cannot say, and which any app that fills a relation through a user task needs to stop a task completion marking a record done with the value still empty.

Two quick-reference lines were added for the same two shapes.

Verification

Every YAML added here was validated against a running instance rather than read off the source:

  • the AND-ed outcome guard — issues: [], parsed as ["Status == 3", "resolution == notFound"]
  • the requiredWhen gate — ('requiredWhen', 'payer', 'Status == 2', 2), both symbols resolved to the seed id
  • the forbidWhen one-hop parent condition — ('forbidWhen', None, 'Invoice.Status == 2', None)

Documentation only; no code changes.

🤖 Generated with Claude Code

…checks

Fixes a sentence I introduced in eclipse-dirigible#6996. It read:

  A `generates:` / `postings:` / process `trigger:` guard compares a status and
  nothing else - `when:` cannot read the `outcome:` string

That is false, and was false when it was written: eclipse-dirigible#6957 had already made a
`when:` an ANDed list of equality terms over the record's own columns, status or
not. Posting the counter-example to /services/ide/intent/validate returns no
issues and parses the guard as ["Status == 2", "resolution == found"] - a status
term and a string term on an ordinary field.

The sentence was not inert. An intent generated from a full process description
split one lookup's two failure outcomes into two statuses because the guide said
it had to, and therefore needed two otherwise identical fallback processes and
two otherwise identical audit rules - while using `resolution == found` a few
lines below for its success guard. Routing both failures to one status and
separating the audit rows on the outcome removes a process, a create-from and a
seeded status, and validates clean.

So the rule is restated as the choice it actually is: one status plus an outcome
term when the two failures are handled the same way and differ only in what the
trail records; separate statuses when they need different flows, because a
process binds at most one trigger and a guard list is ANDed, never ORed. One
status and no outcome term remains the thing that is always wrong.

The underlying reason the sentence was believable is that the grammar was
nowhere written down - the axis section showed `when: "Status == 2"` and left
the rest to be inferred. It is now stated: the operators, that the property is
the record's own (and why a dotted path is refused), that a status name is its
seed id, that only the types with an exact equality are guardable and why a
decimal is refused rather than compared, that a list is ANDed and there is no
OR, and that `onTransition` requires a guard while the other axes do not.

Same gap, same section: `requiredWhen` (eclipse-dirigible#7094) and `forbidWhen` (eclipse-dirigible#7275) appeared
in the guide only as a subordinate clause inside the `compare` bullet - the
parser accepts seven check kinds and the guide taught four. Both now have their
own entry, with the point that decides whether they work at all: without the
`status:` gate a check runs in the controllers only, so a rule meant to gate a
status that a workflow sets needs the gate, because `setField` /
`setRelationField` are repository writes that never pass through a controller.
That is the check that expresses "this may be empty in a draft but not once it
reaches STATUS" - which `required: true` cannot say, and which an app that fills
a relation through a user task needs to keep a task completion from marking a
record done with the value still empty.

Every YAML added here was validated against a running instance rather than read
off the source: the ANDed outcome guard, the `requiredWhen` gate (field, when
and status gate, both symbols resolving to the seed id) and the `forbidWhen`
one-hop parent condition all return no issues.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@delchev delchev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against origin/master at 6bbb794. The resolves: correction is right and the requiredWhen / forbidWhen entries are accurate, but the new "when: - the guard grammar" section makes two claims the parser contradicts, one of them directly against the paragraph immediately above it in the same guide section.

1. "An onTransition binding requires a guard; on the other axes it is optional" is false

On notifications, integrations and outbound the validator returns as soon as when is absent, whatever the trigger kind (validateEventGuard, IntentParser.java:3099). The guide says so itself five lines earlier, with an onTransition example commented # the guard is optional here. The guard is mandatory for onTransition only on generates (validateGeneratesWhen, requireStatusGuard) and postings (IntentParser.java:7724). An assistant reading the more specific later sentence will add guards nobody asked for, or refuse "notify on any status change".

2. The section describes one grammar for the whole axis, but the constructs it generalizes over each have their own

The typed grammar it documents (==|!=, string/integer/boolean, ANDed list, wrong type refused) is exactly what validateGuardTerm + CheckSupport enforce - and only the three glue consumers use it. The others differ:

  • postings.event.when: scalar only, <Property> == <number>. A list is refused with "event when does not take a list here" (IntentParser.java:7718), and there is no string term. So "nothing restricts a guard to the status column" is false for postings, and the sentence this PR deletes was correct for them (the PR body names postings among the axes the old sentence misdescribed).
  • generates.event.when: a list, but at most one numeric term, == only (WHEN_STATUS_TERM), plus <StringField> ==|!= <literal> terms on string/text fields only - an integer or boolean field is refused: "only the source's string/text fields can carry a literal guard" (IntentParser.java:8664); a duplicate property is refused. So both ==|!= and "string, integer and boolean are guardable" overstate here.
  • process trigger.when and a wait step's when: no parser validation at all; both render through the untyped NotificationSupport.guard(Object) (GlueIntentGenerator.java:249, :5274), which turns an unparseable term into true. The "refused rather than compared" guarantee does not hold there.
  • resolves.event.when and transitions[].when are scalar only.

The section sits under the event axis, which the guide defines as the three glue consumers plus event-driven generates. Scoping it is enough: the typed grammar for the three; generates takes exactly one <Status> == <name|id> term plus string-field terms; postings takes exactly one status term and no list. The rewritten resolves example (a generates with one status term and one term on the string field resolution) is valid and renders as Objects.equals(source.Resolution, "notFound").

3. Minor

  • "message is mandatory on both" holds only for forbidWhen (IntentParser.java:5293); validateRequiredWhen never checks it.
  • Worth one clause: a generates guard on an editable string field gets a Generate warning, and readOnly: true on the outcome: field is the fix (warnIfGuardFieldIsEditable).

Confirmed correct

compare does take an optional status: gate on master since #7338, so "the same optional gate as compare" is right. The controller-vs-repository routing by gate presence matches ModelParameterProcessor. forbidWhen refuses a field, allows one hop, and requires the EntityStatus relation when gated. A bare to-one as a requiredWhen field resolves to the FK read, so field: driver works.

…ition-requires-a-guard claim

The new grammar section stated one `when:` shape for the whole event axis and that an
onTransition binding always needs a guard. On master each construct holds its guard to
its own shape: the three glue consumers take the typed `==|!=` list over string/integer/
boolean properties with the guard optional on every trigger kind; a generates event takes
exactly one `==` status term plus string-field terms; a postings event takes the status
term alone and refuses a list; a process trigger and a wait step are not validated at
parse and render an unparseable term as `true`. The resolves paragraph now says the
outcome-reading guard is a generates guard, since a posting per outcome still needs a
status per outcome, and `message` is stated mandatory only where the parser requires it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@delchev

delchev commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Pushed b300d61 onto this branch with the corrections from the review: the grammar section is now scoped per construct (glue consumers: typed ==|!= list, guard optional on every trigger kind; generates: one == status term plus string-field terms; postings: the status term alone, no list; trigger: / wait: unvalidated at parse), the onTransition-requires-a-guard sentence is gone, the resolves paragraph says the outcome-reading guard is a generates one, and message is stated mandatory only on forbidWhen. Every statement was re-checked against the parser on master (validateEventGuard, validateGeneratesWhen, the postings block, TriggerSupport.triggerWhen).

@delchev delchev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrections pushed in b300d61; the guide now matches the parser.

@delchev
delchev merged commit de368f8 into eclipse-dirigible:master Sep 17, 2026
10 checks passed
delchev added a commit that referenced this pull request Sep 17, 2026
… generated source (#7238) (#7424)

Two coverage gaps from #7129 (`checks: requiredWhen`). The assistant-guide half
landed in #7414, which added the kind's bullet; this adds the sentence that was
still missing there - the `status:` gate is OPTIONAL and its PRESENCE is the
routing (no gate: every controller's validate(), a 400; a gate: the repository,
on the synchronous transition path).

The second half is the IT. `IntentEmissionCoverageIT` asserted the gated check
over its generated source only - the hop load, the condition, the message - and
a source assertion cannot tell a gate that fires from one whose condition is
never true, which is exactly the boxing failure mode the sibling #7237 was filed
for. It could not do more: the check read `Account.name`, and that column is
`required: true`, so the value it guards is always present and no write could
ever be refused.

`Account` therefore gains an optional `taxCode` and the check reads that. The
runtime block drives all three outcomes over REST: an audited entry against an
account with no tax code is refused at its gate status with the authored message
(400), an entry with no note reaches the same status against the SAME account
(the rule is a condition, not a `required` nobody declared), and the audited one
passes once the ACCOUNT is amended - the related row, not the entry, which is
what proves the gate reads the hop when the write is checked rather than a copy
taken at create time. The entries carry balanced lines so the two document
checks gating on the same status cannot be what refuses them.

Fixes #7238

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants