Skip to content

docs(adr): spell the ADR-0005/0010 payload code tokens as the ledger members the runtime emits - #19765

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-19706-adr-payload-code-screaming-snake
Sep 23, 2026
Merged

hotlong merged 1 commit into
mainfrom
claude/issue-19706-adr-payload-code-screaming-snake

Conversation

@huangyiirene

@huangyiirene huangyiirene commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #19706

维护者速读(草稿)

改了什么

两份 ADR 里的错误响应示例(ADR-0005 的 422 示例、ADR-0010 §5.3 的四层保护示例),把 code 字段从小写改成运行时真正返回、且在已发布错误码台账里登记的大写码。共 5 处,改 4 处:

  • ADR-0005:invalid_metadata → INVALID_METADATA
  • ADR-0010 L1:not_overridable → NOT_OVERRIDABLE
  • ADR-0010 L2:package_locked → ITEM_LOCKED —— ⚠️ 这一处不是简单改大写:运行时没有 PACKAGE_LOCKED 这个码,"包级锁"的拒绝一律返回 ITEM_LOCKED(并带 lockSource: 'package')
  • ADR-0010 L3:item_locked → ITEM_LOCKED
  • ADR-0010 L4:frozen_path 保持不动 —— 路径冻结(L4)至今没有实现,运行时不会返回任何码,不能凭空造一个

只动 code 这一个值。error 文本开头的 [tag]、状态码、其它字段一个字节都没改([tag] 那一半由另一张卡处理)。

为什么改

错误码是客户端和 AI 写的代码用来判断"为什么失败"的依据,ADR 里的示例会被照抄。照抄小写码写出来的判断永远不会命中,因为运行时按 ADR-0112 返回的是大写码。没有任何门禁读 ADR 正文,所以这个错误不会报错,只会悄悄失效。

风险与代价(含回滚)

纯文档改动,不发布任何包,不影响运行时行为。需要您拍板的判断只有一处:L2 改成 ITEM_LOCKED。依据有三条:ADR-0010 §3.2 自己写明包级锁会被规范化成单项锁(L3)的形状;运行时两个拒绝点都返回 ITEM_LOCKED;并且有测试钉住 lockSource: 'package' 的那个信封。如果您希望将来 L2 有独立错误码,那是给 spec 新增错误码的决定,本 PR 不预设。回滚:revert 这一个 commit。

席位意见

建议批准并合并;L2 认可 ITEM_LOCKED。 四处改动逐一在运行时生产者处核实(protocol.ts 的 spec 校验拒绝、NOT_OVERRIDABLE 的两个拒绝点、lockWriteRefusal,以及包级锁的 readOnlyBaseOverrideError —— 它返回的正是 ITEM_LOCKED + lockSource: 'package',有测试钉住),账本成员逐一在 error-code-ledger.zod.ts 里读到;席位自己重跑成员核对:改前 0 / 5、改后 4 / 5。L2 不是简单改大写而是映射:树里没有 PACKAGE_LOCKED,ADR-0010 §3.2 自己写明包级锁会规范化成单项锁的形状 —— 席内定 A(维持 ITEM_LOCKED);若你希望包级锁有独立错误码,那是 spec 线的线协议决定,目前没有任何消费者需要它。L4 的 frozen_path 留空是量出来的:没有账本成员、也没有任何生产者(_frozenPaths 在 packages/** 下零读者,Phase 2 评估器不存在),按「不编造」条款不动 —— 席内定 A(等 L4 实现时登记码并回改此行);如你想让 ADR 今天就诚实,B(加一句「L4 未实现」)便宜,属 spec 线。另有一条界外发现,席位身份过不了立卡门,请你定夺是否立卡:packages/cli/src/commands/meta/delete.ts :115 的 --if-match 帮助文本写 409 metadata_conflict,而 CLI 实际返回 METADATA_CONFLICT(非受管代码,不能搭本 PR)。复核记录:PR 评论 5787341220(PASS,在席按服务档渲染);ACCEPT 在卡 #19706。CI 读数:31 latest-per-name check runs — 22 success, 9 skipped, 0 in progress, 0 other。

你要做的

  1. 看 L2 那一处(package_locked → ITEM_LOCKED)是否认可。
  2. 认可就批准并合并:docs/adr/** 属于 Tier H,需要您亲手合并,PR 保持 draft。
  3. L4 的 frozen_path 留给 spec 线决定:要么等 L4 实现时在台账登记错误码,要么修订 ADR-0010 标注 L4 尚未实现。不在本 PR 范围内。

Clause-②: no

What this changes

The refusal-envelope examples in docs/adr/0005-metadata-customization-overlay.md (sample 422) and docs/adr/0010-metadata-protection-model.md §5.3 printed code in lowercase. None of those tokens is a member of the published ErrorCode union (packages/spec/src/api/error-code-ledger.zod.ts, ADR-0112), so a matcher copied from them never fires. Each code value now carries the ledger member that the runtime actually emits for that refusal, measured against both the ledger and the producer. Only the token changes: the error string's bracketed opener (tracked separately on #19705), the status numbers and every other byte in both ADRs are untouched. The persisted sys_metadata_audit.code column keeps its lowercase spelling by ADR-0112 D6b. These payloads are wire envelopes, not audit rows, and ADR-0112 (:164) says the catalog governs "the code a failing request answers with".

Per-site evidence (tree dc1b98680, commit 58bf48af1)

Site Before After Ledger member Producer (the token the runtime emits)
0005:349 invalid_metadata INVALID_METADATA error-code-ledger.zod.ts:615 metadata-protocol/src/protocol.ts:16099-16100: saveMetaItem's "failed spec validation" refusal, INVALID_METADATA / 422 (the same sentence the sample prints)
0010:480 L1 not_overridable NOT_OVERRIDABLE :623 protocol.ts:15758-15759 (saveMetaItem, 403); sys-metadata-repository.ts:1639,1652-1653 (assertAllowed, 403)
0010:483 L2 package_locked ITEM_LOCKED :617 see below: no PACKAGE_LOCKED exists; package-layer locks are refused as ITEM_LOCKED / 403
0010:486 L3 item_locked ITEM_LOCKED :617 protocol.ts:14144-14145 (lockWriteRefusal), :14212-14213 (assertLockAllowsDelete), 403
0010:489 L4 frozen_path unchanged none none: see "Unresolved site"

L2: mapped, not uppercased

The ledger has no PACKAGE_LOCKED, and no spelling of package_locked exists anywhere outside ADR-0010 itself (whole-tree git grep -i, exit 1). The runtime answers a package-layer lock with ITEM_LOCKED, in two places:

  • metadata-protocol/src/sys-metadata-repository.ts:1766-1768 (readOnlyBaseOverrideError): code = 'ITEM_LOCKED', status = 403, lockSource = 'package'. Its TSDoc calls 'package' "ADR-0010's own reserved value for a lock the PACKAGE layer asserts". It is pinned by sys-metadata-repository.package-writability.test.ts:767 as { code: 'ITEM_LOCKED', status: 403, lockSource: 'package', packageId }.
  • A package-declared protection.lock is translated by applyProtection (spec/src/shared/protection.zod.ts:276-277, _lockSource = 'package') into the item's _lock, and refused by lockWriteRefusal as ITEM_LOCKED / 403. The setup app the example names carries protection: { lock: 'full' } (platform-objects/src/apps/setup.app.ts:39-40).

ADR-0010 §3.2 already specifies this normalisation: the loader writes the resolved lock "into the artifact's _lock and _lockSource: 'package' fields, so downstream protocol code sees a single normalised shape (L3)". So the L2 token is established from the ledger, the producer and the ADR's own semantics section. It is flagged here because it is the one site where the new token is not the uppercase form of the old one.

Unresolved site: L4 frozen_path (left unchanged)

The runtime emits no code for a frozen-path refusal because no frozen-path refusal exists. There are zero readers of _frozenPaths / defaultFrozenPaths under packages/**: the only non-ADR hit is a forward reference in the header comment of spec/src/kernel/metadata-protection.zod.ts:10. The Phase 2 evaluator packages/spec/src/shared/json-path.ts does not exist, and ADR-0010 Phase 2 carries no "Implemented" mark. Replacing the token would invent a code, so the line stays as it was. The answer belongs to the spec lane: either register a code when L4 is implemented, or amend ADR-0010 to say L4 is unimplemented.

Verification

Ledger membership (checklist item api-backend.error-envelope-ledger), one-shot scratchpad script, not added to the tree: it imports ErrorCode from the zod source, reads ErrorCode.options (338 members), and checks every "code": value in the two ADRs. Control leg: NOT_OVERRIDABLE in the union, not_overridable not.

  • Before (dc1b98680): VERDICT sites=5 members=0 non-members=5, exit 1
  • After (working tree = 58bf48af1): VERDICT sites=5 members=4 non-members=1, exit 1. The remaining non-member is 0010:489 "frozen_path", left unchanged on purpose.

Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 58bf48af1 derived 19 commands. All 19 ran, exit codes captured before any pipe, and --ran reconciliation printed "19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED". Among them:

  • node scripts/check-adr-links.mjs: exit 0, "690 relative link destination(s) under docs/adr/ resolve"
  • node scripts/check-adr-symbol-anchors.mjs: exit 0, "2121 anchors across 140 records resolve"
  • pnpm check:adr-anchors: exit 0, "check-adr-anchors: OK (53 anchored file(s) ...)"
  • pnpm check:doc-authoring: exit 0, "no growth, no burn-down unrecorded"
  • pnpm check:nul-bytes: exit 0, "scanned 9242 text file(s) ... no raw ASCII control bytes"
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions: first run exit 3 (prerequisite: @objectstack/lint unbuilt, nothing measured); built @objectstack/formula + @objectstack/lint under the verify lock (VERDICT command-exit 0); re-run exit 0

Additionally pnpm check:error-code-casing exited 0. It is a neighbour, not a measurement of this diff: it scans only packages/**/*.ts(x) (SCAN_ROOTS = ['packages']) and never reads ADR prose.

No changeset: docs/adr/** publishes nothing. None of the 70 published package.json files[] entries resolves to docs/adr, the root package is private, and no package.json or tsup config references docs/adr.

Acceptance notes

  • ADR-0010 also spells these codes in lowercase in prose, not in the payload "code" lines: :97 and :103 (the layer diagram), :213, :501, :601-602. This PR's contract is the five payload sites only; the prose is untouched.
  • The L2 payload's other fields still describe the unimplemented Phase 3 surface (metadataDefaults.lock=full, a "package" key where the runtime's package-layer refusal carries packageId + lockSource). Only the token was in scope.
  • The CLI --if-match help text says 409 metadata_conflict (packages/cli/src/commands/meta/delete.ts:115, pinned by delete-reset-carriers.test.ts:367 via /metadata_conflict/) while the CLI surfaces METADATA_CONFLICT. That is non-governed code and does not ride this governed PR.

Generated by Claude Code


Generated by Claude Code

…dger members the runtime emits

The wire-payload examples in ADR-0005 (sample 422) and ADR-0010 §5.3
printed `code` in lowercase, which is not a member of the published
`ErrorCode` union (packages/spec/src/api/error-code-ledger.zod.ts). A
matcher copied from them never fires.

- 0005: invalid_metadata -> INVALID_METADATA (protocol.ts saveMetaItem spec-validation refusal)
- 0010 L1: not_overridable -> NOT_OVERRIDABLE (protocol.ts / SysMetadataRepository.assertAllowed)
- 0010 L2: package_locked -> ITEM_LOCKED (no PACKAGE_LOCKED exists; package-layer locks are
  refused as ITEM_LOCKED, per ADR-0010 §3.2's L2-into-L3 normalisation and the runtime's
  lockWriteRefusal / readOnlyBaseOverrideError with lockSource 'package')
- 0010 L3: item_locked -> ITEM_LOCKED (protocol.ts lockWriteRefusal)
- 0010 L4: frozen_path left unchanged: L4 is unimplemented and no producer emits any code for it

Only the `code` token changes; the `error` openers and every other byte stay as they were.

Claude-Session: https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 58bf48af1d4c0f75075ca80484e46efb65828dd5

① Derived judgments

Rendered in-seat by the skills seat 1 session at the served tier the maintainer named on the previous date (the fable tier word at get_session → external_metadata.last_served_model; ruling record 5771798588). The contract is the triage grading 5782685945 on #19706 (domain:skills, p2, Bug, area:api — in scope the lowercase code half only; the bracketed [tag] opener rides #19705): every code the two ADRs print in a refusal payload is a member of the published ledger packages/spec/src/api/error-code-ledger.zod.ts and the token the runtime emits for that refusal; the checklist item api-backend.error-envelope-ledger (P1) is the acceptance; ⛔ a code the runtime never emits is not invented.

  • (a) The diff — measured by this seat on a review worktree at the head. One single-parent commit on dc1b98680; two files, +4 / −4, four token edits and nothing else: docs/adr/0005 :349 invalid_metadata → INVALID_METADATA; docs/adr/0010 :480 not_overridable → NOT_OVERRIDABLE, :483 package_locked → ITEM_LOCKED, :486 item_locked → ITEM_LOCKED; :489 frozen_path unchanged. The [tag] openers, the status numbers and every other byte of both records untouched.
  • (b) Each token is the runtime's, read by this seat in the tree. INVALID_METADATA: packages/metadata-protocol/src/protocol.ts :16096–:16100 (saveMetaItem's spec-validation refusal — the very sentence the ADR-0005 sample prints — code = 'INVALID_METADATA', status = 422); NOT_OVERRIDABLE: protocol.ts :15758–:15759 and sys-metadata-repository.ts :1639 / :1652–:1653 (status = 403); ITEM_LOCKED for L3: protocol.ts :14144–:14145 (lockWriteRefusal) and :14212–:14213; ITEM_LOCKED for L2: sys-metadata-repository.ts :1766–:1768 (readOnlyBaseOverrideError — code = 'ITEM_LOCKED', status = 403, lockSource = 'package'), pinned by sys-metadata-repository.package-writability.test.ts :767 — no PACKAGE_LOCKED exists anywhere in the tree, and ADR-0010's own §3.2 (:161) normalises a package-layer lock into the single _lock + _lockSource: 'package' shape the protocol code sees; so the L2 edit is a MAPPING to the member that exists, ⛔ not an uppercasing, and the PR body puts that one judgment to the maintainer. Ledger rows: :615 / :617 / :623.
  • (c) The fifth site is a measured negative, not an omission. L4 (frozen_path, :489): no ledger member, and no producer — _frozenPaths / defaultFrozenPaths have zero readers under packages/** (the only hit is the header comment of spec/src/kernel/metadata-protection.zod.ts), the Phase 2 evaluator does not exist, and ADR-0010 carries no Implemented mark for it; the control leg finds the L1 / L3 emitters. Left unchanged under the dispatch's ⛔ no-invention clause; the answer is the spec lane's (register a code when L4 is implemented, or amend ADR-0010 to mark L4 unimplemented) — put to the maintainer as a note, ⛔ not this PR's.
  • (d) Acceptance — the ledger membership, re-read by this seat. Every "code": value in the two ADRs against the ledger's quoted rows: before 0 / 5 members; after 4 / 5 — the residue is L4 by design. The dev's tsx probe over ErrorCode.options (338 members) reads the same rows.
  • (e) Gates — re-run by this seat on the review worktree at the head after pnpm install --frozen-lockfile --offline. check-adr-links 0 (690 destinations resolve); check-adr-symbol-anchors 0 (2121 anchors); check:adr-anchors 0; check:doc-authoring 0; check:nul-bytes 0; check-governed-merges.mjs --pr 19765 exit 3 — GOVERNED, Tier H (docs/adr/** ×2, 8 changed lines). The dev's derivation: 19 families by dispatch-gates.mjs --commands, --ran 19 / 19 / 0 NOT-MEASURED (one first-pass exit 3 prerequisite on check:doc-formula-expressions, cured by the declared build through the verify lock, re-run 0), plus check:error-code-casing run as a neighbour (it scans packages/** .ts only — it cannot read ADR prose, which is why nothing caught this). CI at this head at the seat's read: 30 latest-per-name check runs — 19 success, 9 skipped, 2 in progress; the 四件套 waits for green.

② Semver level

None — no release. docs/adr/** is in no package's files[] (the dev measured 70 manifests, 0 resolving to docs/adr); skip-changeset written through label-write.mjs with read-back.

③ Boundary flags

  • Tier H (docs/adr/** on the register): draft stays draft; the 四件套 by this seat on green — ACCEPT on the card, needs-user-decision, 席位意见, review requested from GOVERNED_APPROVERS; the maintainer merges after confirming the L2 mapping.
  • The dev's two open questions, graded in-lane: L2 → A (ITEM_LOCKED documents the contract that exists; a distinct PACKAGE_LOCKED would be a wire change for a condition the runtime already answers, against §3.2 — a spec-lane decision with no measured consumer); L4 → A (leave the line until L4 is implemented and its code is registered; B — amend ADR-0010 to mark L4 unimplemented — is cheap if the maintainer wants the record honest today; C is not viable, the ledger refuses a code no package stamps). Both stated in the 席位意见 for the maintainer's word.
  • Report 5787309652 on the card parses (os-dev-report first line — the id is on the thread); mcp_calls 0; three REST writes plus two pushes; no body PATCH by the dev.
  • Out of scope, noted by the dev, ⛔ not touched: packages/cli/src/commands/meta/delete.ts :115 (--if-match help text says 409 metadata_conflict, the CLI surfaces METADATA_CONFLICT; delete-reset-carriers.test.ts :367 pins the help text) — non-governed, class a, a card the seat cannot file (the filing door) → the maintainer's items; ADR-0010 prose outside the payload lines still spells codes lowercase (:97, :103, :213, :501, :601–:602) — rides the prose round on [finding] Two QA platform-checklist area files quote 404 [no_draft] as the expected refusal text — a spelling #16245 retires #19705; the L2 payload's other fields describe the unimplemented Phase 3 surface — token-only scope, noted.

Implemented-by: claude/issue-19706-adr-payload-code-screaming-snake
Reviewed-by: session_01Wnstp2kTth7sGXfr8fXypc

VERDICT: PASS


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review September 23, 2026 02:41
@hotlong
hotlong added this pull request to the merge queue Sep 23, 2026
Merged via the queue into main with commit 67add13 Sep 23, 2026
48 checks passed
@hotlong
hotlong deleted the claude/issue-19706-adr-payload-code-screaming-snake branch September 23, 2026 03:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs-user-decision size/xs skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants