Skip to content

[finding] composeStacks refuses six authored-entity conflicts as bare Errors — the defineStack family got ADR-0112 envelopes in #15963, its sibling did not, and five of the six sit in helper functions where a reader of composeStacks itself will not find them #16348

Description

@huangyiirene

Blocked-by: #15963

Filed by the domain:spec execution PM seat (session session_01T6HeZvT9wdSJD1ZxJb5Eno, seat post #6017). Handed to me by the #15963 implementer, which was told ⛔ not to widen its PR into this and to report it instead — that instruction was followed exactly. The population below is my own measurement and is larger than the one handed over.No severity asserted, no domain routing — that is triage's.

⚠️ Describes the tree once PR #16342 lands. Line numbers are against that PR's head 273247e56f.

The asymmetry

PR #16342 (card #15963) gives all seven defineStack refusal sites ADR-0112 envelopes — status: 422, one STACK_* code each, findings on issues, message text byte-for-byte unchanged — and registers six new door: 'none' / verdict: 'boot-refusal' classification rows.

composeStacks, in the same file, refuses authored-entity conflicts with new Error(message) and no envelope at all. After #16342 lands, stack.zod.ts carries two refusal families that are the same shape to an author and different shapes to a consumer.

The population — measured, and it is six, not one

The hand-over named the action-key collision at :3699. That is one of six. Every throw new Error( remaining in packages/spec/src/stack.zod.ts at 273247e56f, with its enclosing function resolved:

line enclosing function message
:2916 composeSingleValue composeStacks conflict: top-level key '<key>' is declared with different values by …
:2965 composeFunctions composeStacks conflict: top-level key 'functions' is declared in the map form by …
:2980 composeFunctions composeStacks conflict: function '<name>' is defined by both …
:3179 refuseUnmergeableCollections composeStacks conflict: object '<name>' is defined in multiple stacks and its '<key>' …
:3258 mergeObjects composeStacks conflict: object '<obj.name>' is defined in multiple stacks. …
:3699 composeStacks formatComposedActionKeyCollisions(actionCollisions) ← the one that was handed over
:3367 collectComposedActionKeyCollisions composeStacks internal error: no source stack recorded for composed object '<obj.name>'.different class, see below

Five of the six are in HELPER functions, not in composeStacks's own body. That is why the hand-over read one: composeStacks is declared at :3585 and only :3699 is inside it. A reader who opens the function named in the card finds one site; the family is spread across five helpers 300–800 lines above it.

⚠️ This is the same shape as #16243's census defect, one file over and in the same week: a population counted by looking at one syntactic locus under-reads a family that travels through helpers. Worth naming explicitly so the fix does not repeat it — whoever takes this should count by message-prefix family, not by function body.

Two classes, and ⛔ they must not get the same treatment

Class A — authored-entity refusals (six sites). All six carry the literal prefix composeStacks conflict:. An author composed two stacks that disagree; the platform refuses. Same class as the seven defineStack refusals: an authored entity refused at authoring time, through the same callers. These are the ones that plausibly want STACK_* codes at 422 in the family #16342 just created.

Class B — an internal invariant (:3367, prefix composeStacks internal error:). "No source stack recorded for composed object" is not an authored-entity refusal; it is the code discovering its own bookkeeping is inconsistent. ⛔ Filing it at 422 would tell an author their stack is invalid when the defect is ours. The implementer's judgement — "leave it a bare Error or give it a 500-class shape, its own decision" — reads right to me, and I am ⛔ not deciding it here. Note this is exactly the distinction #15963's own :2553 reading had to make for the zod-aggregate site, so the precedent for taking it seriously exists.

The door classification is already measurable, and it is none

If class A is registered, its ledger rows need a door verdict. Measured on 273247e56f, so whoever takes this does not have to:

composeStacks in packages/runtime/src + packages/rest/src, non-test  ->  6 hits,
   ALL of them in packages/runtime/src/artifact-collections.ts, and all in
   doc comments or a message string (:11, :97, :123, :294, :307, :310)
   — zero call sites.
control: defineStack in the same two trees, non-test                 -> 32 hits

⇒ the zero is a reading, not a dark grep: the sibling symbol lights the same probe 32 times. composeStacks has no live caller in the runtime or REST trees, so class A is door: 'none' / verdict: 'boot-refusal' — the same posture #16342 registers for its six, and check:dispatcher-error-vocabulary will demand a row per code in both directions.

Why this is a card and not a rider on #16342

#16342's dispatch declared its file surface up front and ⛔ forbade widening. Folding six more refusal sites and six more ledger rows into a PR already carrying seven would have doubled it after the claim was posted — the exact scope creep the claim protocol exists to prevent. It is also genuinely a separate decision: class B has no settled answer, and class A's code granularity deserves the same "is it one code or six" reading that triage did for #15963 (5556795849) rather than an assumption that the answer carries over.

⚠️ An at-tier contract review of #16342 is in flight and was asked to opine on this split. If it disagrees with the A/B classification above, that reading should land on this card before anyone acts on it.

Dedup

#15963 (the defineStack half, being fixed by #16342) names composeStacks in one sentence — "composeStacks in the same file carries several more bare refusals (action-key collisions, the object-source internal error)" — as an aside, without a population, a classification or a door reading. This card is that sentence measured. #14367 / #14474 / #14552 are the precedent chain for the envelope shape.


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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions