Repository navigation
Commit 28f9277
fix(spec): every
Fixes #16348
Clause-②: yes — the refusal envelope of a published door is its contract
face: the same input that is refused today with a bare `Error` is
refused after this with a typed 422 carrying a code, and once a code
ships in `dist` a consumer's `catch (e) { switch (e.code) }` pins the
spelling.
`composeStacks` refused six authored-entity conflicts with `new
Error(message)` — `code` and `status` both `undefined` — while the
`defineStack` family in the same file has carried the ADR-0112 envelope
since #15963 (PR #16342). Each of the six now throws its own
`StackRefusalError` member: `status: 422`, one code per refusal site,
and the findings the site collected on `issues`. **Message text is
byte-for-byte unchanged at every site.**
## The class-A population, re-derived on this branch
Located by MESSAGE-PREFIX FAMILY, never by the line numbers on the card
(they are against `273247e56f` and have drifted ~+370). Instrument and
readings, taken at `93cdc43d5` before the first edit:
```
git grep -n 'throw new Error(' packages/spec/src/stack.zod.ts -> 7
git grep -n 'composeStacks conflict:' packages/spec/src/stack.zod.ts -> 7 lines:
6 message sites + 1 docblock mention (:3848)
git grep -n 'composeStacks internal error:' packages/spec/src/stack.zod.ts -> 1
control (the prefix probe is alive): the same prefix outside this file
-> 10 hits across 4 test files, plus 4 in packages/spec/CHANGELOG.md
control (the throw probe is alive): 'throw new Error(' across
packages/spec/src non-test -> 9 files, 24 hits, stack.zod.ts the largest at 7
```
| line (at `93cdc43d5`) | enclosing function | class |
| ---: | :--- | :--- |
| `:3285` | `composeSingleValue` | A |
| `:3334` | `composeFunctions` (map-vs-array shape) | A |
| `:3349` | `composeFunctions` (`claim`, duplicate handler name) | A |
| `:3636` | `refuseUnmergeableCollections` | A |
| `:3715` | `mergeObjects` (`objectConflict: 'error'`) | A |
| `:4265` | `composeStacks` body, via
`formatComposedActionKeyCollisions` | A |
| `:3824` | `collectComposedActionKeyCollisions` | **B — untouched** |
Five of the six raise inside helper functions 300-800 lines above
`composeStacks`' own body, which is the census defect the card warned
about: counting by one syntactic locus under-reads a family that travels
through helpers.
## The granularity reading I mirrored, and where I read it
Read off `main`, not assumed:
- `packages/spec/src/stack.zod.ts:2019-2147` — the seven classes PR
#16342 landed, each `extends StackRefusalError` with exactly one
`readonly code`, one class per refusal site:
`STACK_CROSS_REFERENCE_INVALID`, `STACK_SCHEMA_INVALID`,
`STACK_CAPABILITY_UNKNOWN`, `STACK_NAMESPACE_PREFIX_INVALID`,
`STACK_SINGLE_APP_VIOLATION`,
`STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED`,
`STACK_TRIGGER_CAPABILITY_REQUIRED`. Seven raise sites at `:3068-3129`,
one per class.
- The `StackRefusalError` docblock states the rule in words: "⭐ One code
per site, never a shared `STACK_VALIDATION_FAILED` catch-all".
- The triage reading `5556795849` on #15963, which set that granularity,
in its own words: 「**boot-refusal 这一类在账本里已经是 14 行、粒度是「一处拒绝一行」,不是共用一个总
code。**」 and 「所以剩下六处应当**各自一个 code**」.
Nothing I read contradicts "one code per refusal site", so the fence
holds and I mirrored it: six sites, six codes. The at-tier contract
review of #16342 (comment `5560140069` on #15963, §6) also opines on
this split and AGREES with it — "The card should take the six as one
family (same callers, same 422 class); filing only the action-key
collision would recreate the 1-of-N inconsistency this PR fixes. The
invariant stays out (or 500-class)" — so no reading outranks the
dispatch's claim.
The `STACK_COMPOSE_*` spelling (rather than continuing the per-stack
family's `STACK_SUBJECT_CONDITION` shape) is the one judgement inside
that fence: what these six refuse is a disagreement BETWEEN stacks, a
condition `defineStack` cannot raise — each input is legal alone. A bare
`STACK_OBJECT_CONFLICT` would read as "this stack's object is malformed"
and point a consumer at the wrong half of the artifact.
`STACK_CROSS_REFERENCE_INVALID` stays the deliberate exception in the
other direction, because its per-stack and artifact passes are one rule
family over two scopes.
| refusal | raised by | code |
| :--- | :--- | :--- |
| a single-valued top-level key declared with different values by two
stacks | `composeSingleValue` | `STACK_COMPOSE_KEY_CONFLICT` |
| `functions` authored in the map form by one stack, the array form by
another | `composeFunctions` | `STACK_COMPOSE_FUNCTIONS_SHAPE_CONFLICT`
|
| two stacks defining one handler name | `composeFunctions` |
`STACK_COMPOSE_FUNCTION_CONFLICT` |
| an object-level collection other than `fields` declared differently
under `objectConflict: 'merge'` | `refuseUnmergeableCollections` |
`STACK_COMPOSE_COLLECTION_CONFLICT` |
| the same object name in two stacks under the default `objectConflict:
'error'` | `mergeObjects` | `STACK_COMPOSE_OBJECT_CONFLICT` |
| a cross-stack action key collision |
`collectComposedActionKeyCollisions` |
`STACK_COMPOSE_ACTION_KEY_COLLISION` |
## Class B is out, by the dispatch's fence and by my own reading
`composeStacks internal error: no source stack recorded for composed
object '...'` stays a bare `Error`. It is the code discovering its own
bookkeeping is inconsistent — `mergeObjects` records every composed
object the moment it is first seen, so a miss is an edit to that
function, not an authored input. A 422 would tell an author their stack
is unprocessable when the defect is ours. Its own exit (bare, or a
500-class envelope) is a separate decision and is not taken here.
That fence is now a test rather than a promise:
`compose-stacks-refusal-envelopes.test.ts` asserts that **exactly one**
bare `throw new Error(` is left in `stack.zod.ts` and that it is the
internal-invariant message, so a seventh authored-entity refusal
arriving bare fails.
## The ledger rows, and a premise that had drifted
The card (and the dispatch) say class A needs a `door` classification
row and that `check:dispatcher-error-vocabulary` demands a row per code
in both directions. **That mechanism has moved since the card was filed,
and the dispatch's own instruction is what re-measuring corrected**:
under the #16404 ruling, #16449 and then #16649, a `door: 'none'` code
has no resting place short of a row in `ERROR_CODE_LEDGER`, and the
`boot-refusal` verdicts the dispatcher vocabulary used to carry for
these codes were RATCHETED OUT with that batch — the seven `defineStack`
codes carry zero rows in
`packages/runtime/src/dispatcher-error-vocabulary.ts` today
(`boot-refusal` there is down from 20 rows to 4). So the reachability
reading lives in the ledger comment, one line per row, which is where I
put it. `check:dispatcher-error-vocabulary` is green: a registered code
vanishes from its scan by construction, which is the same ratchet.
The door verdict itself, re-measured on this branch with its own
positive control:
```
PROBE composeStacks, non-test, packages/runtime/src + packages/rest/src
-> 7 occurrences, ALL in packages/runtime/src/artifact-collections.ts
:11 :93 :117 :143 doc comments, :314 a line comment,
:327 :330 inside a message template ==> ZERO call sites
CONTROL defineStack, same two trees, same filter
-> 31 occurrences across 8 files (app-plugin.ts 16, job-handler-context.ts 4,
standalone-stack.ts 3, domains/packages.ts 2, default-host.ts 2,
artifact-collections.ts 2, dispatcher-error-vocabulary.ts 1,
sandbox/body-runner.ts 1)
```
The control is lit, so the zero is a reading rather than a dark grep.
(The card's own figures — 6 and 32 — have drifted with the tree; the
shape of the answer has not.) The only non-test caller of
`composeStacks` anywhere is
`examples/app-multi-package/objectstack.config.ts`, which is authoring
time by construction. `door: 'none'` / boot refusal holds.
composeStacks conflict refusal carries an ADR-0112 envelope (#19685)1 parent 7e1b048 commit 28f9277
6 files changed
Lines changed: 568 additions & 20 deletions
File tree
- .changeset
- content/docs/references/api
- packages/spec/src
- api
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| |||
335 | 335 | | |
336 | 336 | | |
337 | 337 | | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
338 | 344 | | |
339 | 345 | | |
340 | 346 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
503 | 503 | | |
504 | 504 | | |
505 | 505 | | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
506 | 512 | | |
507 | 513 | | |
508 | 514 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1279 | 1279 | | |
1280 | 1280 | | |
1281 | 1281 | | |
1282 | | - | |
| 1282 | + | |
| 1283 | + | |
1283 | 1284 | | |
1284 | 1285 | | |
1285 | 1286 | | |
| |||
1308 | 1309 | | |
1309 | 1310 | | |
1310 | 1311 | | |
| 1312 | + | |
| 1313 | + | |
| 1314 | + | |
| 1315 | + | |
| 1316 | + | |
| 1317 | + | |
| 1318 | + | |
| 1319 | + | |
| 1320 | + | |
| 1321 | + | |
| 1322 | + | |
| 1323 | + | |
| 1324 | + | |
| 1325 | + | |
| 1326 | + | |
| 1327 | + | |
| 1328 | + | |
| 1329 | + | |
| 1330 | + | |
| 1331 | + | |
| 1332 | + | |
| 1333 | + | |
| 1334 | + | |
1311 | 1335 | | |
1312 | 1336 | | |
1313 | 1337 | | |
| |||
0 commit comments