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
rest: metadata rollback coerces toVersion with Number(), so {"toVersion": true} or [2] restores a version and 1.5 answers 404 instead of 400 #25482
Filing gate: ① a product defect with a named location and a live reproduction (finding class (a)).
Reader: the packages/rest lane (meta-item-history-routes.ts, the /rollback handler), next PR touching the metadata history doors.
Dedupe (open AND closed): "metadata rollback toVersion non-integer coerced" → 1 hit, this card only.
reach: public HTTP entry, measured — POST /api/v1/meta/:type/:name/rollback {"toVersion": true} answers 200 and reverts the live body on a stock showcase boot.
Checklist item priority: P1
type: Bug
QA-source: #25479 · studio-authoring.draft-publish-lifecycle · observed beside the clauses
What happens
POST /api/v1/meta/:type/:name/rollback coerces a non-integer toVersion with Number(...) and accepts whatever passes isFinite && >= 1:
{"toVersion": true} → 200, "Reverted to version 1". The live body really changes to v1.
{"toVersion": [2]} and {"toVersion": "2"} → 200, and the item reverts to v2.
{"toVersion": 1.5} → 404 VERSION_NOT_FOUND. This should be the route's own 400 INVALID_REQUEST.
The route's own refusal text says 'toVersion' (positive integer) is required, so a boolean or a one-element array restoring a version is a silent coercion of an input to a destructive door. /diff in the same file already reads its numbers through readDeclaredQueryNumber.
Reproduction (stock showcase, admin)
Publish a dashboard twice in a writable package, so that history holds v1 and v2.
POST /api/v1/meta/dashboard/NAME/rollback {"toVersion":true} → 200. GET /api/v1/meta/dashboard/NAME now serves v1.
Re-measured on a separate boot on a view with no history: {"toVersion":true} answers 404 "No history row at version 1", and {"toVersion":[2]} answers 404 "…version 2". Both were read as integers. {"toVersion":"abc"} → 400.
Measured on main77392e9 (two dashboards, and separately on a view).
Triage: lands in packages/rest/src/meta-item-history-routes.ts, the /rollback handler; rationale: finding class (a), reach measured. A destructive door coerces true / [2] / "2" into a version number; read toVersion through the same declared-number reader /diff already uses and refuse everything else with 400 INVALID_REQUEST.
Path: studio · 先草稿后发布 | studio-authoring.draft-publish-lifecycle (P1, the item passes; this sits beside its clauses) | step ①
Grade: priority:p2 — wrong-but-working on an admin-only door; the item itself does not fail, so it does not inherit P1.
Type: Bug
Session: session_01AkmasAfXt3cu83xGzkuKhe (objectstack triage, run on the maintainer's request in the project thread)
Claim: PM loop · domain:cli seat 1 · p2 · 2026-10-23T18:21Z
Session: session_01CxBKycaiUpA1yGx1XyoGqX
Account: os-sales (the seat's linked user; always the card's assignee)
Branch: claude/issue-25482-rollback-toversion
Worktree: objectstack-issue-25482
Domain: domain:server (this seat's side of the server lane under the coordinator's split: former cli and engine paths)
Seat: domain:cli#1 (seat post #6024; 由Sales账户的项目负责)
File surface (read on origin/maindc3e5b9d7b; stop on a breach): packages/rest/src/meta-item-history-routes.ts (the /rollback handler, ~:808), its tests, one changeset.
Container & model: S, mode:subagent, model: default tier
Clause-②: no
Basis for the Clause-② line: a refusal for inputs the route already documents as invalid (toVersion "positive integer"); revised with a narrowing arm if the measured diff refuses an input a real client sends.
Responsibility: n/a — QA-source #25479, triaged 6370228332.
Thread-read: 6370228332
Serial constraints cleared: none of the 10 open PRs touches this surface (file lists read in this act).
{
"issue": 25482,
"status": "done",
"branch": "claude/issue-25482-rollback-toversion",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25532",
"session": "session_01CxBKycaiUpA1yGx1XyoGqX",
"premise_still_valid": true,
"summary": "Premise held on origin/main dc3e5b9d7b: the rollback handler at meta-item-history-routes.ts:809-811 coerced toVersion with Number(). Now a JSON body toVersion (or the version alias) is judged as it came against UNDECLARED_WHOLE_NUMBER_PARAM, the declaration /diff uses, and ?toVersion= is read through readDeclaredQueryNumber with no second parser. Everything else, and anything below 1, answers the door's existing 400 INVALID_REQUEST with its existing message. A numeric STRING in the JSON body is now refused: the door declares no request schema, and both real clients send a JSON number (@objectstack/client rollbackItem typed number, and objectui packages/data-objectstack/src/metadata-client.ts rollback at the pinned .objectui-sha 6fb31dcc99). So Clause-2 is 'no (narrowing)' in the PR body and the changeset, which revises the claim's 'no'. The census test's exempt ledger row for Number(toVersionRaw) is deleted and its pinned site count goes 9 to 8.",
"tests": "At head e03bd96c3a. (1) pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/rest-server-query-number-census.test.ts src/rest-server-query-multiplicity.test.ts: Test Files 2 passed (2), Tests 67 passed (67), VERDICT command-exit 0. (2) pnpm --filter @objectstack/rest typecheck && pnpm --filter @objectstack/rest test: Test Files 316 passed (316), Tests 6347 passed | 326 skipped, VERDICT command-exit 0. The new pins go through the real RestServer route. Body true, [2], '2', 1.5, 0, -1, 'abc' and {} answer 400 INVALID_REQUEST and rollbackMetaItem is not called. So do alias true, [2], 1.5 and 0; query abc, blank, Infinity, 1.5, 0 and -1; and an absent version. Body 2, alias 4 and ?toVersion=3 answer 200 and reach rollbackMetaItem as 2, 4 and 3, and a body version wins over the query. (3) Ablation, run once with the fix committed. scripts/ablation-replace.mjs swapped the body judgement for a Number() coercion: anchor 1 to 0, replacement 0 to 1, blob cd8508485ed8 to a1c834b191a0. The 25482 block then failed 7 and passed 16 (body true, [2], '2', 1.5 and alias true, [2], 1.5), the expected red. Restore: blob equals HEAD cd8508485ed8 and git diff HEAD is empty. The test imports src, so dist was not involved.",
"gates": "Local, all at head e03bd96c3a. pnpm --filter '@objectstack/rest...' build: VERDICT command-exit 0. pnpm lint (eslint . --no-inline-config) under the lock: exit 0. dispatch-gates --commands derived 67 commands. 65 exited 0 unlocked. pnpm check:type-check-debt first exited 3 (its inner closure build got no lock turn); re-run under the lock it exited 0 ('none above its recorded number'). NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET, it reads every package's dist/ and only the rest closure was built here; CI Lint & Repo Gates runs it. Reconciled with dispatch-gates --ran on 'COMMAND :: exit N' lines: 'dispatch-gates --ran: 67 derived famil(ies) accounted for, 66 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', exit 0. CI on PR 25532 at e03bd96c3a when this report was written: No other open PR may claim the same issue = success; Packed-tarball smoke and 18.x upgrade harness = skipped (opt-in); the rest in_progress (Lint lanes, Type Check lanes, Check Changeset, Governed Surface Queue Guard, Check PR Size, the card-claim check and others).",
"dogfood_battery": "e03bd96c3a · os-verify-lock: VERDICT command-exit 0 (pnpm --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood test, no shard, no path) · Test Files 290 passed | 3 skipped (293) · Tests 2494 passed | 18 skipped (2512) · no failures",
"line_budget": "Size S: 137 changed lines (+115 / -22) over 3 files, under the 3000 human-merge threshold",
"files_changed": [
".changeset/25482-rollback-to-version-integer.md",
"packages/rest/src/meta-item-history-routes.ts",
"packages/rest/src/rest-server-query-number-census.test.ts"
],
"deviations": [
"Clause-2 moved from the claim's 'no' to 'no (narrowing)', as the dispatch instructed for a refused body string. The changeset is '@objectstack/rest': patch with Metadata-protocol: none, a BREAKING banner, FROM/TO and the adr-0087 not-required (no-migration-prescription) marker.",
"The file-surface test is rest-server-query-number-census.test.ts, the existing test of this route's toVersion refusal. Its ledger had to lose the rollback exemption row, or its stale-row assertion would red.",
"The PR body ends with the dispatch's footer form, not the role file's session-URL form, because the dispatch order names it. The first line is the attribution comment the dispatch requires.",
"Several lock calls hit queue-timeout 99 under the stable slot issue-25482 before they got a turn. No result was read from a 99."
],
"mcp_calls": "1 · mcp__github__search_code (read: objectui rollback caller). No MCP write tools.",
"api_writes": "2 · (1) POST /repos/objectstack-ai/objectstack/dispatches via scripts/pm/fleet-write/dispatch.mjs: pr_create, draft, assignees os-sales, PR 25532, read-back 5666 bytes identical; (2) the os-dev-report issue comment on 25482 via scripts/pm/post-stamped.mjs. Separately, 2 git pushes (empty branch probe, then the fix commit).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: the census test's module header says 'ten sites in the whole file today', which was stale before this change (site count now 8). It is in the PR's Acceptance notes."
]
}
Activity
objectstack-fleet commentedon Oct 23, 2026
Triage: lands in
packages/rest/src/meta-item-history-routes.ts, the/rollbackhandler; rationale: finding class (a), reach measured. A destructive door coercestrue/[2]/"2"into a version number; readtoVersionthrough the same declared-number reader/diffalready uses and refuse everything else with400 INVALID_REQUEST.Path: studio · 先草稿后发布 | studio-authoring.draft-publish-lifecycle (P1, the item passes; this sits beside its clauses) | step ①
Grade:
priority:p2— wrong-but-working on an admin-only door; the item itself does not fail, so it does not inherit P1.Type: Bug
Session:
session_01AkmasAfXt3cu83xGzkuKhe(objectstack triage, run on the maintainer's request in the project thread)Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
Claim: PM loop ·
domain:cliseat 1 · p2 · 2026-10-23T18:21ZSession:
session_01CxBKycaiUpA1yGx1XyoGqXAccount:
os-sales(the seat's linked user; always the card's assignee)Branch:
claude/issue-25482-rollback-toversionWorktree:
objectstack-issue-25482Domain:
domain:server(this seat's side of the server lane under the coordinator's split: former cli and engine paths)Seat:
domain:cli#1(seat post #6024; 由Sales账户的项目负责)File surface (read on
origin/maindc3e5b9d7b; stop on a breach):packages/rest/src/meta-item-history-routes.ts(the/rollbackhandler, ~:808), its tests, one changeset.Container & model:
S,mode:subagent,model: default tierClause-②: no
Basis for the Clause-② line: a refusal for inputs the route already documents as invalid (
toVersion"positive integer"); revised with a narrowing arm if the measured diff refuses an input a real client sends.Responsibility: n/a — QA-source #25479, triaged
6370228332.Thread-read: 6370228332
Serial constraints cleared: none of the 10 open PRs touches this surface (file lists read in this act).
Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
os-dev-report
{ "issue": 25482, "status": "done", "branch": "claude/issue-25482-rollback-toversion", "pr": "https://github.com/objectstack-ai/objectstack/pull/25532", "session": "session_01CxBKycaiUpA1yGx1XyoGqX", "premise_still_valid": true, "summary": "Premise held on origin/main dc3e5b9d7b: the rollback handler at meta-item-history-routes.ts:809-811 coerced toVersion with Number(). Now a JSON body toVersion (or the version alias) is judged as it came against UNDECLARED_WHOLE_NUMBER_PARAM, the declaration /diff uses, and ?toVersion= is read through readDeclaredQueryNumber with no second parser. Everything else, and anything below 1, answers the door's existing 400 INVALID_REQUEST with its existing message. A numeric STRING in the JSON body is now refused: the door declares no request schema, and both real clients send a JSON number (@objectstack/client rollbackItem typed number, and objectui packages/data-objectstack/src/metadata-client.ts rollback at the pinned .objectui-sha 6fb31dcc99). So Clause-2 is 'no (narrowing)' in the PR body and the changeset, which revises the claim's 'no'. The census test's exempt ledger row for Number(toVersionRaw) is deleted and its pinned site count goes 9 to 8.", "tests": "At head e03bd96c3a. (1) pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/rest-server-query-number-census.test.ts src/rest-server-query-multiplicity.test.ts: Test Files 2 passed (2), Tests 67 passed (67), VERDICT command-exit 0. (2) pnpm --filter @objectstack/rest typecheck && pnpm --filter @objectstack/rest test: Test Files 316 passed (316), Tests 6347 passed | 326 skipped, VERDICT command-exit 0. The new pins go through the real RestServer route. Body true, [2], '2', 1.5, 0, -1, 'abc' and {} answer 400 INVALID_REQUEST and rollbackMetaItem is not called. So do alias true, [2], 1.5 and 0; query abc, blank, Infinity, 1.5, 0 and -1; and an absent version. Body 2, alias 4 and ?toVersion=3 answer 200 and reach rollbackMetaItem as 2, 4 and 3, and a body version wins over the query. (3) Ablation, run once with the fix committed. scripts/ablation-replace.mjs swapped the body judgement for a Number() coercion: anchor 1 to 0, replacement 0 to 1, blob cd8508485ed8 to a1c834b191a0. The 25482 block then failed 7 and passed 16 (body true, [2], '2', 1.5 and alias true, [2], 1.5), the expected red. Restore: blob equals HEAD cd8508485ed8 and git diff HEAD is empty. The test imports src, so dist was not involved.", "gates": "Local, all at head e03bd96c3a. pnpm --filter '@objectstack/rest...' build: VERDICT command-exit 0. pnpm lint (eslint . --no-inline-config) under the lock: exit 0. dispatch-gates --commands derived 67 commands. 65 exited 0 unlocked. pnpm check:type-check-debt first exited 3 (its inner closure build got no lock turn); re-run under the lock it exited 0 ('none above its recorded number'). NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET, it reads every package's dist/ and only the rest closure was built here; CI Lint & Repo Gates runs it. Reconciled with dispatch-gates --ran on 'COMMAND :: exit N' lines: 'dispatch-gates --ran: 67 derived famil(ies) accounted for, 66 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', exit 0. CI on PR 25532 at e03bd96c3a when this report was written: No other open PR may claim the same issue = success; Packed-tarball smoke and 18.x upgrade harness = skipped (opt-in); the rest in_progress (Lint lanes, Type Check lanes, Check Changeset, Governed Surface Queue Guard, Check PR Size, the card-claim check and others).", "dogfood_battery": "e03bd96c3a · os-verify-lock: VERDICT command-exit 0 (pnpm --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood test, no shard, no path) · Test Files 290 passed | 3 skipped (293) · Tests 2494 passed | 18 skipped (2512) · no failures", "line_budget": "Size S: 137 changed lines (+115 / -22) over 3 files, under the 3000 human-merge threshold", "files_changed": [ ".changeset/25482-rollback-to-version-integer.md", "packages/rest/src/meta-item-history-routes.ts", "packages/rest/src/rest-server-query-number-census.test.ts" ], "deviations": [ "Clause-2 moved from the claim's 'no' to 'no (narrowing)', as the dispatch instructed for a refused body string. The changeset is '@objectstack/rest': patch with Metadata-protocol: none, a BREAKING banner, FROM/TO and the adr-0087 not-required (no-migration-prescription) marker.", "The file-surface test is rest-server-query-number-census.test.ts, the existing test of this route's toVersion refusal. Its ledger had to lose the rollback exemption row, or its stale-row assertion would red.", "The PR body ends with the dispatch's footer form, not the role file's session-URL form, because the dispatch order names it. The first line is the attribution comment the dispatch requires.", "Several lock calls hit queue-timeout 99 under the stable slot issue-25482 before they got a turn. No result was read from a 99." ], "mcp_calls": "1 · mcp__github__search_code (read: objectui rollback caller). No MCP write tools.", "api_writes": "2 · (1) POST /repos/objectstack-ai/objectstack/dispatches via scripts/pm/fleet-write/dispatch.mjs: pr_create, draft, assignees os-sales, PR 25532, read-back 5666 bytes identical; (2) the os-dev-report issue comment on 25482 via scripts/pm/post-stamped.mjs. Separately, 2 git pushes (empty branch probe, then the fix commit).", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed: the census test's module header says 'ten sites in the whole file today', which was stale before this change (site count now 8). It is in the PR's Acceptance notes." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
ACCEPT → PR #25532 comment 6374604346 ·
domain:cliseat 1 · sessionsession_01CxBKycaiUpA1yGx1XyoGqX· 2026-10-23T20:58ZGenerated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
Landed ·
domain:cliseat 1 · sessionsession_01CxBKycaiUpA1yGx1XyoGqX· 2026-10-23T21:33Zmerged: true, merge commit0fab7baf5, merged 2026-10-23T21:32:50Z through the merge queueorigin/main0fab7baf5is an ancestor oforigin/main, single parent (squash)gh-readonly-queue/main/*head for PR 25532 remainsState: card closed
completedbyFixes #25482;pm:dispatchedstripped in this act.Generated by Claude Code