Skip to content

Commit d953fab

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-19129-half-states-h9-node-command-head
2 parents 9391aff + 1d41aa8 commit d953fab

7 files changed

Lines changed: 569 additions & 21 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): every `composeStacks` conflict refusal carries an ADR-0112 envelope — six new `STACK_COMPOSE_*` codes beside the `defineStack` family
6+
7+
`composeStacks` refuses six authored-entity conflicts, and until now every one of them threw
8+
`new Error(message)` with `code` and `status` both `undefined`. The `defineStack` family in the
9+
same file has carried the envelope since #15963, so `packages/spec/src/stack.zod.ts` held two
10+
refusal families that are the same thing to an author — a stack refused at authoring time,
11+
through the same callers — and two different things to a consumer branching on `error.code`.
12+
13+
Every message in the family carries the literal `composeStacks conflict:` prefix, which is how it
14+
is located: FIVE of the six raise inside helper functions 300-800 lines above `composeStacks`'
15+
own body, so reading the function the defect is named after finds one of them.
16+
17+
| refusal | raised by | code |
18+
| :--- | :--- | :--- |
19+
| a single-valued top-level key declared with different values by two stacks | `composeSingleValue` | `STACK_COMPOSE_KEY_CONFLICT` |
20+
| `functions` authored in the map form by one stack, the array form by another | `composeFunctions` | `STACK_COMPOSE_FUNCTIONS_SHAPE_CONFLICT` |
21+
| two stacks defining one handler name | `composeFunctions` | `STACK_COMPOSE_FUNCTION_CONFLICT` |
22+
| under `objectConflict: 'merge'`, an object-level collection other than `fields` declared differently | `refuseUnmergeableCollections` | `STACK_COMPOSE_COLLECTION_CONFLICT` |
23+
| the same object name in two stacks under the default `objectConflict: 'error'` | `mergeObjects` | `STACK_COMPOSE_OBJECT_CONFLICT` |
24+
| a cross-stack action key collision | `collectComposedActionKeyCollisions` | `STACK_COMPOSE_ACTION_KEY_COLLISION` |
25+
26+
Each carries `status: 422` — an unprocessable authored entity, not a server fault — and the
27+
findings the site collected in `issues`, one entry per finding. **Message text is byte-for-byte
28+
unchanged at every site**: this adds the machine-readable half, it rewords no sentence, and the
29+
message pins across the repo read the prose they always did.
30+
31+
One code per refusal site rather than a shared `STACK_COMPOSE_CONFLICT` catch-all — the
32+
granularity the `defineStack` family landed with, and the granularity the ADR-0112 ledger's
33+
boot-refusal class already had before it. The `STACK_COMPOSE_*` spelling says what the
34+
per-stack family's spellings cannot: the defect is a disagreement BETWEEN stacks, each of which
35+
is legal on its own, so the fix is in the composition rather than in one malformed stack.
36+
`STACK_CROSS_REFERENCE_INVALID` stays the deliberate exception in the other direction — its
37+
per-stack and artifact passes share one code because they are one rule family over two scopes.
38+
39+
All six are registered in `ERROR_CODE_LEDGER` under `@objectstack/spec`, under the ruling that
40+
every code shipped in `dist` is the published face, door or no door. No wire door raises them:
41+
`composeStacks` runs at authoring and boot time, and the reading was re-measured here — zero
42+
`composeStacks` call sites under `packages/runtime/src` + `packages/rest/src` (7 non-test
43+
occurrences, all doc comments or message prose in one file), with `defineStack` lighting the
44+
same probe 31 times across 8 files as the positive control.
45+
46+
Not narrowed: `composeStacks` accepts and refuses exactly the inputs it did before, and no export
47+
changes — the error classes stay module-local, as every member of the `defineStack` family is,
48+
because `packages/spec/src/index.ts` re-exports the module with `export *` and the ADR-0112
49+
contract is the `code` / `status` pair read structurally.
50+
51+
⛔ The seventh bare `Error` in that file is deliberately untouched:
52+
`composeStacks internal error: no source stack recorded for composed object …` is the code
53+
discovering its own bookkeeping is inconsistent, not an authored entity being refused. Filing it
54+
at 422 would tell an author their stack is invalid when the defect is ours. Whether it takes a
55+
500-class envelope of its own is a separate decision.
56+
57+
Clause-②: yes

‎.claude/skills/pm-dispatch/references/landing-operations.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
- 更硬的旁证是查上一单的实现体完好:`git grep` 带引号精确名对 `origin/main` 与实现文件。
1313
- 锚点断言的正确措辞:断言 `pnpm --filter @objectstack/spec check:authorable-surface` 绿即可。
1414
- `baseRev` 允许滞后,是一行提示不是错误;⛔ 禁为凑相等手改锚点。
15-
- ⛔ 不得要求 `baseRev == merge-base`。
1615

1716
## B. 跟到 MERGED 为止;入队后的看护归车道 PM 落地窗口
1817

@@ -28,6 +27,7 @@
2827
- Tier H(其余受管面)者:四件套留 draft 等人批,⛔ 不翻正式不入队;获授权批准后认领席落地。
2928
- ⛔ 两层不由席位批准;清标即落地同受此闸,漏判会被队列守卫在 merge group 里拒收。
3029
- 再读 `mergeable_state`:`dirty` ⇒ 先 merge `origin/main` 再挂;生成物在面上按 A 的固定序。
30+
- 挂 auto-merge 的同一动作里重测生成物漂移,⛔ 不复用更早的零;非零 ⇒ 按 A 的固定序。
3131
- ready + 全绿 ≠ 已入队:队列从不主动拉 PR,入队是显式动作。
3232
- 零 `enqueued` 事件按序查三条:① `mergeable_state` 是否 `dirty`。
3333
- ② enable-auto-merge 调用根本没落地,重发与效果验证序列见 `platform-readings.md`。

‎content/docs/references/api/contract.mdx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const result = ApiErrorSchema.parse(data);
2727

2828
| Property | Type | Required | Description |
2929
| :--- | :--- | :--- | :--- |
30-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +325 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
30+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +331 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
3131
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
3232
| **message** | `string` | ✅ | Readable error message |
3333
| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
@@ -335,6 +335,12 @@ const result = ApiErrorSchema.parse(data);
335335
* `SSO_REGISTER_FAILED`
336336
* `SSO_REGISTER_FORBIDDEN`
337337
* `STACK_CAPABILITY_UNKNOWN`
338+
* `STACK_COMPOSE_ACTION_KEY_COLLISION`
339+
* `STACK_COMPOSE_COLLECTION_CONFLICT`
340+
* `STACK_COMPOSE_FUNCTIONS_SHAPE_CONFLICT`
341+
* `STACK_COMPOSE_FUNCTION_CONFLICT`
342+
* `STACK_COMPOSE_KEY_CONFLICT`
343+
* `STACK_COMPOSE_OBJECT_CONFLICT`
338344
* `STACK_CROSS_REFERENCE_INVALID`
339345
* `STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED`
340346
* `STACK_NAMESPACE_PREFIX_INVALID`

‎content/docs/references/api/error-code-ledger.mdx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,12 @@ const result = ErrorCode.parse(data);
503503
* `SSO_REGISTER_FAILED`
504504
* `SSO_REGISTER_FORBIDDEN`
505505
* `STACK_CAPABILITY_UNKNOWN`
506+
* `STACK_COMPOSE_ACTION_KEY_COLLISION`
507+
* `STACK_COMPOSE_COLLECTION_CONFLICT`
508+
* `STACK_COMPOSE_FUNCTIONS_SHAPE_CONFLICT`
509+
* `STACK_COMPOSE_FUNCTION_CONFLICT`
510+
* `STACK_COMPOSE_KEY_CONFLICT`
511+
* `STACK_COMPOSE_OBJECT_CONFLICT`
506512
* `STACK_CROSS_REFERENCE_INVALID`
507513
* `STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED`
508514
* `STACK_NAMESPACE_PREFIX_INVALID`

‎packages/spec/src/api/error-code-ledger.zod.ts‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1279,7 +1279,8 @@ export const ERROR_CODE_LEDGER = {
12791279
'EXTERNAL_SCHEMA_MISMATCH',
12801280
'EXTERNAL_SCHEMA_MODE_VIOLATION',
12811281
'EXTERNAL_WRITE_FORBIDDEN',
1282-
// [#16449] The eight rows below are `door: 'none'` codes — raised at
1282+
// [#16449] The eight rows below — through `STACK_TRIGGER_CAPABILITY_REQUIRED`
1283+
// — are `door: 'none'` codes — raised at
12831284
// authoring / boot, before any HTTP boundary exists — registered under the
12841285
// #16404 ruling (door or no door; see the header). Each ships in this
12851286
// package's `dist`, so its spelling is the face a consumer's
@@ -1308,6 +1309,29 @@ export const ERROR_CODE_LEDGER = {
13081309
'STACK_SCHEMA_INVALID', // `ObjectStackDefinitionSchema.safeParse` failed; `issues` carries the zod issues structurally
13091310
'STACK_SINGLE_APP_VIOLATION', // an `app` package declares more than one app (ADR-0019 D3)
13101311
'STACK_TRIGGER_CAPABILITY_REQUIRED', // an auto-launched flow while `requires` omits `triggers`
1312+
// [#16348] The COMPOSITION half of the same family, and `door: 'none'` on
1313+
// the same reading: the six `composeStacks` refusals, one code per raise
1314+
// site, every one `status: 422` (`StackRefusalError`, `stack.zod.ts`), the
1315+
// findings the site collected on `issues`. Every message carries the
1316+
// literal `composeStacks conflict:` prefix, which is how the family is
1317+
// located — five of the six raise inside helper functions, not in
1318+
// `composeStacks`' own body. Raised where `defineStack` is: `os validate` /
1319+
// `os build` and a hand-written `objectstack.config.ts`, before any HTTP
1320+
// boundary exists (measured: zero `composeStacks` CALL sites under
1321+
// `packages/runtime/src` + `packages/rest/src` — 7 non-test occurrences,
1322+
// all doc comments or message prose in one file; positive control
1323+
// `defineStack`, same two trees, 31 occurrences across 8 files).
1324+
// ⛔ NOT registered, and the absence is load-bearing: the seventh bare
1325+
// `Error` in that file, `composeStacks internal error: no source stack
1326+
// recorded for composed object …`, is an internal-bookkeeping invariant
1327+
// rather than an authored-entity refusal — a 422 would blame the author
1328+
// for our defect. Its disposition is its own decision.
1329+
'STACK_COMPOSE_ACTION_KEY_COLLISION', // two stacks declare the same action key — the collision `defineStack` refuses within one stack, one composition step later
1330+
'STACK_COMPOSE_COLLECTION_CONFLICT', // under `objectConflict: 'merge'`, an object-level collection other than `fields` is declared with different values by two stacks
1331+
'STACK_COMPOSE_FUNCTION_CONFLICT', // two stacks define a handler under the same name; handlers resolve by name at boot
1332+
'STACK_COMPOSE_FUNCTIONS_SHAPE_CONFLICT', // `functions` authored in the map form by one stack and the array form by another
1333+
'STACK_COMPOSE_KEY_CONFLICT', // a single-valued top-level key is declared with different values by two stacks
1334+
'STACK_COMPOSE_OBJECT_CONFLICT', // the same object name is defined by more than one stack under the default `objectConflict: 'error'`
13111335
],
13121336
} as const satisfies Record<string, readonly string[]>;
13131337

0 commit comments

Comments
 (0)