feat(spec): register NAMESPACE_CONFLICT in ERROR_CODE_LEDGER - #16252
Conversation
`POST /api/v1/packages` now answers an install-time namespace collision with `error.code: "NAMESPACE_CONFLICT"` instead of the generic member 422 derives. The refusal has carried the ADR-0112 envelope (`code` + `status: 422`) since the engine half landed, but the code was not an `ErrorCode` member, so the dispatcher door's narrowing demoted the spelling onto the open `declaredCode` sibling and put `VALIDATION_ERROR` in the closed slot. A caller wanting to tell "your namespace is taken, rename it" from every other 422 had to read the channel ADR-0112 declares as not guaranteed. Registered under `@objectstack/objectql`, the package whose `SchemaRegistry.installPackage` stamps it (the only stamp site in `packages/**` non-test source, so the provenance gate is satisfied by the owner key alone). The now-discharged `pending-registration` row ratchets out of `packages/runtime/src/dispatcher-error-vocabulary.ts` in the same commit: registration is what makes that row stale, and both the gate (`check:dispatcher-error-vocabulary`) and the conformance suite's ratchet case red on a registered code that still carries one, so the two halves cannot be split across PRs. `packages/runtime/src/package-door-namespace-conflict-code.test.ts` drives the real route over a real `SchemaRegistry` and asserts the body, so the reachability the removed row asserted without pinning is now held by a test. Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 10 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 — 135 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 dc46442fecd84cfa802ce578cfe33c96de8442b5 && git checkout dc46442fecd84cfa802ce578cfe33c96de8442b5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dacb73f4fc3cd3b3ad55d766047b1e4d6f2c521a eca3ca8e485699c6ded048dfcb72711c95c4dff0 && git checkout -B drift-repro dacb73f4fc3cd3b3ad55d766047b1e4d6f2c521a && git merge --no-ff eca3ca8e485699c6ded048dfcb72711c95c4dff0
node scripts/docs-audit/affected-docs.mjs --json dacb73f4fc3cd3b3ad55d766047b1e4d6f2c521a
|
Contract review (clause ②) — PASS on content · landing held on one cancelled shard — PR #16252 at head
|
…ollows) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
The os-regen driver deferred content/docs/references/api/{contract,error-code-ledger}.mdx
during the merge of origin/main. Regenerated from the merged spec source, so the vocabulary
count reflects the UNION of both sides rather than either side's textual +296.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
落地前检三条,逐条读数 —— 转 ready,挂 auto-merge
① 卡上达档 PASS —— 已备
② 双载体已清 —— 已备,直接 API 读回
⛔ 机读 原因是仪器,不是本 PR:工具需要 label event stream 才能把「载体挂过又被剥掉」和「载体从未挂过」分开 —— 前者是纪律走完,后者是纪律从未生效,两者当下的标签集完全相同。本容器 REST 403,MCP 不暴露 label 事件,离线
它要回答的问题我用更直接的方式回答了:上表两个时刻都是本席自己的写入,挂和剥都在本会话记录里,剥后两边都直接读回 —— 那是记录,不是推断。⛔ 我不声称机读通过。 ③ 检查 —— 必需集全绿,一个分片被杀是已知舰队缺陷
必需的汇总
⛔ 不重跑该分片、⛔ 不抬 ⭐ 这一次的读数本身是给 #16173 的新证据:被杀的是 5/6 而 1/6 通过 —— 暴露面不局限于 1/6 分片。我之前在 #16173 上把它说成 1/6 的问题,那是过窄的,单独去更正。 转 ready 的依据原 body 写着 ⛔ 保持 draft 直到达档 PASS 存在。那个条件现在成立:PASS 在卡上、可点击、档位经 harness 盖章核验。构造取了配额耗尽豁免落在 落地挂 auto-merge,走合并队列。⛔ 不手工合并、⛔ 不绕队列。
Generated by Claude Code Generated by Claude Code |
Fixes #14748
NAMESPACE_CONFLICTis registered inERROR_CODE_LEDGERunder@objectstack/objectql, so the ADR-0048 install-time namespace refusal now carries its own code on the wire instead of riding the opendeclaredCodesibling.Landing round —
mainmerged, the one conflict reconciled by hand, the docs regeneratedThe head was
mergeable_state: dirty. PR #16159 landed onmainafter this branch's base and rewrote the very row this PR deletes inpackages/runtime/src/dispatcher-error-vocabulary.ts(shape: 'classfield'becoming'classconst', since the producer literal became the exportedNAMESPACE_CONFLICT_CODEconstant). Landed viagit merge origin/mainper this repo's convention — no rebase, no force-push.File surface touched this round, declared up front:
packages/runtime/src/dispatcher-error-vocabulary.ts(the conflict), andcontent/docs/references/api/contract.mdx+content/docs/references/api/error-code-ledger.mdx(regenerated). The merge forced nothing outside that list; the PR's file count is unchanged at 6.The conflict. One conflicted path,
dispatcher-error-vocabulary.ts, and it is the whole-block-delete-versus-edit shape: this branch removes theNAMESPACE_CONFLICTrow,mainedited a line inside it. Taken asmain's file with that row block deleted. Verified rather than asserted —git diff origin/main -- packages/runtime/src/dispatcher-error-vocabulary.tsreads 42 deletions, 0 insertions, i.e. exactly the row block and nothing else, andmain's two otherclassconstconversions in the same file are untouched. The row is 42 lines now rather than 38 becausemainadded four comment lines inside it.The generated docs did not text-merge, and the arithmetic is why. Both sides independently bumped
+295to+296onApiErrorSchema.code; a textual merge yields 296 and is silently wrong, becauseFIND_HOOK_RESULT_NOT_ARRAYalso landed onmainin the window. Themerge=os-regendriver deferred both files,scripts/pm/os-regen-merge.sh's ordering was followed (merge committed first, then regeneration as its own commit), and the number was read off the generator rather than predicted:pnpm --filter @objectstack/spec gen:schema && gen:docsproduces+297 more, andcheck:generatedthen reports all 15 artifacts up to date. Againstorigin/mainthe two files differ by exactly 3 lines:+296becoming+297, and oneNAMESPACE_CONFLICTlist row in each file.check:error-code-provenanceis now a LIT instrument on this code. At the pre-merge head its--reportshowed 0 rows forNAMESPACE_CONFLICTagainst 1 for theMULTI_UPDATE_HOOK_KEY_DIVERGENCEcontrol — the gate is blind to class fields, so its green said nothing about the ownership key. After the merge,main's post-#16159registry.tscarriesexport const NAMESPACE_CONFLICT_CODE = 'NAMESPACE_CONFLICT' as const, which the gate'sconstdefpattern does see. Re-measured on the merged head:Both codes at 1 row, gate OK over 311 stamp sites (295 listed, 16 waived). The
@objectstack/objectqlownership key this PR relies on is therefore proven now, not assumed.The wire, measured before and after
Not derived. Both bodies were printed by a throwaway probe driving the shipped route —
HttpDispatcher.handlePackages('', 'POST', manifest, {}, adminCtx)over a realSchemaRegistry, two installs declaring one namespace — withpackages/specrebuilt for each leg. The probe is not in the diff; the durable assertion is, aspackages/runtime/src/package-door-namespace-conflict-code.test.ts.Before (the ledger row removed, spec rebuilt — the ablation leg):
{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Namespace conflict: namespace \"crm\" is already owned by package \"com.acme.crm\", so package \"com.beta.crm\" cannot be installed alongside it. …","httpStatus":422,"declaredCode":"NAMESPACE_CONFLICT"}}After (this branch):
{"success":false,"error":{"code":"NAMESPACE_CONFLICT","message":"Namespace conflict: namespace \"crm\" is already owned by package \"com.acme.crm\", so package \"com.beta.crm\" cannot be installed alongside it. …","httpStatus":422}}Status
422and the message are byte-identical across the two legs (both elided at the same point above). What changes is that the closederror.codeslot carries the producer's own spelling, anddeclaredCodeis gone rather than duplicated — with the code registered,demotedDeclaredCodehas nothing to demote. The first install answered201on both legs, so the refusal is a refusal and not a vacuous second failure.The card's open question: both halves ship in this PR, and the gates leave no choice
The card asked whether the runtime
pending-registrationrow comes out here or in a follow-up. It comes out here, and the answer is mechanical rather than a preference:check:dispatcher-error-vocabularyreportsnow-registered— "the row has done its job and must come out"; anderror-envelope.conformance.test.ts's ratchet case,every pending code is still unregistered, failscheck-dispatcher-error-vocabulary: 1 finding(s) — [unclassified-site] packages/objectql/src/registry.ts stamps unregistered code 'NAMESPACE_CONFLICT' and packages/runtime/src/dispatcher-error-vocabulary.ts does not classify itSo the two halves are mutually required: either one alone is a red PR. This also matches the precedent the card cites —
a5302c7a3/ PR #7224 forFILE_FIELD_BULK_WRITE_REFUSED, and the closerUNIQUE_SCOPE_CONFIRMATION_REQUIREDregistration, whose own changelog entry records that "the now-dischargedpending-registrationrow ratchets out ofpackages/runtime's dispatcher-error-vocabulary table in the same change".The cross-package file surface was declared in the claim comment before any edit, per the cross-domain exception:
packages/spec/src/api/error-code-ledger.zod.tspluspackages/runtime/src/dispatcher-error-vocabulary.ts.What is in the diff
packages/spec/src/api/error-code-ledger.zod.ts— one member on the@objectstack/objectqllist, with the house-style comment recording the producer, why the refusal is wire-reachable, and why it is neither aVALIDATION_ERRORsynonym nor a duplicate of@objectstack/metadata-protocol'sNAMESPACE_PREFIX.standardSynonymOfdoes not flag it (no reason-phrase alias;NAMESPACEis a token no standard member carries), so noSTANDARD_SYNONYM_WAIVERSentry is owed — asserted by the admission gate, not by this paragraph.packages/objectqlis the right owner key, mechanically — the stamp site inpackages/objectql/src/registry.tsis the only one inpackages/**non-test source, socheck:error-code-provenanceis satisfied by the owner key alone. Since objectql: 11 error classes still spell their code as an inline literal, so a consumer cannot follow theby code, not instanceofconvention the docs already teach #16159 that site is the exportedNAMESPACE_CONFLICT_CODEconstant, which the gate can actually see — the reading above.packages/runtime/src/dispatcher-error-vocabulary.ts— the discharged row and its comment block, 42 lines, removed whole.content/docs/references/api/{contract,error-code-ledger}.mdx— regenerated from the merged tree, never by hand and never text-merged. The visible delta againstmainis one list row in each file plus+296 morebecoming+297 moreonApiErrorSchema.code— the union of both sides' additions, which is the contract change stated as a number.packages/runtime/src/package-door-namespace-conflict-code.test.ts— the durable pin (see below).minoron@objectstack/specand@objectstack/runtime. Runtime is named explicitly because the wire change is served by its door, and a reader of runtime's release notes needs it. The summary leads with the disclosure and scopes the denial after it, rather than the other way round.The pin, and what it discriminates
Three sections, and section 3 is why the other two mean something:
SchemaRegistryanswers422/NAMESPACE_CONFLICT/ nodeclaredCode;ErrorCode.safeParse('NAMESPACE_CONFLICT')succeeds, andstandardErrorCodeForHttpStatus(422)is not this member — so a body carrying it proves the producer's code was carried through, never re-derived from the status;error.codehas a limb authored by TENANTS at runtime — registration cannot close it, and ADR-0112 does not say what should happen there #9106 behaviour the narrowing removed.Only the kernel's service lookup is doubled. The refusal, the terminal
catch,resolveThrownHttpError's registered-or-demoted decision andbuildApiErrorare all shipping code.Reverse verification — direction predicted before running
Run on the pre-merge head; the merge changed neither the ledger member nor the pin, so it is not re-taken here.
Prediction: removing
'NAMESPACE_CONFLICT'from the ledger and rebuildingpackages/specturns sections 1 and 2 red and leaves section 3 green in both directions.Measured, from a committed base (the fix was committed first, so the restore leg had a real restore point):
1 -> 0, blob8da9043d…becomingcb03cd28…,git diff --numstat=0 1;ablation-dist-preflight @objectstack/specwith--absent— "marker absent from all 218 built files";error-envelope.conformance.test.tsgreen throughout. Direction as predicted;git checkout HEAD -- …, blob back to8da9043d…, whole-treegit status --porcelainempty, spec rebuilt, preflight without--absent— "marker present in 6 built files", "working tree clean against HEAD". The pin then ran 64 passed again.The first attempt at the preflight used the single-quoted marker, which only ever appears in the sourcemap; the tool refused that reading as void on the restore leg and the legs were re-taken with the double-quoted form, the one the built JS actually carries. Recorded because the void reading would otherwise look like a passing one.
Verification, re-taken on the merged head
At
eca3ca8e48, afterpnpm install --frozen-lockfileand a fullpnpm build(72/72 tasks):pnpm --filter @objectstack/runtime test— 236 files / 3330 passed;pnpm --filter @objectstack/spec test— 482 files / 13102 passedpnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/package-door-namespace-conflict-code.test.ts— 1 file / 3 passedpnpm --filter @objectstack/runtime typecheckandpnpm --filter @objectstack/spec typecheck— both exit 0pnpm check:dispatcher-error-vocabulary—OK — 65 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry, over 304 registered codes (254 ledger + 50 standard)pnpm --filter @objectstack/spec check:generated—All 15 generated artifacts are up to datenode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfrom this diff — every one green, reconciled with--ran:99 derived famil(ies) accounted for — 99 run, 0 NOT-MEASURED. The three that refused withPREREQUISITE NOT METon the previous round (check:skill-examples,check:dual-build-cjs-loads,check:type-check-debt) were re-run after the full build and pass;check:type-check-debtneedsNODE_OPTIONS=--max-old-space-size=6144, the valuelint.ymlsets for that same step, or its re-measure exits on heap exhaustion without measuring.pnpm lint— the repo-wideeslint . --no-inline-config, exit 0. No narrowing was needed, so the population is the whole tree rather than a subset. Run at this head,eca3ca8e48.Not addressed here
#14745remains open and is not touched by this PR. Two of its three residues are about the vocabulary row this PR deletes — residue 1 (thewhystring's over-general comparison to thedoor: nonesiblings) and residue 2 (the row asserting a reachability it does not pin). Deleting the row moots residue 1 outright, and residue 2 is now answered by a live pin instead of by a claim. Residue 3, about a released changeset's summary ordering, is untouched. That card is worth re-grading against this diff rather than dispatched as filed.packages/runtime/src/registry-namespace-install-gate.test.ts:65's comment still describes the door as answering422withdeclaredCode: NAMESPACE_CONFLICT, which this PR makes false. It is a comment in a file outside this PR's surface, so it is reported rather than edited here.Test Core (5/6)was cancelled at the 30-minute job timeout while the other five shards attested, and the requiredTest Corecontext still concludedsuccess— its aggregator printedleg test (declared roster 1..6/6) — aggregate result: cancelled … satisfied (cancelled — run-lifecycle state; expected attestations: 0). That is the behaviour open issue [finding] A single cancelled shard makes the requiredTest Corecheck green over untested packages — the attestation gate zeroes the whole roster oncancelled#16157 describes, with the shard-timing half in CI: the shard-timings file is stale for the CLI package — 672s predicted vs 28m46s measured against a 30-minute timeout, so Test Core shard 1/6 is one slow run from being killed on any PR touching the CLI #16173. Nothing here re-runs the shard, raisestimeout-minutes, or editsscripts/test-shard-timings.json.Generated by Claude Code