You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 397572e
Browse filesBrowse the repository at this point in the historyBrowse files
fix(metadata-protocol): the default /diff from side is the nearest earlier version whose body differs (#20451) (#20518)
Fixes#20451
Clause-②: no
## What changes
`diffMetaItem` (`packages/metadata-protocol/src/protocol.ts`), the
default `from` side only. With no `fromVersion`, the from side is now
the **nearest earlier history row whose body differs from the to
side's**, by the diff's own equality: `diffShallow`'s three buckets not
all empty, with an absent body compared as `{}` (the same `?? {}` the
comparison below it uses). A body-less row (a delete's tombstone)
therefore differs from any non-empty to side, and the walk stops on it
(triage's answer A, 5875579209). With no earlier row that differs, the
from side is absent: `fromVersion: null`, everything added.
- The walk reads only the rows the function already has: the one `find`
over `sys_metadata_history` (no limit), which the function sorts by
`version` in memory. It adds no read and no cap. A unit pin asserts one
history read on a walk path.
- It reads no `operation_type` and adds no state column (ruling B on
#20378, 5865708652).
- An explicit `?from=` / `?to=` names exactly its versions. The to-side
default (the active row's own version, PR #20443) is untouched. An
explicit `?to=` with no `?from=` walks back from the named version's
body.
- The comparison runs on the stored bodies before redaction, as the diff
itself does, so a credential-only change still stops the walk and its
values are still not served (unit pin).
The four statements of the default rule now say the new rule, each in
its own words: the `diffMetaItem` docblock,
`DiffMetaItemResponseSchema`'s JSDoc (`packages/spec`), the route's
OpenAPI summary in `rest-server.ts`, and the SDK's `diffItem` docblock
(`packages/client`). Comment and summary text only: no schema, route or
signature change.
## Measured on the real REST stack
The REST pins
(`packages/rest/src/meta-diff-default-range-labels.test.ts`, real routes
and real writes over better-sqlite3 `:memory:`) were committed first
(`9b308b12b`) and run against the unchanged source with its closure
built: **3 failed, 9 passed**. After the change: **12 passed**.
| history (fixture-proved by reading `sys_metadata_history`) | before |
after |
|:--|:--|:--|
| v1 `create` A (active), v2 `create` B (draft save), v3 `publish` B |
`2 → 3`, empty | `1 → 3`, `label` and `columns` changed, equal to
`?from=1&to=3` |
| the same, then a v4 draft save | `2 → 3`, empty | `1 → 3` |
| v1 `create` A, v2 `delete` (no body), v3 `create` A2 (draft), v4
`publish` A2 | `3 → 4`, empty | `2 → 4`, everything added, equal to
`?from=2&to=4` |
| v1 `create` A, v2 `delete` (no body), v3 `create` B (active) | `2 →
3`, everything added | the same bytes (green before and after) |
| v1 `create` New (draft), v2 `publish` New | `1 → 2`, empty | `null →
2`, everything added |
| v1 `create` (active) only | `null → 1`, everything added | the same
bytes |
| explicit `?from=2&to=3` over the first lineage | `2 → 3`, empty | the
same bytes |
The unit pins in `protocol.diff-dead-history-read.test.ts` repeat these
lineages over seeded rows beside the file's read-counting double.
**Ablation**, from the committed state: `node
scripts/ablation-replace.mjs` replaced the walk's differ test (`if
(d.added.length || d.removed.length || d.changed.length) {` → `if (true)
{`, which is the old immediately-previous rule), anchor 1 → 0, blob
`a2d2b7686f29` → `dd9cffcbd1f9`; the file ran **6 failed / 14 passed**,
exactly the six walk-dependent pins; restored, blob == HEAD and `git
diff HEAD` empty. No build is involved: the metadata-protocol suite
imports `./index.js` from source.
## A pending release note corrected: needs confirmation (Check Changeset
stays red)
`.changeset/20397-diff-default-range-labels.md` (PR #20443, not yet
released) said "The default `fromVersion` is still the history version
immediately before that label." This PR makes that sentence false in the
same release, so it now reads: "The default `fromVersion` rule is not
changed by this entry (#20451, in the same release, then moves it to the
nearest earlier version whose body differs from the to side's)." One
sentence, nothing else in that file.
This is the DELIBERATE CORRECTION class `check-empty-changeset.mjs`
names, so that gate exits 1 locally and **Check Changeset will stay red
on purpose**. Please confirm the correction on this PR. It was outside
the claim's file surface. `skip-changeset` is not applied and must not
be.
## Changeset
`.changeset/20451-diff-default-from-differs.md`:
`@objectstack/metadata-protocol` `patch` and `@objectstack/rest`
`patch`, `Clause-②: no`. The rest line is there because the route's
OpenAPI summary is a runtime string served in the OpenAPI document. The
`packages/spec` JSDoc and the `packages/client` docblock are
comment-only, so they get no line, per the repo's rule that comments do
not publish. All three packages are in one `fixed` group, so versions do
not move differently either way.
## Verification (measured at `1f258bbd5`, after merging `origin/main`
`9449512a3` with a true merge commit)
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 88 commands, all run, each exit code
written to a file before any pipe. **87 exit 0; 1 exit 1**: `node
scripts/check-empty-changeset.mjs --base origin/main`, the release-note
correction above. Also run, outside the derivation: the four roster
gates whose roster sits under a changed path (`check-changeset-fixed`,
`check:meta-url-spelling`, `check:spec-changes`,
`check:error-code-casing`), all exit 0.
- `dispatch-gates --ran`: "88 derived famil(ies) accounted for — 88 run,
0 NOT-MEASURED".
- Tests, each through `os-verify-lock`: metadata-protocol 189 files
passed, 3 skipped (2759 tests); rest `--project local` 219 files passed
(4181 tests); client 50 files passed (641 tests); spec `--project local`
573 files passed (16801 tests).
- Typecheck: metadata-protocol, rest (with `check:test-typecheck`),
client (with `check:test-typecheck`) and spec all exit 0. Both edited
test files are in their tsc programs (`--listFiles`: 1 hit each).
- `pnpm --filter @objectstack/spec check:generated`: all 15 generated
artifacts up to date, against a spec `dist` built from this tree.
- Declared to CI, not run here: the five path-scheduled jobs (Test Core
shards, Temporal Conformance, Dogfood Regression, Dogfood Verify CLI,
Build Core) and the workspace type-check lanes, which `dispatch-gates`
lists as CI's own shell.
## Statements the census named, measured
- **Edited:** the four above, plus the header of
`meta-diff-default-range-labels.test.ts` (this PR adds to that file),
which said the from side is "the history row immediately preceding that
label".
- **Not false, not edited:**
`docs/qa/platform-checklist/areas/studio-authoring.json` lines 346 and
402 ("omit the params for previous-vs-current"). On that probe's
lifecycle (draft save, publish, second draft save, second publish) the
default range now compares the second published revision with the first
(`2 → 4`), which is the comparison those steps describe. Before this PR
it compared the second publish with its own draft save and answered "no
changes".
- **Not false, not edited:** the test title in
`rest-server-query-number-reads.test.ts:300` ("from/to still mean
previous-vs-current (no version members)"). It asserts only that no
version member reaches the verb, which still holds. This PR does not
touch that file.
## Acceptance notes
- The same checklist steps' explicit range `?from=1&to=2` compares the
probe's first draft save (v1) with its own publish (v2). In the
draft-then-publish lifecycle those two rows carry the same body, so that
range answers "no changes", before and after this PR. This is a
checklist wording issue, not a product defect. No card filed; carrier:
none.
- `DiffMetaItemResponseSchema.fromVersion`'s `.describe()` reads "`null`
when that side is absent (e.g. the item had no earlier version)". It is
still true, and now `null` also answers "no earlier version differs". It
was left as is: a `.describe()` edit regenerates spec docs, and the
claim limits `packages/spec` to comment text.
- `.changeset/20139-rest-query-number-census.md` says
"previous-vs-current on `/diff`" about absent parameters keeping their
default. That is still true of the parameter handling. It is somebody
else's pending note and is not touched.
- Test-side deviation: `seedLineage` in
`protocol.diff-dead-history-read.test.ts` moved from inside the #20397
`describe` to module scope, unchanged, so the #20451 block shares it
rather than copying it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: .changeset/20397-diff-default-range-labels.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,6 +6,6 @@ fix(metadata-protocol): `diffMetaItem`'s default range labels its to side with t
6
6
7
7
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, which is a draft save whenever a draft is pending: every draft save appends a history row. The labels and the bodies then named different rows. Measured on the real REST stack, an app with one active save and two draft saves answered `fromVersion 2 → toVersion 3` over its version-1 body, and a view with one active save and one draft save answered "no changes" labelled `1 → 2` while version 2 differs.
8
8
9
-
-**Now:**`toVersion` is the active row's own `version`, read in the same read as its body. The default `fromVersion` is still the history version immediately before that label. An item whose active row is version 2 with a draft pending answers `1 → 2`, the same answer as `?from=1&to=2`.
9
+
-**Now:**`toVersion` is the active row's own `version`, read in the same read as its body. The default `fromVersion`rule is not changed by this entry (#20451, in the same release, then moves it to the nearest earlier version whose body differs from the to side's). An item whose active row is version 2 with a draft pending answers `1 → 2`, the same answer as `?from=1&to=2`.
10
10
-**No active row** (a draft-only item, or a deleted one): the to side is absent, and both labels are `null` with empty buckets, as the response schema declares for an absent side. Before, a draft-only item was labelled with its newest draft save, and its from side could be an earlier draft save's body. A deleted item was labelled `N-1 → N` up to its tombstone. That deletion is still read by naming its versions (`?from=N-1&to=N`).
11
11
- Unchanged: the response shape, explicit `from` / `to` ranges, and the default range of an item with no draft pending.
fix(metadata-protocol): `GET /meta/:type/:name/diff` with no `from` compares against the nearest earlier version whose body differs, so the default diff right after a publish shows what the publish changed (#20451)
7
+
8
+
Clause-②: no — no key, export, route, parameter or response field moves; only which version the default `from` side names.
9
+
10
+
Every draft save appends a `sys_metadata_history` row, and publishing the draft appends the same body again as the next row. The default `from` side was the history row immediately before the `to` side, so right after a publish it was the draft save the publish came from, and the default diff answered "no changes". The change the publish carried was reachable only by naming `?from=`.
11
+
12
+
-**Now:** with no `from`, `diffMetaItem` walks back from the `to` side over the history rows it already reads and takes the nearest earlier row whose body differs, by the diff's own equality (all three buckets empty means equal). A body-less row, a delete's, compares as an empty body, so the walk stops on it and the answer names the deletion. With no earlier row that differs, the `from` side is absent: `fromVersion: null`, everything added.
13
+
-**Measured on the real REST stack**, before → after:
14
+
15
+
| history | default range before | default range now |
16
+
|:--|:--|:--|
17
+
| v1 active, v2 draft save, v3 publish |`2 → 3`, no changes |`1 → 3`, the change the publish carried |
18
+
| the same with a v4 draft pending |`2 → 3`, no changes |`1 → 3`|
| a new item draft-saved, then published |`1 → 2`, no changes |`null → 2`, everything added |
22
+
| a single version |`null → 1`, everything added | unchanged |
23
+
24
+
-**Unchanged:** an explicit `?from=` / `?to=` names exactly its versions (`?from=2&to=3` over the first row still answers "no changes"); the default `to` side is the active version; the response shape; the one history read, with no cap. The walk compares the stored bodies before redaction, as the diff itself does, so a credential-only change still stops it and its values are still not served.
25
+
-`@objectstack/rest`: the route's OpenAPI summary states the new default.
0 commit comments