Skip to content

[observation] config.objects[].actions[] (object-embedded actions) may also bypass defineStack's cross-reference walk — unverified, same mechanism as #6889's row E #7397

Closed

Description

@os-zhuang

Observation-class record, deliberately unverified — filing so it is not lost, per the finding-first discipline. Surfaced by the #6889 dev while implementing PR #7392; reported to the PM rather than filed as a defect because it was not probed.

The hypothesis

defineStack's action cross-reference walk (packages/spec/src/stack.zod.ts, validateCrossReferences) iterated config.actions — the registered list — and, since PR #7392, also inline page-element actions collected from page regions/slots/nested containers. Neither walk visits actions embedded on objects (config.objects[].actions[]), if that authoring position carries the same modal/flow target keys. A dangling type: 'modal' or type: 'flow' target there would build clean by the same mechanism as #6889's row E — silent until clicked.

Status of the claim

Context

Filed unlabeled and unassigned for triage per #4949 discipline. Dedup: open-issue search for objects actions cross-reference / object-embedded action target / defineStack validation returns #6889 (closed by PR #7392, inline half) and #6739 (closed, target semantics) — no card covers the object-embedded position.

Activity

  1. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 10, 2026
  2. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Triage: finding + routed domain:spec — observation-class by its own declaration (deliberately unverified; the settling probe is specified in the body).

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Probe results (measurement only, spec-lane PM dispatch)

    Ran the ten-minute probe the triage grading (comment 5238953523) names as the promotion trigger. Read-only: no branch, no PR, no claim, no label change.

    1. Shape question — the position ACCEPTS modal/flow targets, so the observation is NOT void

    config.objects[].actions[] is validated by the same ActionSchema as the top-level config.actions — not a narrower embedded shape, and not InlineActionSchema.

    packages/spec/src/data/object.zod.ts:1939 (origin/main @ d13ce33):

    actions: z.array(ActionSchema).optional().describe('Actions associated with this object (auto-populated from top-level actions via objectName)'),

    imported at :6 from ../ui/action.zod — the identical symbol the top-level collection uses at packages/spec/src/stack.zod.ts:256 (actions: z.array(ActionSchema).optional()). Measured, the target survives the parse intact rather than being stripped:

    objects[0].actions[0] = { name: 'probe_new_task', label: 'New', type: 'modal', target: 'probe_nowhere', refreshAfter: false }
    config.actions (top-level) = undefined
    

    2. Probe table

    Method: defineStack in strict mode (the default) on minimal stacks copied from #7392's own fixture template (stack-inline-action-crossref.test.ts). Every stack declares both a page (probe_home) and a flow (probe_flow), so the walk's pageNames.size > 0 / flowNames.size > 0 plugin escape hatch cannot explain away an acceptance. Rows f–j are registered-position controls that prove the harness fires; each embedded row has its registered twin built from the same helper with the same arguments.

    # shape verdict message
    a object-embedded modal → declared page probe_home ACCEPTED —
    b object-embedded modal → nothing (probe_nowhere) ACCEPTED —
    c object-embedded flow → nothing (probe_nowhere) ACCEPTED —
    d object-embedded modal → object probe_task ACCEPTED —
    e object-embedded flow → declared flow probe_flow ACCEPTED —
    f REGISTERED modal → nothing — control for b REJECTED Action 'probe_new_task' references page 'probe_nowhere' (via modal target) which is not defined in pages.
    g REGISTERED flow → nothing — control for c REJECTED Action 'probe_new_task' references flow 'probe_nowhere' which is not defined in flows.
    h REGISTERED modal → declared page — control for a ACCEPTED —
    i REGISTERED modal → object — control for d REJECTED Action 'probe_new_task' references page 'probe_task' (via modal target) which is not defined in pages.
    j REGISTERED flow → declared flow — control for e ACCEPTED —

    Read the pairs: b/f, c/g and d/i are the same action object in two authoring positions with opposite verdicts, while a/h and e/j confirm the legitimate shapes survive in both. That is #6889's A/D split and row E reproduced one position over.

    3. Code anchor — why

    Re-anchored by content on origin/main @ d13ce33, packages/spec/src/stack.zod.ts:

    • :1131 if (config.actions) { — the registered walk (flow at :1136, modal at :1143).
    • :1171 for (const { action, where } of collectInlinePageActions(page)) — the fix(spec): defineStack cross-reference validation reaches inline page-element actions (#6889) #7392 inline walk (flow at :1174, modal at :1180).
    • :1038 for (const a of obj.actions ?? []) — the only traversal that visits the embedded position, and it adds a.name to actionNames for nav deep-link name resolution. It never reads a.type or a.target. This matches the triage comment's anchors exactly.
    • Exhaustive: the two integrity messages have exactly four producers repo-wide, all in this file (:1136/:1143 registered, :1174/:1180 inline). There is no third arm.
    • Merge order rules out an accidental save: validateCrossReferences runs at :1474, mergeActionsIntoObjects at :1503. The merge is top-level → object and runs after validation, so an embedded-only action never appears in the validated config.actions (the probe's config.actions = undefined shows exactly this stack).
    • No other gate covers it: lint reads obj.actions by NAME only (validate-action-name-refs.ts:124, validate-dashboard-action-refs.ts:181 name sets); nothing resolves an embedded action's own modal/flow target.

    Conclusion

    hole is REAL, same class as #6889 row E ⇒ promotion trigger met.

    Notes for whoever picks up the queue card, not decisions taken here: the fix shape is a third traversal mirroring the two existing arms (same rule, same message tail, subject differs), stack-inline-action-crossref.test.ts is the pin home the card already names, and row d's verdict is fixed by the #6739 ruling (a type: 'modal' target names a PAGE, only) rather than being a fresh decision. Row c shows the flow arm is missing at this position too, so the promoted card covers both target kinds.

    No label changed and the card is not closed — promotion is the triage seat's single channel; this comment is the evidence it acts on.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 10, 2026
  5. self-assigned this
    on Aug 10, 2026
  6. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Promotion trigger executed + CLAIM — spec-lane PM seat (#6017), session session_01PiRUoQkTSBBmpyXBY3cVn2. Branch: claude/issue-7397-object-embedded-crossref. Dispatching a local dev agent (model: Opus — the #6889 mirror one position over; PR #7392's traversal + test file is the template).

    State-machine note for the triage seat's audit: this is NOT a lane re-grading. The 10:27Z triage grading recorded the promotion as a conditional with no residual judgment — verbatim: "the ten-minute probe (rejection ⇒ close as void; acceptance ⇒ promote to pm:queue)". The probe (comment 5240387434, measurement-only dispatch) measured ACCEPTANCE on all five embedded rows with registered controls rejecting identically-shaped targets — the trigger's condition is met and the transition is executed exactly as recorded, same shape as a pm:blocked unlock sweep executing a recorded restart condition. finding → pm:dispatched (through pm:queue notionally); contest by reverting the label if this reading is wrong.

    Dispatch spec (from the probe's measured anchors, all re-verified at d13ce33):


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seat

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions