intent: a row outside every key-tuple is not guarded, and a rule match needs a literal (#7180) - #7253
Merged
Merged
Conversation
…h needs a literal (#7180) #7166 made `Criteria.eq` / `ne` null-safe, which is right for the idempotency guard it was written for and changed two generated call sites that pass a possibly-null value without saying what they mean by it. The aggregate guard is the one that matters. `aggregates:` is explicit that a source row with any grouping key unset belongs to no tuple and is ignored - the generated aggregate handler says so and materialises no target row for such a row - but the guard that recomputes the same total said nothing about it, and relied on SQL three-valued logic for the answer. So a movement authored without its warehouse used to be weighed against zero and rejected for breaching a pool that does not exist; since #7166 it is weighed against the sum of every other tuple-less row, a pool no aggregate row materialises. Both readings are wrong in the same way: a record that contributes to no total can breach no minimum, so the guard does not apply to it at all - no throw, no marker flip, no forced status - and the generated code now says that in a null test of its own instead of leaving it to how a comparison with no value happens to render. The sibling roll-up capacity guard has always skipped a null FK this way. The skip is logged at debug, naming the aggregate, so a record that is never guarded can be traced to the key it lacks. The posting's rule lookup turns out not to be exposed to the change at all: the value bound there is the authored literal of `rule.match`, never a value read off the source, so it is never null and the null-safe `eq` cannot reach it. What it was exposed to is an authoring accident the parser let through - a blank literal, which emits a lookup for the empty string, matches no rule row, and sends every source document to the unposted worklist with the intent, the generation and the publish all green. That is now refused where it is authored, and the template says why no runtime guard belongs there (a value omitted outright was already refused as an empty selector: the typed mapping drops the null entry). Verified through IntentEmissionCoverageIT, which renders both templates and compiles the generated client Java (327 units, no failures). Fixes #7180 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
#7166 made
Criteria.eq/nenull-safe, which is right for the idempotency guard it was written for and changed two generated call sites that pass a possibly-null value without saying what they mean by it.The aggregate guard
aggregates:is explicit that a source row with any grouping key unset belongs to no tuple and is ignored - the generated aggregate handler says so in its own contract and materialises no target row for such a row. The guard that recomputes the same total said nothing about it and relied on SQL three-valued logic for the answer:Both are wrong in the same way. A record that contributes to no total can breach no minimum, so the guard does not apply to it at all: no throw, no
markerflip, no forcedsetStatus. The generated code now says that in a null test of its own rather than leaving it to how a comparison with no value happens to render, which also keeps the guard and its aggregate two computations of the same total instead of two different ones. The sibling roll-up capacity guard has always skipped a null FK this way. The skip is logged at debug naming the aggregate, so a record that is never guarded can be traced to the key it lacks.A grouping key that must always be there is declared
required- a separate and better statement where it is true.The posting's rule lookup
Not exposed to the change at all, contrary to the issue's reading: the value bound there is the authored literal of
rule.match, never a value read off the source, so it is never null and the null-safeeqcannot reach it. The template now says so, and why no runtime guard belongs there.What it was exposed to is an authoring accident the parser let through - a blank literal. It emits a lookup for the empty string, matches no rule row, and sends every source document to the unposted worklist with the intent, the generation and the publish all green. Refused now where it is authored. A value omitted outright (
documentType:with nothing after it) was already refused as an empty selector, the typed mapping dropping the null entry.Verification
IntentEmissionCoverageITgreen - it renders both templates, asserts the new guard shape, and compiles the generated client Java (Compiled batch: [327] units, [340] class file(s), no failures).engine-intentunit suite green (1187 tests), with the emittedaggregatename and both rule-match refusals asserted.formatter:validateand the release-profile javadoc build clean.Docs
intent-assistant-guide.mdstates therule.matchrule.Fixes #7180
🤖 Generated with Claude Code