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
Blocked-by: #15963
Filed by the
domain:specexecution PM seat (sessionsession_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.273247e56f.The asymmetry
PR #16342 (card #15963) gives all seven
defineStackrefusal sites ADR-0112 envelopes —status: 422, oneSTACK_*code each, findings onissues, message text byte-for-byte unchanged — and registers six newdoor: 'none'/verdict: 'boot-refusal'classification rows.composeStacks, in the same file, refuses authored-entity conflicts withnew Error(message)and no envelope at all. After #16342 lands,stack.zod.tscarries 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. Everythrow new Error(remaining inpackages/spec/src/stack.zod.tsat273247e56f, with its enclosing function resolved::2916composeSingleValuecomposeStacks conflict: top-level key '<key>' is declared with different values by …:2965composeFunctionscomposeStacks conflict: top-level key 'functions' is declared in the map form by …:2980composeFunctionscomposeStacks conflict: function '<name>' is defined by both …:3179refuseUnmergeableCollectionscomposeStacks conflict: object '<name>' is defined in multiple stacks and its '<key>' …:3258mergeObjectscomposeStacks conflict: object '<obj.name>' is defined in multiple stacks. …:3699composeStacksformatComposedActionKeyCollisions(actionCollisions)← the one that was handed over:3367collectComposedActionKeyCollisionscomposeStacks 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:composeStacksis declared at:3585and only:3699is 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.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 sevendefineStackrefusals: an authored entity refused at authoring time, through the same callers. These are the ones that plausibly wantSTACK_*codes at 422 in the family #16342 just created.Class B — an internal invariant (
:3367, prefixcomposeStacks 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:2553reading had to make for the zod-aggregate site, so the precedent for taking it seriously exists.The
doorclassification is already measurable, and it isnoneIf class A is registered, its ledger rows need a
doorverdict. Measured on273247e56f, so whoever takes this does not have to:⇒ the zero is a reading, not a dark grep: the sibling symbol lights the same probe 32 times.
composeStackshas no live caller in the runtime or REST trees, so class A isdoor: 'none'/verdict: 'boot-refusal'— the same posture #16342 registers for its six, andcheck:dispatcher-error-vocabularywill 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.Dedup
#15963 (the
defineStackhalf, being fixed by #16342) namescomposeStacksin one sentence — "composeStacksin 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