docs(intent): correct the guard grammar and document the conditional checks - #7414
Conversation
…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
left a comment
There was a problem hiding this comment.
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.whenand awaitstep'swhen: no parser validation at all; both render through the untypedNotificationSupport.guard(Object)(GlueIntentGenerator.java:249,:5274), which turns an unparseable term intotrue. The "refused rather than compared" guarantee does not hold there. resolves.event.whenandtransitions[].whenare 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
- "
messageis mandatory on both" holds only forforbidWhen(IntentParser.java:5293);validateRequiredWhennever checks it. - Worth one clause: a
generatesguard on an editable string field gets a Generate warning, andreadOnly: trueon theoutcome: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>
|
Pushed b300d61 onto this branch with the corrections from the review: the grammar section is now scoped per construct (glue consumers: typed |
… 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>
The defect
#6996 (mine) added this sentence to the
resolves:section: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: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 == foundfor 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:
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 thatonTransitionrequires a guard while the other axes do not.requiredWhen(#7094) andforbidWhen(#7275) get real entries. They appeared only as a subordinate clause inside thecomparebullet — the parser accepts seven check kinds and the guide taught four. Both now document the point that decides whether they work at all: without thestatus:gate a check runs in the controllers only, so a rule meant to gate a status a workflow sets needs the gate, becausesetField/setRelationFieldare 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: truecannot 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:
issues: [], parsed as["Status == 3", "resolution == notFound"]requiredWhengate —('requiredWhen', 'payer', 'Status == 2', 2), both symbols resolved to the seed idforbidWhenone-hop parent condition —('forbidWhen', None, 'Invoice.Status == 2', None)Documentation only; no code changes.
🤖 Generated with Claude Code