Skip to content

Inline (page-element) actions bypass defineStack's action cross-reference validation entirely — a dangling type: 'modal' target builds clean #6889

Closed

Description

@os-project-manager

Found while working #6739, via a probe against defineStack. Filed separately — #6739 is one showcase corpus line; this is the validator's reach.

Blocked-by: #6739 — the content of the widened rule depends on #6739's contract answer (may a modal target name an object, or only a page?). The hole below is real under either answer.

What

packages/spec/src/stack.zod.ts:972-1006 validates action cross-references by iterating config.actions — the registered action list — only. Actions authored inline on a page element (element:button → properties.action, an InlineActionSchema) are never walked, so none of their targets are checked.

Measured

Probe: five minimal stacks, one defineStack call each, same object (probe_task) and page (probe_home) in all five.

A registered modal -> object  :  REJECTED -> Action 'probe_new_task' references page 'probe_task' (via modal target) which is not defined in pages.
B registered modal -> page    :  ACCEPTED
C registered modal -> nothing :  REJECTED -> Action 'probe_new_task' references page 'probe_nowhere' (via modal target) which is not defined in pages.
D inline     modal -> object  :  ACCEPTED
E inline     modal -> nothing :  ACCEPTED

Row E is the defect on its own terms: target: 'probe_nowhere' names neither a page nor an object nor anything else in the stack. Authored on a registered action it is a build error (row C). Authored inline on a button it builds clean, ships, and fails only when a user clicks — objectui's useActionModal modalHandler answers

Modal target "probe_nowhere" matches no page or object — a modal action's `target` names the page to open.

…i.e. a silent-until-clicked dead button, which is exactly the class row C exists to prevent.

The D/A split is the second half, and is #6739's question rather than this one's: the same type + the same target string gets opposite verdicts depending only on where the action was declared.

Reproduction

Self-contained, no repo fixtures — defineStack from @objectstack/spec with a manifest, one object, one page, and the action authored each way. The inline shape is the one PR #6737 shipped for the showcase home CTA (examples/app-showcase/src/ui/pages/index.ts:95).

Why it matters

Inline actions are the shape the AI-authoring path emits most readily — a button with its behaviour written right there, no separate registry entry. That is precisely the surface that most needs authoring-time rejection, and it is the one surface the cross-reference walk does not visit. "Declared = enforced" currently holds for registered actions and not for inline ones.

flow targets have the same hole (the same loop checks action.type === 'flow'), so an inline flow action naming a nonexistent flow is also unvalidated.

Direction

Walk page/region/component trees for inline actions and subject them to the same cross-reference checks as registered ones — same rule, same message, one more traversal. What the modal branch should accept comes from #6739.

Activity

  1. claude commented on Aug 9, 2026

    @claude
    Contributor

    Triage: pm:queue + pm:blocked + domain:spec.

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


    Generated by Claude Code

  2. self-assigned this
    on Aug 10, 2026
  3. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    CLAIM + UNBLOCK — spec-lane PM seat (#6017), session session_01PiRUoQkTSBBmpyXBY3cVn2. Branch: claude/issue-6889-inline-action-crossref. Dispatching a local dev agent (model: Opus — the card's own Direction section specifies the shape: same rule, same message, one more traversal).

    pm:blocked dropped: the recorded blocker #6739 closed completed 2026-08-10T04:16:36Z and its fix is merged on main (the showcase action is now type: 'form' + target: 'showcase_task.edit', per the maintainer-ruled resolution recorded there).

    Direction per the card: walk page/region/component trees for inline (element:button → properties.action) actions and subject them to the same cross-reference checks as registered ones in defineStack (packages/spec/src/stack.zod.ts — re-anchor the walk by content, line numbers will have drifted). Row E (dangling target matching nothing) is the defect and is unambiguous under any contract answer. The flow-target hole rides the same traversal.

    ⚠️ Contract guard for the modal branch: what a modal target may name comes from #6739's ruling — read that card's comments and anchor the answer before coding. If its resolution did NOT settle the D/A split (registered modal→object is build-rejected today while objectui resolves page-first-then-object at runtime), mirror the registered-action rule exactly and STOP-and-report the residual question rather than inventing the contract. Acceptance-face narrowing ⇒ census the shipped corpus for inline actions that would newly refuse (list them in the PR; expected 0 after #6739's fix), changeset level judged from precedent, four-step regen if generated surfaces move.


    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