fix(intent): a step arg the kind does not read is an error (#6749) - #6751
Merged
Conversation
Follow-up to #6541/#6748, which made an unknown key an error for every TYPED node and for seed rows. The maps it left opaque split in two, and only one half is genuinely author-keyed: - a `map:`/`defaults:` projection, a relation's `where:`, a widget's `at:` and a delegate's injected `fields:` carry names from the model being described - they stay opaque, forever; - a step's `args:`, a process `trigger:`/`abortOn:`, a glue `event:` binding (incl. the nested `onStepReached: { process, step }`), a posting's `rule:`, a generate child's `forEach:` and a lookup's `between:`/`found:`/`notFound:`/ `ambiguous:` are closed vocabularies that merely happen to be typed as `Map` - an invented key there was as silent as anywhere else. The BPMN was emitted, the task existed, and only the behaviour the author asked for was missing. The second group is now checked: the fixed vocabularies from the registry `UnknownKeyValidator.MAP_KEYS`, and a step's `args:` per KIND (`IntentParser.STEP_ARGS_BY_KIND`), because its vocabulary depends on the sibling `kind`. A step therefore gets two messages - `declares unknown arg [assigne] - did you mean [assignee]?` for a typo, and `declares arg [if] but is a userTask - if is a decision argument` for a misplacement, which is the same silent drop. The six keys whose misplacement already has a dedicated, better-worded validator (setField/setRelationField/delegate/notify/timeout/ expire) stay in the vocabulary but are excluded from the generic line, so nobody is told the same thing twice. Unlike #6748's reflected sets these lists are AUTHORED, which is why this was a separate change: a missing entry rejects an intent that works today. Every entry mirrors what the readers actually consult, and the notify block validates against `NotificationIntent.BLOCK_KEYS` - the set its own `fromMap` reads, pinned to the class's embeddable properties by a test, so a new block property cannot ship unauthorable. Verified red-first (10 of 13 unit cases fail without it) and against the corpus sweep: 259 in-repo fixtures + 63 real production intents carrying 389 authored `args:` blocks - no false positives. 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.
Closes #6749. Follow-up to #6541 / #6748.
The gap
#6748 made an unknown key an error for every typed node and for seed rows, deriving the known-key set reflectively off the model classes. The maps it left opaque turn out to split in two, and only one half is genuinely author-keyed:
Mapmap:/defaults:projection, a relation'swhere:, a widget'sat:, a delegate's injectedfields:args:, a processtrigger:/abortOn:, a glueevent:binding (incl. the nestedonStepReached: { process, step }), a posting'srule:, a generate child'sforEach:, a lookup'sbetween:/found:/notFound:/ambiguous:Everything in the right column was as silent as the keys #6748 fixed: the BPMN was emitted, the task existed, and only the behaviour the author asked for was missing.
The fix
UnknownKeyValidator.MAP_KEYS— a registry keyed<SimpleClassName>#<field>[.<nested key>], consulted by the existing walk. Nothing else about the walk changes.IntentParser.validateStepArgs— per kind, because a step's vocabulary depends on its siblingkind. Two messages, because there are two mistakes and the second is the same silent drop:The six keys whose misplacement already has a dedicated, better-worded validator (
setField/setRelationField/delegate/notify/timeout/expire) stay in the vocabulary — so they are never called unknown — but are excluded from the generic misplacement line, so nobody is told the same thing twice in two registers.aMisplacedSetFieldKeepsItsOwnMessageOnlypins that.The nested notify block validates against
NotificationIntent.BLOCK_KEYS, the set its ownfromMapreads.NotificationIntentTestpins that constant to the class's embeddable properties, so a new block property cannot ship unauthorable — the failure this whole family is about, one level up.Why this was a separate change
Unlike #6748's reflected sets, these lists are authored: a missing entry rejects an intent that works today. Every entry mirrors what the readers actually consult (
TriggerSupport,EventBinding,StepEventSupport,SetFieldSupport,ProcessTimerSupport,ProcessWaitSupport,ProcessParallelSupport,BpmnIntentGenerator,ServiceTaskHandlerGenerator), and the corpus sweep is what proves it.Verification
StepArgsIntentTestcases fail with the two call sites disabled (the other 3 are the "still parses" / "no duplicate message" guards).args:blocks (assignee / setRelationField / if / setField / delegate / onCreate / branches / after / next / path) — zero hits.mvn test -pl components/engine/engine-intent: 581 tests green (16 new).IntentEngineIT: 48 tests green locally (headless), including the newparse_rejects_an_unknown_step_arg_and_an_unknown_trigger_key, which asserts the exact 422 messages over HTTP — the outermost layer this is observable at.formatter:validateclean; javadoc clean under-P release.Docs
🤖 Generated with Claude Code