Observation-class finding, surfaced and measured while implementing #7397 (the object-embedded modal/flow cross-reference arm). Filed unassigned per #4949 discipline — deliberately NOT fixed in that PR, because the fix requires a contract decision this seat should not take as a side effect.
What was measured
The registered action walk in packages/spec/src/stack.zod.ts applies three checks, not two: flow target, modal target, and objectName → declared object. #7397's PR mirrors the first two onto config.objects[].actions[]. The third is left as-is, and it splits the same way the target arms did:
embedded { name: 'probe_on', type: 'script', target: 'doThing', objectName: 'probe_missing' } -> ACCEPTED
REGISTERED { name: 'probe_on', type: 'script', target: 'doThing', objectName: 'probe_missing' } -> REJECTED
"Action 'probe_on' references object 'probe_missing' which is not defined in objects."
Same action object, two authoring positions, opposite verdicts — the b/f, c/g, d/i pattern from #7397's probe table, one key over. Measured on origin/main @ d13ce33 with both stacks built from the same helper and the same arguments.
Why it is observation-class rather than a live defect
Nothing consumes the key at that position today, so no user hits it at runtime:
mergeActionsIntoObjects (stack.zod.ts) builds its objectName -> actions[] map only from config.actions — the top-level list. It never reads obj.actions[].objectName. The merge direction is top-level to object, never the reverse.
- The embedded position carries the full
ActionSchema (data/object.zod.ts, actions: z.array(ActionSchema)), so the key is declared there purely because the shape is shared with the registered collection — not because anything reads it.
So it is a declared-but-unconsumed key at this position (Prime Directive #12, "declared = enforced") plus an unvalidated dangling reference — inert drift, not a live break. That is why it carries the finding label and no pm:queue.
The contract question that blocks a mechanical fix
Mirroring the registered check verbatim is not obviously right, which is exactly why #7397's PR did not do it. Two readings, different acceptance surfaces:
- A — existence check (verbatim mirror).
objectName on an embedded action must name a declared object, same as registered. Cheapest, consistent with the other two arms. But it accepts objectName: 'other_object' on an action embedded under task — a contradiction the schema would then bless.
- B — consistency check. On an embedded action,
objectName must equal the owning object's name (or be absent), because the action's owner is already unambiguous from its position. Stricter, and the only reading under which the key carries meaning at this position at all.
- C — retire the key at this position. If nothing reads it and B says it can only ever restate the position, the ADR-0049 enforce-or-remove route may be the honest answer rather than either check.
A/B/C differ in what they refuse, so this is an acceptance-surface decision (domain:spec by the standing red line), and under the startup-focus principle option C deserves real weight: there is no measured business pull for authoring objectName at a position that already names the object.
Anchors
Dedup
Open-issue search for objectName action cross-reference and embedded action objectName defineStack returns only #7397 itself. No open card covers this key.
Observation-class finding, surfaced and measured while implementing #7397 (the object-embedded modal/flow cross-reference arm). Filed unassigned per #4949 discipline — deliberately NOT fixed in that PR, because the fix requires a contract decision this seat should not take as a side effect.
What was measured
The registered action walk in
packages/spec/src/stack.zod.tsapplies three checks, not two: flow target, modal target, andobjectName→ declared object. #7397's PR mirrors the first two ontoconfig.objects[].actions[]. The third is left as-is, and it splits the same way the target arms did:Same action object, two authoring positions, opposite verdicts — the b/f, c/g, d/i pattern from #7397's probe table, one key over. Measured on
origin/main@d13ce33with both stacks built from the same helper and the same arguments.Why it is observation-class rather than a live defect
Nothing consumes the key at that position today, so no user hits it at runtime:
mergeActionsIntoObjects(stack.zod.ts) builds itsobjectName -> actions[]map only fromconfig.actions— the top-level list. It never readsobj.actions[].objectName. The merge direction is top-level to object, never the reverse.ActionSchema(data/object.zod.ts,actions: z.array(ActionSchema)), so the key is declared there purely because the shape is shared with the registered collection — not because anything reads it.So it is a declared-but-unconsumed key at this position (Prime Directive #12, "declared = enforced") plus an unvalidated dangling reference — inert drift, not a live break. That is why it carries the
findinglabel and nopm:queue.The contract question that blocks a mechanical fix
Mirroring the registered check verbatim is not obviously right, which is exactly why #7397's PR did not do it. Two readings, different acceptance surfaces:
objectNameon an embedded action must name a declared object, same as registered. Cheapest, consistent with the other two arms. But it acceptsobjectName: 'other_object'on an action embedded undertask— a contradiction the schema would then bless.objectNamemust equal the owning object's name (or be absent), because the action's owner is already unambiguous from its position. Stricter, and the only reading under which the key carries meaning at this position at all.A/B/C differ in what they refuse, so this is an acceptance-surface decision (
domain:specby the standing red line), and under the startup-focus principle option C deserves real weight: there is no measured business pull for authoringobjectNameat a position that already names the object.Anchors
packages/spec/src/stack.zod.ts— registeredobjectNamecheck (Validate action -> object references); the [observation]config.objects[].actions[](object-embedded actions) may also bypass defineStack's cross-reference walk — unverified, same mechanism as #6889's row E #7397 embedded walk immediately above the inline one, whose comment records this exclusion in code.packages/spec/src/data/object.zod.ts—actions: z.array(ActionSchema), the shared shape.config.objects[].actions[](object-embedded actions) may also bypass defineStack's cross-reference walk — unverified, same mechanism as #6889's row E #7397 / its PR — the modal/flow arms; Inline (page-element) actions bypassdefineStack's action cross-reference validation entirely — a danglingtype: 'modal'target builds clean #6889 / PR fix(spec): defineStack cross-reference validation reaches inline page-element actions (#6889) #7392 — the inline half. Neither covers this key:InlineActionSchemadoes not pickobjectNameat all, so the inline arm had nothing to decide.Dedup
Open-issue search for
objectName action cross-referenceandembedded action objectName defineStackreturns only #7397 itself. No open card covers this key.