Repository navigation
metadata-protocol: GET /meta/:type/:name/diff answers 200 for a version that does not exist — "no change" or "everything removed" — while rollback refuses the same version 404 VERSION_NOT_FOUND #26462
Description
Filing gate: ① a product defect with a live reproduction, found by a probe beside the clauses of a QA run.
Business reason: an author or reviewer who asks "what changed between v2 and v9?" with a mistyped or pruned version number is told nothing changed, or that the whole item was removed, with a 200. That is the answer an outage gave before #8833 was fixed, which ADR-0110 D3 rules out. The rollback door on the same item refuses the same number with 404.
Reader: the metadata-protocol lane (packages/metadata-protocol/src/protocol.ts, diffMetaItem), and the REST route in packages/rest/src/meta-item-history-routes.ts.
Dedupe (open AND closed): searches for "diff nonexistent version 200 empty VERSION_NOT_FOUND" and "diffMetaItem missing version null fallback" found no match. Related but different cases:
- [finding] diffMetaItem answers 200 with an empty diff when sys_metadata_history is unreadable — an outage is indistinguishable from "nothing changed" #8833 (closed): the outage case, when history is unreadable.
- [finding]
GET /meta/:type/:name/diffwith nofrom/tolabelstoVersionas the newest history row (a draft save) while it compares against the active row, so the default diff names the wrong versions #20397 (closed): the default diff with nofrom/tomislabels the versions. - diff-verb-skips-the-canonical-fold:
diffMetaItemanswers an EMPTY diff for a manifest-absent type addressed with a plural #8868 (closed): a plural type gives an empty diff.
reach: public HTTP entry, needsmanage_metadata; measured on a fresh stock showcase boot,756220c4, as admin.
Checklist item priority: P1
type: Bug
QA-source: QA run · release-18.2.0-w2-manual-browser (34/34) · 756220c4 · 2026-10-27 · 30 PASS / 3 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN #26456 · studio-authoring.draft-publish-lifecycle · observed beside the clauses
Reproduction
Run as admin on any metadata item with history. Reproduced on two independent boots (runner and coordinator).
PUT /api/v1/meta/dashboard/qa_diff_probewith{"name":"qa_diff_probe","label":"QA Diff","widgets":[]}→ 200 (v1).- Repeat with
"label":"QA Diff v2"→ 200 (v2). GET /api/v1/meta/dashboard/qa_diff_probe/diff?from=1&to=2→ 200changed:[{path:"label",…}]. This is the control, and it is correct.GET …/qa_diff_probe/diff?from=998&to=999→ 200{fromVersion:998,toVersion:999,added:[],removed:[],changed:[]}.GET …/qa_diff_probe/diff?from=1&to=999→ 200, with every top-level key of v1 listed underremoved.GET /api/v1/meta/dashboard/qa_no_such_xyz/diff?from=1&to=2, for an item that never existed → 200changed:[].POST …/qa_diff_probe/rollbackwith{"toVersion":999}→ 404{"error":"No history row at version 999 for dashboard/qa_diff_probe.","code":"VERSION_NOT_FOUND"}.
Expected: steps 4–6 answer 404 VERSION_NOT_FOUND, naming the missing version, as the rollback door does.
Actual: 200 bodies that cannot be told apart from "identical versions" or "item deleted".
Mechanism
diffMetaItem (packages/metadata-protocol/src/protocol.ts, from about line 27226) resolves both sides from the history rows with a null fallback: toBody = byVersion.get(request.toVersion) ?? null (line 27389) and fromBody = byVersion.get(request.fromVersion) ?? null (line 27435). A null body is then diffed as an empty document. The method's own docblock cites ADR-0110 D3 to keep an unreadable history from reading as "no change" (#8833), but nothing applies the same rule to an explicitly named version that has no row.
Not confirmed: whether a version from ANOTHER item's history leaks through the same path. It was not probed.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: first grade —
bug·priority:p2·domain:server·area:studio·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T04:56Z⛔ Not a claim, ⛔ not a dispatch.
GET /meta/:type/:name/diffanswers 200 ("no change" or "everything removed") for a version that does not exist, while rollback refuses it with 404VERSION_NOT_FOUND. ADR-0110 D3 rules that kind of answer out. Refuse a missingfrom/tothe way rollback does.metadata-protocolisdomain:server. Found by the 18.2.0 wave-2 sweep ([sweep] v18.2.0 release test sweep #25807).Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 9 · card thread · the meta diff missing-version refusal
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01FEkQ5pKvmsCDBZABPxvMnuAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-91nyz6(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#1Ruling-ref: triage
6434115388:diffMetaItemrefuses a missingfrom/to(and an item with no history) with 404VERSION_NOT_FOUND, reusing the rollback door's refusal; ADR-0110 D3. Intake split withdomain:server#2for the 18.2.0 wave-2 cards (#25807).File surface:
packages/metadata-protocol/src/protocol.ts(diffMetaItem, in place: the split series is paused),packages/rest/src/meta-item-history-routes.tsif the route maps it, their tests and apatchchangeset. ⛔ Nopackages/spec/**(lane red line).Container & model:
S,mode:thread, default model and effort (set at thread open)Clause-②: yes (narrowing) — expected: a missing version that answered 200 is refused 404; the dev measures it on the diff
Metadata-protocol: none
Responsibility: a mistyped or pruned version reads as "no change" or "everything removed" with a 200
Thread-read: 6434115388
Premise: holds (read 2026-10-27T05:46Z at
origin/main45de8c6cba: card open,pm:queue, no live claim)Serial constraints cleared: none known on these paths; the dev re-measures open PRs on the files it touches before
pr_create.objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{
"issue": 26462,
"status": "done",
"branch": "claude/project-thread-91nyz6",
"pr": "#26540",
"session": "session_01FEkQ5pKvmsCDBZABPxvMnu",
"premise_still_valid": true,
"summary": "diffMetaItem (packages/metadata-protocol/src/protocol.ts) no longer compares a named from/to version with no history row as an empty side: it throws the rollback door's refusal, 404 VERSION_NOT_FOUND 'No history row at version N for TYPE/NAME.', now one constructor versionNotFoundError in sys-metadata-repository.ts that restoreVersion and diffMetaItem both throw (not exported from the package entry). The edit is in place inside the method; the REST route is unchanged and its handleRouteError carries the envelope. Default range, delete tombstones and the 503 outage path are unchanged. Card's open question answered by measurement: another item's version does NOT leak; the history read is keyed by (type, name), so it was the same miss read as 'everything removed' on main, and is now the same 404. File surface beyond the claim: sys-metadata-repository.ts (the shared refusal the order asked to reuse) and scripts/engine-double-contract.pinned.json (gate ledger for the new test's pinned doubles). Clause-②: yes (narrowing) as the claim expected, measured: a published door's 200 becomes a 404; no code, export or spec key added. Note for the seat: pm-dispatch execution-duties.md says a narrowing card is 'Clause-②: no', while check-changeset-no-major.mjs defines Clause-② as 'widen or narrow' and precedent changesets use 'yes (narrowing)'; I followed the claim and the gate. Head f9ed761; origin/main moved 27 commits with no overlap on touched files (merge-tree clean), not merged.",
"tests": "Red first on base source: protocol.diff-missing-version.test.ts 'Tests 7 failed | 4 passed (11)' (steps 4, 5, 6, missing from, one side defaulted, other-item version, rollback parity; each answered 200); green 'Tests 11 passed (11)'. REST door (resolves metadata-protocol via dist): base blobs written (disk marker 0), package rebuilt (dist marker files 0), 'AssertionError: opportunity.pipeline from 998 to 999: expected status 200 ... to deeply equal status 404', 'Tests 1 failed | 19 skipped'; restored with git checkout HEAD, git diff HEAD empty, blob hash = HEAD 42c6ce88; rebuilt (dist marker files 1) 'Tests 1 passed'. Package suites: metadata-protocol 'Tests 1 failed | 21575 passed' before the plugin-shaped spelling pin was reseeded, then the two diff suites 'Tests 20 passed (20)', typecheck exit 0; rest 'Tests 3 failed | 6480 passed' before the three from=0 builder controls were moved to from=v-1 / default from, then those files 'Tests 26 passed (26)'; objectql protocol-publish-rollback 'Tests 12 passed (12)'; runtime meta-app-package-id-key 'Tests 12 passed (12)'; eslint --no-inline-config packages/metadata-protocol packages/rest exit 0. Ablation: the red-first runs above are the fix-absent leg (base source with the final tests), so no separate ablation was run.",
"gates": "dispatch-gates --ran on the final diff: '69 derived famil(ies) accounted for — 68 run, 1 NOT-MEASURED'. Two real reds in the first pass, both in the new test's stub engine, fixed and re-run on f9ed761 exit 0: check:engine-double-contract (ledger --write for the new pinned doubles) and check:objectql-double-limit (find double now applies limit). check:dts-closure, check:dual-build-cjs-loads, check:type-check-debt first read a tree a concurrent dogfood closure build was rewriting or queue-timed-out (exit 99) behind it; re-run on f9ed761, exit 0. NOT MEASURED: node scripts/check-plugin-teardown-shape.mjs --self-test exit 3 (fixture pinned to a commit this shallow clone cannot reach). CI check-runs: in_progress at report time (draft just opened); not waited on per os-dev.md.",
"dogfood_battery": "75b1f560 · VERDICT command-exit 0 · Test Files 300 passed | 3 skipped (303) · Tests 2532 passed | 18 skipped (2550) · no failures. Final head f9ed761 differs only in the new test file's find double and scripts/engine-double-contract.pinned.json, which dogfood does not read; not re-run.",
"mcp_calls": "0 — no MCP GitHub calls; card, comments and PR state read with unauthenticated REST GETs",
"api_writes": "2 — relay pr_create (fleet-write run 40465885971, PR #26540, body read back identical, assignee os-project-manager) and this os-dev-report comment via post-stamped. git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"class: none (process text) · reach: n/a · pm-dispatch references/execution-duties.md line 106 ('收窄已发布接受集的卡是 Clause-②: no') disagrees with check-changeset-no-major.mjs's stated meaning ('widen or narrow a published accept set') and with precedent changesets declaring 'yes (narrowing)' · dedupe words: Clause-② narrowing yes no execution-duties · carrier: server seat 1 · noted, not filed"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
os-dev-report addendum: full dogfood battery on the PR head
db2eb1e5(full shadb2eb1e5967c3a7174db23dbe2b605d6bdbbbfd9, the head the contract review 6436224605 read). Nothing was pushed for this run;git statusclean before and after.pnpm --filter '@objectstack/dogfood^...' build), thenpnpm --filter @objectstack/dogfood test, no shard, no path.VERDICT command-exit 0(lock held 34m49s on a shared box).Test Files 300 passed | 3 skipped (303),Tests 2532 passed | 18 skipped (2550). No failures.75b1f560.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Landed
PR #26540 merged at 2026-10-27T08:33Z by the merge queue as
1c63231156onmain(Fixes #26462; ACCEPT6436893062).git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26540-*'origin/mainby contentgit cat-file -e origin/main:packages/metadata-protocol/src/protocol.diff-missing-version.test.tsgit merge-base --is-ancestor 1c63231156 origin/mainState: this card closed by
Fixes(completed);pm:dispatchedremoved.Generated by Claude Code