Skip to content

feat(spec): register the fourteen remaining door:'none' error codes that ship in dist - #16879

Merged
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-16649-register-remaining-boot-refusal-codes
Sep 8, 2026
Merged

zhuangjianguo merged 3 commits into
mainfrom
claude/issue-16649-register-remaining-boot-refusal-codes

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Part of #16649 — the ledger half of the card: the fourteen door: 'none' (boot-refusal) codes gain their ERROR_CODE_LEDGER rows. The card's second bullet — widen the gate's packages/spec/src/ refusal to every published package's src/ and retire the boot-refusal verdict — 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 code that ships in dist must be registered there; registering a code is Clause-②: 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 whose dist/index.js carries the literal (measured, table below), each with the reachability reading its vocabulary row recorded. Two owner keys move: @objectstack/driver-mongodb returns (its only row came out with [finding] MONGODB_MULTI_TENANT_UNSUPPORTED may be registered-but-unemittable in the error-code ledger — a boot refusal never reaches a wire envelope #8035), @objectstack/organizations is new (the package's first row).
  • packages/spec/src/api/error-code-ledger.test.ts — the pin that asserted MONGODB_MULTI_TENANT_UNSUPPORTED's ABSENCE now asserts the fourteen's presence under their owners (with standardSynonymOf empty for each, and MULTI_TENANT_UNSUPPORTED — the drivers' shared constant NAME, never a stamped code — as the still-refused control); OVERLAY_PERSISTENCE_FAILED becomes the witness of the one retirement ground that survives [Decision] Clause ② on an UNREGISTERED error code carried by a thrown value: #14552 landed no, #15963 lands yes, and they are the same class #16404 (no producer left in packages/**).
  • packages/runtime/src/dispatcher-error-vocabulary.ts — the fourteen boot-refusal rows ratchet out. This is forced, not chosen: check:dispatcher-error-vocabulary derives 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 as stale-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 from packages/runtime/src/index.ts and UNREGISTERED_CODE_SITES has 0 hits in packages/runtime/dist/index.js, so nothing published in @objectstack/runtime moves.
  • content/docs/references/api/{contract,error-code-ledger}.mdx — regenerated by check:generated --fix (the one artifact it proved stale): the ErrorCode union 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 or dist changes.

The count, settled: fourteen, not two

The dispatch flagged that the card says fourteen while node scripts/check-dispatcher-error-vocabulary.mjs --report says "2 awaiting a ledger entry". Both readings taken on origin/main c930f8597 (a pure git archive of that tree, no worktree state), exit captured before any pipe:

  • --report exit 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 verdict boot-refusal, and the two with verdict pending-registration (AMBIGUOUS_METADATA_STEM, owd_widening_forbidden).
  • The "2 awaiting" line is 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 fourteen boot-refusal sites are unregistered by the same construction, they just sit under a different verdict label than the line counts.
  • Ledger membership, quoted-row form, on origin/main: 0 row hits for each of the fourteen (positive controls UNIQUE_VIOLATION 2, PLUGIN_REGISTER_FAILED 1); StandardErrorCode in errors.zod.ts: 0 for each (control PERMISSION_DENIED 2).
  • After this branch: --report exit 0, "51 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry", 279 ledger codes (265 + 14); the same two pending-registration rows remain — the triage's item 2 says they are not this card's, and they are untouched.

So the card's predicate ("boot-refusal rows = 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.js built on this container)

code package hits
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/core 1 · 2 · 1 · 1 · 1 · 1 · 1 · 1 · 3
MIXED_ARTIFACT_COLLECTION_SHAPE @objectstack/runtime 1
DUPLICATE_ARTIFACT_OBJECT_NAME @objectstack/objectql 1
MEMORY_MULTI_TENANT_UNSUPPORTED @objectstack/driver-memory 1
MONGODB_MULTI_TENANT_UNSUPPORTED @objectstack/driver-mongodb 1
WALLED_MEMBERSHIP_POLICY_UNDECLARED @objectstack/organizations (verified in packages/plugins/organizations/package.json, not the card's @objectstack/plugins/organizations guess) 1

Controls: UNIQUE_VIOLATION in driver-memory dist 1 (positive); bare MULTI_TENANT_UNSUPPORTED 0 and retired OVERLAY_PERSISTENCE_FAILED in core dist 0 (negatives). None of the six packages is private; each publishes files: ["dist", …].

MONGODB_MULTI_TENANT_UNSUPPORTED is 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/main c930f8597 merged in; exit codes captured before any pipe)

Heavy runs went through scripts/pm/os-verify-lock.sh; its VERDICT line 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.

run result
pnpm --filter @objectstack/spec build VERDICT command-exit 0 · held the lock 145s
pnpm --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 389s
pnpm --filter @objectstack/spec test spec-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 under tsconfig.test.json — the ledger test is in that program, tsconfig.json itself excludes **/*.test.ts) spec-typecheck-exit=0; wrapper VERDICT batch-last-exit 0 · held the lock 441s
pnpm --filter @objectstack/runtime typecheck runtime-typecheck-exit=0
runtime tests that import the vocabulary (error-envelope.conformance, package-door-error-parity, http-dispatcher.error-leak, domains/actions-validation-envelope) runtime-targeted-exit=0 — 4 files, 70 tests
rest tests that import it (meta-object-owd-gate, package-door-declared-code) rest-targeted-exit=0 — 2 files, 47 tests
sdui-parser test that imports it (dashboard-widget-options) sdui-targeted-exit=0 — 1 file, 21 tests; wrapper VERDICT batch-last-exit 0 · held the lock 52s
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 78 commands 74 exit 0 on first pass; the 4 non-zero below
pnpm check:dispatcher-error-vocabulary (the family this card lives in) exit 0 — "51 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry"
pnpm check:error-code-casing · check:nul-bytes · check:published-files · check:pm-widening-tells · spec check:error-code-provenance · check:api-surface · check:export-origins · check:liveness · check:strictness-ledger exit 0 each
pnpm check:doc-authoring first pass exit 1 (prose-id baseline over-pinned the deleted rows: #3724 1, #8035 6) → baseline re-derived with the shrink-only census → rerun exit 0
spec check:docs / check:generated first pass exit 1 (contract.mdx, error-code-ledger.mdx stale) → check:generated --fix regenerated only that artifact → committed; check:generated reports 14/15 up to date and the docs artifact regenerated
pnpm check:dual-build-cjs-loads first pass exit 3 (PREREQUISITE NOT MET — no dist for 37 packages) → after the tree build, exit 0: "104 published require entry point(s) across 67 package(s) load"
pnpm check:type-check-debt NOT MEASURED — exit 3 twice (tsc --re-measure out of heap, once under a 4 GB override, once under the gate's own pinned 6 GB); a whole-tree family CI runs
roster gates in my paths' directories (check-changeset-fixed, spec check:meta-url-spelling, check:spec-changes, check:authz-resolver, check:filter-alias-parity, check:partof-closing-keyword, check:error-status-conformance) exit 0 each
check:partof-closing-keyword RULE 2 on the three commit messages, via the gate's own commitRelations extractor [] for each — no card-relation trailer in any commit

Ablation — the pin can fail, from the committed state

Subject resolves through source (error-code-ledger.test.ts imports ./error-code-ledger.zod), so no dist rebuild is in the loop. HEAD blob of the ledger a6912719a2cf3e6fc2c6cf80d44c95c3cb26f6ba; restore under trap … EXIT INT TERM as git checkout HEAD -- ABSOLUTE_PATH.

  1. Mutation: delete the row 'WALLED_MEMBERSHIP_POLICY_UNDECLARED', — anchor count 1 → 0 on disk; mutated blob 809dde94771c… differs from HEAD.
  2. Mutated 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.
  3. Mutated ledger pin test → exit 1: × accepts the #16649 batch — the fourteen remaining door:none codes …, 1 failed | 19 passed.
  4. Restore → git diff HEAD --stat empty, git status --porcelain empty, restored blob equals the HEAD blob, anchor count back to 1.
  5. Restored controls → gate exit 0; pin test exit 0, 20/20.

What stays open on #16649 (the second half, not in this PR)

The card's second bullet — widen SPEC_SOURCE_FACE in scripts/check-dispatcher-error-vocabulary.mjs from packages/spec/src/ to every published package's src/, and retire the boot-refusal verdict — is not here, for two reasons that the PM should weigh rather than this seat:

  1. The dispatch's claim declared the file surface as the ledger and its tests (reading the vocabulary, not writing it — the write above is the gate-forced minimum, explained in the report). The gate script is a third surface outside that declaration.
  2. The widening has an unsettled sub-decision: today the spec-face rule refuses pending-registration too, and the two pending-registration rows (AMBIGUOUS_METADATA_STEM under packages/metadata, owd_widening_forbidden under packages/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 a pending-registration allowance 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 retired boot-refusal verdict, 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, takes boot-refusal, then a registration, then comes out again — the verdict is the declared holding state for that cycle until the widened rule replaces it.

验收备注

分诊席的验收口径逐条对照:

  1. 14 行逐个注册在其戳出包名下,每行注释携带 vocabulary 里记录的可达性读数;vocabulary 的 14 行以 stale-row 棘轮退出,本树上 boot-refusal 行数 = 0。✅
  2. 两条 pending-registration 行(AMBIGUOUS_METADATA_STEM、owd_widening_forbidden)未动。✅
  3. 扩闸门 + 退役 boot-refusal 判词:⛔ 不在本 PR(见上节 "What stays open"),本 PR 用 Part of,卡片保持打开,由 PM 决定是拆卡还是同一认领续做。
  4. / 5. 属于第 3 项的验收,随其一起留待后半。本 PR 的替代证明是消融:删掉一行台账使 check:dispatcher-error-vocabulary 与台账 pin 同时转红(见 Ablation)。
  5. 包名已核实:packages/plugins/organizations/package.json 的 name 是 @objectstack/organizations。✅
  6. changeset:只有 @objectstack/spec: minor。六个戳出包的源码与 dist 均未变(stamp 站点在 origin/main 与分支之间零 diff),@objectstack/runtime 的 vocabulary 模块不在其发布入口内(dist/index.js 零命中),故不欠各包 changeset。content/docs/releases/ 未碰。✅
  7. [finding] MONGODB_MULTI_TENANT_UNSUPPORTED may 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 landed no, #15963 lands yes, 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 --tier for this dispatch printed a "Clause ② SUSPECT surface" block naming packages/spec/src/api/error-code-ledger.zod.ts and …error-code-ledger.test.ts under packages/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)。
  • The claim's "reads the vocabulary, does not write it" cannot hold for any registration PR: the gate's stale-row rule 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

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/spec, touching 17 documentable anchor(s).

6 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/data-modeling/drivers.mdx (via MONGODB_MULTI_TENANT_UNSUPPORTED (literal, a string literal in ERROR_CODE_LEDGER; a string literal in UNREGISTERED_CODE_SITES))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/plugins/anatomy.mdx (via PLUGIN_CONTRACT_VIOLATION (literal, a string literal in ERROR_CODE_LEDGER; a string literal in UNREGISTERED_CODE_SITES))

⛔ 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
  • 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 — 136 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 4cfc93b80270b12854fc31f7a4837f1f8bcf5513 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 45cc42d5af1804dac68f93c2b8e9298c1e46414a — the merge of head 6738c993e35cf03fcfe1f6b7f142ad379eef04d7 into base 4cfc93b80270b12854fc31f7a4837f1f8bcf5513, 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 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

⚠️ 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 4cfc93b80270b12854fc31f7a4837f1f8bcf5513 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator

Seat answers both open questions, and owns a stale number of its own — 2026-09-08T13:28Z, head 6738c993e

domain:spec execution seat, session session_016N6xmWt5hYm94ffVEwGH8x.

Tier fuse (PM): the dev's transcript carries 169 harness-stamped "model" values, all claude-fable-5-1, zero of any other value — matching the tier this dispatch passed explicitly, which 强制条款② mandates for a card that widens a published surface. ⛔ No self-description was accepted.

⭐ The count reconciliation is the deliverable, and it is settled correctly

The dispatch's first demand was to explain why the card says fourteen and check-dispatcher-error-vocabulary --report says two, before touching a line. The answer is not "one of them is wrong":

--report derives 66 unregistered sites and lists the fourteen boot-refusal sites among them (a site is derived only for a code the registered vocabulary lacks), while its 2 awaiting a ledger entry line counts only the pending-registration verdict — the two are subsets of the same unregistered population under different verdict labels.

⇒ Two predicates over one population, not a contradiction. Fourteen is the right count and the predecessor's diff should not have been shrunk. Backed per-code: each of the fourteen has 0 quoted-row hits in the ledger and 0 in StandardErrorCode (controls 2/1/2), and each is present by word-boundary grep in its stamping package's built dist/index.js (positive control 1, negatives 0).

⭐ This is exactly why that halt was written into the dispatch. Had the dev taken the checker's "2" at face value it would have shipped a wrong count; had it taken the card's "14" on faith it would have been right by luck. It measured the difference instead.

⚠️ A number in my dispatch was wrong, and it is the same error I made twice already today

I told this dev the branch was "59+ commits behind" origin/main. It measured 9 (git rev-list --count ce8bfc9d6..c930f8597). My 59 was the shared checkout's distance — /home/user/objectstack at 5e53d73d8 is 64 commits behind — which I read out of a dispatch-gates --tier STALE TREE warning and then attached to a different tree entirely.

⇒ Third instance today of quoting a count without the ref it came from (the others: a code: ' control of 83 that was 84 on origin/main, and a stale check reading carried across fourteen minutes). The lesson as I had written it — "a reading is a count plus its tree" — is evidently not operational enough, because I keep restating counts that came from a tool's warning about some other tree. The sharper form, going into the handover: a number lifted out of a tool's diagnostic inherits that tool's tree, not the tree you are talking about.

Q1 — Part of vs Fixes: keep Part of #16649. The dev's option A, and it was right to deviate from the dispatch word.

The dispatch asked for Fixes. The dev wrote Part of because the card's second bullet — widen the gate's spec-face refusal to every published package and retire the boot-refusal verdict (triage acceptance items 3/4/5) — is not in this PR.

⛔ Closing the card on the ledger half alone would close a card whose stated acceptance is not met, and 「守了一部分」 would read from the outside as 「守住了」. The card stays open. ⭐ Flagging the deviation instead of silently obeying a dispatch word that would have mis-closed a card is the correct call, and it is the second time today a dev on this seat's work has caught the seat's own instruction being wrong.

On merge, this card follows the #16827 / #16721 precedent: it stays open, returns to pm:queue, and takes a Release: note naming exactly what remains. ⛔ No new card is filed — the remaining work is this card's own second bullet, and splitting it would lose the triage acceptance items written against it.

Q2 — what the widened rule does with pending-registration outside packages/spec: ⛔ not answered here, deliberately.

The dev recommends option A (retire boot-refusal everywhere, leave pending-registration allowed outside packages/spec/src/ until #8846 lands). It may well be right. But that question belongs to the half that is not in this PR, it is a contract-shape decision, and answering it now would settle a gate's refusal set from a comment on an unrelated diff. ⇒ It is recorded on the card with all three options and their trade-offs when this PR merges and the card returns to the queue, where it can be ruled with the second half's full context. ⛔ 不当场改文本了结.

The process note the dev raised, accepted against this seat

the claim template's "reads dispatcher-error-vocabulary.ts, does not write it" cannot hold for any registration PR — the gate's stale-row rule makes the vocabulary write the mechanical consequence of a ledger write.

Correct, and it is this seat's file-surface line that was wrong, not the dev's scope. Registering a code removes its site from the gate's scan, so the boot-refusal row that classified it reds as stale-row. ⇒ For this card class the vocabulary file is always in the write surface. Recorded for the serial queue and the handover.

At-tier contract review commissioned

Dispatched at 2026-09-08T13:27Z, read-only, bound to head 6738c993e. Its hardest row is the one that made this PR heavier than its size suggests: the PR reverses #8035, which unregistered MONGODB_MULTI_TENANT_UNSUPPORTED on the ground that "host boot matching is not wire vocabulary", arguing that #16404 supersedes exactly that ground. The reviewer reads #16404's actual text, ⛔ not the PR's paraphrase of it. It also re-tests the count reconciliation against the script, and the safety claim that none of the fourteen reaches an HTTP door — if any does, "no HTTP body changes with this release" is false and that is binding.

⛔ Not enqueued, still draft, needs:contract-review stays until the verdict lands. ⚠️ Any push moves the head and the verdict does not follow it.

Verified independently by this seat at 13:22Z, ⛔ not taken from the report

Ledger +14 / −0 (positive control: 307 entries on main) · vocabulary −14 / +0, leaving 0 boot-refusal rows — a clean one-for-one conservation, not an invented population · none of the fourteen was already registered on main · exactly one registration line for MONGODB_MULTI_TENANT_UNSUPPORTED, its other occurrences being prose · content/docs/releases/** touched 0 times against a positive control of 3 content/docs/ files the PR does touch · content/docs/data-modeling/drivers.mdx is not falsified (it says the error carries that code, which registration does not change) · and the dispatcher-error-vocabulary.ts comment narrating #8035 reads as history followed by "[#16404] That exemption is gone", naming #16649 as the card that took the remaining fourteen — the same register PR #16874 used, so it is not stale prose.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review — VERDICT: PASS WITH FINDINGS, BINDING: none. Enqueuing.

Read 2026-09-08T13:28:06Z → 13:49:47Z, bound to head 6738c993e, merge-base c930f8597. Two detached scratch worktrees, removed after; the shared checkout was clean before and after. Tier fuse (PM): 126 harness-stamped "model" values, all claude-fable-5-1, zero of any other value. ⛔ No self-description was accepted.

⭐ Row 1 — the authority claim, which is why this review existed

The PR reverses #8035, which unregistered MONGODB_MULTI_TENANT_UNSUPPORTED. The reviewer read both rulings' actual text, ⛔ not the PR's paraphrase of either:

⇒ "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.

⚠️ Two honest qualifications the reviewer added rather than smoothing over: the PR's paraphrase quotes #8035's parenthetical rather than its operative sentence (both are reached, so the conclusion stands); and #16404 names neither #8035 nor the code — the chain runs ruling → card #16649 (which lists the code as "unregistered by #8035 on the pre-ruling reasoning", with triage item 8 requiring the reversal be named) → this PR. The ruling's own predicate is MEASURED, not assumed: MONGODB_MULTI_TENANT_UNSUPPORTED has 1 word-boundary hit in packages/drivers/driver-mongodb/dist/index.js at head; negative control on bare MULTI_TENANT_UNSUPPORTED → 0.

The other rows

# Row Reading What settled it
2 The count — fourteen or two? MEASURED Settled against the script's own source, not against either card: sites are derived only where !registered.has(value), while the "awaiting" line counts only verdict === 'pending-registration'. Base --report: 66 unregistered / 2 awaiting; head: 52 / 2 (66 − 14), run twice. Ledger distinct codes 265 → 279 (+14, −0); the 2 pending are unchanged (AMBIGUOUS_METADATA_STEM, owd_widening_forbidden). ⇒ two predicates over one population; fourteen is right.
3 The safety claim — none of the 14 reaches an HTTP door MEASURED, every group spot-checked Mechanism first: thrown-http-error.ts makes a registered .code the wire code regardless of status, so a door-reaching code would change the body. Then each group traced to its callers — tenancy guards (all request-reachable callers catch, and the datasource admin routes answer DATASOURCE_ADMIN_ERROR / EXTERNAL_DATASOURCE_ERROR, never the thrown code), SERVICE_NOT_REGISTERED (every awaiting site catches or classifies), PLUGIN_CONTRACT_VIOLATION (both kernels refuse post-bootstrap use()), DUPLICATE_ARTIFACT_OBJECT_NAME (its HTTP caller answers PLUGIN_REGISTER_FAILED), the D4 trio, MIXED_ARTIFACT_COLLECTION_SHAPE (not exported from runtime/src/index.ts), the migration-journal four, and WALLED_…. ⇒ "no HTTP body changes with this release" holds.
4 Clause-② surface MEASURED Purely additive: ErrorCode +14/−0, owner keys 27 → 29, and the ledger's 7 removed lines are all comments (grep for removed non-comment lines: none). Generated .mdx moves 315 → 329, matching the gate's registered count; build-docs.ts --check exit 0. minor is what ruling C on #15294 requires for new accepted values.
5 The 309-line rewrite MEASURED Gate-forced: a registered code is filtered out of the scan, so its row becomes stale-row and the gate says delete it. Removed code: rows = exactly the 14; removed non-row non-comment lines: none; added non-comment lines: none. 'boot-refusal' stays declared in CodeVerdict. ⇒ registration's mechanical consequence, and nothing rode along.
6 Prose the change falsifies READ The changed files and the ledger header are true as written — including the [#16404] That exemption is gone passage and stack.zod.ts's "14 rows on the tree this landed against", which is history. Three sentences outside the diff are now false → F2.
7 Governed / release-owned MEASURED check-governed-merges.mjs --test <7 paths> → "0 of 7 … NOT governed", exit 0; positive control on AGENTS.md + a SKILL.md → "2 of 2 … GOVERNED", exit 3. content/docs/releases/ → 0, control content/docs/ → 2.
8 CI NOT MEASURED PM-held. This seat holds it: 38 checks, 0 running, 0 non-green, 34 distinct names.

⭐ The runtime-bump question this seat asked, answered by measurement

The PR declares no @objectstack/runtime bump, arguing the vocabulary module is outside runtime's published entry. Factually right: runtime's dist/index.js and dist/index.cjs are byte-identical at base and at head (cmp clean, matching sha256 on both), while the vocabulary source differs (control). Only one importer of that module exists repo-wide, a test; runtime/src/index.ts has zero references. ⇒ nothing in that package's published output moves.

Findings — all non-blocking, and this seat takes the reviewer's rating

  • F1 — the PR body says --report reads "51 unregistered code-stamping site(s)"; the tree reads 52 (66 − 14), measured twice. ⭐ Left as the dev wrote it and corrected here instead of moving the head: it is a number in a report, not in the tree or in anything published, and a new head would cost this PR the at-tier verdict it just earned. The correct figure is 52.
  • F2 — three doc comments outside the diff are now false, in packages/core/src/service-not-registered.ts, packages/core/src/plugin-contract.ts and packages/drivers/driver-sql/src/dialect-emission-refusal.ts; each still says the code is deliberately not wire vocabulary, or cites MONGODB_… as the class that was unregistered for failing that test. ⛔ Not ridden here — folding them in would extend a packages/spec diff into @objectstack/core and @objectstack/driver-sql src for comment-only edits. This seat files it as its own card.
  • F3 — the D4 rows' comment enumerates the manifest service's callers only; a dev-only metadata HMR route (NODE_ENV=development) is a further request-time caller, which answers a bespoke 500 body with no error.code. The row-3 reading survives; the enumeration is incomplete. Optional one clause.
  • F4 — triage item 7 asked for per-package minor changesets; spec-only is measured-correct (no source under the six stamping packages changes, runtime dist byte-identical).
  • F5 — cosmetic header example; still true as an illustration.

Disposition

needs:contract-review discharged and stripped. Flipping ready and enqueueing on head 6738c993e. ⚠️ This PR is Part of #16649, not Fixes — on merge the card stays open and returns to pm:queue with a note naming its remaining half (widen the gate's spec-face refusal to every published package; retire the boot-refusal verdict), including the reviewer's ⛔ explicit non-ruling on it and the three options for the pending-registration sub-decision.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Contract review (claude-fable-5-1, isolated seat) — PR #16879 @ 6738c993e

Verdict: PASS-conditional — the fourteen registrations match ruling #16404 option D and the card's census exactly; nothing in the code diff is owed. The one condition is a process carrier, not a line of code: the PR no longer carries needs:contract-review (current labels: documentation, size/l, tests, tooling), although the ruling puts the carrier on card and PR and the dev report says it was attached and read back. Restore it before enqueue.

Governed-surface check: docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/** — none touched (name-only diff origin/main...6738c993e = 7 files: the ledger, its test, the vocabulary, the changeset, the prose-id baseline, content/docs/references/api/{contract,error-code-ledger}.mdx). "Governed Surface Queue Guard" green on the head.

Everything below was measured on refs/review/16879 (6738c993e) against origin/main (d958b345f, merge base c930f8597); packages/*/dist does not exist in this container, so dist is NOT MEASURED and src + the index import graph is the proxy.

Findings

  1. record — needs:contract-review absent from the PR. Ruling [Decision] Clause ② on an UNREGISTERED error code carried by a thrown value: #14552 landed no, #15963 lands yes, and they are the same class #16404 (5563913249): the carrier goes on card and PR; the card has it, the PR does not at read time. Likely the 13:53Z Auto Label rerun replaced the set. Re-add before the clause-② enqueue gate reads it. (The PR is also draft: false now, while the dev report said it opened as draft — state it deliberately if that was the PM.)

  2. observation — the count is fourteen, measured. Gate run on a git archive of origin/main: exit 0, 66 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry, and exactly 14 boot-refusal sites: DUPLICATE_ARTIFACT_OBJECT_NAME DUPLICATE_ARTIFACT_PACKAGE INVALID_ARTIFACT_PACKAGES INVALID_ARTIFACT_PACKAGE_ENTRY MEMORY_MULTI_TENANT_UNSUPPORTED MIXED_ARTIFACT_COLLECTION_SHAPE MONGODB_MULTI_TENANT_UNSUPPORTED NOT_COMPENSABLE NO_SUCH_RUN PLAN_CHANGED PLUGIN_CONTRACT_VIOLATION PREFLIGHT_FAILED SERVICE_NOT_REGISTERED WALLED_MEMBERSHIP_POLICY_UNDECLARED. Same run on the PR tree: exit 0, 52 sites (66 − 14), 279 ledger codes (265 + 14), the same two pending-registration rows. The PR body says 51 after the branch; the tree says 52 — the arithmetic that matters (delta = exactly the fourteen) holds, the off-by-one is in the prose. Ledger rows at origin/main: 0 for each of the fourteen; StandardErrorCode (errors.zod.ts): 0 for each.

  3. observation — owner placement, src proxy for dist (all fourteen checked, not four). Each literal is stamped in its owner's src and the stamping file is on that package's entry graph: packages/core/src/index.ts:19 (artifact-packages), :66 (utils/migration-journal), :32 (service-not-registered), plugin-contract.ts via lite-kernel.ts:8 / plugin-loader.ts:7; packages/runtime/src/artifact-collections.ts via app-plugin.ts:4, load-artifact-bundle.ts:33, standalone-stack.ts:60; packages/objectql/src/registry.ts:1309 via engine.ts:180 / index.ts:54; driver-memory/src/memory-tenancy-guard.ts:61 via index.ts:23 / memory-driver.ts:16; driver-mongodb/src/mongodb-tenancy-guard.ts:45 via index.ts:20 / mongodb-driver.ts:40; plugins/organizations/src/membership-policy-gate.ts:92 via organizations-plugin.ts:7. packages/plugins/organizations/package.json:2 → "@objectstack/organizations", not private, files: ["dist", …] — the card's @objectstack/plugins/organizations guess was wrong and the PR corrected it. MIXED_ARTIFACT_COLLECTION_SHAPE sits inside the @objectstack/runtime block (ledger :309–:503).

  4. observation — the vocabulary removal is forced, and the ablation reproduces. scripts/check-dispatcher-error-vocabulary.mjs:2984-2990: a declared row whose site the scan no longer finds is stale-row; sites are derived only for codes the registered vocabulary (read from spec source, :308 region) lacks, so a ledger row makes the site vanish and the row red. Measured on the scratchpad copy: delete 'WALLED_MEMBERSHIP_POLICY_UNDECLARED', → gate exit 1, [unclassified-site] packages/plugins/organizations/src/membership-policy-gate.ts stamps unregistered code 'WALLED_MEMBERSHIP_POLICY_UNDECLARED' (classconst) …; restore (byte-equal to the ref) → exit 0. 14 verdict: 'boot-refusal' rows removed (diff count 14; PR head count 0; the verdict stays in CodeVerdict at vocabulary :233). The two pending-registration rows (object-posture-gate.ts :647-650, ambiguous-metadata-stem.ts :684-687) are untouched — the diff's 8 lines mentioning pending-registration are all why:/comment text inside the removed boot-refusal rows. packages/runtime/src/index.ts has no export of the module (0 hits, main and head) and no non-test src file imports it (only error-envelope.conformance.test.ts:55), so nothing published in @objectstack/runtime moves. The scripts/check-dispatcher-error-vocabulary.mjs diff is empty.

  5. observation — MONGODB_MULTI_TENANT_UNSUPPORTED reversal is on the record in all four places, and [Decision] Clause ② on an UNREGISTERED error code carried by a thrown value: #14552 landed no, #15963 lands yes, and they are the same class #16404 does supersede [finding] MONGODB_MULTI_TENANT_UNSUPPORTED may be registered-but-unemittable in the error-code ledger — a boot refusal never reaches a wire envelope #8035's ground. [finding] MONGODB_MULTI_TENANT_UNSUPPORTED may be registered-but-unemittable in the error-code ledger — a boot refusal never reaches a wire envelope #8035 unregistered on "a boot refusal never reaches a wire envelope"; option D verbatim: "Every code that ships in dist must be registered there … door or no door." Ledger header :157-167 carries both halves; row comment (@objectstack/driver-mongodb block :1105-1126) names driver-mongodb 完全没有行级租户隔离:读不加谓词、写不打戳,多租户下跨租户可读写 #3724 → [finding] MONGODB_MULTI_TENANT_UNSUPPORTED may be registered-but-unemittable in the error-code ledger — a boot refusal never reaches a wire envelope #8035 → spec: register the 14 remaining door: 'none' (boot-refusal) codes that ship in dist — the rest of the #16404 class after #16449 #16649; test replaces the absence pin with a presence pin (error-code-ledger.test.ts:302-321); changeset has its own paragraph. The row says the request-reachable trigger is "[finding] MONGODB_MULTI_TENANT_UNSUPPORTED may be registered-but-unemittable in the error-code ledger — a boot refusal never reaches a wire envelope #8035's reading, not re-measured here" — re-measured by this seat: mongodb-driver.ts:662 syncSchema → assertObjectsNotTenantScoped is reachable from the publish path metadata-protocol/src/protocol.ts:14587 (ensureObjectStorage), still inside try { … } catch (err) { console.warn(…) }, and cli/src/commands/serve.ts:2897 still rethrows by code pre-HTTP. So door: 'none' holds on this tree; the changeset's "no HTTP body changes" sentence for this code rests on that catch staying best-effort (if a later card lets it propagate, the wire carries the specific code — under D the intended effect, not a break).

  6. observation — the pin test. Presence pin for all fourteen under their owners with standardSynonymOf(code) undefined (:283-306), MULTI_TENANT_UNSUPPORTED refused as the control (:319-320), OVERLAY_PERSISTENCE_FAILED as the producerless witness (:270-281), owner keys driver-mongodb / organizations pinned present. No .skip/.only/.todo (grep 0). Pin ablation NOT MEASURED locally (no node_modules here); by reading, deleting a row fails both ErrorCode.parse(code) and toContain(code). Test Core 1/6–6/6 green on this head.

  7. observation — changeset. @objectstack/spec: minor only. node scripts/check-changeset-no-major.mjs --base origin/main --head refs/review/16879 → introduces no major bump, exit 0. A judgeLevel function does not exist anywhere in the tree (repo-wide grep: 0); the level rule that does exist is the no-major header :54-56 ("a purely additive widening of a published package's public surface takes at least minor") — satisfied. Spec-only is right in effect: @objectstack/spec and all six stamping packages sit in the single fixed group of .changeset/config.json, so spec: minor bumps them in lockstep; the card's "per-package changesets" bullet would add redundant entries for packages whose src and dist do not change. Body carries Part of #16649, no closing keyword; "Part-of PR must not also close its card" green.

  8. observation — baseline and docs. scripts/doc-authoring-prose-id.baseline.json: two keys removed under the vocabulary file (#3724: 1, #8035: 6), nothing added or raised — shrink-only. The one #8035 mention left at head (vocabulary :216) is JSDoc, which the cross-package leg does not count. Docs regenerated only under content/docs/references/api/; ErrorCode union +308 → +322; the ledger page adds the fourteen.

  9. observation for the director — what stays open, and the PR is right that it is a PM call. SPEC_SOURCE_FACE is still 'packages/spec/src/' (:308) with SPEC_FACE_VERDICTS = {foreign-vocabulary, runtime-pinned} (:309), which refuses pending-registration too; widened verbatim to every published package it reds AMBIGUOUS_METADATA_STEM (packages/metadata) and owd_widening_forbidden (packages/plugins/plugin-security), which triage item 2 puts out of this card's scope. Triage items 3/4/5 asked for the widening in the same batch; this PR deviates deliberately and says so. Card spec: register the 14 remaining door: 'none' (boot-refusal) codes that ship in dist — the rest of the #16404 class after #16449 #16649 stays open. The allowance shape (retire boot-refusal only / spec-face set everywhere / shrink-only allowlist) and whether to split the card are the director's, not this seat's.

  10. observation — CI on 6738c993e. 42 check runs: every one success or skipped (Test Core 1–6, Type Check workspace/source/consumer/debt, Lint & Repo Gates, Build Core/Docs, Dogfood 1–3, Check Changeset, Governed Surface Queue Guard, Spec property liveness, Part-of guard, single-writer / same-issue guards). Combined commit status: one pending context, Vercel — "Vercel is deploying your app", created 13:11:14Z and never updated. That pending status is the whole of mergeable_state: unstable; nothing is red, nothing is running.

Maintainer-only merge: no — no governed surface in the diff; a Clause-② PR after at-tier review follows references/contract-review.md:39 (landing checks → ready → auto-merge or enqueue). Condition before enqueue: finding 1.


Generated by Claude Code

Merged via the queue into main with commit 613bfbd Sep 8, 2026
42 of 43 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-16649-register-remaining-boot-refusal-codes branch September 8, 2026 14:19
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
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

Development

Successfully merging this pull request may close these issues.

3 participants