Repository navigation
feat(spec): register the fourteen remaining door:'none' error codes that ship in dist - #16879
Conversation
…hat ship in dist Under the #16404 ruling (option D) the ledger is the published face: every code shipped in dist is registered, door or no door. #16449 took the nine measured on its tree; the fourteen boot-refusal rows that remained in dispatcher-error-vocabulary.ts — nine @objectstack/core refusals, MIXED_ARTIFACT_COLLECTION_SHAPE (runtime), DUPLICATE_ARTIFACT_OBJECT_NAME (objectql), the two drivers' *_MULTI_TENANT_UNSUPPORTED refusals and WALLED_MEMBERSHIP_POLICY_UNDECLARED (organizations) — gain ledger rows under their stamping packages, each measured present in that package's built dist/index.js. @objectstack/driver-mongodb returns as an owner key (the #8035 removal reversed on the record) and @objectstack/organizations is new. The fourteen vocabulary rows ratchet out as the gate's stale-row rule requires; the test that pinned MONGODB_MULTI_TENANT_UNSUPPORTED's absence now pins its presence, with OVERLAY_PERSISTENCE_FAILED as the retired-class witness and MULTI_TENANT_UNSUPPORTED as the still-refused control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…usal-codes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…prose-id baseline for the fourteen registrations
check:docs proved content/docs/references/api/{contract,error-code-ledger}.mdx
stale against the widened ledger (the ErrorCode union grows by fourteen, the
ledger page lists the fourteen rows); check:generated --fix regenerated only
that one artifact. check:doc-authoring's prose-id baseline over-pinned the
vocabulary module for two ids whose only carriers were the fourteen
boot-refusal rows that ratcheted out; the shrink-only census re-derives it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
📓 Docs Drift CheckThis PR changes 2 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 45cc42d5af1804dac68f93c2b8e9298c1e46414a && git checkout 45cc42d5af1804dac68f93c2b8e9298c1e46414a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4cfc93b80270b12854fc31f7a4837f1f8bcf5513 6738c993e35cf03fcfe1f6b7f142ad379eef04d7 && git checkout -B drift-repro 4cfc93b80270b12854fc31f7a4837f1f8bcf5513 && git merge --no-ff 6738c993e35cf03fcfe1f6b7f142ad379eef04d7
node scripts/docs-audit/affected-docs.mjs --json 4cfc93b80270b12854fc31f7a4837f1f8bcf5513
|
Seat answers both open questions, and owns a stale number of its own — 2026-09-08T13:28Z, head
|
Contract review — VERDICT: PASS WITH FINDINGS, BINDING: none. Enqueuing.Read 2026-09-08T13:28:06Z → 13:49:47Z, bound to head ⭐ Row 1 — the authority claim, which is why this review existedThe PR reverses #8035, which unregistered
⇒ "Door or no door" is the direct negation of "no envelope path ⇒ unregister." The newer ruling reaches the older one's ground, so the reversal is authorised.
The other rows
⭐ The runtime-bump question this seat asked, answered by measurementThe PR declares no Findings — all non-blocking, and this seat takes the reviewer's rating
Disposition
Generated by Claude Code |
Contract review (
|
…decision in words instead of a tracker number (stage 24) (objectstack-ai#21961) Part of objectstack-ai#20749 Clause-②: no Stage 24 of this card: the next area of class (e), the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes the first name-ordered `api/` group: the 27 id-bearing test files directly under `packages/spec/src/api/` from `ai-agents-envelope.test.ts` to `package-lifecycle.test.ts`. Those files carried 100 messages and 106 tracker ids, citing 65 records. All 106 now either state what their record decided, in words (form D), or are dropped where the title already says it. No needle sits in this group. Text only: no assertion, identifier, test count or code comment changes, and no file is renamed. ## Census at the base (`a3bd157730`) Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`), `census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs` (md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5 `dda605c54745b4a60cc14c9a686e4eff`), byte-identical to the copies stages 10 to 23 used. A literal counts as a test title when its folded message is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` / `.only` chains included. Everything else is an "other" string. The worktree was cut from `origin/main` at `a3bd157730`, the claim's base and stage 23's landing. Both instruments read **471 messages / 498 ids in 111 files**, the seat's reading and stage 23's head reading. | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `api/` (this PR: 27 of the 40 files) | 40 | 189 / 201 | 181 / 193 | 8 / 8 | | `system/` | 34 | 154 / 167 | 128 / 138 | 26 / 29 | | (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 | | `ui/` | 5 | 7 / 7 | 0 | 7 / 7 | | `ai/` | 1 | 2 / 2 | 0 | 2 / 2 | | `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 | | **total** | **111** | **471 / 498** | **426 / 450** | **45 / 48** | The group reads **100 messages / 106 ids in 27 files**, the seat's figures file for file: | file (under `api/`) | messages / ids | titles | other | |:--|--:|--:|--:| | `ai-agents-envelope.test.ts` | 1 / 1 | 1 / 1 | 0 | | `analytics.test.ts` | 3 / 3 | 3 / 3 | 0 | | `api-entry-graph.pin.test.ts` | 1 / 1 | 1 / 1 | 0 | | `api-error-code-type.test.ts` | 1 / 1 | 1 / 1 | 0 | | `apis-publish-gates.test.ts` | 12 / 12 | 12 / 12 | 0 | | `auth-endpoints.test.ts` | 2 / 2 | 2 / 2 | 0 | | `auth.test.ts` | 2 / 2 | 1 / 1 | 1 / 1 | | `automation-api.zod.test.ts` | 4 / 5 | 4 / 5 | 0 | | `batch.test.ts` | 2 / 2 | 2 / 2 | 0 | | `contract.test.ts` | 3 / 3 | 3 / 3 | 0 | | `dataset-selection.test.ts` | 5 / 5 | 5 / 5 | 0 | | `discovery-auth-families.pin.test.ts` | 2 / 2 | 2 / 2 | 0 | | `discovery-environment-subset.pin.test.ts` | 2 / 2 | 1 / 1 | 1 / 1 | | `discovery.test.ts` | 10 / 11 | 10 / 11 | 0 | | `dispatcher.test.ts` | 2 / 2 | 2 / 2 | 0 | | `endpoint.test.ts` | 4 / 4 | 4 / 4 | 0 | | `envelope-violations.test.ts` | 1 / 1 | 1 / 1 | 0 | | `error-code-ledger.test.ts` | 7 / 11 | 7 / 11 | 0 | | `errors.test.ts` | 3 / 3 | 3 / 3 | 0 | | `export-job-family-retirement.test.ts` | 6 / 6 | 3 / 3 | 3 / 3 | | `export.test.ts` | 3 / 3 | 3 / 3 | 0 | | `meta-item-response-shapes.test.ts` | 2 / 2 | 2 / 2 | 0 | | `metadata.test.ts` | 1 / 1 | 1 / 1 | 0 | | `odata-orderby-dual-declaration.test.ts` | 1 / 1 | 1 / 1 | 0 | | `package-api.test.ts` | 10 / 10 | 10 / 10 | 0 | | `package-install-one-authority.test.ts` | 1 / 1 | 1 / 1 | 0 | | `package-lifecycle.test.ts` | 9 / 9 | 9 / 9 | 0 | | **27 files** | **100 / 106** | **95 / 101** | **5 / 5** | Five more test files sit in the same name range and carry no id (`documentation.test.ts`, `error-catalog-docs.test.ts`, `events.test.ts`, `http-cache.test.ts`, `odata.test.ts`). The five "other" strings are expect messages, rewritten and declared to the text-only tool: `auth.test.ts:155`, `discovery-environment-subset.pin.test.ts:65` (one leaf of a `+` chain) and `export-job-family-retirement.test.ts:112` (a template literal), `:158` and `:349`. - **Controls.** Lit: `ui/notification.test.ts` (1 id) and `api/protocol.test.ts` (50 ids), outside the group, read the same at the base and at the head. Dark: `package-api.test.ts` reads 0 at the head while 30 of its comment lines still carry a number. Planted in a scratch tree: an id put into a `package-lifecycle.test.ts` title reads 1 / 1 (`title:describe`), and an id put into a `batch.test.ts` comment reads 0. - **A wider pattern** (any `#` plus digits) reads the same as the gate pattern in all 27 files at the base, and 0 in all 27 at the head. - **At the head:** 371 messages / 392 ids in 84 files. The 27 files read 0 / 0, `api/` reads 89 / 95 in 13 files, and no other file moved. ## How the area was chosen `api/` has no subdirectory, so it is taken in name-ordered file groups near the ~100-id bound, the rule stages 20 to 23 used. Stage 23's cut named this group at 106 ids, and this census reads 106, so no re-cut was needed. **Named for the next stages** (cut from the head census, 371 / 392): - **The second `api/` group:** `plugin-rest-api.handler-status-retirement.test.ts` through `zod-issues-to-fields.test.ts`, 13 files, 89 messages / 95 ids (86 / 92 titles, 3 / 3 other), `protocol.test.ts` alone 46 / 50 and `rest-server.test.ts` 19 / 19. That finishes `api/`. - `system/` 167, two stages. The files directly in `src/`, 120, one. - The needles: the three docblock needles, the kept `ui/component-props-unknown-members.pin.test.ts:322` and stage 22's two. One stage, with an at-tier review. The four colour literals stay, as stage 21 decided. ## What each id became - **18 literals (22 ids)** now state a decision in words. - **10 literals (10 ids)** get their subject back in words, where the number stood for a thing. - **72 literals (74 ids)** drop a number the title already explains. Every cited record was fetched with all its comments through REST (357 comments, objectstack-ai#4052's included), and its decision was read from its ruling, ACCEPT and landing comments. 65 records are cited: 59 answer 200 and 6 answer 404. Two of the 200s are PRs (objectstack-ai#4049 and objectstack-ai#20218), read from their bodies. One citation is objectui's and was read from objectui: `objectui#6593`. The six that answer 404 were read from what landed, through the commits endpoint (this checkout is shallow), each named by the commit the stage-2 re-anchoring of `api/` comments gave it: - **objectstack-ai#6287**, from `84c86fb454` (objectstack-ai#6610): `preview` and `trial` fold to `sandbox` by declaration, and the fold table is typed total over `EnvironmentType`; - **objectstack-ai#6704**, from `c3f4916266` (objectstack-ai#7015): `ImportRequest.runAutomations` declares the default the import route applies; - **objectstack-ai#10330**, from `b9e9227e36` (objectstack-ai#11316): `mappingName` declared on `ImportRequestSchema`, with the mutual-exclusion refine; - **objectstack-ai#10338**, from `d2619fd0cd` (objectstack-ai#11290): `ApiEndpoint.target` is optional, and the publish gate holds the flow requirement; - **objectstack-ai#11504**, from `f90e820249` (objectstack-ai#12611): `FLOW_INPUT_SCHEMA_INVALID` registered, the never-dispatched code; - **objectstack-ai#16649**, from `613bfbd3db` (objectstack-ai#16879): the fourteen remaining `door: 'none'` codes registered. One citation names a different record. `batch.test.ts:78` read "(objectstack-ai#3963 follow-up)"; objectstack-ai#3963 is the `api.requireAuth` retirement. The `validateOnly` tombstone is objectstack-ai#4052's decision, read too: never implemented, so tombstoned rather than half-built. The title already says that ("rejects the retired `validateOnly` key with its prescription"), so the number is dropped. Where a record's decision was refined later, the title follows the refined one: - **objectstack-ai#4936 and objectstack-ai#5111:** objectstack-ai#4936's ruling refused every non-empty `apis:`; objectstack-ai#5111 narrowed that to per-endpoint gates. The `:152` title says what held through both: an empty or absent `apis:` was never refused. - **objectstack-ai#4910 Q2:** that ruling left endpoint-level `rateLimit` unwired and tracked under objectstack-ai#4936; objectstack-ai#4936's ruling then kept it in the vocabulary for the endpoint executor to wire. The title names that destination. - **objectstack-ai#17518:** its 2026-09-13 ruling was re-presented and briefly replaced (batch objectstack-ai#149, withdrawn as unexecutable), then confirmed (batch objectstack-ai#159, letter A) and given its mechanism (batch objectstack-ai#192, letter A′), which adds the record-stage body. The title "the row's manifest is the RECORD stage" is that body, so only the number goes. - **objectstack-ai#18605:** ruling letter 1 made the request contract the one authority, and objectstack-ai#18877's later ruling made that key optional so the door sees an absence; the title says only "has ONE authority", which both keep, so only the number goes. **Stated in words:** | record | literal (under `api/`) | now reads | the decision | |:--|:--|:--|:--| | objectstack-ai#18576 | `api-entry-graph.pin.test.ts:77` | "… stays off the assembled package body (ruled: split the entry rather than watch its weight)" | Ruling B (batch objectstack-ai#145 item 1, maintainer 2026-09-17): the cost is removed, not watched; `./api` is split and the assembled-package declarations move to `@objectstack/spec/api-assembled`. | | objectstack-ai#4936 | `apis-publish-gates.test.ts:152` | "still accepts an EMPTY and an ABSENT `apis:` — never refused, even while a non-empty one was" | Maintainer ruling 2026-08-04: v17 loudly refuses a non-empty `apis:` and keeps the vocabulary; an empty or absent one stays publishable, then and after objectstack-ai#5111's narrowing. | | objectstack-ai#4910 | `apis-publish-gates.test.ts:568` | "keeps endpoint-level `rateLimit` in the vocabulary (ruled: left to the endpoint executor, not the server-level seam)" | Q2 = B (2026-08-03): that card wires the server level only; the endpoint-level keys stay, and objectstack-ai#4936's ruling has the endpoint executor wire them. | | objectstack-ai#5189 | `apis-publish-gates.test.ts:597` | "still refuses D6 — the gate with no runtime counterpart, so the per-item publish path runs it too" | Triage disposition (E7b, 2026-08-04): `publishPackage` reuses the same gate function, because D6 alone has no runtime counterpart. | | objectstack-ai#7481 | `auth-endpoints.test.ts:112` | "AuthFeaturesConfig retired flags (ruled: stop advertising them)" | Maintainer ruling 2026-08-11: `passkeys` / `magicLink` leave the `/api/v1/auth/config` payload. | | objectstack-ai#14788 | `auth.test.ts:88` | "SessionUser.language retirement (ADR-0049 — ruled: gone, with no replacement field)" | Maintainer ruling D (2026-09-03): retired under ADR-0049, no producer and no consumer; no replacement field until a real producer exists. | | objectstack-ai#9378, objectstack-ai#9510 | `automation-api.zod.test.ts:327` | "… status, runId and the screen (a pause is the third state, not a failure)" | objectstack-ai#9510's ruling (2026-08-18): a pause is not a failure, and callers learn the third state deliberately; `status: 'paused'` + `runId` + `screen` is the trigger contract's third state. | | objectstack-ai#4828 | `discovery.test.ts:1167` | "scoping (ruled: declare what REST actually emits)" | Maintainer ruling 2026-08-05, item 3: `scoping` is declared on `DiscoverySchema` as an optional key. | | objectstack-ai#4828 | `discovery.test.ts:1207` | "resolveDiscoveryEnvironment (ruled: an enum, not a passthrough)" | Item 4: the schema is authoritative, so every producer's `environment` is mapped into the declared enum. | | objectstack-ai#8211 | `error-code-ledger.test.ts:68` | "standard-synonym detection (ruled: refused unless waived)" | Option C (triage adjudication, 2026-08-12): the admission gate refuses a semantic synonym of a standard member unless a recorded waiver admits it; the four existing ones are waived. | | objectstack-ai#10025, objectstack-ai#11504 | `error-code-ledger.test.ts:220` | "accepts the definition-level input-schema refusal code (ruled non-retryable: a never-dispatched exit)" | Maintainer ruling B (2026-08-20): the refusal is non-retryable and becomes a never-dispatched exit with its own ADR-0112 code. | | objectstack-ai#16449, objectstack-ai#16404 | `error-code-ledger.test.ts:234` | "accepts the nine-code batch — every code that ships in dist, door or no door (ruled: the ledger is the published face)" | objectstack-ai#16404 option D (batch objectstack-ai#62, 2026-09-07): the ledger is the published face, so every code in `dist` is registered; objectstack-ai#16449 registered the nine. | | objectstack-ai#16649, objectstack-ai#16404 | `error-code-ledger.test.ts:308` | "accepts the fourteen remaining door:none codes, each under its stamping package (ruled: the ledger is the published face)" | The same ruling; `613bfbd3db` registered the fourteen. | | objectstack-ai#17158 | `export-job-family-retirement.test.ts:158`, `:349` (expect messages) | "… the retirement is being undone — nothing served, bound or consumed the family" | Ruling A (batch objectstack-ai#122 item 3, 2026-09-12; landing route A, batch objectstack-ai#221 item 2): ADR-0049 retires a declared API that nothing serves, binds or consumes. | | objectstack-ai#12038 | `package-api.test.ts:603` | "package-rollback-response retirement (ruled: it described the wrong operation on the live path)" | Ruling 3A (2026-08-27): the published version-rollback schema, bound to the live commit-rollback path, is retired first. | | objectstack-ai#12038 | `package-lifecycle.test.ts:25` | "the ruled re-export of PackagePublishResultSchema into the `/api` namespace" | Ruling 5A: re-export the existing schema into the namespace the ledger resolver searches, never a second copy. | | objectstack-ai#12038 | `package-lifecycle.test.ts:140` | "RollbackToPackageCommitResponseSchema declares the COMMIT-rollback body (ruled: authored once the wrong-operation schema was retired)" | Ruling 3A's binding sequence: retire the false declaration, then author the true commit-rollback schema. | **Subject back in words** (10 literals): "the pre-objectstack-ai#4053 bare body" becomes "the bare body from before the envelope relocation" (objectstack-ai#4053's end state: both producers relocated the payload under `data`); "(objectstack-ai#3891 shim dialect)" becomes "(the degraded shim dialect)"; "the duplicate-payload drift objectstack-ai#4049 removed" becomes "the duplicate-payload drift the /share-links domain stopped emitting", the PR's own title; "zero holders after objectstack-ai#17158" becomes "after the export-job family retirement"; "the objectstack-ai#10330 TS2353 repro" becomes "the original TS2353 repro"; the three "since PR objectstack-ai#20218" titles become "since the door parses its whole body" (twice) and "so does the door, which parses the whole body", the PR's own title; the `objectstack-ai#17534` title now names "the reverse-domain id rule", that card's ruling A; "the objectui#6593 confusion" becomes "the envelope-vs-payload `success` confusion", the defect objectui#6593 measured. **Dropped where already stated** (72 literals, 74 ids). A number goes only where the title already says its decision. Examples: the eight `[objectstack-ai#5111]` describes ("the flip — a well-formed `apis:` publishes", "gate (a)" to "gate (e)", …), `[objectstack-ai#5310]`, `[objectstack-ai#19920]`, the two `[objectstack-ai#21046]`, `[objectstack-ai#5676]`, `[objectstack-ai#5672]`, `[objectstack-ai#5679]` and `[objectstack-ai#6287]` prefixes; the four `objectstack-ai#17551` / `objectstack-ai#17550` section prefixes in `dataset-selection.test.ts`, which keep the file's own `§1` to `§5`; `objectstack-ai#5384 —`, `objectstack-ai#5227 —`, `objectstack-ai#5950`, `objectstack-ai#5882`, `objectstack-ai#17518`, `objectstack-ai#18058 —` and `objectstack-ai#18605 —`; the four `objectstack-ai#15677` citations on the "→ …Seconds" renames; and the tails `(objectstack-ai#3878)`, `(objectstack-ai#6442)`, `(objectstack-ai#19543)` x2, `(objectstack-ai#7359)`, `(objectstack-ai#3939)`, `(objectstack-ai#18124)`, `(objectstack-ai#3842)` x3, `(objectstack-ai#10338)`, `(objectstack-ai#6704)`, `(objectstack-ai#10330)`, `(objectstack-ai#4587)`, `(objectstack-ai#17667)`, `(objectstack-ai#19116)`, `(objectstack-ai#17431)`, `(objectstack-ai#19441)`, `(objectstack-ai#8211)`, the five `(objectstack-ai#12038)` and the one `(objectstack-ai#12038 4A)` after "declares the four fixed keys and stays open". `(federated ledger, objectstack-ai#4805)`, `(ADR-0076 D12, objectstack-ai#2462)` and `(ADR-0112 amendment 2026-08-18, objectstack-ai#9266)` keep their words and lose the number. The ADR-0087 conversion id `api-endpoint-cache-ttl-to-cache-ttl-seconds` stays: it is not a tracker id. **No file is renamed.** ## Readers - **`error-code-ledger.test.ts`** (11 ids in 7 titles): no ledger, gate or self-test reads its strings. `scripts/check-error-code-casing.mjs` names the file only to exempt it whole ("the ledger admission test"); the ledger's docblock and its generated reference page name the file, never a title; the provenance and dispatcher-vocabulary gates read `error-code-ledger.zod.ts`, not the test. - **Needles:** none. The five declared strings are all assertion failure messages (the second argument of `expect`), none is an expected value, and no title or message in the group is matched against a source docblock or another file's text. - **Test-name filters:** none. No tracked script, workflow or package config passes `-t` / `--testNamePattern` to vitest; the one vitest `-t` hit is a README example under `packages/qa/dogfood` filtering its own fixture. - **Snapshots:** none. No `__snapshots__` directory is tracked under `packages/spec`, and none of the 27 files calls a snapshot matcher. - **Projects:** `export-job-family-retirement.test.ts` is in the `repo` project (`packages/spec/vitest.repo-tests.json:30`); the other 26 run in `local`. The base-versus-head run below takes both projects. - **By substring:** every old literal, its id-bearing fragment and a window around each id (294 needles) was searched with `git grep` at the base, across the tracked tree outside its own file. No gate, doc, filter, snapshot, QA checklist entry or `scripts/check-*.mjs` self-test reads one. The 17 hits are windows that share wording with code comments and one CHANGELOG line: "(ADR-0076 D12, objectstack-ai#2462)" in comments in `runtime/http-dispatcher.ts`, `spec/api/discovery.zod.ts` and `objectql/protocol-discovery.test.ts` and at `packages/runtime/CHANGELOG.md:14161`; "(objectstack-ai#18576 ruling, letter B)" in three comments; "(objectstack-ai#3891 shim dialect)" in `runtime/domains/analytics.ts:41`; "is retired (objectstack-ai#19543)" in `spec/api/automation-api.zod.ts:645`. ## Text-only proof Stage 10's scratch tool (`textonly10.cjs`, md5 `d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file on three legs: 1. **Skeleton:** the full AST, with string pieces masked. It must be identical. 2. **Comments:** every comment, byte-equal. 3. **Strings:** each changed string leaf must sit in a test-call title position or on a declared line, must carry a tracker id before, and must carry no `#` plus digits after. This stage declares the five expect-message lines named above. - **Result:** 27 of 27 files SAME on all three legs, with the per-file counts predicted in writing before the run. - **Totals:** 100 changed string leaves in 100 literals: 95 titles and 5 declared. The diff's `+` and `-` lines are exactly the 100 planned lines as multisets, and every file keeps its line count. - **Controls (14 of 14 as predicted on the first run, on scratch copies, each anchor hit once):** identifier rename DIFF; numeric literal DIFF; comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten title given a new id VIOLATION; a title that was id-free at base edited VIOLATION; one title reverted to base SAME; an `it.each` row given an id VIOLATION; an undeclared expect message changed VIOLATION; a title re-split into a `+` chain DIFF; a declared expect message reverted to base SAME; a declared expect message given a new id VIOLATION; a declared `+`-chain leaf given a new id VIOLATION; a template-literal message given a new id VIOLATION. - **Templates and tables:** no `.each` title and no `$name` placeholder changes. The one template literal, `export-job-family-retirement.test.ts:112`, changes only its text after `${name}`. **Test counts:** the 27 files were run at the base, in a separate base worktree, and at the head, with `--project local --project repo`. Both sides read 831 tests in 27 files, all passed, with the same count and status sequence per file in 27 of 27. 325 full test names change, and each changed name equals the base name with the planned replacements applied: 0 mismatches once the plan's text is read the way the source writes it (the comparison tool reads the plan's `—` escape at `errors.test.ts:439` literally, so its first pass reports that title's three names as mismatches; decoding the escape, as vitest does, reads 0). No full name repeats on either side. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 27 touched files are in it, and no `*.test.ts` at all. The controls `src/api/package-lifecycle.zod.ts`, `src/api/error-code-ledger.zod.ts` and `dist/index.mjs` are in it. - In the built `dist/`, a new phrase and an old one each read in 0 files. The control `Unrecognized key` reads in 42. So this PR publishes nothing, and no changeset is added. ## Verification (at `dffd240655`) - `pnpm turbo run build` over all packages: 71 / 71, through the shared verify lock (`VERDICT command-exit 0`). - `@objectstack/spec`: - `vitest run --project local`: 619 files, 18471 passed, 1 todo. - `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246 errors / 135 pinned signatures held). Its program holds all 27 group files, counted by path with `tsc --listFilesOnly -p tsconfig.test.json`. - `check:generated`: all 15 generated artifacts up to date, against the `dist/` the build above wrote. - **Gates:** `dispatch-gates --commands` derived 80 families: stage 23's 79 plus `check:error-code-casing`, which the two touched files it names bring in. All 80 exit 0. `--ran` reconciles: 80 derived, 80 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded. The same 80 derive from `origin/main` `01e0f71ad8` with this diff applied. The roster families stage 23 also ran (`check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:filter-alias-parity`) each exit 0. - **ESLint, a proven narrowing:** `--no-inline-config` over the 27 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 27 configured, 0 ignored. No file sets `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 200 changed lines (+100 / -100). - A control-byte scan over the 27 changed files finds none. ## `main` since the base Re-fetched just before this PR opened, `origin/main` was two commits past the base (`01e0f71ad8`: objectstack-ai#21940, objectstack-ai#21953). They touch 31 files, none of the 27 and none under `packages/spec`, so `main` was not merged and the census on that tree is the base's. `git merge-tree` onto `01e0f71ad8` is clean, and none of the 5 open PRs touches any of the 27 files. ## Acceptance notes - **Same-id test titles in this card's later stages** go with those stages: 23 lines in `packages/spec/src`, among them `api/protocol.test.ts` (`[objectstack-ai#5672]` x2, `(objectstack-ai#12038)` x5, `(objectstack-ai#12038 1C)`, `(objectstack-ai#19543, door ③)`), `api/plugin-rest-api.test.ts`, `api/router.test.ts` and `api/websocket.test.ts` (`(objectstack-ai#15677)`), `stack-json-stage-package-body.test.ts` (`objectstack-ai#17518` x4), `system/book.test.ts` (`(objectstack-ai#12038)`) and three `system/` titles citing `(objectstack-ai#18124)`. - **Same-id test titles in other packages** stay: 96 lines in 12 packages (`runtime` 37, `rest` 24, `client` 9, `metadata-protocol` 6, `service-automation` 6, `metadata` 5, `cli` 3, `objectql` 2, and one each in `examples/app-showcase`, `core`, `plugin-hono-server` and `verify`), each package's share under the objectstack-ai#20513 lane children. - **Code comments with live ids** remain in these files and their sources, among them the `// package-rollback-response retirement (objectstack-ai#12038 3A)` banner above its describe, the `[objectstack-ai#5111 / objectstack-ai#5040 E7]` and `[objectstack-ai#5189 / objectstack-ai#5040 E7b]` headers in `apis-publish-gates.test.ts`, and the `[objectstack-ai#17158]` header in `export-job-family-retirement.test.ts`. Code comments are not this card's share. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ Co-authored-by: Claude <noreply@anthropic.com>
Part of #16649 — the ledger half of the card: the fourteen
door: 'none'(boot-refusal) codes gain theirERROR_CODE_LEDGERrows. The card's second bullet — widen the gate'spackages/spec/src/refusal to every published package'ssrc/and retire theboot-refusalverdict — is deliberately NOT in this PR (section "What stays open" below), so #16649 remains open after this merges.Clause-②: yes
Ruling executed: #16404 (decision batch #62, 2026-09-07, option D, maintainer 「同意」), verbatim from the ruling comment: "Every
codethat ships indistmust be registered there; registering a code isClause-②: yes, door or no door, because it widens the published face." The first nine landed in #16449; this is the rest of that class.What changed
packages/spec/src/api/error-code-ledger.zod.ts— fourteen new rows, each under the package whosedist/index.jscarries the literal (measured, table below), each with the reachability reading its vocabulary row recorded. Two owner keys move:@objectstack/driver-mongodbreturns (its only row came out with [finding]MONGODB_MULTI_TENANT_UNSUPPORTEDmay be registered-but-unemittable in the error-code ledger — a boot refusal never reaches a wire envelope #8035),@objectstack/organizationsis new (the package's first row).packages/spec/src/api/error-code-ledger.test.ts— the pin that assertedMONGODB_MULTI_TENANT_UNSUPPORTED's ABSENCE now asserts the fourteen's presence under their owners (withstandardSynonymOfempty for each, andMULTI_TENANT_UNSUPPORTED— the drivers' shared constant NAME, never a stamped code — as the still-refused control);OVERLAY_PERSISTENCE_FAILEDbecomes the witness of the one retirement ground that survives [Decision] Clause ② on an UNREGISTERED error code carried by a thrown value: #14552 landedno, #15963 landsyes, and they are the same class #16404 (no producer left inpackages/**).packages/runtime/src/dispatcher-error-vocabulary.ts— the fourteenboot-refusalrows ratchet out. This is forced, not chosen:check:dispatcher-error-vocabularyderives a site only for a code the registered vocabulary lacks, so a registration makes the site vanish from the scan and the row that classified it reds asstale-row. The verdict itself stays declared (0 rows carry it on this tree); the running-log comment records the cycle. The module is not exported frompackages/runtime/src/index.tsandUNREGISTERED_CODE_SITEShas 0 hits inpackages/runtime/dist/index.js, so nothing published in@objectstack/runtimemoves.content/docs/references/api/{contract,error-code-ledger}.mdx— regenerated bycheck:generated --fix(the one artifact it proved stale): theErrorCodeunion count moves +308 → +322, the ledger page lists the fourteen.scripts/doc-authoring-prose-id.baseline.json— shrink-only census: the vocabulary module's#3724(1) and#8035(6) prose-id pins had the deleted rows as their only carriers..changeset/register-remaining-boot-refusal-codes.md—@objectstack/spec: minor(additive widening of a published face, finding(changeset): two independent contract reviews read the repo's own history to opposite bumps for "add an exported symbol to a published index" #15294's floor). Spec-only on purpose: no stamping package's source ordistchanges.The count, settled: fourteen, not two
The dispatch flagged that the card says fourteen while
node scripts/check-dispatcher-error-vocabulary.mjs --reportsays "2 awaiting a ledger entry". Both readings taken onorigin/mainc930f8597(a puregit archiveof that tree, no worktree state), exit captured before any pipe:--reportexit 0: "66 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry (spec: register the dispatcher conformance gate's reported error codes in ERROR_CODE_LEDGER (spec half of the #8087 ruling) #8846)". Its derivation lists the fourteen codes, each with verdictboot-refusal, and the two with verdictpending-registration(AMBIGUOUS_METADATA_STEM,owd_widening_forbidden).declared.filter(d => d.verdict === 'pending-registration')— it counts ONE verdict. The scan itself reports "only codes the registered vocabulary does NOT contain" (header bounds), so every one of the 66 derived sites is an unregistered code; the fourteenboot-refusalsites are unregistered by the same construction, they just sit under a different verdict label than the line counts.origin/main: 0 row hits for each of the fourteen (positive controlsUNIQUE_VIOLATION2,PLUGIN_REGISTER_FAILED1);StandardErrorCodeinerrors.zod.ts: 0 for each (controlPERMISSION_DENIED2).--reportexit 0, "51 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry", 279 ledger codes (265 + 14); the same twopending-registrationrows remain — the triage's item 2 says they are not this card's, and they are untouched.So the card's predicate ("
boot-refusalrows = shipped codes with no ledger row") is the one the #16404 ruling reaches, and the gate's "2 awaiting" is the older #8846 predicate (codes with a DOOR awaiting registration). Fourteen is right; the diff registers exactly those fourteen.Each of the fourteen ships in its package's
dist(word-boundary grep,dist/index.jsbuilt on this container)INVALID_ARTIFACT_PACKAGES·INVALID_ARTIFACT_PACKAGE_ENTRY·DUPLICATE_ARTIFACT_PACKAGE·NO_SUCH_RUN·PLAN_CHANGED·PREFLIGHT_FAILED·NOT_COMPENSABLE·SERVICE_NOT_REGISTERED·PLUGIN_CONTRACT_VIOLATION@objectstack/coreMIXED_ARTIFACT_COLLECTION_SHAPE@objectstack/runtimeDUPLICATE_ARTIFACT_OBJECT_NAME@objectstack/objectqlMEMORY_MULTI_TENANT_UNSUPPORTED@objectstack/driver-memoryMONGODB_MULTI_TENANT_UNSUPPORTED@objectstack/driver-mongodbWALLED_MEMBERSHIP_POLICY_UNDECLARED@objectstack/organizations(verified inpackages/plugins/organizations/package.json, not the card's@objectstack/plugins/organizationsguess)Controls:
UNIQUE_VIOLATIONin driver-memory dist 1 (positive); bareMULTI_TENANT_UNSUPPORTED0 and retiredOVERLAY_PERSISTENCE_FAILEDin core dist 0 (negatives). None of the six packages isprivate; each publishesfiles: ["dist", …].MONGODB_MULTI_TENANT_UNSUPPORTEDis a deliberate reversal of #8035, on the record#8035 unregistered it on the ground "host boot matching is not wire vocabulary"; #16404 supersedes exactly that ground. The ledger header's "Retiring a code" section now carries both halves, the row comment names the reversal, and the old absence pin is replaced by a presence pin — this is not an accidental revert.
Verification (tree
6738c993e=origin/mainc930f8597merged in; exit codes captured before any pipe)Heavy runs went through
scripts/pm/os-verify-lock.sh; itsVERDICTline is quoted where it applies. Each;-joined batch ends in the conjunction of its parts, so the wrapper's last-exit covers every part, and each part's own exit is echoed beside it.pnpm --filter @objectstack/spec buildVERDICT command-exit 0 · held the lock 145spnpm --workspace-concurrency=2 --filter '[BASE]' build(the packages main moved since the branch base; the root package was in the set, so its script ran turbo over the tree)VERDICT command-exit 0 · held the lock 389spnpm --filter @objectstack/spec testspec-test-exit=0— 465 files, 12967 tests passed (384.9s)pnpm --filter @objectstack/spec typecheck(tsc --noEmit+ scripts +check:test-typecheck, which compiles the test layer undertsconfig.test.json— the ledger test is in that program,tsconfig.jsonitself excludes**/*.test.ts)spec-typecheck-exit=0; wrapperVERDICT batch-last-exit 0 · held the lock 441spnpm --filter @objectstack/runtime typecheckruntime-typecheck-exit=0error-envelope.conformance,package-door-error-parity,http-dispatcher.error-leak,domains/actions-validation-envelope)runtime-targeted-exit=0— 4 files, 70 testsmeta-object-owd-gate,package-door-declared-code)rest-targeted-exit=0— 2 files, 47 testsdashboard-widget-options)sdui-targeted-exit=0— 1 file, 21 tests; wrapperVERDICT batch-last-exit 0 · held the lock 52snode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack→ 78 commandspnpm check:dispatcher-error-vocabulary(the family this card lives in)pnpm check:error-code-casing·check:nul-bytes·check:published-files·check:pm-widening-tells· speccheck:error-code-provenance·check:api-surface·check:export-origins·check:liveness·check:strictness-ledgerpnpm check:doc-authoring#37241,#80356) → baseline re-derived with the shrink-only census → rerun exit 0check:docs/check:generatedcontract.mdx,error-code-ledger.mdxstale) →check:generated --fixregenerated only that artifact → committed;check:generatedreports 14/15 up to date and the docs artifact regeneratedpnpm check:dual-build-cjs-loadsdistfor 37 packages) → after the tree build, exit 0: "104 published require entry point(s) across 67 package(s) load"pnpm check:type-check-debt--re-measureout of heap, once under a 4 GB override, once under the gate's own pinned 6 GB); a whole-tree family CI runscheck-changeset-fixed, speccheck:meta-url-spelling,check:spec-changes,check:authz-resolver,check:filter-alias-parity,check:partof-closing-keyword,check:error-status-conformance)check:partof-closing-keywordRULE 2 on the three commit messages, via the gate's owncommitRelationsextractor[]for each — no card-relation trailer in any commitAblation — the pin can fail, from the committed state
Subject resolves through source (
error-code-ledger.test.tsimports./error-code-ledger.zod), so nodistrebuild is in the loop. HEAD blob of the ledgera6912719a2cf3e6fc2c6cf80d44c95c3cb26f6ba; restore undertrap … EXIT INT TERMasgit checkout HEAD -- ABSOLUTE_PATH.'WALLED_MEMBERSHIP_POLICY_UNDECLARED',— anchor count 1 → 0 on disk; mutated blob809dde94771c…differs from HEAD.check:dispatcher-error-vocabulary→ exit 1:[unclassified-site] packages/plugins/organizations/src/membership-policy-gate.ts stamps unregistered code 'WALLED_MEMBERSHIP_POLICY_UNDECLARED' (classconst) and packages/runtime/src/dispatcher-error-vocabulary.ts does not classify it.× accepts the #16649 batch — the fourteen remaining door:none codes …, 1 failed | 19 passed.git diff HEAD --statempty,git status --porcelainempty, restored blob equals the HEAD blob, anchor count back to 1.What stays open on #16649 (the second half, not in this PR)
The card's second bullet — widen
SPEC_SOURCE_FACEinscripts/check-dispatcher-error-vocabulary.mjsfrompackages/spec/src/to every published package'ssrc/, and retire theboot-refusalverdict — is not here, for two reasons that the PM should weigh rather than this seat:pending-registrationtoo, and the twopending-registrationrows (AMBIGUOUS_METADATA_STEMunderpackages/metadata,owd_widening_forbiddenunderpackages/plugins/plugin-security) sit in published packages. Applied verbatim, "spec-face rule for every published package" reds both — and the triage's item 2 says those two are not this card's. Whether the widened rule keeps apending-registrationallowance outside spec until spec: register the dispatcher conformance gate's reported error codes in ERROR_CODE_LEDGER (spec half of the #8087 ruling) #8846 lands, or refuses only the retiredboot-refusalverdict, is a contract-shape question for the follow-up.Retiring the verdict now would also be premature by the gate's own design: the vocabulary's comment says a future pre-HTTP producer the scan finds lands as
unclassified-site, takesboot-refusal, then a registration, then comes out again — the verdict is the declared holding state for that cycle until the widened rule replaces it.验收备注
分诊席的验收口径逐条对照:
stale-row棘轮退出,本树上boot-refusal行数 = 0。✅pending-registration行(AMBIGUOUS_METADATA_STEM、owd_widening_forbidden)未动。✅boot-refusal判词:⛔ 不在本 PR(见上节 "What stays open"),本 PR 用Part of,卡片保持打开,由 PM 决定是拆卡还是同一认领续做。check:dispatcher-error-vocabulary与台账 pin 同时转红(见 Ablation)。packages/plugins/organizations/package.json的name是@objectstack/organizations。✅@objectstack/spec: minor。六个戳出包的源码与dist均未变(stamp 站点在origin/main与分支之间零 diff),@objectstack/runtime的 vocabulary 模块不在其发布入口内(dist/index.js零命中),故不欠各包 changeset。content/docs/releases/未碰。✅MONGODB_MULTI_TENANT_UNSUPPORTEDmay be registered-but-unemittable in the error-code ledger — a boot refusal never reaches a wire envelope #8035 反转已在台账头部、行注释、测试与 changeset 四处点名为依 [Decision] Clause ② on an UNREGISTERED error code carried by a thrown value: #14552 landedno, #15963 landsyes, and they are the same class #16404 的有意反转。✅Container & model:
M,mode:cloud(resumption),model: claude-fable-5-1(CONTRACT_REVIEW_TIER, passed explicitly on this dispatch). dispatch-gates--tierfor this dispatch printed a "Clause ② SUSPECT surface" block namingpackages/spec/src/api/error-code-ledger.zod.tsand…error-code-ledger.test.tsunderpackages/spec/src/**— *"the contract surface (error-code ledger, .zod.ts contract schemas) — the normal landing zone of a clause-② card", plus "whichever tier is dispatched, the PR's actual diff passes the clause-② enqueue gate before the card may enqueue." Card content widens the published error-code ledger ⇒ 强制条款② ⇒ fable.noted, not filed:
pnpm check:type-check-debt(--re-measure) ran out of heap on this container twice — once under a 4 GB override, once under the gate's own pinned 6 GB ceiling — and exited 3 (PREREQUISITE NOT MET, nothing measured). A whole-tree family CI owns; not a defect in the tree. 承接者:无(CI)。stale-rowrule makes the vocabulary write the mechanical consequence of the ledger write. Process note for the PM's file-surface template on this class. 承接者:PM。Generated by Claude Code