Skip to content

composeStacks concatenates actions across packages with no same-scope duplicate-key check — two packages each declaring one global:NAME compose into one collapsed handler key #14662

Description

@os-sam

Found while implementing #14124 (defineStack refuses two actions that resolve to one scope-qualified runtime key). Measured on main @ 2aa8456cf with that refusal already in place in packages/spec/src/stack.zod.ts.

What was measured

composeStacks never routes its output through validateCrossReferences — it merges the collections (actions: 'concat' in the collection strategy table) and returns mergeActionsIntoObjects(composed) directly. Each input has passed defineStack on its own, so each is duplicate-free WITHIN itself, but the composition is not checked:

// two packages, each legal on its own, each declaring a global action named `shared_refresh`
composeStacks([a, b])   :  ACCEPTED
out.actions             =  ["global:shared_refresh", "global:shared_refresh"]

The runtime registers and dispatches every action by the exact key objectName:name (global for an object-less action), so the composed artifact collapses the two handlers to one key — the same dead-button outcome #14124 refuses at defineStack, arriving one composition step later. examples/app-multi-package is the shipped shape that goes through this seam (composeStacks([ordersStack, coreStack], { manifest: 'preserve' })); it carries no colliding key today (corpus pass in the #14124 PR).

Adjacent precedent

composeStacks already owns a duplicate policy for OBJECT names (objectConflict: 'error' | 'override' | 'merge', default 'error'). Actions have no equivalent: 'concat' is the whole policy.

Suggested direction

Apply the same scope-qualified key check to the composed action set (naming the two source stacks by manifest id), or route the composed output through the walk defineStack runs. Whether a cross-package collision should be an error by default or governed by a strategy like objectConflict is the decision to make. #14124's triage deliberately kept composeStacks out of that card's scope, so this is recorded here rather than widened there.

Filing unassigned for triage.

Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions