Skip to content

fix(metadata-protocol): the per-item rollback restores the row the read serves, so a .custom pair's override rolls back beside a base-bound row (#29848) - #30268

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-4jj4ed
Nov 5, 2026
Merged

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

维护者速读

  • 改了什么: 单项回滚(POST /api/v1/meta/:type/:name/rollback)现在回滚「用户看到的那一行」:定制包(BASE.custom)里声明了覆盖的行,即使旁边还躺着一条关停前写进只读基包的旧行,也能正常回滚,旧行不动。
  • 为什么改: 旧逻辑按「存储里第一条同名行」取包,常常取到基包旧行,于是被「点名基包写入已关闭」拒绝(403),报错还指向用户根本没打算碰的行;在 os migrate packageless-overlays 跑之前,这一项的历史回滚等于废了。
  • 风险与回滚: 不加参数、不改错误码与状态;只有同名多行时取行结果会变(取被服务的那一行)。只有基包旧行被服务时仍照旧拒绝。回滚即撤 PR。
  • 先看: packages/metadata-protocol/src/protocol-package-commits.ts(rollbackMetaItemUnaudited 里取包的那一段与 restoreVersion 的 packageId),packages/objectql/src/protocol-packageless-overlay-migration.test.ts(S5 pin 按新寻址重判)。
  • 两分钟验证: pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.named-base-overlay-write.test.ts -t 29848:三条全绿;定制行回滚成功、基包行原样,删掉定制行后回滚基包行仍是 403 NOT_OVERRIDABLE。
  • 需要你做的: 无,席位落地。

Fixes #29848

Clause-②: no — the door's accept set does not change (no new parameter, no new value); the package is derived from the row the by-name read serves. ADR-0146 D2: an item of the pair that declares overrides "replaces the base's item whole for every read that serves the base's name", and its revert ("Restore package original" = delete the overriding item from .custom, so the base's document is served again) is the pair's own row's; neither ever addressed the base row. The 403 refused the pair's own row because the binding read named the base row, a refusal those texts already deny.
Metadata-protocol: none

What changes

packages/metadata-protocol/src/protocol-package-commits.ts (rollbackMetaItemUnaudited), two lines of behaviour:

  1. The row the rollback addresses is the row the read serves. The package is now findServedOverlayRow at packagelessReadAddress with servedOverride: true: the same resolution getMetaItem naming no package uses (ADR-0146 D8 item 2, the pair's declared override first, isServedOverrideRowOf the one predicate). Before, resolveOverlayPackageBinding answered the first active row of the name in store order. When no row is served (a row only another package binds under the name), the binding read answers as before.
  2. restoreVersion restores into that row (packageId: rollbackPackageId, the parameter PR fix(metadata-protocol)!: a package commit's plan and revert address the committing package's own row; a deleted row's restore asks the package-less close #29797 added for the commit doors). Before, the restore's active-row read was package-agnostic too, so even with the right binding the parent hash and the write could land on the base row.

Every gate after the binding read is unchanged and now reads the served row's package: the package-less close, the named-base close, restoreWriteFor (the pair's row is a runtime-only write judged by the override write gate, ADR-0146 D2), the registry write-through. protocol.ts gains two docblock lines on rollbackMetaItem. No new parameter, no new error code, no spec change.

Tests

packages/metadata-protocol/src/protocol.named-base-overlay-write.test.ts, new top-level describe [#29848] over the file's existing harness:

  • Red first, the measured two-row state. A row bound to the base stored first, then the pair's declared override saved twice. On 5fa9375e the rollback to the pair's first version answered 403 NOT_OVERRIDABLE with the named-base sentence ("a rollback of its row bound to the read-only base is closed"), the card's reading. Now: success, the pair row holds PairFirst with its overrides declaration, the base row is untouched, and both the read naming no package and the read naming the base serve PairFirst.
  • The pair's rollback to the base row's version (a body that declares no override) is the override write gate's 403 NOT_OVERRIDABLE, not the named-base close's; nothing is written.
  • The named-base close still holds. Once the pair's row is deleted, the read serves the base row again, and its rollback is refused with the close's envelope (status 403, code NOT_OVERRIDABLE, the sentence naming the pair); the row is unchanged.

packages/objectql/src/protocol-packageless-overlay-migration.test.ts, the shadowed (S5) case re-judged: it pinned "the rollback's row is the first active row of the name in store order", the package-less row, while the read served the pair's declared row beside it. That is this card's defect on a package-less row instead of a base-bound one. The rollback now addresses the served pair row; its version 1 is the package-less row's undeclared body, so the D2 gate refuses it and both rows stay as they were. The save and DELETE halves of the pin are unchanged; the final read now serves the package-less row's current body, since it is no longer rolled back.

Ablations

scripts/ablation-replace.mjs (wrap mode), each leg restored with blob == HEAD (35ffb243) and git status --porcelain empty.

# Mutation Result
A1 the served-row derivation off (rollbackPackageId back to the binding read) 2 / 34 red: the two-row rollback, the D2-gate pin
A2 restoreVersion without packageId 1 / 34 red: the two-row rollback

Acceptance notes

Verification

Head abd5d69f (base 5fa9375e, origin/main 771b2db2 merged in once, no conflict, no rebase). The merge brought 8 commits (core dispatch loop, objectql lifecycle service, the spec i18n resolver, ui); none touches this diff's files. The affected closure was rebuilt after the merge under the shared lock.

  • pnpm --filter @objectstack/metadata-protocol test (pre-merge head 2d469b09): 312 files passed (4 skipped), 22 357 tests passed (26 skipped). typecheck: exit 0 pre- and post-merge (--listFiles includes the new test file).
  • pnpm --filter @objectstack/objectql test (pre-merge head 2d469b09): 548 / 548 files, 10 863 / 10 863 tests.
  • Post-merge, scoped (§10): protocol.named-base-overlay-write.test.ts 34 / 34; objectql protocol-packageless-overlay-migration.test.ts + protocol-publish-rollback.test.ts 40 / 40; pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date.
  • pnpm lint: exit 0 (pre-merge head 2d469b09; the merge adds no lintable change of this PR's).
  • Red on 5fa9375e: the two-row pin answered the named-base sentence, 403 NOT_OVERRIDABLE; the D2-gate pin read that same sentence.
Gates: dispatch-gates --commands, each exit code captured before any pipe

61 derived families (head 2d469b09; the list derived at abd5d69f is byte-identical): 60 exit 0, 1 NOT MEASURED. dispatch-gates --ran: exit 0, "61 derived famil(ies) accounted for — 60 run, 1 NOT-MEASURED".

  • NOT MEASURED: pnpm check:dual-build-cjs-loads exit 3, "Run pnpm build first. This is NOT a pass: nothing was measured" (70 packages outside this diff's closure have no dist/); left to CI.
  • pnpm check:lean-entry-closure first exited 3 (no packages/objectql/dist); after turbo run build --filter=@objectstack/objectql it exits 0 ("Admitted set held exactly (15 packages)").
  • node scripts/check-plugin-teardown-shape.mjs --self-test ran after git fetch origin 621a4876… (the pinned fixture commit): exit 0.
  • Re-run on the merged head abd5d69f: check:type-check-coverage, check:type-check-debt, check:durability-log-level, check:nul-bytes, check:cross-package-test-inputs, check-empty-changeset, check-adr-0087-registration, check-changeset-no-major: all exit 0.

Change set: 5 paths, +134 / −8 against 5fa9375e (two source files +33 / −2, two test files +93 / −6, one changeset +8).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 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 maintainer brief is Chinese by the seat's convention).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Utp6CXdBvpJrhnHyW825Hb


Generated by Claude Code

…tom pair's row beside a base-bound row

Claude-Session: https://claude.ai/code/session_01Utp6CXdBvpJrhnHyW825Hb
Co-authored-by: Claude <noreply@anthropic.com>
…he by-name read serves

A .custom pair's declared override beside a row bound to its read-only base
was addressed through a package-agnostic binding read, so its rollback
restored (and was refused on) the base row. The door now derives the package
from the served row (findServedOverlayRow at packagelessReadAddress, the
declared override first) and hands it to restoreVersion.

Claude-Session: https://claude.ai/code/session_01Utp6CXdBvpJrhnHyW825Hb
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 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

2 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), getOverlayRepo (literal, a string literal in rollbackMetaItemUnaudited))
  • content/docs/kernel/cluster.mdx (via applyRegistryWriteThrough (literal, a string literal in rollbackMetaItemUnaudited))

⛔ 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 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 31260ca8e446fa4fbe94ff8fd2a9030ddfb53a9a → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 31260ca8e446fa4fbe94ff8fd2a9030ddfb53a9a

⚠️ 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 31260ca8e446fa4fbe94ff8fd2a9030ddfb53a9a → 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#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

席位核对 · ACCEPT · head abd5d69f · 2026-11-05T04:19Z

Head-sha: abd5d69

Item Reading
Shape Draft, base main, relay pr_create. Body: attribution, signature, ## 维护者速读, Fixes #29848; Clause-②: / Metadata-protocol: at line start. Report 6649552076 (status done).
Door choice (seat's order) Package derived from the served row (findServedOverlayRow at packagelessReadAddress, servedOverride: true, the read getMetaItem uses), with the binding read as fallback when nothing is served; no ?package=. Measured: in the two-row state the served row names the pair's row uniquely, so needs_decision did not trigger. restoreVersion receives that package (#29797's parameter).
Pins (card) Red first on 5fa9375e (403 named-base sentence); now the pair row rolls back, the base row is untouched, and both reads serve the restored body. The pair's rollback to an undeclared body is the D2 gate's 403. Once the pair row is deleted, the base rollback is still refused with the close's envelope (status 403, code NOT_OVERRIDABLE).
Accepted re-judge objectql S5 (shadowed: a package-less row plus the pair's declared row). The old pin rolled back the package-less row the read did not serve, which is this card's defect on a package-less row. The rollback now addresses the served row, whose v1 is undeclared, so D2 refuses it and both rows stay as they were. The door's accept set is unchanged, so Clause-②: no stands; the refusal comes from the existing D2 gate on the row the user sees.
Gates Dev: derived 61, 60 green, 1 left to CI (dual-build-cjs-loads); pnpm lint green; metadata-protocol 22357/22357, objectql 10863/10863. CI on abd5d69f running.
Declarations Clause-②: no, citing ADR-0146 D2 and its revert text. Metadata-protocol: none; changeset patch.
Docs Read concepts/metadata-lifecycle.mdx and the /meta rollback mentions (references/api/protocol.mdx, permissions/system-context.mdx): none says which row a by-name rollback addresses, so none is made false.
Serial #30226 is not landed; this PR does not touch its two files (protocol-packaged-base-refusal.ts, protocol-package-bindings.ts).
Landing Not governed (0 of 5 paths), 142 changed lines. Seat lands: pr_ready + automerge_enable.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 04:20
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 04:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 713beba Nov 5, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-4jj4ed branch November 5, 2026 05:11
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