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: rollbackMetaItem / revertCommit restore a version without the save-door refusal family, so a restore can re-create a refused shape #25278
Filing gate ③: an observation of the contract review of PR #25257 (record 6347940824, observation 1), disposed in the seat's ACCEPT 6348040277 (item 5). Pre-existing door shape; it does not block that PR, which merged as bda53bb0b (card #25211).
The restore doors (rollbackMetaItem, revertCommit; see the door list at packages/metadata-protocol/src/protocol.ts:137) write a history body back through repo.restoreVersion (:7254 onward) and do not ask that family. A restore of a version saved before a refusal existed — or of the other half of a pair (an object version that binds external while its datasource is now managed) — re-creates the refused shape.
Wanted
Measure first: which refusals the restore doors already re-judge (some restore paths call a judge on the history body, :7265–:7284) and which they skip; list them.
The restore doors refuse what saveMetaItem refuses, through the same judges (one copy each), with the same envelope; or a written reason per refusal why a restore may bypass it.
Acceptance
Pinned: restoring a version that the save door would refuse answers the save door's refusal and stores nothing; restoring an acceptable version still succeeds.
No refusal is copied: the restore path calls the same function the save door calls.
Claim: PM loop (card thread) · 2026-10-22T22:45Z · p3, domain:engine. The metadata-protocol restore doors (rollbackMetaItem, revertCommit) ask the save-door refusal family, so a restore cannot re-create a refused shape.
Session: session_01HZVZ7xrs691S9potR9mSL1 (card thread of domain:engine seat 2, seat session session_01FaVyM6YvQLg9CsjPVCYc5K)
Account: os-bill
Branch: claude/project-thread-5v0lx1
Worktree: the card thread's own checkout
Domain: domain:engine
Seat: domain:engine#2 (seat post #20966)
File surface: packages/metadata-protocol/src/** (the restore doors and their tests), packages/rest/src/meta-item-history-routes.ts only if the REST envelope needs it; one changeset per moved package.
(stop on breach; explain in the report)
⛔ Not in scope: any refusal's rule itself; the datasource-admin service (#25277, domain:services); the stored-shape detector (#25279, domain:spec); packages/spec/**.
Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates.mjs --tier: no path-derived mandate), effort: high. Review: CONTRACT_REVIEW_TIER (a metadata-protocol door's accept set narrows: judged from the card content).
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Responsibility: the restore doors write a history body without the save door's refusals | the save door already refuses each shape on save | every author or admin who restores a version through REST or the console
Thread-read: none
Serial constraints cleared: open PRs on packages/metadata-protocol/src/protocol.ts re-checked at claim time: PR #25300 (spec lane, the list-query window near :4777) and PR #25299 (spec lane, the stored-shape listing near :6817, :22553, :25058) touch other regions of the file — ordinary concurrency; whichever lands second merges main.
{
"issue": 25278,
"status": "done",
"branch": "claude/project-thread-5v0lx1",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25312",
"head": "e5daf940f552900c0c35eeedd2b16b35af9a19a6",
"session": "session_01HZVZ7xrs691S9potR9mSL1",
"premise_still_valid": true,
"summary": "rollbackMetaItem and revertCommit's restore limb now ask the save door's post-Zod refusal family (hook without body, default record page, external binding both halves, anonymous elevated flow, runtime authoring rules, plugin authoring gate) through the same functions the save door calls, in its order, inside restoredBodyWriter before repo.restoreVersion writes anything. A refusal carries the save door's code and status; rollback rethrows it, revertCommit reports it in failed[] and reverts the commit's other items; nothing is stored. ADR-0070 D1 and the credential-channel store refusal are not asked, each with a written reason in the code and PR body; the Zod gate stays unasked and is the open question.",
"census": [
{"n": 1, "refusal": "runtimeHookWithoutBodyRefusal", "save_call_site_base": "protocol.ts:24094", "rollback_before": "no", "revert_before": "no", "after": "asked"},
{"n": 2, "refusal": "defaultRecordPageSaveRefusal", "save_call_site_base": "protocol.ts:24107", "rollback_before": "no", "revert_before": "no", "after": "asked (verb restored)"},
{"n": 3, "refusal": "externalBindingSaveRefusal (both halves)", "save_call_site_base": "protocol.ts:24118", "rollback_before": "no", "revert_before": "no", "after": "asked"},
{"n": 4, "refusal": "anonymousElevatedFlowRefusal", "save_call_site_base": "protocol.ts:24129", "rollback_before": "no", "revert_before": "no", "after": "asked"},
{"n": 5, "refusal": "assertRuntimeAuthoringRules", "save_call_site_base": "protocol.ts:24146", "rollback_before": "no", "revert_before": "no", "after": "asked (state active, row's own package, channel held paths)"},
{"n": 6, "refusal": "runAuthoringGate", "save_call_site_base": "protocol.ts:24218", "rollback_before": "no", "revert_before": "no", "after": "asked (same channel condition as save)"},
{"n": 7, "refusal": "ADR-0070 D1 readOnlyBaseCreateError", "save_call_site_base": "protocol.ts:24348", "rollback_before": "no", "revert_before": "no", "after": "not asked: D1 judges a package a caller names for a new item; a restore names none and writes into the active row's own binding"},
{"n": 8, "refusal": "credential channel store", "save_call_site_base": "protocol.ts:24465", "rollback_before": "no (R2 strip)", "revert_before": "no (R2 strip)", "after": "not asked: a restore stores no credential (R2 keeps the current one)"}
],
"tests": "protocol.restore-doors-save-refusal-family.test.ts (28 tests: 7 cases x {rollback, revert} x {refusal, control}). base d0cbd909: 14 refusal pins red (rollback returned success / revert success:true), 14 controls green. fix 2cafc371: 28/28 green. ablation (scripts/ablation-replace.mjs --delete of the refuseRestoredBodyTheSaveDoorRefuses call, blob 3d4430ce -> b2a9c352, restored to HEAD blob, git diff HEAD empty): 14 red / 14 green. metadata-protocol tsc --noEmit exit 0; full suite on e5daf940: 259 files passed (3 skipped), 20943 tests passed, 19 skipped. Downstream after rebuild: objectql 465 files / 8691 tests passed; rest 321 files / 6607 passed, 344 skipped.",
"gates": "dispatch-gates.mjs --commands on the final diff: 77 families, each exit 0 (68 on 2cafc371, the 9 the ledger file adds on e5daf940); --ran reconciles 77 run, 0 NOT-MEASURED. check:engine-double-contract was red on the new double until --write (3 rows, e5daf940). check-plugin-teardown-shape --self-test and check:dual-build-cjs-loads exited 3 on the shallow checkout, exit 0 after deepening. PR CI at report time on e5daf940: every completed check success or skipped (Check Changeset, Dogfood Regression Gate, Lint PM dispatch-gates, single-writer/claim guards, Check PR Size among them); Test Core 3/6 and 4/6 still in_progress.",
"dogfood_battery": "e5daf940 · os-verify-lock: VERDICT command-exit 0 (held 1302s) · Test Files 289 passed | 3 skipped (292) · Tests 2491 passed | 18 skipped (2509) · no failures",
"mcp_calls": "0 MCP GitHub calls (no GitHub MCP tool used, read or write); session tool subscribe_pr_activity once for #25312",
"api_writes": "3 — git push of claude/project-thread-5v0lx1; relay dispatch POST repos/objectstack-ai/objectstack/dispatches (fleet-write pr_create -> #25312, draft, assignee os-bill, body read back byte-identical); this os-dev-report comment via post-stamped (POST issues/25278/comments)",
"line_budget": "+691 / -18 over 6 files (protocol.ts +120/-14, sys-metadata-repository.ts +7/-2, new test +515, credential-channel test +5/-2, changeset +29, engine-double ledger +15)",
"files_changed": [
"packages/metadata-protocol/src/protocol.ts",
"packages/metadata-protocol/src/sys-metadata-repository.ts",
"packages/metadata-protocol/src/protocol.restore-doors-save-refusal-family.test.ts",
"packages/metadata-protocol/src/protocol.credential-channel.test.ts",
".changeset/25278-restore-doors-save-refusal-family.md",
"scripts/engine-double-contract.pinned.json"
],
"deviations": [
"scripts/engine-double-contract.pinned.json is outside the claim's declared surface: the mechanical consequence of the new test file's engine double (check:engine-double-contract --write).",
"protocol.credential-channel.test.ts R2 pin moved on purpose from ['strip'] to ['heldPaths:active','strip']: the restore now feeds the runtime rules the channel's held paths, as draft promotion does; restore still never calls store.",
"Reviewer request left to the seat; the assignee rode pr_create.",
"Docs drift bot listed 4 hand-written pages (client-sdk, metadata-api, metadata-lifecycle, metadata-service): re-read, none states what a restore does or does not refuse, so no doc edit."
],
"open_questions": [
{
"question": "Should a restore run today's schema parse (the save door's Zod gate) on the history body?",
"options": ["A leave as is: stored rows keep their ADR-0087 conversion path; a version that fails today's parse still restores", "B parse on restore and refuse 422 INVALID_METADATA, which can strand old versions that only a conversion would fix"],
"recommendation": "A for this card, because it is the ADR-0087 stored-shape question and needs a ruling, not a door edit; file it if the seat wants it decided."
}
],
"out_of_scope_findings": [
"class: c · reach: named producer packages/rest/src/meta-item-history-routes.ts rollback route comment says rollback 'runs no content gate at all', false after this PR · evidence: the route forwards the thrown 4xx via handleRouteError · dedupe words: rollback route stale comment content gate · noted, not filed",
"class: c · reach: refusals the save door runs before its Zod gate (protection block, destructive diff, view-container name collision, ADR-0029 D9.9 package mismatch, namespace prefix, tenant-authored flow stamp) are not asked by a restore; each judges a request or diff against the live row, none measured on a restore · dedupe words: restore pre-zod refusals · noted, not filed"
]
}
ACCEPT → PR #25312 comment 6355626347 (contract review PASS 6355317577 on e5daf940f). Ready and auto-merge follow now; every check on the head is green.
merged: true at 2026-10-23T02:39Z, merge commit 69913f8ba, through the merge queue
origin/main
69913f8ba3 fix(metadata-protocol)!: rollbackMetaItem and revertCommit ask the save door's refusal family before they write a history body back (#25312)
queue branch
gh-readonly-queue/main/pr-25312-* gone
card
closed completed by the PR
rollbackMetaItem and revertCommit's restore limb now ask the save door's six post-Zod refusals before they write a history body back, through the same functions the save door uses. A refused version is answered with the save door's code and status, and nothing is stored. revertCommit reports the refusal per item in failed[]. Three items were left open and not filed, because the seat is winding down: the Zod parse on a restored body (the ADR-0087 stored-shape question), the stale rollback comment in packages/rest/src/meta-item-history-routes.ts:746, and the imprecise reason for two pre-Zod body judges (ACCEPT 6355626347 item 5). State: pm:dispatched removed in this act.
Activity
objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop (card thread) · 2026-10-22T22:45Z · p3,
domain:engine. The metadata-protocol restore doors (rollbackMetaItem, revertCommit) ask the save-door refusal family, so a restore cannot re-create a refused shape.Session:
session_01HZVZ7xrs691S9potR9mSL1(card thread ofdomain:engineseat 2, seat sessionsession_01FaVyM6YvQLg9CsjPVCYc5K)Account:
os-billBranch:
claude/project-thread-5v0lx1Worktree: the card thread's own checkout
Domain:
domain:engineSeat:
domain:engine#2(seat post #20966)File surface:
packages/metadata-protocol/src/**(the restore doors and their tests),packages/rest/src/meta-item-history-routes.tsonly if the REST envelope needs it; one changeset per moved package.(stop on breach; explain in the report)
⛔ Not in scope: any refusal's rule itself; the datasource-admin service (#25277,
domain:services); the stored-shape detector (#25279,domain:spec);packages/spec/**.Container & model:
M,mode:thread,model: TIER_DEFAULT(dispatch-gates.mjs --tier: no path-derived mandate),effort: high. Review:CONTRACT_REVIEW_TIER(a metadata-protocol door's accept set narrows: judged from the card content).Clause-②: no (narrowing)
Metadata-protocol: narrowed
Responsibility: the restore doors write a history body without the save door's refusals | the save door already refuses each shape on save | every author or admin who restores a version through REST or the console
Thread-read: none
Serial constraints cleared: open PRs on
packages/metadata-protocol/src/protocol.tsre-checked at claim time: PR #25300 (spec lane, the list-query window near:4777) and PR #25299 (spec lane, the stored-shape listing near:6817,:22553,:25058) touch other regions of the file — ordinary concurrency; whichever lands second mergesmain.objectstack-fleet commentedon Oct 23, 2026
os-dev-report
{ "issue": 25278, "status": "done", "branch": "claude/project-thread-5v0lx1", "pr": "https://github.com/objectstack-ai/objectstack/pull/25312", "head": "e5daf940f552900c0c35eeedd2b16b35af9a19a6", "session": "session_01HZVZ7xrs691S9potR9mSL1", "premise_still_valid": true, "summary": "rollbackMetaItem and revertCommit's restore limb now ask the save door's post-Zod refusal family (hook without body, default record page, external binding both halves, anonymous elevated flow, runtime authoring rules, plugin authoring gate) through the same functions the save door calls, in its order, inside restoredBodyWriter before repo.restoreVersion writes anything. A refusal carries the save door's code and status; rollback rethrows it, revertCommit reports it in failed[] and reverts the commit's other items; nothing is stored. ADR-0070 D1 and the credential-channel store refusal are not asked, each with a written reason in the code and PR body; the Zod gate stays unasked and is the open question.", "census": [ {"n": 1, "refusal": "runtimeHookWithoutBodyRefusal", "save_call_site_base": "protocol.ts:24094", "rollback_before": "no", "revert_before": "no", "after": "asked"}, {"n": 2, "refusal": "defaultRecordPageSaveRefusal", "save_call_site_base": "protocol.ts:24107", "rollback_before": "no", "revert_before": "no", "after": "asked (verb restored)"}, {"n": 3, "refusal": "externalBindingSaveRefusal (both halves)", "save_call_site_base": "protocol.ts:24118", "rollback_before": "no", "revert_before": "no", "after": "asked"}, {"n": 4, "refusal": "anonymousElevatedFlowRefusal", "save_call_site_base": "protocol.ts:24129", "rollback_before": "no", "revert_before": "no", "after": "asked"}, {"n": 5, "refusal": "assertRuntimeAuthoringRules", "save_call_site_base": "protocol.ts:24146", "rollback_before": "no", "revert_before": "no", "after": "asked (state active, row's own package, channel held paths)"}, {"n": 6, "refusal": "runAuthoringGate", "save_call_site_base": "protocol.ts:24218", "rollback_before": "no", "revert_before": "no", "after": "asked (same channel condition as save)"}, {"n": 7, "refusal": "ADR-0070 D1 readOnlyBaseCreateError", "save_call_site_base": "protocol.ts:24348", "rollback_before": "no", "revert_before": "no", "after": "not asked: D1 judges a package a caller names for a new item; a restore names none and writes into the active row's own binding"}, {"n": 8, "refusal": "credential channel store", "save_call_site_base": "protocol.ts:24465", "rollback_before": "no (R2 strip)", "revert_before": "no (R2 strip)", "after": "not asked: a restore stores no credential (R2 keeps the current one)"} ], "tests": "protocol.restore-doors-save-refusal-family.test.ts (28 tests: 7 cases x {rollback, revert} x {refusal, control}). base d0cbd909: 14 refusal pins red (rollback returned success / revert success:true), 14 controls green. fix 2cafc371: 28/28 green. ablation (scripts/ablation-replace.mjs --delete of the refuseRestoredBodyTheSaveDoorRefuses call, blob 3d4430ce -> b2a9c352, restored to HEAD blob, git diff HEAD empty): 14 red / 14 green. metadata-protocol tsc --noEmit exit 0; full suite on e5daf940: 259 files passed (3 skipped), 20943 tests passed, 19 skipped. Downstream after rebuild: objectql 465 files / 8691 tests passed; rest 321 files / 6607 passed, 344 skipped.", "gates": "dispatch-gates.mjs --commands on the final diff: 77 families, each exit 0 (68 on 2cafc371, the 9 the ledger file adds on e5daf940); --ran reconciles 77 run, 0 NOT-MEASURED. check:engine-double-contract was red on the new double until --write (3 rows, e5daf940). check-plugin-teardown-shape --self-test and check:dual-build-cjs-loads exited 3 on the shallow checkout, exit 0 after deepening. PR CI at report time on e5daf940: every completed check success or skipped (Check Changeset, Dogfood Regression Gate, Lint PM dispatch-gates, single-writer/claim guards, Check PR Size among them); Test Core 3/6 and 4/6 still in_progress.", "dogfood_battery": "e5daf940 · os-verify-lock: VERDICT command-exit 0 (held 1302s) · Test Files 289 passed | 3 skipped (292) · Tests 2491 passed | 18 skipped (2509) · no failures", "mcp_calls": "0 MCP GitHub calls (no GitHub MCP tool used, read or write); session tool subscribe_pr_activity once for #25312", "api_writes": "3 — git push of claude/project-thread-5v0lx1; relay dispatch POST repos/objectstack-ai/objectstack/dispatches (fleet-write pr_create -> #25312, draft, assignee os-bill, body read back byte-identical); this os-dev-report comment via post-stamped (POST issues/25278/comments)", "line_budget": "+691 / -18 over 6 files (protocol.ts +120/-14, sys-metadata-repository.ts +7/-2, new test +515, credential-channel test +5/-2, changeset +29, engine-double ledger +15)", "files_changed": [ "packages/metadata-protocol/src/protocol.ts", "packages/metadata-protocol/src/sys-metadata-repository.ts", "packages/metadata-protocol/src/protocol.restore-doors-save-refusal-family.test.ts", "packages/metadata-protocol/src/protocol.credential-channel.test.ts", ".changeset/25278-restore-doors-save-refusal-family.md", "scripts/engine-double-contract.pinned.json" ], "deviations": [ "scripts/engine-double-contract.pinned.json is outside the claim's declared surface: the mechanical consequence of the new test file's engine double (check:engine-double-contract --write).", "protocol.credential-channel.test.ts R2 pin moved on purpose from ['strip'] to ['heldPaths:active','strip']: the restore now feeds the runtime rules the channel's held paths, as draft promotion does; restore still never calls store.", "Reviewer request left to the seat; the assignee rode pr_create.", "Docs drift bot listed 4 hand-written pages (client-sdk, metadata-api, metadata-lifecycle, metadata-service): re-read, none states what a restore does or does not refuse, so no doc edit." ], "open_questions": [ { "question": "Should a restore run today's schema parse (the save door's Zod gate) on the history body?", "options": ["A leave as is: stored rows keep their ADR-0087 conversion path; a version that fails today's parse still restores", "B parse on restore and refuse 422 INVALID_METADATA, which can strand old versions that only a conversion would fix"], "recommendation": "A for this card, because it is the ADR-0087 stored-shape question and needs a ruling, not a door edit; file it if the seat wants it decided." } ], "out_of_scope_findings": [ "class: c · reach: named producer packages/rest/src/meta-item-history-routes.ts rollback route comment says rollback 'runs no content gate at all', false after this PR · evidence: the route forwards the thrown 4xx via handleRouteError · dedupe words: rollback route stale comment content gate · noted, not filed", "class: c · reach: refusals the save door runs before its Zod gate (protection block, destructive diff, view-container name collision, ADR-0029 D9.9 package mismatch, namespace prefix, tenant-authored flow stamp) are not asked by a restore; each judges a request or diff against the live row, none measured on a restore · dedupe words: restore pre-zod refusals · noted, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
ACCEPT → PR #25312 comment 6355626347 (contract review PASS 6355317577 on
e5daf940f). Ready and auto-merge follow now; every check on the head is green.objectstack-fleet commentedon Oct 23, 2026
Landed ·
domain:engineseat 2 (seat post #20966) ·os-bill·session_01FaVyM6YvQLg9CsjPVCYc5K· 2026-10-23T02:39ZFixes #25278)merged: trueat 2026-10-23T02:39Z, merge commit69913f8ba, through the merge queueorigin/main69913f8ba3 fix(metadata-protocol)!: rollbackMetaItem and revertCommit ask the save door's refusal family before they write a history body back (#25312)gh-readonly-queue/main/pr-25312-*gonecompletedby the PRrollbackMetaItemandrevertCommit's restore limb now ask the save door's six post-Zod refusals before they write a history body back, through the same functions the save door uses. A refused version is answered with the save door's code and status, and nothing is stored.revertCommitreports the refusal per item infailed[]. Three items were left open and not filed, because the seat is winding down: the Zod parse on a restored body (the ADR-0087 stored-shape question), the stale rollback comment inpackages/rest/src/meta-item-history-routes.ts:746, and the imprecise reason for two pre-Zod body judges (ACCEPT 6355626347 item 5). State:pm:dispatchedremoved in this act.Generated by Claude Code