Repository navigation
fix(metadata-protocol)!: /diff refuses a version with no history row 404 VERSION_NOT_FOUND, as rollback does - #26540
Conversation
… history row (red first) Claude-Session: https://claude.ai/code/session_01FEkQ5pKvmsCDBZABPxvMnu Co-authored-by: Claude <noreply@anthropic.com>
…history row 404 VERSION_NOT_FOUND A from/to version the item has no history row at was compared as an empty side and answered 200 "no change" or "everything removed" (ADR-0110 D3). It is now refused with the rollback door's own refusal, shared as versionNotFoundError. The default range is unchanged. Claude-Session: https://claude.ai/code/session_01FEkQ5pKvmsCDBZABPxvMnu Co-authored-by: Claude <noreply@anthropic.com>
…m rows of its own Claude-Session: https://claude.ai/code/session_01FEkQ5pKvmsCDBZABPxvMnu Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pKvmsCDBZABPxvMnu Co-authored-by: Claude <noreply@anthropic.com>
…wire; the from=0 controls read from the published version Claude-Session: https://claude.ai/code/session_01FEkQ5pKvmsCDBZABPxvMnu Co-authored-by: Claude <noreply@anthropic.com>
…on refusal Claude-Session: https://claude.ai/code/session_01FEkQ5pKvmsCDBZABPxvMnu Co-authored-by: Claude <noreply@anthropic.com>
…s the caller's bound; ledger learns its pinned doubles Claude-Session: https://claude.ai/code/session_01FEkQ5pKvmsCDBZABPxvMnu Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 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 daf92bef44a17caea337e08698e19b7d269465d5 && git checkout daf92bef44a17caea337e08698e19b7d269465d5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 185512ccaf6394364a0da1af55cd8f1bfae37869 db2eb1e5967c3a7174db23dbe2b605d6bdbbbfd9 && git checkout -B drift-repro 185512ccaf6394364a0da1af55cd8f1bfae37869 && git merge --no-ff db2eb1e5967c3a7174db23dbe2b605d6bdbbbfd9
node scripts/docs-audit/affected-docs.mjs --json 185512ccaf6394364a0da1af55cd8f1bfae37869
|
Claude-Session: https://claude.ai/code/session_01FEkQ5pKvmsCDBZABPxvMnu Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read 2026-10-27T07:25Z: card #26462 body and all 3 comments (triage ① Derived judgments
Nothing wrong found. One doc-precision note, not a defect: the changeset's "An item that never existed answered 200 'no change'" is the protocol door's answer and the REST door's for types gating on their own key (the card's case); it is not claimed for every type, and the "Now" and "Unchanged" lines are accurate as written. ② Semver level
Clause-②: no (narrowing) — measured on this diff: ③ Boundary flags
Check-runs on Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #26462, with a ! title. 8 files in metadata-protocol and rest tests, plus scripts/engine-double-contract.pinned.json. One patch changeset for @objectstack/metadata-protocol, with a BREAKING banner and ADR-0087 not-required (no-migration-prescription). Clause-②: no (narrowing) and Metadata-protocol: none appear in both the body and the changeset. |
| Fix | Triage 6434115388, ADR-0110 D3. diffMetaItem refuses a named from or to with no history row of (type, name) with 404 VERSION_NOT_FOUND. It reuses versionNotFoundError, now shared with the rollback door (the same sentence, byte for byte). Tombstones are still served. The default range, the item-with-no-history default answer and the 503 outage path are unchanged. |
| Contract | Review 6436224605: no export, error code or spec key is added. The REST door narrows through the unchanged handleRouteError, and each rewritten pin keeps its subject. |
| Evidence | CI on head db2eb1e596: 41 succeeded, 13 expected skips, none failed or pending; mergeable clean. The full dogfood battery on this head is in 6436845057: VERDICT exit 0, 300 files and 2532 tests passed, 3 and 18 skipped, no failures (review-checklist.md :50). |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #26462
Clause-②: no (narrowing) — measured on this diff:
diffMetaItemand the REST/diffdoor that forwards to it refuse a namedfrom/toversion with no history row of the item (404VERSION_NOT_FOUND), which answered 200 before. The default range and every version that has a row answer as before. No error code is added (VERSION_NOT_FOUNDis already inERROR_CODE_LEDGER), no export is added to a package entry, and nothing underpackages/specchanges.Metadata-protocol: none
Skills-docs: none — no
skills/**file or docs page teaches the/diffanswer for a version that has no row; the client SDK page shows a range of versions that exist.维护者速读
GET /api/v1/meta/:type/:name/diff遇到不存在的from/to版本号时,改为返回 404VERSION_NOT_FOUND(与回滚接口同一句话),不再返回 200 的“没有变化”或“全部被删除”。from/to的默认对比、存在的版本(包括删除墓碑)都不变。回滚即 revert 本 PR。packages/metadata-protocol/src/protocol.ts(diffMetaItem两处取版本)、packages/metadata-protocol/src/sys-metadata-repository.ts(共用的versionNotFoundError)、packages/rest/src/meta-history-diff-authoring-door.test.ts(线上 404 与原来用from=0的对照测试)。pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.diff-missing-version.test.ts,预期 11 个用例全绿;在 main 上同一文件 7 个转红。What changes
diffMetaItem(packages/metadata-protocol/src/protocol.ts) no longer reads a named version withbyVersion.get(v) ?? null. Afromortowith no history row of the item throws the rollback door's refusal: 404VERSION_NOT_FOUND,No history row at version N for TYPE/NAME.The edit stays in place inside the method; no code moved.SysMetadataRepository.restoreVersionbuilt it inline; it is nowversionNotFoundError(ref, version)insys-metadata-repository.ts, whichrestoreVersionanddiffMetaItemboth throw. It is not exported from the package entry, likepackagelessAddressAmbiguityRefusalbeside it. This file is outside the claim's declared file surface; it is the shared constructor the order asks for ("reuse the rollback door's refusal … do not mint a second one").undefinedfrom the map, not on anullbody, so a tombstone is still diffed as an empty side.catchalready hands the throw tohandleRouteError, which answers the same envelope the rollback door answers.@throwsline and the unprovisioned-table sentence says what "benign" answers now.The open question on the card: does a version from ANOTHER item's history leak through? No. Measured on
mainwith the stub engine: itemqa_diff_probehas v1–v2, itemotherhas v1–v3, anddiffMetaItem({ name: 'qa_diff_probe', fromVersion: 1, toVersion: 3 })answered{"fromVersion":1,"toVersion":3,"added":[],"removed":[{"path":"name",…},{"path":"label","value":"QA Diff"},{"path":"widgets","value":[]}],"changed":[]}. The history read is keyed by(type, name), soother's v3 was never a candidate; it was the same miss read as "everything removed". It is now the same 404, pinned inprotocol.diff-missing-version.test.ts.Tests
packages/metadata-protocol/src/protocol.diff-missing-version.test.ts(11 cases): the step 3 control, steps 4, 5 and 6, a missingfrom, one side named and missing with the other defaulted, the other-item version, and a parity arm comparing{ code, status, message }withSysMetadataRepository.restoreVersion's refusal for the same item and version. Controls: a delete tombstone version still answers, and the default range answers unchanged for an item with history and for one with none.packages/rest/src/meta-history-diff-authoring-door.test.ts, on the real protocol behindRestServer: both missing,tomissing,frommissing and an item that never existed each answer{ status: 404, code: 'VERSION_NOT_FOUND' }naming the version; the step 3 control answers 200 with itslabelchange.protocol.diff-canonical-type-and-history-outage.test.ts, "a genuinely-absent table still answers benignly": the [finding] diffMetaItem answers 200 with an empty diff when sys_metadata_history is unreadable — an outage is indistinguishable from "nothing changed" #8833 subject is that an unprovisioned table is not the 503. It now asserts both benign answers: the default range's empty diff, and the 404 miss for named versions (no rows exist).fieldz. It now seeds two versions offieldz/xand asserts the real diff, because with no rows the named versions are a 404 that a weaker assertion could not tell from a boundary refusal.meta-history-diff-authoring-door.test.tsandmeta-draft-read-author-exemption.test.ts: three builder controls read the draft by diffingfrom=0, a version that has no row. They now diff from the published version (v - 1) and read the draft's navigation as thetoside of its change, or use the default from side for the draft-only view. What each pins (who reads the draft whole, who reads it pruned) is unchanged.Acceptance notes
from=0was a test idiom only. No production caller sends it:ui/has no caller of the metadata client'sdiff, and the client SDK'sdiffItemforwards what it is given. The changeset's remedy line names it.dashboardon the card. In the REST case I used aview, the type the existing suite boots; the dashboard path reaches the same protocol line.Verification
Base
45de8c6cb. Final headdb2eb1e5:f9ed7619plus the changeset'sClause-②line moved tono (narrowing), on which the four changeset gates (check-changeset-no-major,check-adr-0087-registration,check-empty-changesetwith--base origin/main, andcheck:changeset-gate-self-tests) re-ran with exit 0. Every other reading below was taken onf9ed7619.origin/mainhas moved 27 commits since the base; none touches a file this diff touches, andgit merge-tree --write-tree HEAD origin/mainis clean, so the branch was not merged. CI validates the merge ref.Red first on base, green on the fix
protocol.diff-missing-version.test.tson the base source (the fix not yet committed):Tests 7 failed | 4 passed (11). The 7 are steps 4, 5, 6, the missingfrom, the one-side-defaulted case, the other-item version and the rollback parity arm, eachexpected null(the call answered 200). The 4 controls passed. On the fix:Tests 11 passed (11).removed: [name, label, widgets]ofqa_diff_probev1, notother's body.meta-history-diff-authoring-door.test.ts -t 26462. The suite resolves@objectstack/metadata-protocolthroughdist/, so the base leg wrote the base blobs ofprotocol.tsandsys-metadata-repository.ts(marker count on disk 0), rebuilt the package (dist marker files 0), and ran:AssertionError: opportunity.pipeline from 998 to 999: expected status 200, code undefined to deeply equal status 404,Tests 1 failed | 19 skipped. Restored withgit checkout HEAD --;git diff HEADempty and the blob hash equals HEAD's (42c6ce88). Rebuilt (dist marker files 1):Tests 1 passed.Package suites
pnpm --filter @objectstack/metadata-protocol test: before the two pin updatesTest Files 1 failed | 268 passed | 3 skipped (272),Tests 1 failed | 21575 passed | 19 skipped; the one red was the plugin-shaped spelling pin (updated, see Tests). The two diff suites after the update:Tests 20 passed (20).pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.pnpm --filter @objectstack/rest test:Test Files 2 failed | 322 passed (324),Tests 3 failed | 6480 passed | 326 skippedbefore thefrom=0controls were updated; the 3 were those controls (expected 404 to be 200). After: both filesTests 26 passed (26)(with the new REST case).packages/objectqlprotocol-publish-rollback.test.ts:Tests 12 passed (12).packages/runtimemeta-app-package-id-key.test.ts:Tests 12 passed (12).eslint --no-inline-config packages/metadata-protocol packages/rest: exit 0.Full dogfood (the narrowing arm)
pnpm --filter @objectstack/dogfood testunder the lock after building its closure, on75b1f560:Test Files 300 passed | 3 skipped (303),Tests 2532 passed | 18 skipped (2550),VERDICT command-exit 0. The final head differs from75b1f560only in the new test file'sfinddouble andscripts/engine-double-contract.pinned.json, neither of which dogfood reads.Derived gates: 69 derived, 68 run with exit 0, 1 NOT MEASURED
node scripts/pm/dispatch-gates.mjs --ranon the final diff:69 derived famil(ies) accounted for — 68 run, 1 NOT-MEASURED. The first pass found two real reds in the new test's stub engine, both fixed and re-run on the final head:check:engine-double-contract(the ledger had to learn the new pinned doubles,--write) andcheck:objectql-double-limit(thefinddouble now applies the caller'slimit).check:dts-closure,check:dual-build-cjs-loadsandcheck:type-check-debtfirst read a tree a concurrent closure build was rewriting or could not take the lock behind it; each was re-run on the final head and exited 0. NOT MEASURED:check-plugin-teardown-shape.mjs --self-test, exit 3, its fixture is pinned to a commit this shallow clone cannot reach; CI runs it.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (Not touched.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code