Skip to content

fix(metadata-protocol)!: a commit revert or package rollback that would restore a row bound to the read-only base is refused per item, naming the .custom pair - #29632

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-gvqm55
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-gvqm55

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Requested by Steve · project thread
Fixes #29390

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none needed — skills/objectstack-platform/SKILL.md places every customization of an installed base in its paired .custom, declaring overrides, and says nothing about reverting a commit recorded against the base itself; nothing it says becomes false.

维护者速读

  • 改了什么:按提交撤销(POST /packages/:id/commits/:commitId/revert)和整包回滚(POST /packages/:id/rollback)时,若要把旧版本恢复进"绑定只读基础包本身"的那一行(五种可覆盖类型),现在逐项拒绝:该项进 failed[],code: NOT_OVERRIDABLE,提示与逐项回滚完全同一句(改写进 .custom 配对包并声明 overrides),不写任何东西;同一提交的其它项照常撤销。
  • 为什么改:metadata-protocol: a save naming a read-only code package itself (?package=<base>) for an overlay type is still accepted and lands an overlay bound to the base #28905 已关掉对这一行的保存、发布和逐项回滚,但按提交撤销绕过了它。实测同一行逐项回滚答 403,按提交撤销却答 200 并改写它;整包回滚每次都把 metadata-protocol: a save naming a read-only code package itself (?package=<base>) for an overlay type is still accepted and lands an overlay bound to the base #28905 之前的提交重放一遍。
  • 风险与回滚:只收窄恢复路径,不动存量行;撤销"新建该行"的提交(即删除这行)保持开放,删除命名基础包也照旧开放。回滚即撤销本 PR。另知:配对包提交若与旧基础行并存,因提交计划按任意包读行(席位另立卡),其撤销现在会被拒而不是静默"成功"。
  • 先看:packages/metadata-protocol/src/protocol.ts 中 revertCommit 恢复分支新增的判定(restoreVersion 之前)、protocol.named-base-overlay-write.test.ts 末尾新增的 describe、package-commit-revert-path-package.dogfood.test.ts 头注释与最后一个用例。
  • 两分钟验证:cd packages/metadata-protocol && npx vitest run src/protocol.named-base-overlay-write.test.ts,应见 22 个用例全部通过(其中 5 个为本卡新增)。
  • 需要你做的:无,席位落地。

What changes

@objectstack/metadata-protocol — revertCommit's restore limb (and rollbackToPackageCommit, which reverts through it) now asks the #28905 close before repo.restoreVersion: namedBaseOverlayWriteRefusal, on the binding the restore writes (resolveOverlayPackageBinding's answer, the key rollbackMetaItem already uses), with the rollback arm of the same sentence (closedOverlayWriteClauses, no second spelling).

  • Closed, per item: a commit item whose restore would land in a row bound to the read-only base itself, on one of the five overlay types that base ships, where the pair fix can be taken (namedBaseCloseSubject, unchanged). The item lands in failed[] with code: NOT_OVERRIDABLE and the sentence; nothing is written; the revert answers 200 with success: false; the commit's other items still revert and the revert commit is still recorded. rollbackToPackageCommit reports each refused item (Item 'TYPE/NAME' was not reverted: …) and continues.
  • Kept open: the soft-remove limb (the revert of an item the commit created removes the row bound to the base, which writes nothing, as a delete naming the base does); every restore into a row of the pair, a writable package or no package; every restore the pair could not take.
  • Seat ruling (record 6579511992): option A, key A1, per-item channel.
  • Where the lines live: all of them stay in protocol.ts, in revertCommit's class body (the [maintainer] refactor(metadata-protocol): split src/protocol.ts (33,065 lines, +10,169 in 30 days) along its ObjectStackProtocolImplementation seams, one move-only slice per PR #25467 E2 section 1 split moved readItemFromMetadataService through getUiView, not revertCommit). They cannot move into protocol-packaged-base-refusal.ts without a second spelling: the restore limb's only calls before repo.restoreVersion are resolveOverlayPackageBinding (also read by the cluster write-through replay and by rollbackMetaItem, which asks the package-less close first) and SysMetadataRepository.restoreVersion (the repository layer, no registry, shared with rollbackMetaItem).

The sentence an item carries, for view/showcase_task.default on the showcase:

'view/showcase_task.default' ships in 'com.example.showcase', and a rollback of its row bound to the read-only base is closed (ADR-0146 D1, D11): save the version's body into 'com.example.showcase.custom' (?package=com.example.showcase.custom), declaring overrides: { package: 'com.example.showcase', basedOn }. The row is served until the pair declares the override, or a delete naming the base removes it.

Measured before the change (origin/main 7f277a8, booted showcase, REST)

Commits recorded before the close were produced through the real save and publish-drafts doors with the close shadowed on the running protocol instance only, then the close restored (a save naming the base answered 403 again).

Door, on the row bound to com.example.showcase Before After
per-item rollback POST /meta/view/X/rollback 403 NOT_OVERRIDABLE unchanged
revert of the commit that edited the row 200 success: true, restored; row rewritten (L3 to L2), served at both addresses 200 success: false, failed[0].code NOT_OVERRIDABLE; row and served document unchanged
package rollback to the first commit 200 success: true; every pre-close apply commit replayed (one reverted twice) 200 success: false, each refused item in failed[]; row unchanged
revert of the commit that created the row 200 removed; base document served unchanged (open)

Full readings, including which commits can carry such a row: only an apply commit the base recorded before the close whose item edited an existing row (creation items take the soft-remove limb; revert commits record prevVersion: null; post-close publishes into the base are refused by promoteDraftForPublish where the fix can be taken).

Tests

  • packages/metadata-protocol/src/protocol.named-base-overlay-write.test.ts, new describe "the commit doors: restoring a row bound to the base is closed, removing one stays open (metadata-protocol: a commit revert or package-commit rollback can restore a row bound to a read-only base package that the #28905 close now refuses to write #29390)" (5 cases, the file's engine double reused, so the engine-double ledger is untouched). Pre-close commits come from saveMetaItem + publishPackageDrafts with the close shadowed on the instance only:
    • the revert of the base's commit that edited its row is refused per item naming the pair, nothing written (red first);
    • a package rollback through those commits reports each refused item and continues, the row unchanged (red first);
    • CONTROL: the revert of the base's commit that created its row removes it, the base served again;
    • CONTROL: a writable package's commit of the same item reverts its row in place;
    • CONTROL: the pair's own commit (no row bound to the base) reverts its row in place.
  • packages/qa/dogfood/test/package-commit-revert-path-package.dogfood.test.ts, new last case: on the booted showcase through REST, the showcase's commits recorded before the close: the edited item's revert and the package rollback are refused naming the pair, the served document unchanged; the created item's revert still removes the row and the shipped document is served again.
  • Forced edit (prose only): the same dogfood file's header said a commit recorded before the close "keeps its revert, as the commit doors still take no read-only guard". That is now true only for the soft-remove limb, so the header says so and names the new case (seat ruling, item 1). No assertion changed.
Ablation (drop the close from the restore limb)

scripts/ablation-replace.mjs replaced if (namedBase) throw namedBase; before repo.restoreVersion in the restore limb (anchor x1 to x0, blob 19bf7c8f to 15c51b71), held under an EXIT trap.

Verification on `a15f252f`

All on a15f252f (this branch after merging origin/main 0ab4a09e, which carries the #25467 E2 section 1 split and the #29451 dogfood fix), in a card-thread container; heavy runs under scripts/pm/os-verify-lock.sh.

  • pnpm install --frozen-lockfile, then the closure pnpm --filter '@objectstack/dogfood^...' build (with declarations): VERDICT command-exit 0 each.
  • src/protocol.named-base-overlay-write.test.ts: Tests 22 passed (22).
  • pnpm --filter @objectstack/metadata-protocol test: Test Files 308 passed, 4 skipped (312); Tests 22251 passed, 26 skipped (22277); VERDICT command-exit 0.
  • pnpm --filter @objectstack/metadata-protocol typecheck: VERDICT command-exit 0.
  • pnpm --filter @objectstack/objectql test (its revert pins included: rollbackMetaItem / revertCommit always 409 on a package-bound overlay row — restoreVersion's put() scopes the parent lookup to package_id IS NULL #6215's package-bound revert in place, the email-locale stage-2 batch doors): Test Files 540 passed (540); Tests 10488 passed (10488); VERDICT command-exit 0.
  • pnpm --filter @objectstack/dogfood typecheck: VERDICT command-exit 0.
  • test/package-commit-revert-path-package.dogfood.test.ts: Tests 9 passed (9) (test(dogfood): pin that an uninstalled package's commit is refused through its own revert and rollback path #29450's case, merged from main, included).
  • Full dogfood battery, pnpm --filter @objectstack/dogfood test (no shard, no path): Test Files 398 passed, 3 skipped (401); Tests 3281 passed, 28 skipped (3309); VERDICT command-exit 0.
  • All packages built (turbo run build --filter='./packages/**', 78/78), then node scripts/pm/dispatch-gates.mjs --commands (no paths): 65 families, all 65 run, every exit 0; --ran reconciliation: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN.
  • pnpm lint (eslint . --no-inline-config): exit 0.
  • Earlier heads: on ecf913ed and d776e258 the same suites, gates and lint were green, and the battery had 4 reds in flow-runas.dogfood.test.ts (3) and declarative-endpoint-anonymous-guest.dogfood.test.ts (1), red too with this PR's src files restored to origin/main (control leg); test(dogfood): re-point the runAs and anonymous-guest flow pins to flow start admission's record rule #29451 on main fixed them, and they are green here.

Acceptance notes

  • Out of scope, filed by the seat as its own card (serial after this one): a pair commit's revert beside a row bound to the base stored before the close is a silent no-op today, because publishPackageDrafts' revert plan and the restore limb both read any package's active row. With this change (key A1) that revert is refused naming the pair instead of answering success over nothing. The plan's prevVersion is not changed here (seat ruling).
  • packages/runtime/src/domains/packages.ts (the REST revert door's comment, outside this card's file surface) says undoing a read-only package's overlay commit "stays open": still true of the door (no package-level guard) and of the soft-remove limb; an item-level refusal now applies to the restore limb. Left for the seat to word if wanted.
  • The restore limb does not ask the metadata-protocol: close the package-less write onto a packaged item (PUT/DELETE, rollback, source save, reset) naming <base>.custom; Studio drops its overlay link #28576 package-less close; a package-less row is reached there only through the any-package lookup above (same family as the seat's new card).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 section is the required Chinese digest).
  • The Before Submitting list in CONTRIBUTING.md is done.

Card thread session session_01ADHkmTKKCrTrtA7H6oSj28, seat domain:server#3.


Generated by Claude Code

claude and others added 5 commits November 2, 2026 14:26
… read-only base is refused per item, naming the pair

revertCommit's restore limb (and rollbackToPackageCommit through it) now
asks the named-base close on the binding the restore writes, as
rollbackMetaItem does, with the same rollback sentence. The item lands in
failed[] (NOT_OVERRIDABLE) with nothing written; the soft-remove limb stays
open.

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

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 3 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/metadata-protocol/src/protocol-packaged-base-refusal.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

⛔ 3 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/v18/18-1.mdx (via revertCommit (symbol, a method of class ObjectStackProtocolImplementation))

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
  • 1 changed file(s) yielded no anchor (packages/metadata-protocol/src/protocol-packaged-base-refusal.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 10 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 4008e6290f4812b1c18a42ec2589b8a2733cf6c3 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from fa9f0d69a1d2fb69324e8122a04581bb9b8c31e8 — the merge of head 85261a810dff62b530193fbe92e7b607158a76d3 into base 4008e6290f4812b1c18a42ec2589b8a2733cf6c3, 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 fa9f0d69a1d2fb69324e8122a04581bb9b8c31e8 && git checkout fa9f0d69a1d2fb69324e8122a04581bb9b8c31e8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4008e6290f4812b1c18a42ec2589b8a2733cf6c3 85261a810dff62b530193fbe92e7b607158a76d3 && git checkout -B drift-repro 4008e6290f4812b1c18a42ec2589b8a2733cf6c3 && git merge --no-ff 85261a810dff62b530193fbe92e7b607158a76d3

node scripts/docs-audit/affected-docs.mjs --json 4008e6290f4812b1c18a42ec2589b8a2733cf6c3

⚠️ 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 4008e6290f4812b1c18a42ec2589b8a2733cf6c3 → 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

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Contract-review-claim: session_01ADHkmQJdgarUjPLNgFBY67 · head a15f252 · 2026-11-02T22:45Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a15f252f6ae1bad0978ae63469a9b13f29caf712
Local-runs: none
Scope: packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol-packaged-base-refusal.ts, packages/metadata-protocol/src/protocol.named-base-overlay-write.test.ts, packages/qa/dogfood/test/package-commit-revert-path-package.dogfood.test.ts + packages/metadata-protocol/src/protocol-stored-row-bindings.ts (resolveOverlayPackageBinding), packages/metadata-protocol/src/sys-metadata-repository.ts (restoreVersion, whereFor), packages/metadata-protocol/src/protocol-package-commit-record.ts (recordPackageCommit), packages/runtime/src/domains/packages.ts (the commit revert, rollback and package revert doors), packages/spec/src/migrations/entries/semantic/18.packageless-overlay-write-closed.ts, packages/spec/src/api/error-code-ledger.zod.ts (NOT_OVERRIDABLE), docs/adr/0146 D1 and D11, docs/adr/0067 D3, .changeset/28905-named-base-overlay-write-closed.md (the #28905 close, PR #29270 at 838a802), protocol.ts rollbackMetaItemUnaudited and the publishPackageDrafts commit-plan read (lines 17501-17512 on the head)

① Derived judgments

Read on the head's tree, git diff origin/main...pr29632 (fa07650...a15f252), 5 files, +246/-12.

  • Which items now refuse — right. Only the restore limb of revertCommit (it.existedBefore && it.prevVersion !== null), and only when namedBaseCloseSubject answers a pair: the row's binding (resolveOverlayPackageBinding) is a READ-ONLY package among overlayBaseShippers(type, name) (so one of the five overlay types, else []), the pair id forms, the ensure door would not refuse, the base records a version, and the override write gate admits the declared body. Every other restore stays as on main: the soft-remove limb (!it.existedBefore, never reaches the new lines), a row of the pair (the pair is not a shipper of the name), a writable package, a package-less row (binding null returns before any read), and every fix the pair cannot take. Matches the seat ruling (A, A1, per item) exactly.
  • Key checked = key written — right. resolveOverlayPackageBinding(it.type, it.name, member) reads sys_metadata with {type, name, state:'active', member-where} and returns that row's package_id; SysMetadataRepository.restoreVersion reads its active row with whereFor(ref, 'active', undefined, member), the same four predicates, and threads that row's package_id into put. Same table, same predicate shape, same member handling; the stored spelling (it.type) is used for both row reads and the singular (PLURAL_TO_SINGULAR[it.type] ?? it.type) only for the registry-side subject, exactly as rollbackMetaItemUnaudited (binding read on singularType, subject on singularType). Where the two rows disagree at all is the any-package coin toss both share (card metadata-protocol: a pair commit's revert beside a pre-close base-bound row answers success while writing nothing, because the commit plan and the restore both read any package's active row #29414), not a key mismatch introduced here.
  • One predicate, one sentence — right. The door calls the class method namedBaseOverlayWriteRefusal(subject, 'rollback'), which is namedBaseCloseSubject + the module's namedBaseOverlayWriteRefusal emitter; the 'rollback' arm's text ("a rollback of its row bound to the read-only base is closed (ADR-0146 D1, D11): save the version's body into PAIR, declaring overrides ...") is the one rollbackMetaItem already answers. No new string, no new code, no new status. NamedBaseWriteSubject.member?: string accepts ...memberOption(member); item absent means the subject is judged on {name, member-discriminator} + overrides, as the per-item rollback is judged.
  • A refused item writes nothing — right. The throw sits after the binding read and before repo.restoreVersion, inside the existing per-item try: no put, no history event, no applyRegistryWriteThrough, no projector, no mutation emit. The catch maps it with clientFacingFailureCode: NOT_OVERRIDABLE is a member of ErrorCode (error-code-ledger.zod.ts, lines 690/1241/1295), so the code passes through untouched, and clientFacingFailureText keeps a 403 authored sentence whole. The item lands in failed[] with {type, name, error, code:'NOT_OVERRIDABLE'}; the loop continues to the next item; success is failed.length === 0 && reverted.length > 0. This is the channel VERSION_NOT_FOUND, ITEM_LOCKED, STORED_TYPE_NOT_CANONICAL (finding: revertCommit has no STORED_TYPE_NOT_CANONICAL pre-flight, so a non-canonical stored item degrades to a console.warn where the sibling doors give a wire-visible code #9174) and the metadata-protocol: rollbackMetaItem / revertCommit restore a version without the save-door refusal family, so a restore can re-create a refused shape #25278 family already ride; the REST door (packages.ts 2421-2442) wraps the result in deps.success(result) whatever success says, so the wire shape is 200 with success:false as before for any per-item failure.
  • Revert commit still recorded — right, with one shape to know. recordPackageCommit is called unconditionally with items: reverted.map(...); the refused item is absent from it, so the revert commit never claims it. For a commit whose ONLY item is refused, a zero-item revert commit is recorded (the host function has no empty-items guard) and rollbackToPackageCommit still lists it in revertedCommits — the behaviour the rollback docblock already declares ("the step still ... records its revert commit, so that commit is also listed") and the behaviour STORED_TYPE_NOT_CANONICAL already produces on main. Not changed by this PR; named so the ledger shape is known.
  • rollbackToPackageCommit on a refused item — report and continue, right; no loop, no double revert, no worse partial state. Its loop filters apply commits strictly newer than the target, skips revert commits, calls revertCommit once per commit and copies each step.failed[] entry as Item 'TYPE/NAME' was not reverted: ...; success is failed.length === 0. A refused item leaves the apply commit in place, so a second rollback re-refuses it (idempotent, finite). The per-commit mixed outcome (neighbours reverted, the base-bound row kept at its current version and still served) is the same partial shape every per-item refusal of this channel already leaves, and it replaces a write the published spec forbids; before the change every package rollback replayed the pre-close commits onto the base-bound row (measured by the dev, 7f277a8), which was the worse state.
  • protocol-packaged-base-refusal.ts — JSDoc only. The +8/-5 is inside the docblock of NamedBaseWriteOperation (the 'rollback' operation is now described as every restore of a stored version, the per-item rollback and the commit revert's restore limb). The type alias, both emitters, closedOverlayWriteClauses, namedBaseCloseSubject and packagelessCloseSubject are byte-unchanged, so no caller's answer moves.
  • The dev's flagged consequence (a pair commit beside a pre-close base row) — a correct refusal of the write that would have happened, not a new false refusal of a correct one. The plan read (publishPackageDrafts, head lines 17501-17512) and the restore both read ANY package's active row; when that read lands on the base-bound row, the write the restore would perform is the base-bound row rewritten with the base's own snapshot — exactly the write metadata-protocol: a save naming a read-only code package itself (?package=<base>) for an overlay type is still accepted and lands an overlay bound to the base #28905 closes — so refusing it under the same key is right, and it replaces a success:true over nothing written (measured). When the read lands on the pair's row the restore proceeds as before. The addressing defect is the real bug and is filed as metadata-protocol: a pair commit's revert beside a pre-close base-bound row answers success while writing nothing, because the commit plan and the restore both read any package's active row #29414 (open, ruled serial after this card). The sentence names the pair as the destination, which for an operator already in the pair reads oddly but is loud and actionable, and no previously correct outcome is lost.
  • Tests. The five unit cases drive the real saveMetaItem + publishPackageDrafts + revertCommit + rollbackToPackageCommit over the file's existing stub engine, shadowing the instance's close only while producing the pre-close commits and asserting the close is live again before the revert; fake Date only, released in afterEach. The dogfood case does the same through REST on the booted showcase, runs last, and ends by reverting the creation commit so the shipped document is served again (the preceding pair case removes its own pair row, so no declared override masks the base-bound row). Controls cover the soft-remove limb, the writable package (rollbackMetaItem / revertCommit always 409 on a package-bound overlay row — restoreVersion's put() scopes the parent lookup to package_id IS NULL #6215 shape) and the pair's own commit.

② Semver level

  • Package moved: @objectstack/metadata-protocol only (no packages/spec path in the file list). Changeset .changeset/29390-named-base-commit-revert-closed.md bumps that package patch.
  • Declaration lines, both in the PR body and the changeset, line-initial and exactly once each (grep on the fetched body and on git show pr29632:.changeset/...): the ② key reads
    Clause-②: no (narrowing)
    and Metadata-protocol: none. One occurrence of the key in each document, so the level axis is MEASURED. The form is the one AGENTS.md admits (no plus the (narrowing) arm, which is BREAKING), so: ! on the PR title and the changeset title (both present), a BREAKING banner (present, "write narrowing at the commit doors, shipped as patch"), FROM → TO migration (present: the revert now answers per-item refusal; TO is the declared write into the pair, or the delete naming the base), and one adr-0087: marker.
  • Level: none on the metadata-protocol axis takes patch ("a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition"). none is right: what an author can write is unchanged against main; the five-type flag already stopped opening the write naming the base in metadata-protocol: a save naming a read-only code package itself (?package=<base>) for an overlay type is still accepted and lands an overlay bound to the base #28905's changeset (which declared narrowed and minor for that reason). This diff closes a residual door on the same rows and introduces no key, value or requirement change. Consistent with the seat's claim.
  • ADR-0087 marker: not-required (already-registered packageless-overlay-write-closed). The id resolves at HEAD and at the merge base (origin/main:packages/spec/src/migrations/entries/semantic/18.packageless-overlay-write-closed.ts, line 19), so the category is the honest one (the gate's rule: exists at base ⇒ already-registered, never registered). Fit: the entry's replacement is this change's TO (write the customization into the base's .custom pair with the overrides declaration; os migrate packageless-overlays --apply moves bound rows too since fix(metadata-protocol): os migrate packageless-overlays also moves overlay rows bound to the read-only base itself into its .custom pair #29477), and no stored row is rewritten, so objectstack migrate meta has nothing to do. S1 (should-fix, text): the marker's why repeats fix(metadata-protocol)!: a save, publish or rollback naming a read-only base itself onto its view, dashboard, report, translation or email template is refused, naming the .custom pair #29270's sentence "The entry's surface names the package-less write only; widening its text is a packages/spec edit this change does not make" — stale on main since 1bdf674 (fix(spec): the packageless-overlay-write-closed upgrade entry names both closed overlay write addresses #29535), which widened the entry to BOTH meta-door addresses (package-less and naming the read-only package, including "a rollback of a row bound to the package"). What the entry still does not name is the commit doors (POST /api/v1/packages/:id/commits/:commitId/revert, POST /api/v1/packages/:id/rollback) as a third address of the same close. The disposition stands; the sentence should be corrected and the entry widening carded to the spec lane (text only). Not a FAIL: the category, id and TO are right and the gate (Check Changeset, dispatch-gates 65/65) reads the arm and the kind, not the prose.
  • Check Changeset on the head: completed, success (both runs).

③ Boundary flags

④ Design judgment

Implemented-by: claude/project-thread-gvqm55
Reviewed-by: session_01ADHkmQJdgarUjPLNgFBY67

VERDICT: PASS

…ntry names both meta-door addresses, not yet the commit doors

The sentence carried over from the named-base close said the
packageless-overlay-write-closed entry names the package-less write only;
1bdf674 widened it to both meta-door addresses. The commit doors are what
it does not name yet; that text widening rides the any-package lookup card.
Category and id unchanged.

Claude-Session: https://claude.ai/code/session_01ADHkmTKKCrTrtA7H6oSj28
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

席位核对 · ACCEPT · PR #29632 · 2026-11-02T23:34Z

Head-sha: 85261a810dff62b530193fbe92e7b607158a76d3

维护者速读(席位终稿)

核对六项

  1. 形态:draft → main,Fixes #29390。5 个文件,+246/−12,都在认领面内。dogfood 文件头注释有一处被迫的 prose 改动,因为 fix(runtime)!: package commit revert and rollback act only on a commit of the package their path names #29090 那句话现在只对删除分支成立,断言没改。PR 正文原有重复页脚,席位已 patch 删掉,没改别处。
  2. 前提实测:narrowing 面契约复审 PASS(6590665469,CONTRACT_REVIEW_TIER,head a15f252f,独立会话)。复审逐条核对了:被检查的键与 restoreVersion 写入的键是同一行;被拒项一字不写;同提交其它项照常撤销,撤销提交照常记录;整包回滚报告后继续,不循环、不重复撤销。复审之后只多了一个提交(a15f252f..85261a81),只改 changeset 中 adr-0087 标记的一句,修正复审 S1 指出的过时措辞,类别和 id 不变,是纯 prose。席位按 diff 核过,不另起复审。
  3. 门禁:本 head 全部 check-run 已读,36 success、13 skipped、0 失败。必需项全部 success:TypeScript Type Check、Test Core(1–8/8)、Build Core、Lint & Repo Gates、Governed Surface Queue Guard、Test UI、Live E2E UI。Lint UI、Type Check UI、Build & E2E UI 按路径跳过。Check Changeset 也是 success。PR 上的 Dogfood 分片按设计跳过,dogfood 唯一读数是 dev 在 a15f252f 上本地跑的整套:398 个文件通过、3 个跳过,3281 条测试通过,0 失败。之后只改了 changeset prose,不需重跑。dev 其他读数:metadata-protocol 22251 条通过,objectql 10488 条通过,派生门禁 65/65,0 NOT-MEASURED。消融在 ecf913ed 上做:unit 2 红,dogfood 1 红,复原。复审用 git 核实,从那个 head 到本 head 代码字节不变,三个合并提交都是干净合并。
  4. 接受的偏差:改动行全部留在 protocol.ts 的 revertCommit 里,没有移进 protocol-packaged-base-refusal.ts,因为 E2 第 1 段没有移动提交门。复审确认这样不构成第二种写法。protocol-packaged-base-refusal.ts 只改了 JSDoc。
  5. 开放问题裁定:裁定 6579511992:选项 A,键 A1,逐项通道。复审的两条 should-fix 都已安置。S1 的措辞在本 head 修正,入口文本加宽挂到 metadata-protocol: a pair commit's revert beside a pre-close base-bound row answers success while writing nothing, because the commit plan and the restore both read any package's active row #29414。S2 是恢复分支在行已删除时走无绑定插入的路径,属 main 上的既有问题,同样挂到 metadata-protocol: a pair commit's revert beside a pre-close base-bound row answers success while writing nothing, because the commit plan and the restore both read any package's active row #29414 附言。any-package 计划与恢复查找属 metadata-protocol: a pair commit's revert beside a pre-close base-bound row answers success while writing nothing, because the commit plan and the restore both read any package's active row #29414,串行在本卡之后。
  6. 落地条款:Clause-②: no (narrowing),Metadata-protocol: none,PR 正文与 changeset 都有。标题带 !,有 BREAKING 横幅,并带 adr-0087: not-required (already-registered packageless-overlay-write-closed) 标记(id 在合并基上存在)。semver:@objectstack/metadata-protocol 为 patch(非协议变更)。check-governed-merges --pr 29632:NOT governed。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Hand-off · PR #29632 · 2026-11-02T23:57Z

Seat 3 is going off duty on the maintainer's order. This PR is ACCEPTed (6591234666) on head 85261a81 and is in the merge queue. Nothing in review or code is open. Card #29390 is released to pm:queue, with the hand-off note on the card: on merge the next domain:server seat writes the landing record; on an ejection it reads the queue run first. A head change after 85261a81 needs the seat's diff check before re-queueing.

Merged via the queue into main with commit 14df37f Nov 2, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-gvqm55 branch November 2, 2026 23:59
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/m tests tooling

Projects

None yet

1 participant