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
metadata-protocol: rollbackToPackageCommit drops revertCommit's per-item failed[], so an item the restore refuses is reported as a successful rollback #27868
Filing gate: ① product defect with a named fix site (finding, class b: code read, not measured).
Found by: the #27818 card thread (PR #27860), from a code read outside its claimed surface. Filed by domain:server seat 3.
What is wrong
rollbackToPackageCommit (packages/metadata-protocol/src/protocol.ts) calls revertCommit once per commit, and only a thrown error lands in its own failed[]. revertCommit reports an item it refuses (for example, a restored body the save door's rules refuse) in its returned per-item failed[], without throwing. rollbackToPackageCommit discards that return value. So a package rollback where some items were refused answers success: true, and nothing lists the refused items.
This predates #27818 and holds for every restore refusal; #27818 adds one more refusal that reaches it.
Expected
rollbackToPackageCommit carries each revertCommit result's per-item failures into its answer (commit id, item, and the refusal's client-facing text), and success is false when any item was refused. Pin it with one refused item in a multi-commit rollback.
Business reason
An administrator who rolls a package back is told it succeeded while some items stayed as they were, so the package is left half-rolled-back without any notice.
Reader: the domain:server lane seat. protocol.ts is a hot file on the #25467 serial queue, so keep the edit inside the method.
Dedupe: I searched "rollbackToPackageCommit failed revertCommit per-item". #21821 (on hold) is the revertCommit plural-type finding and does not cover this.
Read fresh. A partial rollback must never answer success: true. Carry each revertCommit per-item failure (commit, item, client-facing text) into the answer, set success to false on any refusal, and pin it with a multi-commit rollback.
protocol.ts is a hot file, so run the in-flight intersection check.
Claim: PM loop round 5
Session: session_01L6bhgUCizoYWQVJWjhdR6F
Account: os-sales
Branch: claude/issue-27868-rollback-carries-item-failures
Worktree: objectstack-issue-27868
Domain: domain:server
Seat: domain:server#2
File surface: packages/metadata-protocol/src/protocol.ts (inside rollbackToPackageCommit only), its tests, .changeset/* (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate), effort: medium
Clause-②: no
Metadata-protocol: none
Responsibility: metadata-protocol package rollback answer (this lane) | revertCommit already returns per-item failed[] | every administrator rolling a package back
Thread-read: 6513882101
Premise: holds (read on main e320e366 2026-10-30T03:12Z: rollbackToPackageCommit calls revertCommit per commit and discards its returned per-item failed[]; only a thrown error reaches its own failed[])
Serial constraints cleared: the dev runs git merge-tree against every open PR on protocol.ts (#27829's dev may touch the gate's field list there) before pr_create.
{
"issue": 27868,
"status": "done",
"branch": "claude/issue-27868-rollback-carries-item-failures",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27957",
"head": "233b1f0eb9c66199967072c6c18c20453e6a7fb9",
"session": "session_01L6bhgUCizoYWQVJWjhdR6F",
"premise_still_valid": true,
"summary": "Premise held at e320e366: rollbackToPackageCommit awaited revertCommit and discarded its returned per-item failed[]. Now, inside rollbackToPackageCommit only, each returned per-item failure becomes one entry in the rollback's failed[], shaped { commitId, error }. error reads \"Item 'TYPE/NAME' was not reverted: \" followed by the step's client-facing text. success stays failed.length === 0, so it is false on any refusal. The declared spec shape (RollbackToPackageCommitResponseSchema.failed[] = { commitId, error }) has no item fields, so the item is named inside error and packages/spec is not edited. The per-item code is not carried (see out_of_scope_findings). Behaviour kept: the rollback continues past a refused item (revertCommit never throws for one), the partly reverted commit still records its revert commit and stays in revertedCommits, and a thrown step still lands as one { commitId, error } entry before the rollback moves on. Clause-② judged plain no: the response shape is unchanged, and success becoming false for a partial rollback restores the published contract 'True exactly when nothing failed'. No accept set is narrowed, so there is no !, no BREAKING note and no ADR-0087 marker. Claim lines copied: Clause-②: no, Metadata-protocol: none.",
"tests": "New packages/metadata-protocol/src/protocol.rollback-carries-item-failures.test.ts runs through the real protocol and the real SysMetadataRepository; the engine double serves the commit ledger, so listCommits plans the rollback. It has 3 cases: (1) multi-commit rollback to c0 over c1 and c2 where c2's fields/a.b restore is refused with STORED_TYPE_NOT_CANONICAL -> success false, failed has exactly one entry {commitId: c2, error naming fields/a.b with the refusal text}, revertedCommits c1 and c2, the neighbours are reverted and fields/a.b stays V2; (2) control: all clean -> {success: true, revertedCommits [c1, c2], failed: []}; (3) control: a thrown step (c2 unreadable) -> failed [{commitId: c2, error: \"No commit 'c2'.\"}] and c1 still reverted. Ablation via scripts/ablation-replace.mjs: anchor 'for (const item of step.failed) {' replaced by 'for (const item of [] as typeof step.failed) {', anchor x1->x0, blob f0181b90 -> 9780b8b5. Predicted case 1 red and the controls green; observed 'Tests 1 failed | 2 passed (3)' with 'AssertionError: expected true to be false'. Restore: blob after == HEAD f0181b90, git diff HEAD empty. The test imports ./protocol.js from src, not through package exports, so no dist build sits between mutation and run. Package suite at c47a8170: pnpm --filter @objectstack/metadata-protocol test exit 0 (Test Files 283 passed | 4 skipped (287); Tests 21846 passed | 26 skipped (21872)). At final test content 233b1f0e: the new pin, batch-verb-driver-text and commit-timeline-instant-order files gave 34/34 passed; typecheck exit 0 (tsc --listFiles includes the new test file); eslint packages/metadata-protocol --no-inline-config exit 0.",
"gates": "Local union at final head 233b1f0e: dispatch-gates --commands --repo objectstack-ai/objectstack derived 69 families, all 69 run with exit codes captured before any pipe, 69 exited 0. --ran reconciliation: '69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)'. The earlier union at c47a8170 had check:engine-double-contract (ledger), check:objectql-double-limit and check:where-matcher red (all three fixed in 233b1f0e), plus check:dual-build-cjs-loads and check:lean-entry-closure at exit 3 (prerequisite: dist missing; green after the build). In-flight intersection: no open PR touches protocol.ts (27 open PRs read, including all 721 files of the Version Packages PR); git merge-tree --write-tree against in-flight branch claude/issue-27829-searchfields-credential-refused (no PR, does not touch protocol.ts) exits 0. origin/main merged once before pr_create (0a370917); the closure was rebuilt after the merge. PR check-runs on 233b1f0e when the report was written: 21 runs, 3 skipped (opt-in or schedule gates), 18 in_progress (Lint ·*, Type Check ·*, Check Changeset, Governed Surface Queue Guard, PR-body guards, Check PR Size, filter, Flag docs affected). Test Core and Build Core had not started yet. in_progress is the honest value.",
"line_budget": "331 changed lines (+330 / -1) across 4 files, under the 3000 human-merge threshold; production code +18/-1 in protocol.ts, all inside rollbackToPackageCommit and its JSDoc",
"files_changed": [
".changeset/27868-rollback-carries-item-failures.md",
"packages/metadata-protocol/src/protocol.rollback-carries-item-failures.test.ts",
"packages/metadata-protocol/src/protocol.ts",
"scripts/engine-double-contract.pinned.json"
],
"deviations": [
"scripts/engine-double-contract.pinned.json is outside the claim's declared file surface (protocol.ts, its tests, .changeset/*). It is the gate-prescribed ledger row for the new test's engine double, written by 'node scripts/check-engine-double-contract.mjs --write'. The seat should add it to the claim's file surface.",
"Red-first was shown by the ablation leg, which is exactly the pre-fix behaviour. The test-only commit da49d5a5 was pushed before the fix, but the verify lock was held by another dev's full build plus dogfood (two queue-timeouts, exit 99, NOT MEASURED) and it was not run on its own.",
"The full package test suite ran at c47a8170. The final commit 233b1f0e changes only the new test's double and the ledger. At 233b1f0e the three affected test files, typecheck, eslint and the full gate union were re-run, but not the 287-file suite.",
"Commit trailers are the model-free pair from AGENTS.md (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named trailer; os-dev.md and the common terms take precedence."
],
"dogfood_battery": "n/a — no ! in the PR title and Clause-② is plain no (no (narrowing) arm)",
"mcp_calls": "0",
"api_writes": "2 — relay pr_create (repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches -> fleet-write run 41006217770 -> POST /pulls #27957, draft, no assignees, no reviewers, no labels) + this os-dev-report comment via scripts/pm/post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/27868/comments); git push is not a REST write",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: POST /packages/:id/rollback answers each refused item only as error text: RollbackToPackageCommitResponseSchema.failed[] declares { commitId, error } (packages/spec/src/api/package-lifecycle.zod.ts), so revertCommit's per-item type/name/code (for example STORED_TYPE_NOT_CANONICAL) cannot be carried as fields, and the schema's 'Every commit the rollback could not revert.' now also covers a partly reverted commit's refused items · Seam: spec:RollbackToPackageCommitResponseSchema.failed → runtime:ObjectStackProtocolImplementation.rollbackToPackageCommit · a structured item field would be a spec widening (Clause-② yes) · carrier: none, noted in PR Acceptance notes, not filed · dedupe words: rollbackToPackageCommit failed code, RollbackToPackageCommitResponseSchema item fields, rollback failed type name"
]
}
Claim amendment 1 to 6514040985 · 2026-10-30T05:08Z · one ledger file added to the file surface
scripts/engine-double-contract.pinned.json is added. check:engine-double-contract requires a ledger row for the engine double of the new test protocol.rollback-carries-item-failures.test.ts (verbs delete, findOne, update; written with --write). The change adds rows only, for that file. PR #27957.
Activity
objectstack-fleet commentedon Oct 30, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 可对外的 API | none | P②
Triage: p2 ·
domain:server·area:api· 2026-10-30T03:00Z⛔ Not a claim.
success: true. Carry eachrevertCommitper-item failure (commit, item, client-facing text) into the answer, setsuccessto false on any refusal, and pin it with a multi-commit rollback.protocol.tsis a hot file, so run the in-flight intersection check.bug,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 30, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Claim: PM loop round 5
Session:
session_01L6bhgUCizoYWQVJWjhdR6FAccount:
os-salesBranch:
claude/issue-27868-rollback-carries-item-failuresWorktree:
objectstack-issue-27868Domain:
domain:serverSeat:
domain:server#2File surface:
packages/metadata-protocol/src/protocol.ts(insiderollbackToPackageCommitonly), its tests,.changeset/*(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: default (dispatch-gates --tier: no path-derived mandate),effort: mediumClause-②: no
Metadata-protocol: none
Responsibility:
metadata-protocol package rollback answer (this lane) | revertCommit already returns per-item failed[] | every administrator rolling a package backThread-read: 6513882101
Premise: holds (read on main
e320e3662026-10-30T03:12Z:rollbackToPackageCommitcallsrevertCommitper commit and discards its returned per-itemfailed[]; only a thrown error reaches its ownfailed[])Serial constraints cleared: the dev runs
git merge-treeagainst every open PR onprotocol.ts(#27829's dev may touch the gate's field list there) beforepr_create.objectstack-fleet commentedon Oct 30, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
os-dev-report
{ "issue": 27868, "status": "done", "branch": "claude/issue-27868-rollback-carries-item-failures", "pr": "https://github.com/objectstack-ai/objectstack/pull/27957", "head": "233b1f0eb9c66199967072c6c18c20453e6a7fb9", "session": "session_01L6bhgUCizoYWQVJWjhdR6F", "premise_still_valid": true, "summary": "Premise held at e320e366: rollbackToPackageCommit awaited revertCommit and discarded its returned per-item failed[]. Now, inside rollbackToPackageCommit only, each returned per-item failure becomes one entry in the rollback's failed[], shaped { commitId, error }. error reads \"Item 'TYPE/NAME' was not reverted: \" followed by the step's client-facing text. success stays failed.length === 0, so it is false on any refusal. The declared spec shape (RollbackToPackageCommitResponseSchema.failed[] = { commitId, error }) has no item fields, so the item is named inside error and packages/spec is not edited. The per-item code is not carried (see out_of_scope_findings). Behaviour kept: the rollback continues past a refused item (revertCommit never throws for one), the partly reverted commit still records its revert commit and stays in revertedCommits, and a thrown step still lands as one { commitId, error } entry before the rollback moves on. Clause-② judged plain no: the response shape is unchanged, and success becoming false for a partial rollback restores the published contract 'True exactly when nothing failed'. No accept set is narrowed, so there is no !, no BREAKING note and no ADR-0087 marker. Claim lines copied: Clause-②: no, Metadata-protocol: none.", "tests": "New packages/metadata-protocol/src/protocol.rollback-carries-item-failures.test.ts runs through the real protocol and the real SysMetadataRepository; the engine double serves the commit ledger, so listCommits plans the rollback. It has 3 cases: (1) multi-commit rollback to c0 over c1 and c2 where c2's fields/a.b restore is refused with STORED_TYPE_NOT_CANONICAL -> success false, failed has exactly one entry {commitId: c2, error naming fields/a.b with the refusal text}, revertedCommits c1 and c2, the neighbours are reverted and fields/a.b stays V2; (2) control: all clean -> {success: true, revertedCommits [c1, c2], failed: []}; (3) control: a thrown step (c2 unreadable) -> failed [{commitId: c2, error: \"No commit 'c2'.\"}] and c1 still reverted. Ablation via scripts/ablation-replace.mjs: anchor 'for (const item of step.failed) {' replaced by 'for (const item of [] as typeof step.failed) {', anchor x1->x0, blob f0181b90 -> 9780b8b5. Predicted case 1 red and the controls green; observed 'Tests 1 failed | 2 passed (3)' with 'AssertionError: expected true to be false'. Restore: blob after == HEAD f0181b90, git diff HEAD empty. The test imports ./protocol.js from src, not through package exports, so no dist build sits between mutation and run. Package suite at c47a8170: pnpm --filter @objectstack/metadata-protocol test exit 0 (Test Files 283 passed | 4 skipped (287); Tests 21846 passed | 26 skipped (21872)). At final test content 233b1f0e: the new pin, batch-verb-driver-text and commit-timeline-instant-order files gave 34/34 passed; typecheck exit 0 (tsc --listFiles includes the new test file); eslint packages/metadata-protocol --no-inline-config exit 0.", "gates": "Local union at final head 233b1f0e: dispatch-gates --commands --repo objectstack-ai/objectstack derived 69 families, all 69 run with exit codes captured before any pipe, 69 exited 0. --ran reconciliation: '69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)'. The earlier union at c47a8170 had check:engine-double-contract (ledger), check:objectql-double-limit and check:where-matcher red (all three fixed in 233b1f0e), plus check:dual-build-cjs-loads and check:lean-entry-closure at exit 3 (prerequisite: dist missing; green after the build). In-flight intersection: no open PR touches protocol.ts (27 open PRs read, including all 721 files of the Version Packages PR); git merge-tree --write-tree against in-flight branch claude/issue-27829-searchfields-credential-refused (no PR, does not touch protocol.ts) exits 0. origin/main merged once before pr_create (0a370917); the closure was rebuilt after the merge. PR check-runs on 233b1f0e when the report was written: 21 runs, 3 skipped (opt-in or schedule gates), 18 in_progress (Lint ·*, Type Check ·*, Check Changeset, Governed Surface Queue Guard, PR-body guards, Check PR Size, filter, Flag docs affected). Test Core and Build Core had not started yet. in_progress is the honest value.", "line_budget": "331 changed lines (+330 / -1) across 4 files, under the 3000 human-merge threshold; production code +18/-1 in protocol.ts, all inside rollbackToPackageCommit and its JSDoc", "files_changed": [ ".changeset/27868-rollback-carries-item-failures.md", "packages/metadata-protocol/src/protocol.rollback-carries-item-failures.test.ts", "packages/metadata-protocol/src/protocol.ts", "scripts/engine-double-contract.pinned.json" ], "deviations": [ "scripts/engine-double-contract.pinned.json is outside the claim's declared file surface (protocol.ts, its tests, .changeset/*). It is the gate-prescribed ledger row for the new test's engine double, written by 'node scripts/check-engine-double-contract.mjs --write'. The seat should add it to the claim's file surface.", "Red-first was shown by the ablation leg, which is exactly the pre-fix behaviour. The test-only commit da49d5a5 was pushed before the fix, but the verify lock was held by another dev's full build plus dogfood (two queue-timeouts, exit 99, NOT MEASURED) and it was not run on its own.", "The full package test suite ran at c47a8170. The final commit 233b1f0e changes only the new test's double and the ledger. At 233b1f0e the three affected test files, typecheck, eslint and the full gate union were re-run, but not the 287-file suite.", "Commit trailers are the model-free pair from AGENTS.md (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named trailer; os-dev.md and the common terms take precedence." ], "dogfood_battery": "n/a — no ! in the PR title and Clause-② is plain no (no (narrowing) arm)", "mcp_calls": "0", "api_writes": "2 — relay pr_create (repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches -> fleet-write run 41006217770 -> POST /pulls #27957, draft, no assignees, no reviewers, no labels) + this os-dev-report comment via scripts/pm/post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/27868/comments); git push is not a REST write", "open_questions": [], "out_of_scope_findings": [ "class: b · reach: POST /packages/:id/rollback answers each refused item only as error text: RollbackToPackageCommitResponseSchema.failed[] declares { commitId, error } (packages/spec/src/api/package-lifecycle.zod.ts), so revertCommit's per-item type/name/code (for example STORED_TYPE_NOT_CANONICAL) cannot be carried as fields, and the schema's 'Every commit the rollback could not revert.' now also covers a partly reverted commit's refused items · Seam: spec:RollbackToPackageCommitResponseSchema.failed → runtime:ObjectStackProtocolImplementation.rollbackToPackageCommit · a structured item field would be a spec widening (Clause-② yes) · carrier: none, noted in PR Acceptance notes, not filed · dedupe words: rollbackToPackageCommit failed code, RollbackToPackageCommitResponseSchema item fields, rollback failed type name" ] }objectstack-fleet commentedon Oct 30, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Claim amendment 1 to
6514040985· 2026-10-30T05:08Z · one ledger file added to the file surfacescripts/engine-double-contract.pinned.jsonis added.check:engine-double-contractrequires a ledger row for the engine double of the new testprotocol.rollback-carries-item-failures.test.ts(verbsdelete,findOne,update; written with--write). The change adds rows only, for that file. PR #27957.Everything else in the claim is unchanged.
objectstack-fleet commentedon Oct 30, 2026
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
ACCEPT → PR #27957 · head
233b1f0e· 2026-10-30T05:38Z · landing sent (ACCEPT 6515838377).