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

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. 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

  2. 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

  3. self-assigned this
    on Aug 10, 2026
  4. 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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions