…同源残留 (#5783)
#5264(PR #5782)删掉 `saveMetaItem` 的 legacy raw-engine 分支后,
`OVERLAY_PERSISTENCE_FAILED` 在全仓再无任何发射端 —— 它唯一的发射点就是被删
分支里的那个 catch。ledger 仍登记着它,于是词表向客户端承诺了一个任何响应都
带不出来的 code,正是 ADR-0112「no silent fourth state」反过来的形状;ledger
自检查大小写、重复与遮蔽,唯独不查「有没有生产者」,所以不会变红。
删除前逐条核验(证据写在 PR 正文):全仓精确名搜索的 8 处命中无一是生产者;
objectui 与 cloud 各自 origin/main 的 grep 均 0 命中;没有任何门禁以 ledger
成员资格为发射条件,因此移除不改变任何「接受/拒绝」判定;ADR-0087 的转换表
管的是可写元数据形状而非响应词表,四个生成物门禁全绿且不需要新条目。
线上无影响 —— 没有响应携带过这个 code;收窄只发生在类型层。
两个 packages/rest 测试保留全部断言实质(ADR-0112 信封的 code 与 status 都
继续断言),改钉一个仍然存在的生产者:`metadata-protocol` 的 `batchData`
原子拒绝(501 / NOT_IMPLEMENTED)。这是本边界上唯一同时声明信封两半的活
生产者;偏离裁定字面(500 / ledger 扩展码)的原因与实测证据写在 PR 正文。
unknown-object 那条还先直接测出 `mapDataError` 对同一段文本的判定是
404 OBJECT_NOT_FOUND,让「声明状态先于启发式」这个断言不再空转。
同源的三处注释一并修正:`metadata-plugin.zod.ts` 的 `agent` 条目(结论不变,
机制改写为 #5086 的 403 拒绝)、`rest-5xx-message-sanitization.test.ts` 的可
达性论证(artifact-backed 的 code-only 类型同样更早被 403 NOT_OVERRIDABLE
拒掉)、以及 `resolveErrorResponse` docblock 里三处以现在时描述已删生产者的
句子。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JTSZAjgtL3oR6YcpNDhW3T
Fixes #5783
Three residues left by #5264 / PR #5782, which deleted
saveMetaItem's legacy raw-engine branch. One is vocabulary drift and two are stale comments; none of them changes a byte on the wire.1.
OVERLAY_PERSISTENCE_FAILEDleaves the ledger (disposition (a))The code's only emission point was the
catchinside the branch #5264 deleted, so the ledger row promised acodethat no response can carry — ADR-0112's "no silent fourth state" read backwards.error-code-ledger.test.tscannot notice: it checks casing, duplication and shadowing, never whether a producer exists.Premise, verified on
origin/mainbefore touching anything. A search for the exact name finds it in eight places and none of them is a producer:packages/spec/src/api/error-code-ledger.zod.ts:216content/docs/references/api/contract.mdx,error-code-ledger.mdxpackages/rest/src/rest-5xx-message-sanitization.test.tsx4packages/rest/src/rest-unknown-object-heuristic.test.tsx2packages/rest/src/rest-server.ts:1070.changeset/rest-5xx-message-withheld.md,packages/rest/CHANGELOG.mdgit -C /home/user/objectui grepandgit -C /home/user/cloud grepagainst each repo'sorigin/mainboth exit 1 with zero matches.check:error-code-casingonly flags lowercase literals,error-code-ledger.test.tsasserts the three admission invariants plus the union, andresolveErrorResponsereadserror.codeas a string. The row is dead vocabulary and comes out as dead vocabulary.api-surface/,api-surface-signatures.json,authorable-surface*.jsonor the json-schema manifests contains the string, so the only regenerated files are reference pages.authorable-surface.base.jsonis byte-identical, as expected for a change that touches no authorable key, andcheck:spec-changes/check:upgrade-guideare green with no new registry entry.Type-level narrowing only:
ErrorCode(StandardErrorCode∪ the ledger, whatApiErrorSchema.codevalidates) stops admitting the string — and there is no call site left fortscto reject. Changeset ispatchfor that reason.The ledger's docblock gains a short "Retiring a code" paragraph, since the file documents registration and had nothing to say about the inverse.
The two rest tests keep their substance, on a producer that still exists
Both cases assert
resolveErrorResponse's handling of a DECLARED 5xx, and both keep asserting the full ADR-0112 envelope —codeandstatus— plus the sanitization substance. What changes is the specimen.Replacement, picked by evidence:
NOT_IMPLEMENTED/501, produced bymetadata-protocol'sbatchDataatomic refusal (protocol.ts, theif (!engineTx)refusal:err.status = 501; err.code = 'NOT_IMPLEMENTED').One deviation from the ruling's letter, reported rather than papered over. The ruling asked for a still-produced ledger 5xx declaring
status: 500. Measured onorigin/main, no such code exists for this boundary.resolveErrorResponseserves the metadata / UI / discovery / batch routes, andmetadata-protocolhas exactly three declared-5xx sites in the whole package:metadataStoreUnavailableError—503/SERVICE_UNAVAILABLE(standard catalog)batchData's atomic refusal —501/NOT_IMPLEMENTED(standard catalog)deleteMetaItem's catch —500, and it declares nocodeat allSo after this removal the boundary has no ledger-registered 5xx producer, and its only
500producer carries no code. Picking a ledger500from elsewhere (READ_SCOPE_COMPILE_FAILED, say) would have paired an analytics code with a metadata-route mock — a fresh phantom of exactly the kind this issue is closing, and one that does not even reachresolveErrorResponse, since/analytics/dataset/querybuilds its own envelope.NOT_IMPLEMENTEDwas chosen instead because it is the only live producer on this boundary that declares BOTH envelope halves, it is the sibling specimen #5437's own changeset names beside the retired code, andresolveErrorResponse's docblock already cites its message as the surviving example of the accepted cost.It is also a strictly harder case for
rest-unknown-object-heuristic.test.tsthan the fixture it replaces. The old error merely CONTAINED a heuristic trigger; the new one is actually claimed by the limb that file was written about — its message quotes the request's own object name and says "cannot", solooksLikeUnknownObject's last limb fires. The test measures that first (mapDataErroron the same text answers404/OBJECT_NOT_FOUND) and only then asserts that the route answers501/NOT_IMPLEMENTED. Without that first pair the route assertion would be vacuous — it would not show there was anything for the declared-status band to get in front of.The sanitization file's two remaining uses of the deleted producer moved to
deleteMetaItem's500, which is live, still interpolates the driver line, and was already that file's live specimen in section 1.Reverse verification, direction predicted before running
Predicted: disabling
resolveErrorResponse'sif (error.status >= 500)withholding branch turns the re-coded cases RED on the BODY while leaving the status and themapDataErrorfall-through assertions green — because the status comes from the passthrough above and the fall-through call never touches that branch. Measured,2 failed (2) | 10 failed | 24 passed (34), and the reds are exactly that shape:The failure is on
res.body.errorat the line after the501andNOT_IMPLEMENTEDassertions, which both passed — so the case is red for the withholding, not for the envelope.The ledger removal itself has no red direction, and saying so is the honest report. Restoring the row breaks nothing, because nothing pins it; that is the defect, not a gap in the verification. The evidence for it is the search, not a test.
2. The
agentcomment inmetadata-plugin.zod.tsThe conclusion is kept:
agenthas no governed write path, and git is the record. The MECHANISM was stale — it saidsaveMetaItem"would route it down the legacy raw-engine branch, and nothing calls it". TodaysaveMetaItemrefuses code-only types outright on every kernel with a 403 (#5086):NOT_OVERRIDABLEwhen the name is artifact-backed,NOT_CREATABLEotherwise (codeOnlyOverrideError/codeOnlyCreateErrorat the code-only gate). The branch the old sentence described no longer exists.3. The reachability argument in
rest-5xx-message-sanitization.test.tsThe comment argued the legacy branch was merely hard to reach: it needed a code-only type AND an artifact-backed item of it, "and a runtime-created one is refused earlier with
403 NOT_CREATABLE". The artifact-backed half is refused just as early, with403 NOT_OVERRIDABLE— pinned byprotocol.code-only-types.test.ts's "refuses overlaying an artifact-backed [type] with not_overridable". Both halves were refused, so the branch was unreachable rather than rare. The correction says so and keeps the specimen value of the mistake: the comment was CORRECT when written and was outlived by one week by the gate tightening (#5086 / PR #5263) that falsified it.Also in scope:
rest-server.tsThe PM's sweep flagged the comment at
rest-server.ts:1070. It did not survive as an accurate historical example — it is written in the present tense ("reaches the client as the generic sentence plus itscode") about a body no producer can build any more, so it is corrected rather than left. Two neighbours in the same docblock had the same defect and are corrected with it: the census of "two client-facing 500s" (now one) and "the two overlay 500s come back as 404" (now one). The history of why the branch exists is kept and explicitly marked as history rather than as a present-tense census.Verification
npx vitest run src/rest-5xx-message-sanitization.test.ts src/rest-unknown-object-heuristic.test.ts—Test Files 2 passed (2) | Tests 34 passed (34).@objectstack/restand@objectstack/specsuites, plustsc --noEmiton both.check:*step enumerated from.github/workflows/lint.yml, run one by one — including the four ADR-0087 / generated-artifact gates (check:spec-changes,check:upgrade-guide,check:authorable-surface,check:docs), which is where a removal of this class would escalate if it were going to.pnpm --filter @objectstack/spec gen:schema && ... gen:docsand committed unedited. Eleven reference pages move; the whole diff is the removed bullet inerror-code-ledger.mdx, the code dropping out of the giant enum incontract.mdx, and the truncation counter going+258 moreto+257 moreeverywhere anApiErroris rendered.content/docs/releases/untouched.Generated by Claude Code