Repository navigation
Commit 1c63231
fix(metadata-protocol)!: /diff refuses a version with no history row 404 VERSION_NOT_FOUND, as rollback does (#26540)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XDtV8BPxJvKToJNtpCCYF3x)_
Fixes #26462
Clause-②: no (narrowing) — measured on this diff: `diffMetaItem` and the
REST `/diff` door that forwards to it refuse a named `from` / `to`
version with no history row of the item (404 `VERSION_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_FOUND` is already
in `ERROR_CODE_LEDGER`), no export is added to a package entry, and
nothing under `packages/spec` changes.
Metadata-protocol: none
Skills-docs: none — no `skills/**` file or docs page teaches the `/diff`
answer 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` 版本号时,改为返回 404 `VERSION_NOT_FOUND`(与回滚接口同一句话),不再返回 200
的“没有变化”或“全部被删除”。
- **为什么改**:输错或已清理的版本号会让作者和审核人误以为两版相同或条目已删除;回滚接口对同一个号码早已拒绝,ADR-0110 D3
也不允许把“查不到”说成事实。
- **风险与回滚**:只影响显式指定了不存在版本号的调用;不带 `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 with `byVersion.get(v) ?? null`. A `from`
or `to` with no history row of the item throws the rollback door's
refusal: 404 `VERSION_NOT_FOUND`, `No history row at version N for
TYPE/NAME.` The edit stays in place inside the method; no code moved.
- The refusal is not minted twice.
`SysMetadataRepository.restoreVersion` built it inline; it is now
`versionNotFoundError(ref, version)` in `sys-metadata-repository.ts`,
which `restoreVersion` and `diffMetaItem` both throw. It is not exported
from the package entry, like `packagelessAddressAmbiguityRefusal` beside
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").
- A delete tombstone is a row: the check is on `undefined` from the map,
not on a `null` body, so a tombstone is still diffed as an empty side.
- The REST route is unchanged. Its `catch` already hands the throw to
`handleRouteError`, which answers the same envelope the rollback door
answers.
- The method's docblock gains the `@throws` line 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 `main` with the stub engine:
item `qa_diff_probe` has v1–v2, item `other` has v1–v3, and
`diffMetaItem({ 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)`, so `other`'s v3 was never a candidate; it
was the same miss read as "everything removed". It is now the same 404,
pinned in `protocol.diff-missing-version.test.ts`.
## Tests
- New
`packages/metadata-protocol/src/protocol.diff-missing-version.test.ts`
(11 cases): the step 3 control, steps 4, 5 and 6, a missing `from`, one
side named and missing with the other defaulted, the other-item version,
and a parity arm comparing `{ code, status, message }` with
`SysMetadataRepository.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.
- New REST case in
`packages/rest/src/meta-history-diff-authoring-door.test.ts`, on the
real protocol behind `RestServer`: both missing, `to` missing, `from`
missing and an item that never existed each answer `{ status: 404, code:
'VERSION_NOT_FOUND' }` naming the version; the step 3 control answers
200 with its `label` change.
- Pins whose asserted value moved, each with its subject kept:
- `protocol.diff-canonical-type-and-history-outage.test.ts`, "a
genuinely-absent table still answers benignly": the #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).
- Same file, "does NOT refuse a spelling that reaches for no declared
type": the subject is that the type boundary does not refuse `fieldz`.
It now seeds two versions of `fieldz/x` and 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.ts` and
`meta-draft-read-author-exemption.test.ts`: three builder controls read
the draft by diffing `from=0`, a version that has no row. They now diff
from the published version (`v - 1`) and read the draft's navigation as
the `to` side 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=0` was a test idiom only. No production caller sends it: `ui/`
has no caller of the metadata client's `diff`, and the client SDK's
`diffItem` forwards what it is given. The changeset's remedy line names
it.
- The step 6 item was a `dashboard` on the card. In the REST case I used
a `view`, the type the existing suite boots; the dashboard path reaches
the same protocol line.
## Verification
Base `45de8c6cb`. Final head `db2eb1e5`: `f9ed7619` plus the changeset's
`Clause-②` line moved to `no (narrowing)`, on which the four changeset
gates (`check-changeset-no-major`, `check-adr-0087-registration`,
`check-empty-changeset` with `--base origin/main`, and
`check:changeset-gate-self-tests`) re-ran with exit 0. Every other
reading below was taken on `f9ed7619`. `origin/main` has moved 27
commits since the base; none touches a file this diff touches, and `git
merge-tree --write-tree HEAD origin/main` is clean, so the branch was
not merged. CI validates the merge ref.
<details><summary>Red first on base, green on the fix</summary>
- Protocol, `protocol.diff-missing-version.test.ts` on the base source
(the fix not yet committed): `Tests 7 failed | 4 passed (11)`. The 7 are
steps 4, 5, 6, the missing `from`, the one-side-defaulted case, the
other-item version and the rollback parity arm, each `expected null`
(the call answered 200). The 4 controls passed. On the fix: `Tests 11
passed (11)`.
- The other-item probe on base answered `removed: [name, label,
widgets]` of `qa_diff_probe` v1, not `other`'s body.
- REST, `meta-history-diff-authoring-door.test.ts -t 26462`. The suite
resolves `@objectstack/metadata-protocol` through `dist/`, so the base
leg wrote the base blobs of `protocol.ts` and
`sys-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 with `git checkout HEAD --`; `git diff HEAD` empty and the blob
hash equals HEAD's (`42c6ce88`). Rebuilt (dist marker files 1): `Tests 1
passed`.
</details>
<details><summary>Package suites</summary>
- `pnpm --filter @objectstack/metadata-protocol test`: before the two
pin updates `Test 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 skipped` before the
`from=0` controls were updated; the 3 were those controls (`expected 404
to be 200`). After: both files `Tests 26 passed (26)` (with the new REST
case).
- `packages/objectql` `protocol-publish-rollback.test.ts`: `Tests 12
passed (12)`. `packages/runtime` `meta-app-package-id-key.test.ts`:
`Tests 12 passed (12)`.
- `eslint --no-inline-config packages/metadata-protocol packages/rest`:
exit 0.
</details>
<details><summary>Full dogfood (the narrowing arm)</summary>
`pnpm --filter @objectstack/dogfood test` under the lock after building
its closure, on `75b1f560`: `Test Files 300 passed | 3 skipped (303)`,
`Tests 2532 passed | 18 skipped (2550)`, `VERDICT command-exit 0`. The
final head differs from `75b1f560` only in the new test file's `find`
double and `scripts/engine-double-contract.pinned.json`, neither of
which dogfood reads.
</details>
<details><summary>Derived gates: 69 derived, 68 run with exit 0, 1 NOT
MEASURED</summary>
`node scripts/pm/dispatch-gates.mjs --ran` on 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`) and
`check:objectql-double-limit` (the `find` double now applies the
caller's `limit`). `check:dts-closure`, `check:dual-build-cjs-loads` and
`check:type-check-debt` first 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.
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 3
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:filter-alias-parity :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:pnpm-flag-spellings :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:scripts-symbol-anchors :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5pKvmsCDBZABPxvMnu)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7dd5a2f commit 1c63231
8 files changed
Lines changed: 429 additions & 26 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- rest/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
278 | 278 | | |
279 | 279 | | |
280 | 280 | | |
281 | | - | |
282 | | - | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
283 | 287 | | |
284 | 288 | | |
285 | 289 | | |
| |||
290 | 294 | | |
291 | 295 | | |
292 | 296 | | |
293 | | - | |
| 297 | + | |
294 | 298 | | |
295 | 299 | | |
296 | 300 | | |
| |||
320 | 324 | | |
321 | 325 | | |
322 | 326 | | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
323 | 332 | | |
324 | 333 | | |
325 | 334 | | |
326 | | - | |
327 | | - | |
328 | | - | |
329 | | - | |
330 | | - | |
331 | | - | |
| 335 | + | |
332 | 336 | | |
333 | 337 | | |
334 | 338 | | |
335 | 339 | | |
336 | 340 | | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
337 | 349 | | |
338 | 350 | | |
339 | 351 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
0 commit comments