Skip to content

fix(metadata-protocol)!: rollbackMetaItem and revertCommit ask the save door's refusal family before they write a history body back - #25312

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-5v0lx1
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-5v0lx1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

Fixes #25278

Clause-②: no (narrowing)

Metadata-protocol: narrowed

BREAKING (what a metadata restore writes narrows). FROM: rollbackMetaItem and revertCommit's restore limb wrote back a stored version that the save door refuses today. TO: they refuse it with the save door's own code and status and store nothing. Fix: restore another version, or change the other half of the pair first, then restore.

The two restore doors (rollbackMetaItem, POST /api/v1/meta/TYPE/NAME/rollback; and revertCommit's restore limb, which rollbackToPackageCommit also uses) write a history body back as the active row through repo.restoreVersion. Before this PR they asked only the save door's name judge and permission-set owner judge (restoredBodyWriter). They asked none of the refusals saveMetaItem runs after its schema gate. A version saved before a refusal existed, or one half of a pair whose other half moved after the version was saved, came back live.

Census (base 5801a4f2c)

The refusals saveMetaItem runs after its Zod gate (protocol.ts:23986), in its order, and whether each restore door asked them:

# Save-door refusal Call site on base · function rollbackMetaItem before revertCommit before After this PR
1 hook with no body :24094 · runtimeHookWithoutBodyRefusal (:1000) no no asked
2 second default record page of an object :24107 · defaultRecordPageSaveRefusal (:22429) no no asked (verb restored)
3 external binding on a managed datasource, both halves :24118 · externalBindingSaveRefusal (:22542) no no asked
4 runAs: 'system' flow an anonymous endpoint names :24129 · anonymousElevatedFlowRefusal (:22658) no no asked (a restore writes an active row)
5 shared runtime authoring rules :24146 · assertRuntimeAuthoringRules (:7372) no no asked: state active, the row's own package, the credential channel's held paths
6 domain plugin authoring gate :24218 · runAuthoringGate (:7309) no no asked, on the same channel condition as the save door
7 ADR-0070 D1 read-only-base create :24348 · SysMetadataRepository.readOnlyBaseCreateError no no not asked, reason below
8 credential channel store refusal (no crypto provider) :24465 · MetadataCredentialChannel.store no (R2 strip) no (R2 strip) not asked, reason below

Already asked by both restore doors before this PR, through restoredBodyWriter (:7291), and unchanged: savedItemNameRefusal (save door :23813) and permissionSetOwnerBindingRefusal (save door :23824). The Zod gate itself (:23986) is not in the family and is still not asked; reason below.

Refusals a restore does not ask, with the reason (also written in the code)

  • ADR-0070 D1. D1 judges a package a caller names for a new item. A restore names none: it writes into the active row's own binding, which put keeps. SysMetadataRepository.put's package-door note records why D1 is not extended to the doors that carry a row's own binding: it would refuse the repair of legacy package-bound rows.
  • Credential channel store. A restore stores no credential. The channel keeps its current one (R2, restoredBodyDerivation), so there is nothing for store to refuse.
  • The schema gate. A history body is a stored row. Whether today's parse may refuse a stored version is the ADR-0087 stored-shape question, which this PR does not decide (see Open question). Every judge above reads its body defensively, and the runtime gate reports a rule that throws instead of blocking on it.

Fix

  • refuseRestoredBodyTheSaveDoorRefuses (new, protocol.ts) asks refusals 1–6 through the same functions the save door calls, in the save door's order, with their envelopes. ⛔ No rule is copied.
  • restoredBodyWriter calls it after the name and owner judges and before the R2 strip. It is now async and takes the restoring door's source.
  • SysMetadataRepository.restoreVersion awaits deriveRestoredBody (its type now allows a promise). It is still asked on the history body before the active row is read and before put, so a refused version writes nothing.
  • rollbackMetaItem rethrows the refusal. revertCommit reports it per item in failed[] with its code, as it does for its other per-item refusals, and reverts the commit's other items.
  • defaultRecordPageSaveRefusal's verb union gains 'restored', so its sentence says "cannot be restored as the default record page". Three docblocks that listed the doors asking a refusal now name the restore doors.

The pair rule, both halves. A datasource restored to managed (or with no schemaMode) while objects bind it external is refused at schemaMode, naming each object. An object restored with external on a datasource that is managed now is refused at external. Both come from externalBindingSaveRefusal, which reads the stored side as it stands when the restore runs.

Measured

packages/metadata-protocol/src/protocol.restore-doors-save-refusal-family.test.ts (new, 28 tests): refusals 1–6 (with both halves of #3), each through rollbackMetaItem and through revertCommit, plus a CONTROL per door that restores an acceptable version of the same item. A refusal pin asserts the code and status, one named subject (the holder page, the bound object, the issues[].path), and that rows, history and registry are byte-identical afterwards.

Run Tree Refusal pins (14) Controls (14)
base d0cbd909 (test only, on 5801a4f2c) 14 red: rollback returned success (expected null to be an instance of Error), revert returned success: true 14 green
fix 2cafc371 14 green 14 green
ablation 2cafc371 with the refuseRestoredBodyTheSaveDoorRefuses call deleted (scripts/ablation-replace.mjs --delete: anchor 1 → 0, blob 3d4430ce → b2a9c352, restored to the HEAD blob, git diff HEAD empty) 14 red 14 green

Tests and gates

All on 2cafc371; e5daf940 adds only the engine-double ledger rows. The test file resolves ./protocol.js from source, so the ablation needed no dist/ leg.

  • @objectstack/metadata-protocol: tsc --noEmit exit 0. Full suite on e5daf940: 259 files passed (3 skipped), 20,943 tests passed, 19 skipped. That run includes one pin updated on purpose (next bullet); before that update it was the only failure.
  • One existing pin moved, on purpose. protocol.credential-channel.test.ts (R2 rollback) pinned the channel's calls during a rollback as ['strip']. A rollback now first reads heldPaths:active, so the runtime authoring rules see a credential the channel holds as present (the draft promotion does the same). The pin now reads ['heldPaths:active', 'strip']. What it guards is unchanged: the restore never calls store, the channel keeps its current secret, and nothing goes back at rest.
  • Downstream, after rebuilding metadata-protocol: @objectstack/objectql 465 files / 8,691 tests passed (it holds the real-repository rollback and revert suites). @objectstack/rest 321 files / 6,607 passed, 344 skipped.
  • Derived gates (dispatch-gates.mjs --commands): 77 families, every one exit 0. --ran reconciles 77 run, 0 NOT-MEASURED. 68 ran on 2cafc371 and the 9 the ledger file adds ran on e5daf940. Three were re-run on e5daf940:
    • check:engine-double-contract was red on the new test file's double. --write recorded 3 rows in scripts/engine-double-contract.pinned.json.
    • check-plugin-teardown-shape --self-test and check:dual-build-cjs-loads exited 3 on the shallow checkout and went green after deepening.
      The ledger file is outside the claim's declared surface; it is the gate's mechanical consequence of the new double.
  • Full dogfood (the title carries !): run on the final head after this PR opens; its VERDICT line is reported in the os-dev-report on metadata-protocol: rollbackMetaItem / revertCommit restore a version without the save-door refusal family, so a restore can re-create a refused shape #25278.

Acceptance notes

  • Refusals before the Zod gate that a restore does not ask (outside this card's family, observations only, not filed): the protection-block refusal, the destructive-change diff, the view-container name collision, the ADR-0029 D9.9 package mismatch, the namespace-prefix rule and the tenant-authored flow stamp refusal. Each judges a request or a diff against the live row rather than a stored body's shape, and none was measured on a restore here.
  • Batch order in revertCommit. Each item is judged against the store as that item is reached, which is the save door's own judgement. Reverting a commit whose forward order was legal visits the same states in reverse, so a pair rule that held going forward holds going back.
  • Stale REST comment. packages/rest/src/meta-item-history-routes.ts (the rollback route) says rollback "runs no content gate at all". After this PR that sentence is false. The file is outside this claim's surface except for an envelope change, which was not needed (the route already forwards a thrown 4xx through handleRouteError), so it is left for the seat.
  • Ledger row. scripts/engine-double-contract.pinned.json gains the new test file's three engine-double rows (check:engine-double-contract --write).
  • Advisories. The runtime authoring gate's advisory half is not added to either restore receipt: neither declares a key for it, and adding one is a response-shape change.

Open question

Should a restore run the schema parse on the history body? Today a version that fails the current parse restores, and the save door would refuse it with 422 INVALID_METADATA. This PR leaves that alone: it is the ADR-0087 stored-shape question (stored rows keep their conversion path) and needs a ruling, not a door edit.


Generated by Claude Code

…s refusal family (measurement, before the fix)

Claude-Session: https://claude.ai/code/session_01HZVZ7xrs691S9potR9mSL1
Co-authored-by: Claude <noreply@anthropic.com>
…l family before they write a history body back

Claude-Session: https://claude.ai/code/session_01HZVZ7xrs691S9potR9mSL1
Co-authored-by: Claude <noreply@anthropic.com>
…nel's held paths before it strips

Claude-Session: https://claude.ai/code/session_01HZVZ7xrs691S9potR9mSL1
Co-authored-by: Claude <noreply@anthropic.com>
…he pinned ledger

Claude-Session: https://claude.ai/code/session_01HZVZ7xrs691S9potR9mSL1
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 13 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via meta.rollbackItem (sdk, the route ledger binds it to POST /api/v1/meta/:type/:name/rollback, selected by route anchor /:type/:name/rollback), rollbackItem (sdk, the bare tail of client method meta.rollbackItem, bound to POST /api/v1/meta/:type/:name/rollback))
  • content/docs/api/metadata-api.mdx (via /:type/:name/rollback (route, bridged from symbol rollbackMetaItem — its route source's handler names it))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/kernel/contracts/metadata-service.mdx (via /:type/:name/rollback (route, bridged from symbol rollbackMetaItem — its route source's handler names it))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 123029be96651c38b8c7aacfc6ae2d623c667a66 — the merge of head e5daf940f552900c0c35eeedd2b16b35af9a19a6 into base 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 123029be96651c38b8c7aacfc6ae2d623c667a66 && git checkout 123029be96651c38b8c7aacfc6ae2d623c667a66
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275 e5daf940f552900c0c35eeedd2b16b35af9a19a6 && git checkout -B drift-repro 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275 && git merge --no-ff e5daf940f552900c0c35eeedd2b16b35af9a19a6

node scripts/docs-audit/affected-docs.mjs --json 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 258f4e6aaa5c1f75675b4195a35cb5da2ce5e275 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e5daf940f552900c0c35eeedd2b16b35af9a19a6
Local-runs: none
Scope: 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, scripts/engine-double-contract.pinned.json, .changeset/25278-restore-doors-save-refusal-family.md + packages/rest/src/meta-item-history-routes.ts, packages/rest/src/error-response.ts, packages/spec/src/migrations/entries/semantic/18.object-external-binding-managed-datasource-refused.ts, packages/spec/src/migrations/entries/semantic/18.page-default-record-page-one-per-object-refused.ts, packages/spec/src/migrations/entries/semantic/18.api-endpoint-anonymous-elevated-flow-refused.ts

Inputs: card #25278 (body, claim 6352974944, os-dev-report 6354044792), PR #25312 (body, 6 files, net diff against main), and the 46 check-runs on the head: every one success or skipped (Check Changeset, Lint · CI, release & engine gates, Type Check · workspace, Test Core 1–6, Dogfood Regression Gate, both single-writer guards, Governed Surface Queue Guard among them). Nothing built, run or re-run here.

① Derived judgments

Accept set — what a restore writes narrows (the card's ask). restoredBodyWriter (protocol.ts:7296–7306) now awaits refuseRestoredBodyTheSaveDoorRefuses (:7366–7396) after the two judges it already asked (savedItemNameRefusal, permissionSetOwnerBindingRefusal) and before the R2 strip. SysMetadataRepository.restoreVersion awaits deriveRestoredBody at sys-metadata-repository.ts:1669, before the active row is read (:1688) and before put (:1695); the only writes in that method are the put, so a refused version stores nothing. Right. Both restore doors pass the writer (protocol.ts:30233 revert, :30586 rollback); rollbackMetaItem's catch rethrows anything that is not a ConflictError (:30664–30683); revertCommit's per-item catch lands it in failed[] with clientFacingFailureCode (:30301–30303). The card's pins (refusal answers the save door's refusal, stores nothing; an acceptable version still restores) are measured in the new file: 7 cases x {rollback, revert} x {refusal, control}, with a byte-identical snapshot of rows, history length and registry after each refusal. Right.

Census, checked judge by judge against the save door at this head (protocol.ts:24196–24325). Each restore call is the save door's own function, no rule copied:

  • 1 runtimeHookWithoutBodyRefusal(type, body, name) — same three arguments (:24196 vs :7373). Right.
  • 2 defaultRecordPageSaveRefusal(type, body, name, 'restored') — same judge; the verb union widens by 'restored' (:22531) and the verb is used only in the sentence (:22588). The saved name is excluded from the holders, so a page whose current active row already holds the default restores. Right.
  • 3 externalBindingSaveRefusal(type, body, name) — same judge, both halves (object at external, datasource at schemaMode, :22651–22666), read against the live store. The save door passes a fourth argument request.writeFace (:24220); the restore passes none, so the refusal's sentence is rendered on the default face (specValidationFindings, :4246–4275) while code, status and issues[] are identical. The rollback request declares no face (:30439–30446), so there is nothing to pass; the REST route forwards issues[] as-is (error-response.ts:593). Right; prose-only difference, named here so it is not read as a copied rule.
  • 4 anonymousElevatedFlowRefusal(type, body, name) — same judge; the save door skips it for drafts and migrate-stored (:24230), a restore is an active write from a protocol door, so asking it unconditionally is the save door's own condition. Right.
  • 5 assertRuntimeAuthoringRules({ state: 'active', packageId: binding, source, restoredCredentialPaths }) — same judge, same early-returns (package-author channel, migrate-stored, :7548–7564). The credential-paths provider differs from the save door's restoredCredentialPathsFor (:9961–9993), which unions the carry-forward's redacted paths with the channel's held paths; the restore passes only the channel's held paths (:7390). Right: restoreVersion performs no carry-forward (the history body is written as it is), so the carried half does not exist on a restore, and this is the exact shape promoteDraftForPublish passes (:26032–26034). The one input that differs from the save door: a flow stored on the canonicalizer's fallback is judged by the save door in its D2-converted spelling (flowGateVerdictBody, :24257) and by the restore in its stored spelling. That is the stored-shape question (③, open question), not a copied rule.
  • 6 runAuthoringGate({ type, name, state: 'active', body }) on authoringChannel !== 'package-author' — same function, same condition (:24319 vs :7393). Right.
  • 7 ADR-0070 D1 readOnlyBaseCreateError not asked — D1 judges a package the caller names for a new item (:24394 on base); restoreVersion writes into the active row's own package_id (sys-metadata-repository.ts:1688–1704). Reason written in the code (:7347–7353). Right.
  • 8 credential channel store not asked — restoredBodyDerivation only strips (:7255–7258); the restore never stores a credential; the moved R2 pin (protocol.credential-channel.test.ts:272–279, ['heldPaths:active', 'strip']) records exactly the new read and still proves no store. Right.
  • Zod gate not asked — reason in the code (:7340–7346); see ③.

Public surface. SysMetadataRepository is exported (index.ts:142); its restoreVersion option deriveRestoredBody widens from a synchronous callback to one that may also return a promise (sys-metadata-repository.ts:1616), additive for every caller, and the single implementer awaits it (:1669). No other restoreVersion implementation or deriveRestoredBody consumer exists at the head (git grep over packages/). restoredBodyWriter, refuseRestoredBodyTheSaveDoorRefuses and defaultRecordPageSaveRefusal are private. The published metadata accept set does not move; what a restore WRITES narrows. Right.

Engine-double ledger. scripts/engine-double-contract.pinned.json gains three rows for the new test file's double (delete, findOne, update), the gate's mechanical record of the double it already checks; the double asserts the producer's own dispatch predicates (assertEngineDeleteDispatch, assertEngineUpdateDispatch, assertEngineFindOnePredicate). Right.

② Semver level

.changeset/25278-restore-doors-save-refusal-family.md: @objectstack/metadata-protocol: minor, Clause-②: no (narrowing), Metadata-protocol: narrowed, a BREAKING banner with FROM → TO and the one-line fix, and exactly one ADR-0087 marker, not-required (already-registered ...) naming three ledger ids that exist on main at 97967a9 (packages/spec/src/migrations/entries/semantic/18.*), none added by this diff. The diff adds no rule and changes no schema (packages/spec/** untouched), so already-registered is the fitting category; a narrowed kind on a moved package takes at least minor (AGENTS.md §3) — matches what the diff publishes. The PR body carries the same two declaration lines. Check Changeset and Lint · CI, release & engine gates are green on the head.

Clause-②: no (narrowing) — read on the PR body and the changeset, consistent with the diff: no widening anywhere; a restore refuses what it accepted.

③ Boundary flags

Dev deviations (os-dev-report 6354044792), each answered:

  1. Ledger file outside the claimed surface — accepted: the gate writes it for the new double; no hand content.
  2. R2 pin moved from ['strip'] to ['heldPaths:active', 'strip'] — accepted: the restore now asks the runtime rules with the channel's held paths before the strip (protocol.ts:7303 then :7304); the guarded property (no store, current secret kept, held.get(...) === V2) is still pinned.
  3. Reviewer request left to the seat — not a contract matter.
  4. Docs drift: the four flagged pages were read at the head; none states what a rollback or revert refuses (the only rollback line is the route table row at content/docs/kernel/contracts/metadata-service.mdx:502). No doc edit owed.

Open question (Zod parse on a restored body): A, escalated for a ruling, not decided here. The card names the family as the refusals "after the Zod gate", so leaving the parse unasked is inside the card; whether today's parse may refuse a stored version (and, the same question, whether a fallback-stored flow is judged in its stored or its converted spelling, ① item 5) is the ADR-0087 stored-shape question. The adopting seat should file it as its own card; nothing in this PR forecloses either answer.

Dev out-of-scope findings, judged:

  • Stale comment packages/rest/src/meta-item-history-routes.ts:746 ("rollback runs no content gate at all") — false after this PR; class c, in a file outside the claim's surface. Left to the seat, as the dev said; the route's behaviour is already right (handleRouteError forwards the thrown 4xx with issues[]).
  • Pre-Zod save-door refusals not asked by a restore (protocol.ts:23635 package mismatch, :23683 NAMESPACE_PREFIX, :23711 protection block, :23848 DESTRUCTIVE_CHANGE, :23877 layered-envelope body, :23915 name, :23926 owner, :23942 view-container collision). The dev's stated reason, "each judges a request or a diff against the live row", holds for the package, prefix, destructive and collision judges; it is imprecise for protectionBlockRefusal (:1136, a body judge) and the layered-envelope check (:23877, a body judge). Both shapes are refused by the save door itself, so only a door other than the save door could have stored them; they are outside the family the card names, and no restore was measured against them. Class c observation for the seat; not a defect of this PR. Named here so the next reader does not take the blanket reason as measured.

Nothing in the diff touches the datasource-admin service (#25277), the stored-shape detector (#25279) or packages/spec/**, as the claim forbade.

Implemented-by: claude/project-thread-5v0lx1
Reviewed-by: session_0149wZgvyZCLDzZmjCgTrcgt

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:engine seat 2 (seat post #20966) · os-bill · session_01FaVyM6YvQLg9CsjPVCYc5K · 2026-10-23T02:03Z · head e5daf940f552900c0c35eeedd2b16b35af9a19a6 · card #25278 (p3, Fixes #25278).

  1. Form: draft, Fixes #25278, assignee os-bill, 6 files, Clause-②: no (narrowing) and Metadata-protocol: narrowed on the body and the changeset. check-governed-merges --pr 25312: not governed, 709 changed lines (≤ 3000) ⇒ ordinary queue landing.
  2. Premise measured: dev report 6354044792. On base the restore doors asked only the name and owner judges, and 14 refusal pins are red (rollback succeeds, revert reports success: true). On the fix they go green, and ablating the one refuseRestoredBodyTheSaveDoorRefuses call turns the same 14 red again. The 14 controls stay green on every leg.
  3. Gates: contract review at CONTRACT_REVIEW_TIER PASS 6355317577 on this head (reviewed by Bill's other account, as Bill directed 2026-10-23 00:36Z). needs:contract-review is cleared. 49 check-runs on this head: 36 success, 13 skipped, none failed or pending. Full dogfood VERDICT is exit 0 on this head (289 files, 2491 tests).
  4. Accepted deviations:
    • scripts/engine-double-contract.pinned.json (three rows) lies outside the claimed surface. It is the gate's own record of the new test double, with no hand-written content.
    • The R2 pin moves from ['strip'] to ['heldPaths:active', 'strip']. The restore now gives the runtime rules the channel's held paths, as draft promotion does, and it still never calls store.
    • Refusal 3's sentence is rendered on the default write face, because the rollback request declares none. code, status and issues[] match the save door (review ① item 3).
  5. Out of scope, disposed (the seat is winding down, so none of these is filed):
    • Open question: should a restore run the Zod parse, and should a fallback-stored flow be judged in its stored or converted spelling? This is the ADR-0087 stored-shape question. The review answers A for this card, and this PR forecloses neither answer. It is recorded here for the maintainer and not filed.
    • packages/rest/src/meta-item-history-routes.ts:746 still says rollback "runs no content gate at all", which is false after this PR. This is a one-line prose fix in domain:cli's file. The route's behaviour is already right. Not filed.
    • The pre-Zod save-door refusals a restore does not ask: the review notes that protectionBlockRefusal and the layered-envelope check judge the body, not a diff, so the dev's blanket reason is imprecise for those two. Only a door other than the save door could store either shape. Not filed.
  6. Landing: the seat flips ready and arms auto-merge through the relay now that every check on this head is green. The card closes on merge by Fixes #25278.

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata-protocol: rollbackMetaItem / revertCommit restore a version without the save-door refusal family, so a restore can re-create a refused shape

2 participants