Skip to content

fix(spec,rest): OVERLAY_PERSISTENCE_FAILED leaves the error-code ledger, plus three same-origin residues (#5783) - #6389

Merged
hotlong merged 3 commits into
mainfrom
claude/issue-5783-savemetaitem-residue
Aug 7, 2026
Merged

fix(spec,rest): OVERLAY_PERSISTENCE_FAILED leaves the error-code ledger, plus three same-origin residues (#5783)#6389
hotlong merged 3 commits into
mainfrom
claude/issue-5783-savemetaitem-residue

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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_FAILED leaves the ledger (disposition (a))

The code's only emission point was the catch inside the branch #5264 deleted, so the ledger row promised a code that no response can carry — ADR-0112's "no silent fourth state" read backwards. error-code-ledger.test.ts cannot notice: it checks casing, duplication and shadowing, never whether a producer exists.

Premise, verified on origin/main before touching anything. A search for the exact name finds it in eight places and none of them is a producer:

where what it is
packages/spec/src/api/error-code-ledger.zod.ts:216 the ledger row (removed here)
content/docs/references/api/contract.mdx, error-code-ledger.mdx generated (regenerated here)
packages/rest/src/rest-5xx-message-sanitization.test.ts x4 the test constructs the error itself
packages/rest/src/rest-unknown-object-heuristic.test.ts x2 same
packages/rest/src/rest-server.ts:1070 a comment (corrected here)
.changeset/rest-5xx-message-withheld.md, packages/rest/CHANGELOG.md historical release prose
  • No consumer reads the literal, in this repo or in the siblings. git -C /home/user/objectui grep and git -C /home/user/cloud grep against each repo's origin/main both exit 1 with zero matches.
  • Removal is not an acceptance change. Nothing gates an emission on ledger membership: check:error-code-casing only flags lowercase literals, error-code-ledger.test.ts asserts the three admission invariants plus the union, and resolveErrorResponse reads error.code as a string. The row is dead vocabulary and comes out as dead vocabulary.
  • No removal machinery is owed. ADR-0087's conversion registry governs authorable metadata shapes, not response vocabularies, and no ledger row carries a tombstone. The nearest precedent is cloud#1124 / cloud#1136, which deleted a documented-but-unemitted code under ADR-0049 with text-and-tests and nothing else. Confirmed against the artifact gates: none of api-surface/, api-surface-signatures.json, authorable-surface*.json or the json-schema manifests contains the string, so the only regenerated files are reference pages. authorable-surface.base.json is byte-identical, as expected for a change that touches no authorable key, and check:spec-changes / check:upgrade-guide are green with no new registry entry.

Type-level narrowing only: ErrorCode (StandardErrorCode ∪ the ledger, what ApiErrorSchema.code validates) stops admitting the string — and there is no call site left for tsc to reject. Changeset is patch for 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 — code and status — plus the sanitization substance. What changes is the specimen.

Replacement, picked by evidence: NOT_IMPLEMENTED / 501, produced by metadata-protocol's batchData atomic refusal (protocol.ts, the if (!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 on origin/main, no such code exists for this boundary. resolveErrorResponse serves the metadata / UI / discovery / batch routes, and metadata-protocol has exactly three declared-5xx sites in the whole package:

  • metadataStoreUnavailableError503 / SERVICE_UNAVAILABLE (standard catalog)
  • batchData's atomic refusal — 501 / NOT_IMPLEMENTED (standard catalog)
  • deleteMetaItem's catch — 500, and it declares no code at all

So after this removal the boundary has no ledger-registered 5xx producer, and its only 500 producer carries no code. Picking a ledger 500 from 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 reach resolveErrorResponse, since /analytics/dataset/query builds its own envelope. NOT_IMPLEMENTED was 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, and resolveErrorResponse'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.ts than 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", so looksLikeUnknownObject's last limb fires. The test measures that first (mapDataError on the same text answers 404 / OBJECT_NOT_FOUND) and only then asserts that the route answers 501 / 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's 500, 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's if (error.status >= 500) withholding branch turns the re-coded cases RED on the BODY while leaving the status and the mapDataError fall-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:

FAIL src/rest-unknown-object-heuristic.test.ts > the declared-status band is untouched
     > a declared 5xx that the heuristic WOULD claim keeps its status and its code
Expected: "Internal server error"
Received: "Atomic batch on 'showcase_account' requires engine transaction support;
           this runtime cannot roll back. Retry without options.atomic, or probe
           capabilities.transactionalBatch on /discovery first."

The failure is on res.body.error at the line after the 501 and NOT_IMPLEMENTED assertions, 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 agent comment in metadata-plugin.zod.ts

The conclusion is kept: agent has no governed write path, and git is the record. The MECHANISM was stale — it said saveMetaItem "would route it down the legacy raw-engine branch, and nothing calls it". Today saveMetaItem refuses code-only types outright on every kernel with a 403 (#5086): NOT_OVERRIDABLE when the name is artifact-backed, NOT_CREATABLE otherwise (codeOnlyOverrideError / codeOnlyCreateError at the code-only gate). The branch the old sentence described no longer exists.

3. The reachability argument in rest-5xx-message-sanitization.test.ts

The 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, with 403 NOT_OVERRIDABLE — pinned by protocol.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.ts

The 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 its code") 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.tsTest Files 2 passed (2) | Tests 34 passed (34).
  • Full @objectstack/rest and @objectstack/spec suites, plus tsc --noEmit on both.
  • Every 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.
  • Regenerated with pnpm --filter @objectstack/spec gen:schema && ... gen:docs and committed unedited. Eleven reference pages move; the whole diff is the removed bullet in error-code-ledger.mdx, the code dropping out of the giant enum in contract.mdx, and the truncation counter going +258 more to +257 more everywhere an ApiError is rendered.
  • content/docs/releases/ untouched.

Generated by Claude Code

…同源残留 (#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
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 7, 2026 5:02pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/spec.

114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

`content/docs/references/**` 走 `merge=os-regen`,合并驱动只把 contract.mdx /
metadata.mdx 记为 pending 而不做文本合并(AGENTS.md §11)。按其规定顺序重建
工作区后 `check:generated --fix`,产物现同时含 #6377 的新版式(顶层长枚举移入
Allowed Values 小节、联合变体印数量)与本单从 ledger 摘掉的那一个码。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JTSZAjgtL3oR6YcpNDhW3T

hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Resynced onto main after #6377 — head is now 480eaab7b, CI fully green

Two commits were added after the review: a merge of origin/main and the regeneration it required. One correction to the "Verification" section above, which was written before the resync: the generated-docs diff is now two pages, not eleven, because #6377 changed the renderer for long top-level enums.

Why a merge was needed. #6377 (参考文档顶层长枚举移入 Allowed Values,联合变体印数量) landed while this PR was open and rewrote contract.mdx, adding an "Allowed Values" section that lists every ApiError.code on its own line. content/docs/references/** carries merge=os-regen, so this does not surface as a conflict — the driver exits 0 with no markers and defers the path. It showed up as mergeable_state: dirty and in $GIT_DIR/os-regen-pending.

Sequence (AGENTS.md §11, no rebase, no force-push): git merge origin/main, let the merge commit land first, rebuild the workspace, then check:generated --fix. Regeneration never ran while the merge was uncommitted, so #5370's resolveSurfaceBase() trap could not fire. The pre-commit hook verified it: os-regen: all deferred artifacts are current — marker cleared.

What the resync had to get right. With #6377 in, the removed code lives in contract.mdx twice — once in the inline code-span union, once as an Allowed Values bullet. Both are gone:

  • git grep -c "OVERLAY_PERSISTENCE_FAILED" content/docs/references/api/ → 0 matches across the directory.
  • contract.mdx is 509 lines here vs 510 on origin/main and 246 at my pre-merge tip — fix(spec): 参考文档顶层长枚举移入 Allowed Values,联合变体印数量 (#6225, #6226) #6377's section is fully present, and this branch sits exactly one line below main, that line being the removed bullet.
  • The complete diff of contract.mdx against origin/main is the shortened union code-span, the deleted bullet, and the inline truncation counters moving +258 more to +257 more. Nothing else.

packages/spec/json-schema/openapi.json survived the regeneration, so #5371 did not fire — no expected 503 to be 200.

Re-verified on the merged tree. @objectstack/rest 64 files / 881 tests passed; @objectstack/spec 338 files / 8644 tests passed; all 44 check:* steps re-enumerated from the merged lint.yml (it gained check:meta-type-normalized and check:quick-reference-counts from main) and run one by one, green. CI on 480eaab7b: 25 checks, 24 success and 1 skipped (Console Pin Gate) — including ESLint (which carries the family gates) and TypeScript Type Check, both read as job conclusions rather than as the aggregate status.


Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit d9bef45 Aug 7, 2026
26 checks passed
@hotlong
hotlong deleted the claude/issue-5783-savemetaitem-residue branch August 7, 2026 18:35
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

删除 saveMetaItem legacy 写入分支后的三处残留:一个无生产者的 ledger 错误码 + 两处已过期的跨包注释

3 participants