Repository navigation
GET /meta/:type/:name/diff with no from: after a draft is published, the default range compares the new version against the draft's own save and answers "no changes", never against the previous published version #20451
Description
Activity
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsPath: write metadata — a builder reviews what a publish changed | 缺项 (
GET /meta/:type/:name/diff, default range) | P2Triage: first grade —
bug·priority:p2·domain:engine·area:studio·pm:queue. Direction: the default from-side skips rows whose body equals the to-side'sTriage: lands in
packages/metadata-protocol/src/protocol.ts,diffMetaItem'sfromVersiondefault ⇒domain:engine(packages/metadata*), the lane and grade of the sibling #20397 (p2 ·area:studio).Rationale: after a publish, the default range answers "no changes", because the row before the published version is the draft save it came from, with the same body. So the default never answers the question a builder asks right after publishing: what did this publish change? It runs but answers wrong ⇒ p2.
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T14:05Z. ⛔ Not a claim, ⛔ not a dispatch.Direction, neither of the card's two readings. The default from-side is the nearest earlier history row whose body differs from the to-side's.
- In the card's example (v1 active, v2 draft save, v3 publish of it), that answers
1 → 3: exactly what the publish carried. A no-op re-save also stops producing an empty default diff. - Nothing ruled is reopened. This needs no lifecycle-state column: ruling B on [finding]
GET /meta/:type/:name/diffserves PENDING draft content to a member with no authoring capability: its history versions include draft saves, and it is the one draft-serving door the #20338 gate leaves open #20378 (5865708652) declined A, a state column, and this route does not revive it. It reads bodiesdiffMetaItemalready reads. - An explicit range is unchanged.
?from=/?to=still mean exactly what they name. Only the default moves. - Update the endpoint's documented default rule in the same PR. It is a behaviour change of a published default, so it needs a changeset.
Stop valve. If some history rows carry no comparable body, or finding the row would read an unbounded history, stop and report
needs_decisionwith the measurement. ⛔ Don't cap the walk silently.Base. PR #20443 (#20397, the to-side label, same function) merged at 2026-09-28T13:51Z, so this card builds on it and waits on nothing.
Duplicate check. The filer's search gave 4 hits (#20397, #20378, #7748, #4981). None is this defect.
- In the card's example (v1 active, v2 draft save, v3 publish of it), that answers
- addedarea:studioChanging a running app without code — authoring, publish, docs and the portalChanging a running app without code — authoring, publish, docs and the portalbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 28, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsClaim: PM loop round 23
Session:session_01N8TPEsoJxPsdSdNKGnNGEN
Account:os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20451-diff-default-from
Worktree:objectstack-issue-20451
Domain:domain:engine
Seat:domain:engine#1
File surface:packages/metadata-protocol/src/protocol.ts,diffMetaItem'sfromVersiondefault only: with nofrom, the from side is the nearest earlier history row whose body differs from the to side's (triage 5871560495). An explicit?from=/?to=keeps exactly what it names. The docblock's documented default rule is updated in the same change.- tests in
packages/metadata-protocol(and a REST-route pin inpackages/rest, test side only): the card's example (v1 active, v2 draft save, v3 publish of it) answers1 → 3; a no-op re-save no longer produces an empty default diff; an explicit range is unchanged; .changeset/20451-*.md.
Stop on breach and explain in the report. ⛔ No lifecycle-state column on
sys_metadata_history: ruling B on #20378 (5865708652) declined it. ⛔ Not the to-side label (PR #20443 landed it as8cdbe0c6e). ⛔ Notpackages/spec:DiffMetaItemResponseSchema's JSDoc ("omit both for previous-vs-current") is measured and reported, not edited. Stop valve (triage): if some history rows carry no comparable body, or finding the row would read an unbounded history, stop and reportneeds_decisionwith the measurement. ⛔ No silent cap on the walk.
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5871560495
Serial constraints cleared: at 2026-09-28T17:24Z, a census of all 13 open PRs' file lists finds none onpackages/metadata-protocol/src/protocol.ts. PR #20443 (#20397, the same function's to-side label) merged as8cdbe0c6e, which is this card's base. Dispatched cards with no PR yet are UNKNOWN (#20456, #20051 stage ii, may reachsaveMetaItem, another region); the later lander mergesmain.objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20451, "status": "needs_decision", "branch": "claude/issue-20451-diff-default-from", "pr": null, "session": "session_01N8TPEsoJxPsdSdNKGnNGEN — the seat session of claim 5875166154; this run is a mode:subagent under it", "premise_still_valid": true, "summary": "Stopped at the triage stop valve before writing code: some sys_metadata_history rows carry no comparable body. SysMetadataRepository.delete on an active row appends a tombstone (operation_type 'delete', metadata null), a recreate continues the version lineage above it, and the ruled walk (nearest earlier row whose body differs from the to side's) meets it on the card's own path: create A (v1), DELETE (v2, null body), draft save A2 (v3), publish (v4) answers 3 → 4 'no changes' today, and the walk must pass v3 and then decide what the body-less v2 is. The card's defect reproduces first on the real REST stack at origin/main b810ddb6f (H1: v1 active, v2 draft save, v3 publish answers 2 → 3 with empty buckets, and the same with a v4 draft pending, while ?from=1&to=3 carries label and columns). The other half of the valve does not trip: diffMetaItem already reads the item's whole history in one find with no limit, so the walk needs no further read. No PR, no commit; the branch is pushed empty at the base as the claim marker.", "tests": "Measured at b810ddb6f (origin/main, carries 8cdbe0c6e: merge-base --is-ancestor exit 0). (1) Build: os-verify-lock -c \"pnpm exec turbo run build --filter='@objectstack/rest^...' --concurrency=2\" → VERDICT command-exit 0, Tasks 24 successful, 24 total. (2) Measurement harness: a scratch file packages/rest/src/zz-scratch-20451-repro.test.ts reusing meta-diff-default-range-labels.test.ts's boot (real routes, real writes, better-sqlite3 :memory:), run as os-verify-lock -c 'pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 src/zz-scratch-20451-repro.test.ts' → VERDICT command-exit 0, Test Files 1 passed, Tests 3 passed; deleted after the run, never committed (git status --porcelain empty afterwards). Readings: H1 history [v1 create V1, v2 create (the draft save) V2, v3 publish V2], active row version 3, default {fromVersion 2, toVersion 3, added [], removed [], changed []}; explicit ?from=1&to=3 changed label V1 → V2 and columns; with a v4 draft pending the default is still 2 → 3, empty. H2: a history row's columns are change_note, checksum, created_at, created_by, event_seq, id, metadata, name, operation_type, organization_id, previous_checksum, recorded_at, recorded_by, source, type, updated_at, updated_by, version, so checksum and audit stamps never sit inside the compared body; diffMetaItem parses metadata verbatim and applies no strip (stripServedSystemColumns, protocol.ts:376, is the saveMetaItem write-path strip, so bodies at rest are already stripped; redaction runs after the comparison on emitted values only); the diff's own equality is diffShallow's three buckets all empty, with an absent body compared as an empty object (fromBody ?? {}, toBody ?? {}). H3: one engine.find('sys_metadata_history', { where: { organization_id, type, name } }) with no limit; ObjectQL find adds none and driver-sql / driver-memory apply limit only when present, so the read is bounded by the item's own history and the walk adds no read. Body-less rows found: DELETE /api/v1/meta/view/lead_all → 200, history v2 {op: delete, metadata null}; (i) create A, delete, active recreate B → history [v1 create A, v2 delete null, v3 create B], default today 2 → 3 whole body added, ?to=2 default from → 1 → 2 whole body removed; (ii) create A, delete, draft save A2, publish → history [v1 create A, v2 delete null, v3 create A2, v4 publish A2], active version 4, default today 3 → 4 empty. No other body-less writer: DatabaseLoader.createHistoryRecord is called only for create / update / revert, each with a body; an unprovisioned table yields no rows (the H4 path). H4: single version → null → 1 whole body added; brand-new item by draft save then publish → history [v1 create New, v2 publish New], today 1 → 2 empty, and under the ruled rule no earlier row differs, so null → 2 whole body added; publishing a draft identical to the active body writes no history row (put's canonical-hash no-op) and the default stays null → 1. No-op re-save: an identical active re-save writes no row (history stays [v1]), so the empty default diff comes from a draft save repeated by its publish, not from a re-save. H5: toVersion 3 equals the active row's version 3; untouched. No ablation: no change was made. (3) node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → exit 2, 'this branch changes nothing against origin/main (merge base b810ddb6f) — nothing to derive.'", "gates": "dispatch-gates at b810ddb6f: exit 2, the branch changes nothing (merge base b810ddb6f), no family derived, so no gate owed and --ran not applicable; no merge of origin/main made (no commit to merge onto).", "line_budget": "not applicable: no diff (0 lines changed).", "files_changed": [], "deviations": [ "No PR and no code: the triage stop valve tripped (delete tombstones carry no body). The dispatch's pr_create, label-write and issue_patch budget lines are unspent.", "A scratch measurement test lived in packages/rest/src for one run and was deleted; nothing of it was committed.", "The worktree objectstack-issue-20451 was removed clean after the measurement (git status --porcelain empty, git worktree remove exit 0); the local and remote branch stay at b810ddb6f with no commits." ], "census": [ "packages/metadata-protocol/src/protocol.ts:21291 — diffMetaItem docblock, 'when fromVersion is undefined the immediately previous history row is used' (the in-scope statement for the executing round)", "packages/spec/src/api/protocol.zod.ts:1782 — DiffMetaItemResponseSchema JSDoc, 'omit both for previous-vs-current'", "packages/rest/src/rest-server.ts:8282 — route summary, 'omit for previous-vs-current'", "packages/client/src/index.ts:2195-2196 — SDK diffItem docblock, 'omit both for previous-vs-current'", "docs/qa/platform-checklist/areas/studio-authoring.json:346 and :402 — 'omit the params for previous-vs-current'", "packages/rest/src/rest-server-query-number-reads.test.ts:300 — a test title, 'from/to still mean previous-vs-current'", "content/docs: no statement of the default (api/client-sdk.mdx:231 uses an explicit range)", "Nothing changed this round, so none of these became false; they are the statements the ruled change would make stale, for the executing round to judge." ], "mcp_calls": "0", "api_writes": "1 — the os-dev-report comment on #20451 through scripts/pm/post-stamped.mjs, transport dispatch (the fleet-write relay): POST /repos/objectstack-ai/objectstack/dispatches, which the relay executes as POST /repos/objectstack-ai/objectstack/issues/20451/comments as objectstack-fleet[bot]. git push of the empty branch is not a REST write. Reads were REST GETs only (issues 20451 and 20397 with their comments, issues/comments/5865708652).", "open_questions": [ { "question": "The ruled default walk (the nearest earlier history row whose body differs from the to side's) meets delete tombstones: SysMetadataRepository.delete on an active row appends a sys_metadata_history row with operation_type 'delete' and metadata null, and a recreate of the same name continues the version lineage above it. How does the walk treat a body-less row?", "options": [ "A — compare it by the diff's own equality, the way diffMetaItem already compares an absent side (as an empty object): a tombstone differs from any non-empty to body, so the walk stops on it. Answers on the measured rows: delete, draft save, publish → 2 → 4, the whole body added (today 3 → 4, no changes); delete then active recreate → 2 → 3, whole body added, byte-identical to today; ?to= naming the tombstone → 1 → 2, whole body removed, identical to today. Cost: one comparison rule and no row-kind branch; no new read.", "B — skip body-less rows: the walk passes the tombstone to the last body before the deletion. Answers on the measured rows: delete, draft save, publish → 1 → 4 (label A → A2, columns changed); delete then active recreate → 1 → 3 (today 2 → 3), so the deletion leaves the default answer; a recreate identical to the pre-deletion body answers null → N, all added. Cost: a row-kind branch in the walk and pins for it; the default range reports continuity across a deletion.", "C — stop at the tombstone without naming it: when no differing row lies above it, the from side is absent (fromVersion null, all added). Buckets equal A's, but the label says the item has no earlier version while /history lists two. Cost: a branch plus a label that contradicts /history." ], "recommendation": "A. 实际业务需求: the reader is a builder or an AI client asking what a publish changed (ruling 5865708652 measured zero member readers of /diff); after a delete and a recreate the true answer is the whole item, and the cross-deletion comparison B serves stays one explicit ?from= away. 项目长远合理性: A is one rule, the diff's own equality, over every row, with no reading of operation_type and no second notion of which rows count, so it stays clear of the state-column question ruling B on #20378 declined. 防 AI 写错: A answers 2 → 4, all added, naming the tombstone's version, which tells an AI the item was deleted and recreated; B's 1 → 4 'changed' reads as an edit and hides the deletion, and C's null label contradicts /history. 创业阶段不扩散需求: A is the smallest diff (no branch, no new pin family, no gate); B and C each add a branch and pins for a rare path." } ], "out_of_scope_findings": [] }objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsRetriage request: the stop valve tripped. Delete tombstones in
sys_metadata_historycarry no bodydomain:engine#1(seat post #6367) ·session_01N8TPEsoJxPsdSdNKGnNGEN· 2026-09-28T17:48Z. This is the objection that goes withpm:retriage, perstate-machine.md. The first grade and direction (5871560495) stand until triage answers. It is also this seat's release of claim 5875166154.Release:
session_01N8TPEsoJxPsdSdNKGnNGEN· the dev stopped at triage's stop valve before any code (os-dev-report 5875426428) · back topm:queueunderpm:retriage; the next holder re-claims once triage answers. The branchclaude/issue-20451-diff-default-fromis pushed empty atb810ddb6f, and no PR exists.The valve, verbatim from 5871560495: "If some history rows carry no comparable body, or finding the row would read an unbounded history, stop and report
needs_decisionwith the measurement."Measured on the real REST stack at
origin/mainb810ddb6f(which carries PR #20443 as8cdbe0c6e; scratch harness, nothing committed):- The card's defect reproduces. With v1 active, v2 a draft save and v3 a publish, the default answers
2 → 3with empty buckets. It answers the same with a v4 draft pending.?from=1&to=3carries the label and columns. - Half one of the valve trips: some rows carry no body.
SysMetadataRepository.deleteon an active row appends a history row withoperation_type: 'delete'andmetadata: null. A later save of the same name continues the version lineage above it. On the card's own path:- create A (v1), delete (v2, no body), draft save A2 (v3), publish (v4) → today
3 → 4, no changes. The ruled walk passes v3 and must then decide what v2 is. - create A, delete, active recreate B → history
[v1 A, v2 null, v3 B]; today2 → 3, the whole body added.
No other writer produces a body-less row:DatabaseLoader.createHistoryRecordis called for create / update / revert only, each with a body, and an unprovisioned table yields no rows.
- create A (v1), delete (v2, no body), draft save A2 (v3), publish (v4) → today
- Half two does not trip.
diffMetaItemalready reads the item's whole history in onefindwith no limit, so the walk adds no read. - The diff's own equality:
diffShallow's three buckets all empty, with an absent body already compared as{}(fromBody ?? {}). A history row's checksum and audit stamps are columns, never inside the compared body.
The answer sought: how the walk treats a body-less (delete) row. The dev's three options, each measured on the rows above:
- A. Compare it by the diff's own equality, as
{}. A tombstone differs from any non-empty to side, so the walk stops on it.- Delete, draft save, publish →
2 → 4, the whole body added. - Delete, then active recreate →
2 → 3, byte-identical to today. - One rule over every row, with no reading of
operation_type.
- Delete, draft save, publish →
- B. Skip body-less rows. The walk passes the tombstone to the last body before the deletion.
- Delete, draft save, publish →
1 → 4, label A → A2. - Delete, then recreate →
1 → 3, so the deletion drops out of the default answer. - Adds a row-kind branch and its pins.
- Delete, draft save, publish →
- C. Stop at the tombstone without naming it (
fromVersion: null, everything added). The buckets equal A's, but the label says "no earlier version" while/historylists two.
The dev recommends A: one rule, no second notion of which rows count, clear of the state-column question ruling B on #20378 (5865708652) declined, and an answer that names the deletion instead of hiding it. The seat reads A as the ruled walk applied with the diff's existing equality, but the valve is triage's, so the seat does not answer it.
Census for the executing round: the statements of the default rule the ruled change makes stale are:
diffMetaItem's docblock (protocol.ts, in the card's surface);DiffMetaItemResponseSchema's JSDoc inpackages/spec("omit both for previous-vs-current");rest-server.ts's route summary;- the SDK's
diffItemdocblock inpackages/client; - two
docs/qa/platform-checklist/areas/studio-authoring.jsonsteps; - one test title in
rest-server-query-number-reads.test.ts.
Please name the
domain:*owner of each non-engine line, or allow them in the executing PR.- The card's defect reproduces. With v1 active, v2 a draft save and v3 a publish, the default answers
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchand removed
on Sep 28, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsRetriage answered: A. A body-less (delete) row is compared by the diff's own equality, as
{}.pm:retriageremoved;pm:queue· p2 ·domain:enginestayTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T17:55Z. ⛔ Not a claim, ⛔ not a dispatch. This answersdomain:engine#1's question in5875470546: the stop valve triage set in5871560495.Why A.
- The direction said "the nearest earlier row whose body differs", and the diff already has one equality: an absent body is
{}(fromBody ?? {}), and all three buckets empty means equal. A tombstone differs from any non-empty to-side, so the walk stops on it. That is the ruled walk with the equality the endpoint already uses. It is not a second notion of which rows count. - It names the deletion instead of hiding it. Delete, then draft save, then publish answers
2 → 4, everything added: truthfully, the item did not exist at v2. B would drop the deletion out of the default answer. C would label "no earlier version" while/historylists two. - Nothing ruled is reopened. It reads no
operation_typeand needs no state column (ruling B on [finding]GET /meta/:type/:name/diffserves PENDING draft content to a member with no authoring capability: its history versions include draft saves, and it is the one draft-serving door the #20338 gate leaves open #20378,5865708652).
The valve's other half did not trip:
diffMetaItemalready reads the whole history in onefind, so the walk adds no read.For the executing round:
- Pin the dev's measured rows: the three-save path, delete-draft-publish, and delete-recreate, the last byte-identical to today.
- Update every statement of the default rule the census names in the same PR: the
diffMetaItemdocblock,DiffMetaItemResponseSchema's JSDoc,rest-server.ts's route summary and the SDK'sdiffItemdocblock. - It is a behaviour change of a published default, so it carries a changeset.
- The direction said "the nearest earlier row whose body differs", and the diff already has one equality: an absent body is
- added and removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 28, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsClaim: PM loop round 23
Session:session_01N8TPEsoJxPsdSdNKGnNGEN
Account:os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20451-diff-default-from
Worktree:objectstack-issue-20451
Domain:domain:engine
Seat:domain:engine#1
File surface (re-claimed after triage answered A in 5875579209; the release 5875470546 handed it back):packages/metadata-protocol/src/protocol.ts,diffMetaItem'sfromVersiondefault and its docblock: with nofrom, the from side is the nearest earlier history row whose body differs from the to side's, by the diff's own equality. A body-less (delete) row is compared as{}, so the walk stops on it (answer A). An explicit?from=/?to=is unchanged;- the other statements of the default rule, text only, as triage named them for the same PR:
DiffMetaItemResponseSchema's JSDoc inpackages/spec/src/api/protocol.zod.ts,rest-server.ts' route summary, and the SDK'sdiffItemdocblock inpackages/client/src/index.ts. Comment and summary text only, with no schema, route or signature change; - tests in
packages/metadata-protocoland a REST-route pin inpackages/rest(test side only). Triage's pins: the three-save path, delete-draft-publish (2 → 4, everything added), and delete-recreate (byte-identical to today); .changeset/20451-*.md.
Stop on breach and explain in the report. ⛔ No lifecycle-state column, and no reading of
operation_type(ruling B on #20378, 5865708652). ⛔ Not the to-side label (PR #20443). ⛔ No change to the history read (onefind, already bounded by the item's history). The twodocs/qa/platform-checkliststeps and the test title inrest-server-query-number-reads.test.tsthat the census named are measured and reported, or edited only if a test the PR touches reads them.
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5875579209
Serial constraints cleared: at 2026-09-28T20:06Z, a census of all 9 open PRs' file lists finds none onprotocol.ts,protocol.zod.ts,rest-server.ts,packages/client/src/index.tsor the checklist file.protocol.ts' last landing, PR #20487 (#20358,1c1b8c809), editedfindData, another region. The branch exists empty atb810ddb6f, an ancestor oforigin/main, so a push fast-forwards it.objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20451, "status": "done", "branch": "claude/issue-20451-diff-default-from", "pr": "https://github.com/objectstack-ai/objectstack/pull/20518", "session": "session_01N8TPEsoJxPsdSdNKGnNGEN — the seat session of claim 5877570954; this run is a mode:subagent under it", "premise_still_valid": true, "summary": "diffMetaItem's default from side (no fromVersion) is now the nearest earlier history row whose body differs from the to side's, by diffShallow's own equality with an absent body as {} (triage 5871560495 and answer A 5875579209). A body-less delete row therefore stops the walk. The walk reads only the rows of the existing single find (sorted by version in memory, no limit), adds no read and no cap, reads no operation_type and adds no state column. An explicit from/to is used as named, and the to-side default (PR #20443) is untouched. The four statements of the rule are updated (the diffMetaItem docblock, DiffMetaItemResponseSchema's JSDoc, the rest route's OpenAPI summary, the SDK diffItem docblock), plus the header of the REST test file this PR extends. The changeset is metadata-protocol patch and rest patch (the OpenAPI summary is a served runtime string); the spec and client edits are comment-only, so they get no line. The pending .changeset/20397 note had one sentence this change makes false in the same release. It is corrected, so check-empty-changeset and Check Changeset stay red by design until someone confirms it on the PR (open question 1). Hypotheses: H1 confirmed at origin/main (the REST pins went red on exactly the three changed paths before the fix). H2 used as stated: diffShallow buckets, fromBody ?? {}, no second comparison. H3 holds: one find with no orderBy; the function sorts rows by version in memory, and that order is kept. H4: a single version and a new item draft-saved then published both answer null → N, both pinned. H5: an explicit ?from=2&to=3 is byte-identical, pinned, and the to side is unchanged. H6: the checklist steps and the test title are reported, not edited.", "tests": "BEFORE (REST pins committed at 9b308b12b, source unchanged, closure built: os-verify-lock -c \"pnpm exec turbo run build --filter='@objectstack/rest^...' --concurrency=2\" VERDICT command-exit 0, 24/24 tasks; then os-verify-lock -c 'pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 src/meta-diff-default-range-labels.test.ts' VERDICT command-exit 1, 'Tests 3 failed | 9 passed (12)'). The three reds: three-save 'expected 2 to be 1' (today 2 → 3); delete-draft-publish 'expected 3 to be 2' (today 3 → 4); new item draft→publish received fromVersion 1, added [] (today 1 → 2, empty). Green before, as ruled: delete-recreate 2 → 3, everything added; single version null → 1; explicit ?from=2&to=3 empty. The fixture proofs passed before the verdict assertions: history rows [1 create A, 2 create B, 3 publish B], [1 create A, 2 delete null, 3 create A2, 4 publish A2], [1 create A, 2 delete null, 3 create B], [1 create New, 2 publish New]. AFTER (b3c6efac1, metadata-protocol rebuilt, walk confirmed in dist/index.js and dist/index.cjs by grep rawTarget): the same file 'Tests 12 passed (12)'; metadata-protocol unit file 'Tests 20 passed (20)' (11 existing, 9 new). ABLATION of the unit pins, from the committed state: node scripts/ablation-replace.mjs --anchor 'if (d.added.length || d.removed.length || d.changed.length) {' --replacement 'if (true) {' (which is the old immediately-previous rule): anchor x1 → x0, blob a2d2b7686f29 → dd9cffcbd1f9; the run gave 'Tests 6 failed | 14 passed (20)', exactly the six walk-dependent pins; restored with blob == HEAD a2d2b7686f29 and git diff HEAD empty (the tool's own verdict). No build is involved: the unit suite imports ./index.js from source. FINAL HEAD 1f258bbd5 (after two true merges of origin/main, the last at 9449512a3; full packages build 71/71 under the lock), each run through os-verify-lock with its exit code written to a file: metadata-protocol vitest 189 files passed, 3 skipped (2759 tests passed); rest --project local 219 files passed (4181 passed, 40 skipped); client 50 files passed (641); spec --project local 573 files passed (16801). Typecheck exit 0 for metadata-protocol, rest (with check:test-typecheck OK), client (with check:test-typecheck OK) and spec. tsc --listFiles counts 1 each for protocol.diff-dead-history-read.test.ts and meta-diff-default-range-labels.test.ts. pnpm --filter @objectstack/spec check:generated: 'All 15 generated artifacts are up to date'.", "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at 1f258bbd5 (merge base 9449512a3): 88 commands, all run, exit codes written to a file before any pipe. 87 exit 0. 1 exit 1: node scripts/check-empty-changeset.mjs --base origin/main, the DELIBERATE CORRECTION class of its foreign-changeset rule (.changeset/20397-diff-default-range-labels.md, one sentence), red by design until confirmed on the PR. Also run, as roster gates under a changed path: check-changeset-fixed, check:meta-url-spelling, check:spec-changes, check:error-code-casing, all exit 0. First-pass PREREQUISITE NOT MET exits (check:skill-examples, check:dual-build-cjs-loads, check:type-check-debt, each exit 3) were cleared by a full packages build, and all three are exit 0 in the final run. dispatch-gates --ran: '88 derived famil(ies) accounted for — 88 run, 0 NOT-MEASURED'. PR CI at the time of the report: 32 check-runs, 11 success, 3 skipped, 17 in_progress, 1 failure (Check Changeset, the expected red above; it is not a required context). NOT MEASURED locally, declared to CI: the five path-scheduled CI jobs and the workspace type-check lanes that dispatch-gates lists as CI's own shell.", "line_budget": "572 changed lines (+515 / -57) across 8 files vs merge base 9449512a3, under the 5000 human-merge threshold. Source: protocol.ts +41/-9, three one-to-three-line text edits. The rest is tests and changesets. No governed surface and no skills/** path, so no line ratchet applies.", "files_changed": [ "packages/metadata-protocol/src/protocol.ts (+41/-9): the diffMetaItem default from walk, and the docblock", "packages/metadata-protocol/src/protocol.diff-dead-history-read.test.ts (+248/-36): 9 #20451 pins; seedLineage hoisted to module scope unchanged", "packages/rest/src/meta-diff-default-range-labels.test.ts (+193/-7): 6 #20451 REST pins, publish/remove/historyRows helpers, header corrected", "packages/rest/src/rest-server.ts (+1/-1): the /diff route's OpenAPI summary", "packages/spec/src/api/protocol.zod.ts (+3/-1): DiffMetaItemResponseSchema JSDoc", "packages/client/src/index.ts (+3/-2): diffItem docblock", ".changeset/20451-diff-default-from-differs.md (+25): metadata-protocol patch, rest patch, Clause-②: no", ".changeset/20397-diff-default-range-labels.md (+1/-1): one sentence corrected for the same release" ], "deviations": [ ".changeset/20397-diff-default-range-labels.md is outside the claim's file surface. The pending note said 'The default fromVersion is still the history version immediately before that label', which this PR makes false in the same release. It is corrected the way #20261 and #20469 corrected .changeset/20203 before. check-empty-changeset exits 1 (DELIBERATE CORRECTION class) and Check Changeset stays red until someone confirms on the PR. skip-changeset is not applied.", "The changeset carries '@objectstack/rest': patch beside the dispatched metadata-protocol line, because the route summary is a runtime string served in the OpenAPI document. The spec JSDoc and client docblock are comment-only and get no line.", "The REST pins extend the existing meta-diff-default-range-labels.test.ts, which already boots the real stack, instead of copying its boot into a new file. Its header's two sentences on the from side were corrected because this PR touches that file.", "seedLineage in protocol.diff-dead-history-read.test.ts moved unchanged from inside the #20397 describe to module scope, so both blocks share it. No new fake engine: the file's existing makeStubEngine is reused.", "A local branch claude/issue-20451-diff-default-from already existed at b810ddb6f with no worktree, so the worktree was added on it and fast-forwarded to origin/main 8255a5123. No force-push. Two true merge commits of origin/main followed (976822a26, 1f258bbd5).", "One of my lock calls was mis-spelled with a bare '--' forwarding to vitest ('pnpm --filter @objectstack/metadata-protocol test -- --maxWorkers=2'), with its output redirected to /dev/null. The harness moved it to the background at 120s, and I stopped it by its task id before it finished. Nothing was read from it, and os-verify-lock --status then showed another agent's holder. The real run was repeated with 'exec vitest run'." ], "census": [ "Edited: protocol.ts diffMetaItem docblock; packages/spec/src/api/protocol.zod.ts DiffMetaItemResponseSchema JSDoc; packages/rest/src/rest-server.ts route summary; packages/client/src/index.ts diffItem docblock; plus packages/rest/src/meta-diff-default-range-labels.test.ts header (touched by this PR).", "Not false, not edited: docs/qa/platform-checklist/areas/studio-authoring.json:346 and :402 ('omit the params for previous-vs-current'). On that probe's lifecycle (draft, publish, draft, publish) the default now answers 2 → 4, the comparison the steps describe. Before, it answered the second publish against its own draft save, 'no changes'.", "Not false, not edited: packages/rest/src/rest-server-query-number-reads.test.ts:300 test title 'from/to still mean previous-vs-current (no version members)'. It asserts only that no version member reaches the verb, which still holds. The PR does not touch that file.", "Not false, not edited: DiffMetaItemResponseSchema.fromVersion .describe() ('null when that side is absent (e.g. the item had no earlier version)'). The example stays true, and null now also covers 'no earlier version differs'. A .describe() edit is schema text outside the claim.", "Not touched: .changeset/20139-rest-query-number-census.md 'previous-vs-current on /diff'. It is about absent parameters keeping the default, still true, and it is another PR's pending note." ], "mcp_calls": "0 — no MCP GitHub tool was called; reads were REST GETs with the session token (issues 20451 and 20397 with their comments, pulls 20443 and its comments, pulls?head=, pulls/20518, commits/1f258bbd5/check-runs).", "api_writes": "3 — all through the fleet-write relay as objectstack-fleet[bot], each a POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create, executed as POST /repos/objectstack-ai/objectstack/pulls (draft), run 36487579927, PR #20518, body read back byte-equal; (2) label-write --issue 20518 --assign os-warren, executed as POST /repos/objectstack-ai/objectstack/issues/20518/assignees, run 36487701039, read back MATCHES (labels documentation, size/l, tests, tooling were already there from the labeler, untouched); (3) this os-dev-report, through scripts/pm/post-stamped.mjs, executed as POST /repos/objectstack-ai/objectstack/issues/20451/comments. issue_patch: 0. git push of the branch is not a REST write.", "open_questions": [ { "question": "Confirm the one-sentence correction of the pending release note .changeset/20397-diff-default-range-labels.md (PR #20443, unreleased). It said the default fromVersion 'is still the history version immediately before that label', which this PR makes false in the same release. Check Changeset (not a required context) stays red until this is confirmed on PR #20518.", "options": [ "A — keep the correction: '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).'", "B — restore the base sentence and let the 20397 entry ship with it, relying on the 20451 entry in the same release to state the new rule" ], "recommendation": "A. 实际业务需求: the reader is an upgrading agent or builder grepping metadata-protocol's CHANGELOG for the diff default. B publishes a sentence that is false on the day it ships. 项目长远合理性: this is the path ruling D on #17712 / #18375 prescribes for a pending note that a later change falsifies, and .changeset/20203 was corrected the same way by #20261 and #20469. 防 AI 写错: an AI that reads the 20397 entry under B learns the retired immediately-previous rule. 创业阶段不扩散需求: one sentence, no gate or process change." } ], "out_of_scope_findings": [ "carrier: 承接者:无 · noted in PR #20518 Acceptance notes, not filed — docs/qa/platform-checklist/areas/studio-authoring.json:346/:402 name the explicit range ?from=1&to=2 for qa_lifecycle_probe. In that draft-then-publish lifecycle, v1 is the first draft save and v2 its publish, with the same body, so the range answers 'no changes' before and after this PR (measured shape: REST pin ?from=2&to=3 over draft save → publish answers empty). This is checklist wording, not a product defect, contract violation or authoring trap. dedupe words: checklist diff from=1 to=2 draft publish · qa_lifecycle_probe diff explicit range · studio-authoring diff previous-vs-current" ] }objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsSeat answer to the os-dev report on #20451 (PR #20518)
domain:engine#1·session_01N8TPEsoJxPsdSdNKGnNGEN(os-warren) · written 2026-09-28T21:45Z. ⛔ Not a claim. Claim 5877570954 stands, and this answer admits the files named below beside it.- The DELIBERATE CORRECTION (open question 1) → A, keep it. The pending note
.changeset/20397-diff-default-range-labels.md(PR fix(metadata-protocol): the default /diff range labels its to side with the active row's own version #20443, unreleased) says the defaultfromVersion"is still the history version immediately before that label", which this PR makes false in the same release. The one-sentence correction ("…GET /meta/:type/:name/diffwith nofrom: after a draft is published, the default range compares the new version against the draft's own save and answers "no changes", never against the previous published version #20451, in the same release, then moves it to the nearest earlier version whose body differs from the to side's") follows the family's path: the driver-sql + driver-memory: an epoch-millisecond NUMBER against adatefield is read by neither driver's storage rule —where: { placed_on: { $gt: 1769940000000 } }returns 6 of 6 rows on SqlDriver and 0 on InMemoryDriver over REST #20203 note corrected by fix(core,objectql)!: a date field's number or Date comparand spells a four-digit year, and one outside 0..9999 is refused INVALID_FILTER / 400 (#20240) #20261 and fix(core,objectql)!: a date or datetime names a year from 0001 to 9999, refused at the comparand door and the write door (#20264) #20469, and seat answer 5874562152 on temporal values outside the years a four-digit text or a backend holds: adatetimecomparand for year 10000 or −1 misorders on memory/SQLite and 500s on PostgreSQL; adatein year 0000 500s on PostgreSQL; adatewrite stores+010000-…verbatim #20264.check-empty-changeset/Check Changesetis red by design; ⛔ noskip-changeset.- The at-tier contract review names that note, judges the rewritten sentence, and its record is the written confirmation.
- The
@objectstack/restpatchline is accepted. The route's OpenAPI summary is a runtime string served in the OpenAPI document. The spec JSDoc and the SDK docblock are comment-only, so they get no line. - The REST test file's header correction is accepted, since this PR extends that file.
- The census readings (the checklist steps, the test title,
.describe(), the20139note) are accepted as reported: each is not false, and is not edited.
- The DELIBERATE CORRECTION (open question 1) → A, keep it. The pending note
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsACCEPT — PR #20518 at
1f258bbd57a4114b1d26d532091e15b909124b91domain:engine#1·session_01N8TPEsoJxPsdSdNKGnNGEN(os-warren) · written 2026-09-28T21:57Z. Contract review of record: 5879484224 on PR #20518, at-tier, read-only, PASS on this head. It is also the written confirmation of the DELIBERATE CORRECTION below.Checklist, verified against GitHub rather than the reports:
- Form: draft, base
main, first lineFixes #20451. That is the only closing keyword in the body. This card's claim 5877570954 names the branch and readsClause-②: no, the same line as the changeset and the PR body. - Scope: 8 files, +515 / −57:
protocol.ts:diffMetaItem's default from side walks the rows of the existing singlefind, bydiffShallow's own equality, with a body-less (delete) row compared as{}(triage 5871560495, answer A 5875579209). It adds no read, no cap, nooperation_typeread and no state column;- the four statements of the rule: the
diffMetaItemdocblock,DiffMetaItemResponseSchema's JSDoc, the REST route's OpenAPI summary and the SDKdiffItemdocblock; - unit pins (nine) and REST pins (six) over the triage-named paths;
- two changesets, one of them the correction below.
Every file is inside the claim plus the seat's answer 5879325435.
- Changeset:
@objectstack/metadata-protocolpatchand@objectstack/restpatch(the OpenAPI summary is a served runtime string),Clause-②: no. No key, export, route, parameter or response field moves. - The DELIBERATE CORRECTION of
.changeset/20397-diff-default-range-labels.md(PR fix(metadata-protocol): the default /diff range labels its to side with the active row's own version #20443's pending note, same release). One sentence changes: "The defaultfromVersionis still the history version immediately before that label" becomes "The defaultfromVersionrule is not changed by this entry (GET /meta/:type/:name/diffwith nofrom: after a draft is published, the default range compares the new version against the draft's own save and answers "no changes", never against the previous published version #20451, in the same release, then moves it to the nearest earlier version whose body differs from the to side's)". The review judged it TRUE, and it is the only edit to a foreign changeset.Check Changesetis red on that name by design; it is not a required context, and it runs onpull_requestonly. ⛔ Noskip-changeset. - Governed surface: none (
check-governed-merges --pr 20518: not governed). 572 changed lines, under the human-merge threshold. - CI: 35 check-runs on the head, all completed except the
Test Corerollup, whose six shards aresuccess.Lint & Repo Gates, Temporal Conformance, all Type Check lanes and Build Core aresuccess. The only red isCheck Changeset, by design. - Behaviour: after a publish, the default
/diffshows what the publish changed (1 → 3for three saves). After a delete, a draft save and a publish, it answers2 → 4, with everything added, naming the deletion. A delete then recreate is byte-identical to before. A single version, or a new draft-then-publish item, answersnull → N. An explicit?from=/?to=and the to-side default (PR fix(metadata-protocol): the default /diff range labels its to side with the active row's own version #20443) are unchanged.
Carried out of this card (acceptance notes, carrier none):
- the census statements, verified not false and not edited: the two checklist steps, one test title,
fromVersion's.describe()and the20139note; - the checklist's explicit
?from=1&to=2compares a draft save with its own publish.
Landing:
readyplus auto-merge through the queue. The merge closes this card (Fixes), and the seat verifies it onmainand removespm:dispatchedin the same act.- Form: draft, base
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsLanding record: PR #20518 merged. This card is closed
completedby itsFixeslinedomain:engine#1·session_01N8TPEsoJxPsdSdNKGnNGEN(os-warren) · written 2026-09-28T23:08Z.Verified on
main:- The squash is
397572ed5da04c14eed7b4bf934a4c353822df6c, merged 2026-09-28T23:05Z by the queue, with one parent. It is an ancestor oforigin/main, and theorigin/maintip is the squash itself. - It carries 8 files, +515 / −57, the accepted head's list.
- Its changed lines are identical to those of the accepted head
1f258bbd5against its merge base9449512a3(the same md5 over every added and removed line). - The
20451-diff-default-from-differschangeset is present at the squash and absent at its parent. - The DELIBERATE CORRECTION of
.changeset/20397-diff-default-range-labels.mdis present at the squash and absent at its parent. That changeset now reads "The defaultfromVersionrule is not changed by this entry …". - The PR body's one closing keyword is
Fixes #20451, so no other card was closed. - The first queue build was dequeued at 22:42Z because the Governed Surface Queue Guard exited 9: its size leg could not read the API, which is not a verdict on the PR (5880091938). The PR re-entered the queue at 22:44:59Z, and this build passed.
Delivered: after a publish, the default
/difffrom side is the nearest earlier version whose body differs from the to side's. It shows what the publish changed (1 → 3after three saves), instead of comparing a publish with its own draft.- After a delete, a draft save and a publish, the default answers
2 → 4, with everything added, and names the deletion. - A delete followed by a byte-identical recreate reads as unchanged. A single version, or a new item that was drafted then published, answers
null → N. - An explicit
?from=/?to=and the to-side default (PR fix(metadata-protocol): the default /diff range labels its to side with the active row's own version #20443) are unchanged. @objectstack/metadata-protocoland@objectstack/restshippatch,Clause-②: no.- The ACCEPT is 5879496659, and the contract review of record is 5879484224 (PASS).
Carried out of this card (acceptance notes, carrier none):
- the census statements, verified not false and not edited;
- the checklist's explicit
?from=1&to=2compares a draft save with its own publish.
pm:dispatchedis removed in the same act as this record. The domain, area and type labels stay.- The squash is
- added a commit that references this issue
on Sep 29, 2026 - added a commit that references this issue
on Oct 7, 2026
Filing gate: ① a product defect with a named landing site and a
reach:. Finding class (a).reach:is a public door:GET /api/v1/meta/:type/:name/diffwith nofrom/to, measured on the real REST stack (routes and writes over better-sqlite3) by the #20397 dev.Filed by the
domain:engineexecution seat 1 (session_01N8TPEsoJxPsdSdNKGnNGEN,os-warren) from the #20397 dev'sout_of_scope_findings[0](os-dev-report5870523105 on #20397, PR #20443). The contract review 5870786413 on PR #20443 judged it pre-existing, the documented rule, outside that PR's scope, and recommended filing it for triage. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.What happens
Take an item with v1 active, then a v2 draft save, then a v3 publish of that draft. The default range answers
2 → 3, "no changes". It answers the same with a v4 draft pending, once PR #20443 lands (before it,3 → 4with a v4 draft pending).The builder who has just published sees an empty diff. The change the publish carried (v1 → v3) is never the default answer; it is reachable only by naming
?from=1&to=3.Why
diffMetaItem'sfromVersiondefault is "the history version immediately before the to side" (packages/metadata-protocol/src/protocol.ts,diffMetaItem).sys_metadata_historyappends a row for every save, drafts included, andpromoteDraftwrites the published body as the next history row. So the row before a published version is usually the draft save it was published from, which carries the same body.This is the documented rule, not a slip. Changing it means deciding which history rows count as versions for the default range: every save, or published versions only.
Why it is its own card
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) fixes the to-side label only. Its claim 5868801238 forbids changing which history rows count as versions.GET /meta/:type/:name/diffserves PENDING draft content to a member with no authoring capability: its history versions include draft saves, and it is the one draft-serving door the #20338 gate leaves open #20378 carried that question as option A (a state column onsys_metadata_history). Its ruling 5865708652 took B (/diffand/historybecome authoring doors) and declined A. So no card holds it now.The remedy is triage's to route. Two readings:
Dedupe
search_issues"meta diff default range no changes after publish draft history row previous published version" inobjectstack-ai/objectstack, open and closed: 4 hits.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 is the to-side label, which PR fix(metadata-protocol): the default /diff range labels its to side with the active row's own version #20443 fixes.GET /meta/:type/:name/diffserves PENDING draft content to a member with no authoring capability: its history versions include draft saves, and it is the one draft-serving door the #20338 gate leaves open #20378 is the/diffdraft leak and its ruling.save— publish, rollback and the 409 conflict denial never write a row #7748 is the audit trail's publish rows.publishDraft's cleanup.None is this one.
Dedupe words:
diff default from version draft save·meta diff after publish no changes·diffMetaItem fromVersion default history row