Skip to content

[finding] objectName on an object-embedded action is inert AND unvalidated — the third arm PR for #7397 deliberately left open #7456

Description

@os-zhuang

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions