Repository navigation
Commit 8cdbe0c
fix(metadata-protocol): the default /diff range labels its to side with the active row's own version (#20443)
Fixes #20397
Clause-②: no
## What changes
`diffMetaItem` (`packages/metadata-protocol/src/protocol.ts`), its
default range only. With no `toVersion` the to side is the current
active `sys_metadata` row. Its body was compared, but `toVersion` came
from the newest `sys_metadata_history` row. That row is a draft save
whenever a draft is pending, because every draft save appends a history
row too. So the labels and the bodies named different rows.
Now one read of the active row supplies both facts: the body compared,
and the row's own `version` as the label. `SysMetadataRepository.put`
stamps that column in the same transaction that appends the history row
carrying the same body. The `fromVersion` default is unchanged: the
history version immediately before the label. With no active row, the to
side is absent, so `toVersion` is `null`, as
`DiffMetaItemResponseSchema` declares for an absent side.
The hunk also deletes the two locals the old read used (`repo`,
`fullRef`; nothing else in the function read them), and adds one clause
to the method's docblock. Nothing else in `protocol.ts` moves.
`rest-server.ts`, `packages/spec` and `sys_metadata_history` are
untouched. The response shape is unchanged:
`@objectstack/metadata-protocol` `patch` changeset.
## Measured before and after, on the real REST stack
Real routes and a real `ObjectStackProtocolImplementation` over
better-sqlite3 `:memory:` with the real `sys_metadata*` objects. Each
read is `GET /meta/:type/:name/diff` with no `from` / `to`, as an author
(`manage_metadata`). "Before" is `origin/main` `dbddf02c1` (the dist
carried the old label line). "After" is `53cad078f`. The probe was a
scratch file, deleted and never committed.
| lineage (v = history version) | before | after |
|:--|:--|:--|
| app: v1 active, v2 draft, v3 draft (the card) | `2 → 3`, `label`
changed "Atlas v2 draft" → "Atlas v1": the to side is the v1 body |
`null → 1`, the v1 body as added |
| view: v1 active, v2 draft (the card) | "no changes", labelled `1 → 2`
| `null → 1`, the v1 body as added |
| view: v1 active, v2 active, v3 draft | `2 → 3`, no changes | `1 → 2`,
`label` A → B, byte-equal to `?from=1&to=2` |
| app: the same lineage | `2 → 3`, no changes | `1 → 2`, byte-equal to
`?from=1&to=2` |
| view: v1 draft, v2 draft (never published) | `1 → 2`, every v1 key
under removed (a draft body served as the from side) | `null → null`,
empty |
| view: v1 draft only | `null → 1`, empty | `null → null`, empty |
| app: draft only | 404 (the REST handler's absence for a gated type) |
404, unchanged |
| view: v1, v2 active, then deleted (v3 tombstone) | `2 → 3`, every v2
key under removed | `null → null`, empty (see Acceptance notes) |
| view: deleted, then a draft save v4 | `3 → 4`, no changes | `null →
null`, empty |
| view: v1 active, v2 draft, v3 publish (± a pending v4 draft) | `2 → 3`
no changes (`3 → 4` with v4 pending) | `2 → 3` no changes in both cases
(see Acceptance notes) |
## The order's mechanism hypotheses
- **H1, confirmed.** It reproduced as the table's first two rows.
Source: the `request.toVersion === undefined` arm read the body through
`repo.get(..., { state: 'active' })` and the label from
`histRows[histRows.length - 1].version`.
- **H2, confirmed at the row, not at the projection.** The
`sys_metadata` row carries `version`, equal to the history row whose
body it is. Measured in every lineage above: active v1 = history v1,
active v2 = history v2, the published row v3 = the `publish` history row
v3. The `MetadataItem` that `repo.get` returns does not carry it.
`rowToItem` builds `ref` from `fullRef` (no `version`) and exposes only
the content `hash`. So `diffMetaItem` reads the row itself, with the
same predicate `repo.get` uses (active state, no package scope). The
same file has two precedents: the ADR-0067 commit capture in
`publishPackageDrafts` reads the raw active row's `version` as
`prevVersion`, and `resolveOverlayPackageBinding` reads the raw row
rather than widening `MetadataItem`. Nothing looks a version up by body
or hash.
- **H3, measured.** The corrected `toVersion` does not change the from
rule. With v1 active, v2 draft, v3 publish, the default answers `2 → 3`
"no changes". The from side is the unpublished draft save v2, whose body
is the one v3 published. The answer is the same with a v4 draft pending.
The previously published v1 differs and is not the from side. Which
history rows count as versions is not changed here. See Acceptance
notes.
- **H4, pinned.** No active row ⇒ `null → null`, empty buckets, and no
draft body on either side. Before: labelled with the newest draft save,
and with two draft saves the first one's body was served as the from
side.
## Tests
-
`packages/metadata-protocol/src/protocol.diff-dead-history-read.test.ts`:
a new `#20397` describe block, 5 cases over seeded rows. It sits beside
that file's already-pinned engine double, which honours the `where` on
both tables, so the engine-double ledger is untouched. The cases: a
pending draft for `view` and for `app` equals the explicit range; the
card's app reading; draft-only; deleted, with the deletion still
readable as `2 → 3`.
- `packages/rest/src/meta-diff-default-range-labels.test.ts` (new, test
side only): 6 cases through the real routes and the real writes.
`toVersion` equals the active row's `version`, read past every door, and
the answer equals `?from=1&to=2` for an `app` and a `view`. The two card
readings, draft-only, and a lit control with no draft pending, which
answers `1 → 2` both before and after.
**Ablation**, from the committed fix (`53cad078f`), through
`scripts/ablation-replace.mjs`. The mutation puts the old label line
back (anchor 1 → 0, blob `3bb7041da297` → `7e3ce0508bd8`). After a
`@objectstack/metadata-protocol` rebuild, `ablation-dist-preflight`
found the marker in 2 built files, exit 0.
`protocol.diff-dead-history-read.test.ts`: 5 failed, 6 passed (the 5 new
cases red, the 6 older ones green).
`meta-diff-default-range-labels.test.ts`: 5 failed, 1 passed (the lit
control). The restore leg brought the blob back to the HEAD blob with
`git diff HEAD` empty. After a rebuild, `--absent` found the marker
absent from all 24 built files and the tree clean (exit 0). The re-runs
gave 11/11 and 6/6. Direction: red, as predicted. (A first run proved
the same mutation and the same 5 + 5 reds. Its preflight tree reading
refused only because the source spells the marker with a `!` that the
build drops, so it was re-run with `--source-marker`.)
## Gates, at the measured head `fd767fec0` (after a true merge of
`origin/main`)
- `pnpm --filter @objectstack/metadata-protocol exec vitest run`: 189
files passed, 3 skipped; 2750 tests passed, 19 skipped. `typecheck` (tsc
--noEmit) exit 0. Its program includes the edited test file
(`--listFilesOnly`: 1).
- `pnpm --filter @objectstack/rest exec vitest run --project local`: 215
files passed; 3898 tests passed, 26 skipped. `typecheck` (tsc --noEmit
plus `check:test-typecheck`, 0 debt) exit 0.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 62 commands. All 62 exit 0. `--ran`
reconciliation: 62 derived, 62 run, 0 NOT-MEASURED, a derived zero with
every exit code recorded. `check:dual-build-cjs-loads` and
`check:type-check-debt` first answered PREREQUISITE NOT MET (exit 3).
They were re-run exit 0 after `turbo run build --filter='./packages/*'
--filter='./packages/*/*'`, and the dist-reading gates were re-run on
that full build.
- eslint, narrowed to the 3 touched TypeScript files:
`--no-inline-config --format json` gave 3 results, 0 errors, 0 warnings.
All 3 are in eslint's own population (`isPathIgnored` false for each).
This config enables no type-aware linting (no `parserOptions.project`),
so the diff cannot move an untouched file's verdict. The repo-wide `pnpm
lint` is CI's.
## Acceptance notes
- **H3, carrier #20378.** The default from side is the history row
immediately before the label, draft saves included. So after a
draft-then-publish, the default range answers "no changes" (`N-1 → N`)
against the draft that was published, not against the previous published
version. That is the documented rule ("the immediately previous history
row"), and this PR does not change which history rows count as versions.
The order names #20378 as the carrier. Its ruling (5865708652) took B
and declined A (a state column on `sys_metadata_history`), so as the
thread stands no card holds that question.
- **Deleted items.** The default range of a deleted item was `N-1 → N`
up to its tombstone, every key removed. It is now `null → null`, the
same rule as a draft-only item: the to side is absent. The deletion is
still one explicit range away (`?from=N-1&to=N`, pinned).
- **A stale caller mention, outside this surface.**
`SysMetadataRepository.get`'s docblock (`sys-metadata-repository.ts`)
names "`diffMeta` compares this body against `sys_metadata_history`
bodies" as a reason `get` stays verbatim. `diffMetaItem` no longer calls
`get`. It reads the same row verbatim itself, and its comment carries
the same no-conversion rule. The bullet's reasoning holds, but it names
a caller that is gone. Carrier: none; noted, not filed.
- Consumers: `/diff` has no in-repo caller of its default range
(`client.meta.diffItem` has one docs example). objectui's
`MetadataClient.diff` has zero callers at the pin, as the #20378 round
measured.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7fa3e3e commit 8cdbe0c
4 files changed
Lines changed: 451 additions & 12 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- rest/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
Lines changed: 154 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
282 | 282 | | |
283 | 283 | | |
284 | 284 | | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21286 | 21286 | | |
21287 | 21287 | | |
21288 | 21288 | | |
21289 | | - | |
21290 | | - | |
21291 | | - | |
| 21289 | + | |
| 21290 | + | |
| 21291 | + | |
| 21292 | + | |
21292 | 21293 | | |
21293 | 21294 | | |
21294 | 21295 | | |
| |||
21391 | 21392 | | |
21392 | 21393 | | |
21393 | 21394 | | |
21394 | | - | |
21395 | | - | |
21396 | | - | |
21397 | | - | |
21398 | | - | |
21399 | | - | |
21400 | 21395 | | |
21401 | 21396 | | |
21402 | 21397 | | |
| |||
21467 | 21462 | | |
21468 | 21463 | | |
21469 | 21464 | | |
21470 | | - | |
21471 | | - | |
21472 | | - | |
| 21465 | + | |
| 21466 | + | |
| 21467 | + | |
| 21468 | + | |
| 21469 | + | |
| 21470 | + | |
| 21471 | + | |
| 21472 | + | |
| 21473 | + | |
| 21474 | + | |
| 21475 | + | |
| 21476 | + | |
| 21477 | + | |
| 21478 | + | |
| 21479 | + | |
| 21480 | + | |
| 21481 | + | |
| 21482 | + | |
| 21483 | + | |
| 21484 | + | |
| 21485 | + | |
| 21486 | + | |
| 21487 | + | |
| 21488 | + | |
| 21489 | + | |
| 21490 | + | |
| 21491 | + | |
| 21492 | + | |
| 21493 | + | |
| 21494 | + | |
| 21495 | + | |
| 21496 | + | |
| 21497 | + | |
| 21498 | + | |
21473 | 21499 | | |
21474 | 21500 | | |
21475 | 21501 | | |
| |||
0 commit comments