intent: a value required only under a condition - checks: requiredWhen (#7094) - #7129
Merged
Conversation
…en` (#7094) `required: true` is unconditional, and most rules about a missing value are not: the value is needed for ONE way of handling the record and meaningless for the others. An invoice sent by e-mail needs the customer's e-mail address; one sent by post does not - so `required` on the address is not the rule, `checks:` had no kind that was, and the real rule was therefore not declared anywhere. An e-mailed invoice whose customer carried no address went through Send: status SENT, the mail step logging a no-op for a recipient it did not have, nothing stamped on the record, and the clerk who pressed the button told it had succeeded. `- { kind: requiredWhen, field: Customer.email, when: "sentMethod == 1", status: SENT, message: ... }`. **The value may be one hop away**, which is why the kind exists at all - the rule is about the record being sent and the address belongs to its customer. `field:` is walked by `ResolvePathSupport`, the resolver every other path in the DSL uses, so a cross-model target reads too and a path walking on past one is refused there. The hops travel into the `.model` as the check's `pathLoads` and `ModelParameterProcessor` turns them into the generated Entity/Repository FQNs - the pass that knows the generation folder, exactly as for a master's inherited lock. The reader loads each hop by id and reads the field null-guarded, so a missing link is an EMPTY value the check fires on rather than a throw inside a repository. **The `status:` gate is optional, and its presence is the routing.** No gate = a row check: every generated controller's `validate()`, a 400 with the authored message, like `exactlyOne`. A gate = the repository's `enforceChecks`, like `itemsMin`, which puts it on the synchronous path #7014/#7063 opened - the refusal reaches the person completing the task instead of dead-lettering as a process incident. **The condition is closed and typed.** `CheckSupport` owns the pattern the parser refuses on and the Java the generator emits, in one class so they cannot drift. Two refusals, both about a guard that would otherwise be silently wrong: a condition the generator cannot compile is a parse error (degrading it to `true`, as the shared glue guard does, would make the value unconditionally required - a `required` nobody authored), and a literal that is not a value of the property's declared type is refused, because `Objects.equals(Long, int)` never holds and a guard on a `long` column would switch the rule off while looking authored. Only strings, integers, booleans and a to-one's key are guardable; a decimal, a double or a date is compared for equality by nobody who means it. A status name resolves to its seed id like every other guard, and the list form is an implicit AND. Unit: `IntentParserTest`, `EdmIntentGeneratorTest`, `ModelParameterProcessorTest`. IT: `IntentEmissionCoverageIT` - a gated check over a hop in `EntryRepository`, an ungated one in `DocController`, and that the ungated one is not in the repository. Fixes #7094 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 9, 2026
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.
required: trueis unconditional, and most rules about a missing value are not: the value is needed for ONE way of handling the record and meaningless for the others. An invoice sent by e-mail needs the customer's e-mail address; one sent by post does not - sorequiredon the address is not the rule,checks:knew no kind that was (exactlyOne/itemsSumEqual/itemsMin), and the real rule was therefore declared nowhere. An e-mailed invoice whose customer carried no address went through Send: status SENT, the mail step logging a no-op for a recipient it did not have, nothing stamped on the record, and the clerk who pressed the button told it had succeeded.The value may be one hop away, which is why the kind exists at all - the rule is about the record being sent and the address belongs to its customer.
field:is walked byResolvePathSupport, the resolver every other path in the DSL uses, so a cross-model target reads too and a path walking on past one is refused there. The hops travel into the.modelas the check'spathLoadsandModelParameterProcessor.resolveCheckPathLoadsturns them into the generated Entity/Repository FQNs - the pass that knows the generation folder, exactly as for a master's inherited lock, and the.modeltwin could not re-derive another model's folder at all. The generated reader loads each hop by id and reads the field null-guarded, so a missing link is an EMPTY value the check fires on rather than a throw inside a repository.The
status:gate is optional here, and its presence is the routing. No gate = a row check:ModelParameterProcessorfiles it withrowChecksand all three controller templates enforce it invalidate()as a 400 carrying the authored message, likeexactlyOne. A gate = the repository'senforceChecks, likeitemsMin- which puts it on the synchronous path #7014/#7063 opened, so the refusal is the 400 the Inbox shows the person who pressed the button instead of a dead-lettered process incident. A gated check requires afunction: EntityStatusrelation to read the gate from (parser-enforced).The condition is closed and typed.
CheckSupportowns both halves - the pattern the parser refuses on and the Java the EDM generator emits - in one class so they cannot drift. Two refusals, both about a guard that would otherwise be silently wrong:true, which is what the sharedNotificationSupport.guarddoes for a glue listener, would make the value unconditionally required - arequirednobody authored;Objects.equals(Long, int)never holds and a guard on alongcolumn would switch the rule off while looking authored.Only
string/text/integer/int/long/booleanand a to-one's integer FK are guardable at all; a decimal, a double or a date is compared for equality by nobody who means it. A status name in the condition resolves to its seed id like every other guard (StatusSymbolResolver.rewriteWhenon the check node), and the list form is an implicit AND, as in #6957.A model declaring no
requiredWhengenerates byte-identically; the only pre-existing emission change is that theCriteriaimport in the DAO now follows the item-counting kinds specifically, so a model whose only document check is a gatedrequiredWhendoes not import it unused.Tests
IntentParserTest.conditionallyRequiredValuesParseAndValidate(the four refusals),EdmIntentGeneratorTest.conditionallyRequiredValuesEmitTheirConditionAndTheHopsTheirValueIsReadThrough,ModelParameterProcessorTest(the routing split + the hop FQNs). 1129 unit tests green acrossengine-intent+ide-template.IntentEmissionCoverageIT(green locally, 137 s) - a gated check over a relation hop inEntryRepository, an ungated one inDocControllerwith its status name resolved, and the assertion that the ungated one is NOT in the repository. The generated client Java compiles in that run.Docs
.claude/docs/intent-layer.md,components/engine/engine-intent/CLAUDE.md,components/engine/engine-intent/README.md.0036) and docs: a value required only under a condition - checks: requiredWhen IntentFile/intentfile.github.io#53.Fixes #7094
🤖 Generated with Claude Code