Skip to content

feat(spec): register every error code that ships in dist — nine door: 'none' codes enter ERROR_CODE_LEDGER under the #16404 ruling (#16449) - #16652

Merged
os-trump merged 5 commits into
mainfrom
claude/issue-16449-register-shipped-error-codes
Sep 8, 2026
Merged

feat(spec): register every error code that ships in dist — nine door: 'none' codes enter ERROR_CODE_LEDGER under the #16404 ruling (#16449)#16652
os-trump merged 5 commits into
mainfrom
claude/issue-16449-register-shipped-error-codes

Conversation

@os-trump

@os-trump os-trump commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16449

Clause-②: yes — on the #16404 ruling's own authority (「registering a code widens the published face and is therefore Clause-②: yes, door or no door」); this PR carries needs:contract-review.

What this does

Under the #16404 ruling (director seat, decision batch #62, 2026-09-07, option D; maintainer 「同意」) the published contract face for error codes is ERROR_CODE_LEDGER / StandardErrorCode, and every code that ships in a package's dist is registered there, door or no door. Nine codes were shipping unregistered on this base and now have ledger rows, each under the package that stamps it:

Their nine boot-refusal classification rows in packages/runtime/src/dispatcher-error-vocabulary.ts ratchet out (a registered code is skipped by the scan, so the rows would be stale-row findings); the reachability reading each row recorded now lives on its ledger row. The ledger header records the ruling as the "Door or no door" rule in its own words, and the boot-refusal verdict doc now says a row carrying it is a registration owed, not an exemption.

The door: 'none' shape — one shape, no second list (ZONE 3)

The ledger already distinguishes rows only by the string and its comment; two door-less codes were already registered that way (SETTINGS_ENGINE_NOT_BOUND, METADATA_SCHEMA_INVALID). So no new key and no second list: a door: 'none' code is a string under the stamping package plus a comment stating its status and the reachability reading. What the ledger lacked was the doctrine — its own "Retiring a code" paragraph cited a boot refusal (MONGODB_MULTI_TENANT_UNSUPPORTED, #8035) as grounds for NOT registering, which the ruling supersedes; that paragraph now says so, and the retirement ground is narrowed to the producerless class only.

Measured unregistered population (A2.1) — on origin/main at 8341ed21c

Instrument: hits of 'CODE' inside the StandardErrorCode = z.enum([...]) block plus hits inside the ERROR_CODE_LEDGER = {...} block (the card's own instrument, anchored on the two declaration blocks rather than whole files; the card's whole-file controls read 2, this instrument's read 1 — either way a non-zero control beside the zeros).

code std block ledger block verdict on this base
VALIDATION_ERROR (control) 1 0 registered
PERMISSION_DENIED (control) 1 0 registered
NAMESPACE_CONFLICT 0 1 ALREADY registered — @objectstack/objectql row, #14748, with a door-driving pin (packages/runtime/src/package-door-namespace-conflict-code.test.ts)
OBJECT_OWNERSHIP_CONFLICT 0 0 unregistered — registered here
STACK_CROSS_REFERENCE_INVALID 0 0 unregistered — registered here
STACK_SCHEMA_INVALID + 5 STACK_* siblings 0 0 unregistered — registered here
PLUGIN_UI_REQUIRED_KEY_MISSING (#16334, landed after the card's measurement) 0 0 unregistered — registered here

So the card's "nine" was eight unregistered plus one already registered; PR #16342 (#15963) landed at 1ecee3e53 WITHOUT registering its six (its diff touched only the vocabulary, stack.zod.ts, its test and its changeset), contrary to the ruling's "register in that PR or land after #16449" — this PR closes that. PLUGIN_UI_REQUIRED_KEY_MISSING is the ninth actually registered: it surfaced from the general pin (a packages/spec/src stamp site the vocabulary classified boot-refusal), it is the same defect class, the fix is one mechanical row under the same gate family, and plugin.zod.ts is held by no other claim — the bounded in-place-fix exemption, declared here because the claim's file surface did not name that file (only its docstring moved). The genuinely-excluded set under packages/spec/src is the five foreign-vocabulary sites (ERR_BULK_PER_ROW_HOOK_LIMIT, OS_METADATA_CONVERSION_CONFLICT, OS_METADATA_CONVERTED, SQLITE_ERROR ×2) — a driver errno, conversion outcomes and a conformance fixture, not ADR-0112 codes; no allowlist was added.

A2.2 — the doored codes, re-measured

Wire consequence (stated in the changeset, @objectstack/spec minor)

For a code that reaches an HTTP door, registration changes what a client reads — error.code becomes the specific code instead of the status-derived member (VALIDATION_ERROR for 422) with the spelling demoted to declaredCode. Measured on this tree, none of the nine has such a door, so no HTTP body changes with this release; what changes is the face — ErrorCode gains nine members, REGISTERED_ERROR_CODES lists them, the generated docs references carry them (content/docs/references/api/error-code-ledger.mdx, contract.mdx; check:generated found nothing else stale), and each refusal's e.code is a member of the union a consumer's exhaustive switch is written over. Should a door ever answer with one of these, the wire carries the specific code from then on; the declaredCode demotion stays for genuinely unknown / third-party spellings. No BREAKING banner: nothing is removed or renamed, every existing body parses as before; the only consumer-visible cost is type-level (nine more cases in an exhaustive switch over the ErrorCode type). @objectstack/runtime gets no changeset: its only source change is the classification table, which tsup never reaches (entry src/index.ts; no src file imports dispatcher-error-vocabulary), so nothing published moves.

The pin that keeps the class closed (A2.3)

scripts/check-dispatcher-error-vocabulary.mjs (required Lint & Repo Gates) now refuses to classify a packages/spec/src/** stamp site as anything but foreign-vocabulary or runtime-pinned — a boot-refusal / pending-registration / sandbox-authored row for a spec site is a spec-face-unregistered finding naming the ruling and the remedy. It rides the gate's existing scanner (all ten stamp shapes, packages/** non-test source), so it is general over every code: literal raised under the spec tree rather than a list of nine names; its --self-test gains a [#16449] battery of 7 cases (boot-refusal and pending-registration rows red; foreign-vocabulary admitted; the same row outside packages/spec/src not flagged — the control; a runtime-pinned template admitted; the finding text names #16404 and the ledger file; a registered code derives no site). Beside it, runtime pins by value: stack-refusal-envelopes.test.ts asserts every defineStack refusal's code parses against ErrorCode and is under @objectstack/spec; plugin-ui-required-keys.test.ts section D does the same for its code; registry-ownership-refusal-envelope.test.ts asserts OBJECT_OWNERSHIP_CONFLICT parses and that 422 cannot have derived it; error-code-ledger.test.ts accepts the batch under its owning packages with standardSynonymOf undefined for each.

Reverse verification — direction predicted before running: RED

Both legs run from the committed state, mutation proven on disk, restored by git checkout HEAD -- path and verified by git diff HEAD empty plus git hash-object equal to the HEAD blob (b671a497… ledger, 0bf8fdb6… vocabulary):

  1. Gate leg (real tree): remove the 'STACK_SCHEMA_INVALID' ledger row (grep count 1 → 0) and re-add its former boot-refusal vocabulary row (0 → 1, marker 1) → node scripts/check-dispatcher-error-vocabulary.mjs exit 1, exactly 1 finding, kind spec-face-unregistered, naming packages/spec/src/stack.zod.ts / STACK_SCHEMA_INVALID / boot-refusal. Restored: rows 1 / 0 / marker 0, hashes match.
  2. Vitest leg: remove the same ledger row → vitest run src/stack-refusal-envelopes.test.ts src/api/error-code-ledger.test.ts exit 1 with exactly the two spec: register every error code that ships in dist — the nine unregistered defineStack / ownership / namespace codes enter ERROR_CODE_LEDGER / StandardErrorCode (the ledger is the published face, per the #16404 ruling) #16449 cases failing (every code is a member of the closed ErrorCode union… and accepts the #16449 batch…), everything else green. Restored, hash matches.

No build is involved in either leg: the gate reads spec SOURCE (parseLedgerCodes anchors on the declaration) and the spec tests import the ledger source relatively, so the dist-ablation preflight does not apply.

Verification (implementation head c35fbe58f1; exit codes captured before any pipe)

  • pnpm --filter @objectstack/spec build under os-verify-lock.sh → build exit 0 (held 148s)
  • pnpm --filter @objectstack/spec check:generated → 1 of 15 stale (content/docs/references/**), --fix regenerated only that; rerun on c35fbe58f1: exit 0
  • pnpm --filter @objectstack/spec typecheck → 0; vitest run over 7 spec files (error-code-ledger, stack-refusal-envelopes, plugin-ui-required-keys, error-catalog-docs, contract, stack-cross-reference-envelope, type-alias-convention.pin) → 7 files / 147 tests passed
  • objectql dependency closure built (14 packages, spec reused) → 0; vitest run --project local over registry-ownership-refusal-envelope, registry-conflict-code-constants, registry-namespace-install-gate → 3 files / 20 tests passed; pnpm --filter @objectstack/objectql typecheck → 0
  • check:dispatcher-error-vocabulary (--self-test: 10 shapes + 329 assertions OK; run: 65 sites all classified, 264 ledger + 50 standard, spec face 5 sites) → 0; check:error-code-provenance → 0 (319 stamp sites, 303 listed, 16 waived); check:error-code-casing → 0; check:error-status-conformance → 0; check:nul-bytes → 0
  • Derived gate family (node scripts/pm/dispatch-gates.mjs --commands on the real diff, reconciled with --ran: 123 of 123 families accounted for); exit codes in the report comment on spec: register every error code that ships in dist — the nine unregistered defineStack / ownership / namespace codes enter ERROR_CODE_LEDGER / StandardErrorCode (the ledger is the published face, per the #16404 ruling) #16449. NOT MEASURED locally, by prerequisite not by finding: check:dual-build-cjs-loads (exit 3, needs every package's dist), check:type-check-debt (exit 3, its re-measure prerequisite), check:pm-dispatch-gates (exit 124 twice — its own self-test exceeded 270s and 540s on the shared box). check:engine-split-ratio --days 90 first refused on the shallow clone (exit 2), deepened with git fetch --shallow-since=2026-06-02, rerun → 0.
  • pnpm lint narrowing, declared: eslint --no-inline-config --format json over the 9 changed files in the config's population (**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} minus NEVER_LINTED; the changed .md/.mdx are outside it) → exit 0, 9 files linted, 0 errors, 0 warnings; invariance: eslint.config.mjs line 328 declares no parserOptions.project and no typed rules, so this diff cannot move any untouched file's verdict. The repo-wide run is CI's.
  • Runtime, declared narrowing: the only runtime test that reads the vocabulary table (error-envelope.conformance.test.ts) checks per-row invariants over the remaining rows and iterates the dispatcher-reachable set, which this diff does not touch (every removed row was door: 'none'); the 31-package runtime closure was not built locally. CI runs Test Core.

Post-merge re-verification (head db0ad84790)

origin/main moved to 0a61db1f5d while this PR was open; the predecessor #16618 registered a code in the same ledger and regenerated the same content/docs/references/api/contract.mdx line (the enum member count), which a driver-less merge-tree probe showed as a content conflict. Merged through scripts/pm/os-regen-merge.sh (merge commit 8800d90641, ⛔ no rebase, no force-push), then the two routed docs artifacts regenerated on the merged source and committed (db0ad84790). Re-taken on that head: check:dispatcher-error-vocabulary (self-test 329 assertions OK; run 65 sites all classified, spec face 5) → 0; check:error-code-provenance, check:error-code-casing, check:error-status-conformance, check:nul-bytes, spec check:generated → 0; pnpm --filter @objectstack/spec typecheck → 0; the 7 spec test files → 147 passed; the 3 objectql test files → 20 passed; eslint over the same 9 files → 0 errors / 0 warnings; the gate family re-derived on the merged tree (124, one newly named family check:pm-widening-tells → 0) reconciled 124 of 124. Driver-less probe on the merged head: clean (exit 0); the same probe on the pre-merge head against the same main still conflicts (exit 1) — the control that the probe discriminates. Main added no code: stamp under packages/spec/src (its only spec-src code mentions are lowercase zod issue-code comparisons, outside the gate's grammar).

验收备注

  • filed as spec: register the 14 remaining door: 'none' (boot-refusal) codes that ship in dist — the rest of the #16404 class after #16449 #16649: the 14 remaining boot-refusal rows (core, objectql, runtime, driver-memory, driver-mongodb, plugins/organizations) are the same class under the ruling; the card also names widening the spec-face rule to every published package once they are registered.
  • noted, not filed: .changeset/stack-refusal-envelopes.md (fix(spec): every defineStack refusal carries an ADR-0112 envelope — six STACK_* codes beside STACK_CROSS_REFERENCE_INVALID #16342, pending release) still says "None of the six is registered in ERROR_CODE_LEDGER"; both changesets ship in the same release. 承接者: the maintainer compiling release notes centrally (AGENTS.md, content/docs/releases/), not a code PR.
  • noted, not filed: the ledger's header prose — including the Clause-② / needs:contract-review process sentences — is rendered wholesale into content/docs/references/api/error-code-ledger.mdx by gen:docs. 承接者: none identified (build-docs.ts owner; observation only).
  • noted, not filed: stack.zod.ts's docstring count "14 rows on the tree this landed against" for the boot-refusal class is now historical (14 remain after this PR by coincidence of a different composition). 承接者: none.
  • noted, not filed: check:pm-dispatch-gates self-test needs more than nine minutes on a loaded shared box. 承接者: the pm-dispatch lane (scripts/pm/).
  • noted, not filed: on this PR an EDIT sent without a footer came back with NO appended bare footer block (read back after the edit), which differs from the measured behaviour AGENTS.md records for update_pull_request; the session-URL footer below was re-sent on a second edit. 承接者: pm-dispatch references/platform-readings.md.

维护者速读(草稿)

改了什么:把九个已经随 npm 包发布、但没有登记在错误码总账(ERROR_CODE_LEDGER)里的错误码正式登记:OBJECT_OWNERSHIP_CONFLICT(objectql)、七个 defineStackSTACK_* 拒绝码和 PLUGIN_UI_REQUIRED_KEY_MISSING(spec)。同时给门禁加了一条规则:spec 包源码里抛出的任何错误码,要么在总账里,要么 CI 变红。

为什么改:#16404 的裁决(决策批次 #62,选项 D)定了「总账就是对外契约面,只要随 dist 发布的码都必须登记,有没有 HTTP 出口都一样」。这九个码已经发布在外,下游 catch (e) { switch (e.code) } 一旦依赖,就再也不能悄悄改名;没登记等于契约面上有个洞。卡里说的九个里,NAMESPACE_CONFLICT 其实 #14748 已经登记过了;#16342 合并时没有按裁决登记它的六个码;PLUGIN_UI_REQUIRED_KEY_MISSING 是卡之后新落地的同类,顺手一起补齐。

风险与代价(含回滚):这一版不改任何 HTTP 响应体——九个码在当前代码里都没有 HTTP 出口(实测),所以线上行为不变;变的是类型层面:ErrorCode 联合类型多了九个成员,穷举 switch 的下游要多写九个分支(只会多不会少)。@objectstack/spec 发 minor,不带 BREAKING。回滚就是 revert 这个 PR,不需要数据迁移。

席位意见:(留空,由席位定稿)

你要做的:这是 Clause-② yes 的 PR(裁决自带),需要合同评审席位过一遍 needs:contract-review;确认「无 HTTP 出口 ⇒ 不改响应体」这个读数你认可;剩下 14 个同类未登记码在 #16649,按裁决排期即可。


Generated by Claude Code

…'none' codes enter ERROR_CODE_LEDGER

Under the #16404 ruling (option D) the ledger is the published face: every
code shipped in dist is registered, door or no door. OBJECT_OWNERSHIP_CONFLICT
(objectql), the seven STACK_* defineStack refusals and
PLUGIN_UI_REQUIRED_KEY_MISSING (spec) gain rows; their boot-refusal
classification rows ratchet out of dispatcher-error-vocabulary.ts, and
check:dispatcher-error-vocabulary now refuses any verdict but
foreign-vocabulary / runtime-pinned under packages/spec/src (the pin that
keeps the class closed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
…for the nine new codes

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
…oot-refusal rows; state the artifact reading in the changeset

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
@github-actions github-actions Bot added the size/l label Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/spec, touching 12 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/src/kernel/plugin.zod.ts, packages/spec/src/stack.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-catalog.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-handling-server.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/src/kernel/plugin.zod.ts, packages/spec/src/stack.zod.ts) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 135 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json cc238db8ba1755a40181a80a9d843835d69d8cdbpackageMentionDocs.

Which tree this was computed on

This run read content/docs from e517c2b0f4946e07b1b9bec99054ae260a4cdfc5 — the merge of head db0ad84790f2ce94d62b3858c9f3572a54e22fe5 into base cc238db8ba1755a40181a80a9d843835d69d8cdb, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e517c2b0f4946e07b1b9bec99054ae260a4cdfc5 && git checkout e517c2b0f4946e07b1b9bec99054ae260a4cdfc5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc238db8ba1755a40181a80a9d843835d69d8cdb db0ad84790f2ce94d62b3858c9f3572a54e22fe5 && git checkout -B drift-repro cc238db8ba1755a40181a80a9d843835d69d8cdb && git merge --no-ff db0ad84790f2ce94d62b3858c9f3572a54e22fe5

node scripts/docs-audit/affected-docs.mjs --json cc238db8ba1755a40181a80a9d843835d69d8cdb

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs cc238db8ba1755a40181a80a9d843835d69d8cdb → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 7, 2026
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 size/l tests tooling

Projects

None yet

2 participants