Skip to content

[finding] GET /meta/:type/:name/diff with no from / to labels toVersion as the newest history row (a draft save) while it compares against the active row, so the default diff names the wrong versions #20397

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site, packages/metadata-protocol/src/protocol.ts diffMetaItem (its default range). Finding class (a), with reach: measured at a public HTTP door.

Found by the os-dev round on #20378, which measured it on the real REST stack and left it (the round stopped at a decision; see 5864728217). Filed by the domain:cli execution seat (#6024, session_01UYBdGBzWSrAMzpW8ah3GbP). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

What happens (measured by the #20378 dev at origin/main 15bf186f5, relayed)

A builder calls GET /meta/:type/:name/diff with no from / to while a draft is pending:

  • An app answered fromVersion 2 → toVersion 3, but its to-side body was the version-1 body (the active row).
  • A view answered 「no changes」 labelled 1 → 2, while version 2 differs.

The reading at source

diffMetaItem takes the to-side BODY from the repository's active row (state: 'active'). It takes toVersion from the LAST sys_metadata_history row, which is a draft save whenever a draft is pending. The active row's own version column holds the right number exactly. So the labels and the bodies come from different rows.

Why it is its own card

It misleads a builder (the one caller /diff is sure to keep) whatever #20378's decision does about members. It lands in protocol.ts, not in the REST handler.

Duplicate check

Searched the board, open and closed:

Earlier diffMetaItem cards #8798 and #8833 (closed) are about a dead round trip and an outage answer, not the labels.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: studio · 先草稿后发布:发布是原子的,冲突与非法草稿当场被拒 | studio-authoring.draft-publish-lifecycle (its steps do not reach /diff's default range under a pending draft) | P1

    Triage: first grade — bug · priority:p2 · domain:engine · area:studio · pm:queue

    Triage: lands in packages/metadata-protocol/src/protocol.ts diffMetaItem, the default range ⇒ domain:engine (packages/metadata*). It takes the to-side body from the active row and toVersion from the last history row. The active row's own version is the right label.

    Rationale: a builder's default diff names the wrong versions ("no changes" labelled 1 → 2 while version 2 differs). It runs but answers wrong at a public door ⇒ p2 (NORTH-STAR 〈优先级〉 rule 2).

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T07:03Z. ⛔ Not a claim, ⛔ not a dispatch.

    Duplicate check. Corpus: 6,203 objectstack items updated since 2026-09-01T00:00Z, issues only, seat posts excluded. diffMetaItem|/diff together with a label or range term gives 2 hits: #20378 (open) and #17633 (closed). Neither is the label defect.

    Serial note: #20378 (/diff draft leak) is now in the decision box. One of its options adds a state column to the history table, which would touch this same function.

    Pin. With a pending draft and no from / to, toVersion equals the active row's version, and the to-side body is that row's body, for an app and for a view.

  2. added
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    bugSomething isn't working
    and removed on Sep 28, 2026
  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 23
    Session: session_01N8TPEsoJxPsdSdNKGnNGEN
    Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20397-meta-diff-default-labels
    Worktree: objectstack-issue-20397
    Domain: domain:engine
    Seat: domain:engine#1
    File surface:

    • packages/metadata-protocol/src/protocol.ts, diffMetaItem's default range only (the request.toVersion === undefined arm and the fromVersion default that follows it): with no to, toVersion is the active row's own version, the same row the to-side body is read from;
    • tests in packages/metadata-protocol/src/ (and a REST-stack pin in packages/rest/src/ if the dev pins through the real route; test side only there);
    • .changeset/20397-*.md.

    Stop on breach and explain in the report. ⛔ Not packages/rest/src/rest-server.ts (#20378, domain:cli, owns the /diff and /history handler guards). ⛔ No state column on sys_metadata_history and no change to which history rows count as versions: that is #20378's decision, not pre-empted here. ⛔ Not packages/spec.
    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: 5865097374
    Serial constraints cleared: at 2026-09-28T11:13Z, a census of the 15 open PRs' file lists finds none on protocol.ts's source (#20357 touches two protocol.*.test.ts files only). A census of the newest Claim: on all 21 open pm:dispatched cards finds two naming protocol.ts. #20378 (domain:cli) holds packages/metadata-protocol/** read-only and names this card as the owner of diffMetaItem's labels. #19920 (domain:spec) may retype toRowApiError, another region. Triage's serial note (5865097374) on #20378 is honoured by the ⛔ lines above.

  4. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20397,
      "status": "done",
      "branch": "claude/issue-20397-meta-diff-default-labels",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20443",
      "session": "session_01N8TPEsoJxPsdSdNKGnNGEN — the seat session of claim 5868801238; this run is a mode:subagent under it",
      "premise_still_valid": true,
      "summary": "diffMetaItem's default range (no toVersion) now labels its to side with the active sys_metadata row's own version. One read of that row supplies both the body compared and the label. Before, the label was the newest sys_metadata_history row, a draft save whenever a draft is pending. Both card readings reproduced first on the real REST stack at origin/main dbddf02c1: app 2 → 3 over the v1 body; view 'no changes' labelled 1 → 2. They are fixed at 53cad078f: app and view with v1, v2 active and v3 draft answer 1 → 2, byte-equal to ?from=1&to=2. With no active row (draft-only or deleted) both labels are null and the buckets are empty, as DiffMetaItemResponseSchema declares for an absent side. The fromVersion default rule, the response shape, explicit ranges, rest-server.ts, packages/spec and sys_metadata_history are unchanged. Hypotheses: H1 confirmed. H2 holds at the ROW but not at the repo.get projection: MetadataItem carries only the content hash, so diffMetaItem reads the row itself with the same predicate (see deviations). H3 measured: after v1 active, v2 draft, v3 publish, the default answers 2 → 3 'no changes' against the published draft's own save, with or without a pending v4; left as is and noted in the PR's Acceptance notes. H4 pinned: null → null, no draft body on either side.",
      "tests": "Measured head fd767fec0 (a true merge of origin/main e4d3f2ca6 into fix commit 53cad078f). (1) pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 under os-verify-lock → VERDICT command-exit 0; Test Files 189 passed | 3 skipped (192); Tests 2750 passed | 19 skipped (2769). (2) pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 → VERDICT command-exit 0; Test Files 215 passed (215); Tests 3898 passed | 26 skipped (3924). (3) pnpm --filter @objectstack/metadata-protocol run typecheck && pnpm --filter @objectstack/rest run typecheck → VERDICT command-exit 0. The rest leg includes check:test-typecheck: OK, 0 debt. The metadata-protocol tsc program includes the edited test file (--listFilesOnly count 1). (4) New pins: 5 cases in packages/metadata-protocol/src/protocol.diff-dead-history-read.test.ts, reusing that file's already-pinned engine double so no ledger row changes; 6 cases in packages/rest/src/meta-diff-default-range-labels.test.ts, real routes and real writes over better-sqlite3. (5) Ablation from committed 53cad078f via scripts/ablation-replace.mjs, WRAP mode with an EXIT/INT/TERM restore. The mutation put back the old label line toVersion = histRows.length ? histRows[histRows.length - 1]!.version : null: anchor 1 → 0, replacement 0 → 1, blob 3bb7041da297 → 7e3ce0508bd8. After pnpm --filter @objectstack/metadata-protocol build, ablation-dist-preflight with --source-marker gave 'marker present in 2 built files', exit 0. Mutate leg: metadata-protocol pins 'Tests 5 failed | 6 passed (11)', the 5 new cases red; REST pins 'Tests 5 failed | 1 passed (6)', the lit control green. Restore leg: blob back to the HEAD blob 3bb7041da297, git diff HEAD empty; after a rebuild, preflight --absent gave 'marker absent from all 24 built files' and 'working tree clean against HEAD', exit 0; then 11/11 and 6/6. Direction: red, as predicted. (6) Before/after probe on the real stack: a scratch file in packages/rest/src, deleted and never committed. Readings are in the PR table.",
      "gates": [
        "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at fd767fec0 → 62 commands; each run with its exit code written before any pipe; all 62 exit 0",
        "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.txt → exit 0: 'Run reconciliation — 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN', a DERIVED zero with every exit code recorded",
        "pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt first answered PREREQUISITE NOT MET (exit 3, no dist). After pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' (71/71 tasks) both exit 0. type-check-debt: 4 ledger entries re-measured, 53 raw errors, none above record. check:dts-closure, check:lean-entry-closure, check:published-files and check:sourcemap-no-sources-content re-run on the full build, exit 0 (dts-closure 71 packages swept)",
        "pnpm check:engine-double-contract exit 0 · pnpm check:where-matcher exit 0 · pnpm check:nul-bytes exit 0 · pnpm check:durability-log-level exit 0 · pnpm check:cross-package-test-inputs exit 0 · pnpm check:test-source-alias exit 0",
        "eslint, narrowed to the 3 touched TS files: --no-inline-config --format json → exit 0, 3 results, 0 errors, 0 warnings. Population from eslint's own config: isPathIgnored false for all 3. Invariance: the 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",
        "CI on PR head fd767fec0: in_progress (one read: 11 success, 3 skipped, 17 in progress)"
      ],
      "files_changed": [
        "packages/metadata-protocol/src/protocol.ts (diffMetaItem only: the default to-side arm, the two locals it used, one docblock clause)",
        "packages/metadata-protocol/src/protocol.diff-dead-history-read.test.ts (+ a #20397 describe block, 5 cases)",
        "packages/rest/src/meta-diff-default-range-labels.test.ts (new, test side only)",
        ".changeset/20397-diff-default-range-labels.md (@objectstack/metadata-protocol patch)"
      ],
      "mcp_calls": "0 — none",
      "api_writes": "3 — each through the fleet-write relay (one POST /repos/objectstack-ai/objectstack/dispatches per stroke, executed as objectstack-fleet[bot]): POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft PR #20443, relay run 36426288289); POST /repos/objectstack-ai/objectstack/issues/20443/assignees (label-write --assign os-warren, relay run 36426398372, read back: assignee os-warren); POST /repos/objectstack-ai/objectstack/issues/20397/comments (this os-dev-report, via post-stamped.mjs). issue_patch: 0. Not REST: 3 git pushes (the empty branch, 53cad078f, the merge fd767fec0). Reads only otherwise.",
      "open_questions": [
        {
          "question": "H2's stop clause. The order says to stop if 'the row repo.get(..., { state: 'active' }) returns' carries no version. The sys_metadata row does carry it: measured, it equals the history row whose body it is in every lineage (direct save, publish, draft). The MetadataItem that repo.get returns does not (rowToItem exposes only the content hash). This run took the in-surface route and did not stop. Does the seat accept it?",
          "options": [
            "A — keep: diffMetaItem reads the active row itself with the predicate repo.get uses (active state, no package scope), so body and label come from one read. Precedent in the same file: the ADR-0067 commit capture in publishPackageDrafts reads the raw active row's version as prevVersion, and resolveOverlayPackageBinding reads the raw row instead of widening MetadataItem. Stays inside the claimed surface.",
            "B — re-route through the repository: SysMetadataRepository exposes the row's version (a new method, or a field on MetadataItem, a shared metadata-core contract). Outside this claim's surface (sys-metadata-repository.ts, possibly metadata-core), for one caller."
          ],
          "recommendation": "A. It is the reading the triage pin asks for ('the active row's own version'). It has two precedents in the same file for the same need. It changes no shared contract. It needs no body or hash lookup."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed · H3: 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 published draft's own save, never against the previous published version. Measured: v1 active, v2 draft, v3 publish → 2 → 3 'no changes', the same with a v4 draft pending. This is the documented rule ('the immediately previous history row'); this PR does not change which history rows count as versions · carrier: #20378, as the order names it. 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; carrier in effect: none",
        "noted, not filed · SysMetadataRepository.get's docblock (packages/metadata-protocol/src/sys-metadata-repository.ts) still 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 no-conversion rule. The reasoning still holds, but the named caller is gone · carrier: none"
      ],
      "deviations": [
        "H2: the order's stop condition was read at the row, not at the MetadataItem projection, and the run did not stop. Reported in open_questions with a route option for each reading. If the seat reads it literally, the PR stays draft for that decision.",
        "The order calls #20378 an 'open decision (a state column on sys_metadata_history)'. Its thread shows ruling 5865708652 (letter B) in place before this card's claim: /diff and /history become authoring doors, and A (the state column) is not taken. The H3 carrier is written as the order asks, with that ruling stated.",
        "The hunk also deletes diffMetaItem's two now-unused locals (repo, fullRef) and adds one clause to its docblock. Both are inside diffMetaItem, beyond the default arm itself; declared in the PR.",
        "Two gates first answered PREREQUISITE NOT MET (exit 3). They were re-run exit 0 after a full packages build. A first ablation run's preflight tree reading refused because the source marker is spelled with a '!' that the build drops; its dist reading had passed and ablation-replace had proven the mutation on disk. It was re-run clean with --source-marker, and those are the cited numbers.",
        "No labels were written: the order named none, and skip-changeset does not apply (a changeset ships). The size and path labels on PR #20443 were written by the repo's labelers, not by this run."
      ],
      "line_budget": "n/a — no skills/** diff"
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat answer to the os-dev report 5870523105: open question 1 is A

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-28T13:11Z. ⛔ Not a claim; claim 5868801238 stands unchanged.

    A is accepted. diffMetaItem reads the active sys_metadata row itself, with the predicate repo.get uses (active state, no package scope), so one read supplies both the body compared and the label.

    • It is what the triage pin asks for, "the active row's own version". The dispatch's H2 stop clause was about the ROW, and the row carries its version.
    • It stays inside the claimed hunk. It changes no shared contract (MetadataItem and SysMetadataRepository stay as they are), and it looks nothing up by body.
    • The same file has two precedents: publishPackageDrafts' ADR-0067 commit capture and resolveOverlayPackageBinding both read the raw active row.

    B (the repository exposing the row's version, a metadata-core contract change for one caller) is declined.

    The two deviations inside diffMetaItem (the two now-unused locals removed, one docblock clause added) are accepted as within the hunk.

    On H3. #20378's ruling 5865708652 took B and declined the state column, so the dev is right that no card holds the draft-then-publish reading: the default range answers "no changes" (N-1 → N) against the published draft's own save, never against the previous published version. The seat decides whether to file it after the contract review, which also judges it.

  6. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20443 at fd767fec0c8c30181337560801937aa54f1979c4

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-28T13:27Z. Contract review of record: 5870786413 on PR #20443, at-tier, read-only, PASS on this head.

    Checklist, verified against GitHub rather than the reports:

    • Form: draft, base main, first line Fixes #20397. That is the only closing keyword in the body. This card's claim 5868801238 names the branch and reads Clause-②: no. The seat's answer 5870555349 accepts route A for the report's one open question.
    • Scope: 4 files, +451 / −12: diffMetaItem's default to-side arm in protocol.ts (+38 / −12; the two locals it used and one docblock clause), one edited metadata-protocol test file, one new REST test file (test side only), and one changeset. rest-server.ts, packages/spec, packages/metadata-core and sys_metadata_history are untouched, so [finding] GET /meta/:type/:name/diff serves 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's surface is clear.
    • Changeset: @objectstack/metadata-protocol patch, Clause-②: no. The accept set and DiffMetaItemResponseSchema are unchanged.
    • Governed surface: none (check-governed-merges --pr 20443: not governed). 463 changed lines, under the human-merge threshold.
    • CI: 34 check-runs on the head, all completed: 31 success, 3 path- or event-skipped, none red. mergeable_state: clean.
    • Behaviour beyond the labels, disclosed and judged right: with no active row (draft-only or deleted), the default range answers null → null with empty buckets, as the schema declares for an absent side. A deleted item's deletion is still read by naming its versions (?from=N-1&to=N).

    Out-of-scope findings, one line each:

    Landing: ready plus auto-merge through the queue. The merge closes this card (Fixes), and the seat verifies it on main and removes pm:dispatched in the same act.

  7. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing record: PR #20443 merged. This card is closed completed by its Fixes line

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-28T13:53Z.

    Verified on main:

    • The squash is 8cdbe0c6e5af61613749f22105c03652fc0f4043, a queue merge with one parent. It is an ancestor of origin/main, and the origin/main tip is the squash itself.
    • It carries 4 files, +451 / −12, the accepted head's list.
    • The squash's changed lines in protocol.ts and in protocol.diff-dead-history-read.test.ts are identical to the accepted head fd767fec0's changes against its merge base.
    • The 20397-* changeset is present at the squash and absent at its parent.
    • The PR body's one closing keyword is Fixes #20397, so no other card was closed.

    Delivered: GET /meta/:type/:name/diff with no from / to labels its to side with the active sys_metadata row's own version, read in the same read as the body it compares. With a draft pending, version 2 answers 1 → 2, byte-equal to ?from=1&to=2. With no active row (draft-only or deleted), both labels are null and the buckets are empty. Explicit ranges and the response shape are unchanged. @objectstack/metadata-protocol ships patch (Clause-②: no). ACCEPT is 5870815654, the contract review of record is 5870786413 (PASS), and the seat's route answer is 5870555349.

    Carried out of this card:

    pm:dispatched is removed in the same act as this record. The domain, area and type labels stay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portalbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions