Skip to content

fix(metadata)!: a sole row slot's history and rollback answer its unattributed and environment versions at every door, a history row (never the body stamp) says whose version it is, and a rollback writes its body stamped for its slot (#32717) - #33464

Queued
objectstack-fleet[bot] wants to merge 10 commits into
mainfrom
claude/area-E2-32717

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Part of #32717

Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: none — git grep -n -E "registerRollback|refuseUnattributedVersion|getHistoryRecord|queryHistory" -- skills content/docs (releases and references excluded) finds no line, and no rollback line under skills/ is about metadata history; no skill or hand-written doc teaches these doors, so no sentence there turns false.

domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw

维护者速读

  • 改了什么:按 seat 裁决 6885958183(修订 B)收口三道回滚门(manager、loader、/meta)。一个名字只有一个行槽时,未归属的旧版本和环境自己的版本都算这个槽的:点名包或不点名都能列出、diff、回滚落地,manager 的点名历史读也列出它们。同时保留“版本归属只看历史行,不看调用方 body 上的包戳”。回滚写入的 body 一律改成该槽的包戳:包槽重盖为该包,环境槽去掉外来包戳。
  • 为什么改:上一版把 loader 改成答“not found”,这和 18.4.0 已发布的说法、SysMetadataRepository 仍在写未归属行的现状、以及控制台 ResourceHistoryPage 在历史和回滚都带 ?package= 的用法都冲突,席位裁决改为放宽 manager。review#3 指出放宽后 body 上的外来包戳会被原样写入:写进环境行会把该行变成那个包的槽,写进 A 的行会以 B 的身份对外服务。本版在写入前把包戳改成槽主人的。
  • 风险与回滚:manager 门是放宽,原先报“not found”的请求现在落地写入。唯一收紧的是 loader 直调:多个行槽、未归属版本、点名某包并带该包包戳,原先写进那个包的行,现在报 409。仓内唯一调用方是 manager,它碰不到这一路。包戳重盖只在 body 包戳与槽不一致时发生。没有数据迁移,回滚即还原本 PR。
  • 先看:packages/metadata/src/loaders/database-loader.ts:rollbackSlot 的合并分支、isAddressedVersion/isSoleRowSlot、registerRollback 里的 written;packages/metadata/src/package-slotted-store.ts:stampedForSlot;packages/metadata/src/metadata-manager.ts:rollback 用 loader 写入的 body 写注册表。
  • 两分钟验证:运行 pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 src/metadata-history.test.ts,预期 302 条全绿(12 类 × 每类新增 8 条 pin)。
  • 需要你做的:无,席位落地。

What changes

ADR-0150 D3/D4, seat ruling 6885958183 (amended B), which overrides this PR's earlier "not found" rule, and review#3 (comment 6885958083) M1 and N1. For a type stored per (package, name) (PACKAGE_SLOTTED_STORE_TYPES, 12 types at this release):

  1. A sole row slot's versions (ruling items 1 and 4). The two one-holder "not found" narrowings in DatabaseLoader.registerRollback are dropped. On a name one row slot holds, a version attributed to no package and the environment's own version (M1: a package took over the package-less row, or saved after it was removed) land in that row whether the rollback is addressed or not. Its history is widened to match: DatabaseLoader.getHistoryRecord and queryHistory, and so MetadataManager.getHistory, diff and rollback, addressed to the name's one row slot, now list those versions (isAddressedVersion, isSoleRowSlot). This is the /meta doors' isVersionOf rule, so an addressed history read lists exactly what an addressed rollback restores. FROM "Version N not found in history for TYPE/NAME (package P)" (or (environment)) at the manager, while the loader landed it. TO listed and landed at both.
  2. Several row slots (ruling item 2). Unchanged: such a version is in no addressed history and its rollback answers 409 ambiguous-in-environment naming every holder, addressed or bare.
  3. Attribution (ruling item 3, kept from the earlier revision). A version's history row attributes it whenever the row exists. The restored body's package stamp stands in only when there is no row. The one narrowing of this PR comes from here. A direct registerRollback addressing a package, to an unattributed version on a name several row slots hold, with a body stamped for a package, used to write the version into that package's row, or answer a plain "the version is package P's" error, or answer 409 not-held-by-qualifier. It now answers 409 ambiguous-in-environment.
  4. The body a rollback writes (review#3 M1, and N1). registerRollback writes the restored body stamped for the row slot it lands in (stampedForSlot). A package stamp (_packageId, or the envelope's packageId) that names another package than the slot's is replaced by the slot's package, or removed for the environment's package-less row. A body naming no package, or the slot's own, is written byte for byte. Before this, a foreign stamp written into the environment's row made rowSlots key that row as the stamped package's, and one written into package A's row served A's item as _packageId: B. One guard covers the unattributed, environment, M1 and A-attributed (N1) versions. registerRollback now resolves to the body it wrote (it resolved to nothing before; an additive change), and MetadataManager.rollback files and returns that body, so the registry and the row agree.
  5. Docs. The sys_metadata_history.package_id doc comment (metadata-core, shipped in its dist) says a NULL row is the version of the name's one row slot, and of no slot while several hold the name. The loader and manager docblocks name the rule and the ruling.

Why re-stamp and not refuse (review#3 offered both): under ruling B these versions land on one slot, and refusing a body whose stamp is not the slot's would contradict "lands". The stamp is storage bookkeeping. The slot is decided before the write and is never re-derived from the body, so re-stamping changes whose row is written only in the defect case. Nothing found makes it unsafe.

Tests

packages/metadata/src/metadata-history.test.ts: 8 pins, each run once per type (x12). The earlier revision's three "not found" pins and its one-slot stamp pin are replaced. The two-slot stamp pin stays, with an addressed-read leg added. One existing pin moves with the declared widening: the legacy-row pin's versions(type, { packageId: A }) changes from [3, 1] to [3, 2, 1].

  • One row slot (package A, and the environment): an unattributed version is in the addressed history (getHistory, getHistoryRecord, diff) and rolls back addressed or bare, at the manager and the loader.
  • M1: the environment's version is in the taking-over package's addressed history and rolls back addressing it, at both doors. The environment address stays not-held-by-qualifier.
  • Two row slots: an addressed read gives null, and the rollback is 409 ambiguous-in-environment at both doors for a body stamped for either holder.
  • Environment slot (review#3 M1): an unattributed and an environment version, restored with a B-stamped body, bare or addressed to the environment. Each is written unstamped, served without _packageId, and leaves the row the environment's. A B-addressed rollback is refused not-held-by-qualifier with candidates [environment], and the environment's addressed history reads the new version.
  • A slot (review#3 M1): an unattributed and an A version, restored with a B-stamped body, are written and served stamped A, and so is the history snapshot.
  • N1 at the loader: two slots, A's version restored into A's row with a B-stamped body, is written stamped A. B's row is byte-identical.
  • N1 at the manager: a recorded A version whose snapshot carries B's stamp lands stamped A. manager.get(A) serves it stamped A, and manager.get(B) is undefined.

Red first. The pins were committed (cfd4f28b16) before the fix:

  • Against the pre-fix head (3300a61212 code): Tests 84 failed | 218 passed (302), 7 pins x12. The two-slot pin was already green there.
  • Against origin/main code (811b976aa3), the published behaviour: Tests 108 failed | 194 passed (302), 9 pins x12. This includes the two-slot pin (main wrote into the stamped package's row) and the legacy-row history pin.
  • Green on the fix: 302 passed (302).

Ablations. Each one ran through scripts/ablation-replace.mjs: the anchor hit 1 -> 0, and the restore was proven by blob == HEAD and an empty git diff HEAD. The suite imports the loader from source, so there is no dist leg.

Ablation Red (x12 each)
A0 refuseUnattributedVersion returns at once 24: F1, two-slot
A1 stampedForSlot returns the body unchanged 48: env slot, A slot, N1 loader, N1 manager
A2 addressed history without the sole-slot clause 48: one-slot (A, env), M1, legacy-row history
A3 the addressed "not found" re-added 60: one-slot (A, env), M1, env slot, A slot
A4 an unattributed row attributed by the body stamp again (main's rule) 36: two-slot, env slot, A slot
A5 the manager files the snapshot, not the written body 12: N1 manager

Acceptance notes

  • Card item 1 (the released metadata: history and rollback for a slotted type are keyed by (type, name), so rolling back one package's version can overwrite the other package's row (ADR-0150 D4) #32351 changeset text) is carried by the docs-only PR docs(changelog): amend the released history-by-package entry: the 18.4.0 type list, the legacy-row reading and the addressed diff #33526, not here.
  • /meta was not edited (area E1). Its isVersionOf already answers the one-slot cases this way. From reading the code only, without a live run, it differs on one case. POST /meta/TYPE/NAME/rollback?package=P to an unattributed version on a name several row slots hold reaches restoreVersion and answers 404 VERSION_NOT_FOUND, which says the version belongs to no package and several hold the name. The manager and loader answer 409 ambiguous-in-environment there, as ruling item 2 states. The bare /meta rollback refuses 409 first. This goes to the seat as a note.
  • A non-holder address is unchanged. An addressed history read to a package that holds no row of the name still lists that package's own attributed versions (a removed package's), while the rollback refuses them 409 not-held-by-qualifier (S8a). /meta refuses the read itself. This divergence predates the PR and is outside the ruling.
  • A name with no row (all rows removed): a /meta address treats the empty slot as sole (slots.length === 0). The manager and loader do not, because their rollback finds no row to write ("not found for rollback"). Unchanged.
  • Commit cfd4f28b16 is the red-first pin commit, and CI at it is red by construction. CI at beeac9ccb8 shows Lint & Repo Gates and TypeScript Type Check failed only because every lane was cancelled by the superseding push of ed9e3a95e2 (check-typecheck-lanes/check-lint-lanes: "concluded cancelled -- expected success"). They are not real failures.

Verification

The local runs below were measured at head ed9e3a95e2. Readings and sub-topics:

Package suites and typecheck
  • @objectstack/metadata: 79/79 files, 2536/2536 tests.
  • @objectstack/metadata-core: 27/27 files, 1024/1024 tests.
  • @objectstack/metadata-protocol (downstream consumer): 340 files passed and 9 skipped; 22866 tests passed and 43 skipped.
  • typecheck for metadata, metadata-core and metadata-protocol: exit 0 each.
  • eslint --no-inline-config on the five changed source and test files: exit 0.
Derived gate families (dispatch-gates)

--commands against merge base 811b976aa: 66 families. 64 exit 0. Two exit 3 (PREREQUISITE NOT MET, full build absent): check:dual-build-cjs-loads and check:lean-entry-closure. Both are NOT MEASURED locally and left to CI. --ran reconciliation: 66 derived, 64 run, 2 NOT-MEASURED, 0 UNRUN.

Dogfood battery (narrowing arm)

ed9e3a95e2 · closure build then pnpm --filter @objectstack/dogfood test under os-verify-lock: VERDICT command-exit 0 · Test Files 467 passed, 3 skipped (470) · Tests 3950 passed, 42 skipped (3992) · no failure.

CI at ed9e3a95e2: 51 check-runs, 35 success and 16 skipped, none failed. TypeScript Type Check, Lint & Repo Gates, Test Core, Build Core, Check Changeset and Dogfood Regression Gate all succeeded.


Generated by Claude Code

…ttributed or environment version, and history-row attribution over the body stamp (red)

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
…'s history does not hold answers "not found", as the manager's; an existing history row, not the body stamp, owns a version

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
… one-slot landing

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
…buted, never the environment; the manager and loader rollback docs name the one answer

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-core, @objectstack/metadata, touching 14 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/concepts/metadata-lifecycle.mdx (via MetadataManager (symbol, a top-level class), packageId (literal, a string literal in stampedForSlot))
  • content/docs/deployment/app-dependencies.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/deployment/install-modes.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/deployment/validating-metadata.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/frontend/plugins/plugin-chatbot.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/kernel/cluster.mdx (via MetadataManager (symbol, a top-level class), packageId (literal, a string literal in stampedForSlot))
  • content/docs/kernel/contracts/metadata-service.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/permissions/authorization.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/permissions/capabilities.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/permissions/permission-sets.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/plugins/adding-a-metadata-type.mdx (via MetadataManager (symbol, a top-level class))
  • content/docs/protocol/kernel/metadata-service.mdx (via DatabaseLoader (symbol, a top-level class), MetadataManager (symbol, a top-level class))
  • content/docs/protocol/kernel/plugin-spec.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/ui/doc-pages.mdx (via packageId (literal, a string literal in stampedForSlot))

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

  • content/docs/releases/implementation-status.mdx (via DatabaseLoader (symbol, a top-level class), MetadataManager (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via MetadataManager (symbol, a top-level class))
  • content/docs/releases/v18/18-0.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/releases/v18/18-3.mdx (via packageId (literal, a string literal in stampedForSlot))
  • content/docs/releases/v18/18-4.mdx (via DatabaseLoader (symbol, a top-level class), packageId (literal, a string literal in stampedForSlot))

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 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 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 — 16 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 e3f0cfd422d69100448ccd4f6a329c90a25715f7 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json e3f0cfd422d69100448ccd4f6a329c90a25715f7

⚠️ 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 e3f0cfd422d69100448ccd4f6a329c90a25715f7 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…mment in its dist

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

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw
Request: review · area E2 · head 3300a61

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head 3300a61 · 2026-11-14T23:33Z

review#3.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3300a61212c6dbbb5c92499df5ab1806126c35e3
Local-runs: none
Scope: packages/metadata/src/loaders/database-loader.ts, packages/metadata/src/metadata-manager.ts, packages/metadata-core/src/objects/sys-metadata-history.object.ts, .changeset/32717-rollback-door-one-answer.md + packages/metadata/src/metadata-history.test.ts

This reviews the mechanics only. The platform rule (/meta rollback answering the other way) is with seat 5 and is not judged here.

① Derived judgments

  • The two narrowing arms are right.
    • Addressed rollback to an unattributed version on one slot. rollbackSlot reads the history row (historyVersionRow) and takes its attribution whenever the row exists. A row attributed to none therefore stays undefined. Addressed, it answers historyVersionNotFound (the manager's own words). With several slots it gets refuseSeveralSlots first.
    • M1: the environment's version addressing the package that took over the one slot. This now answers "not found". Addressing the environment there is already refused by refuseSlotNotHeld, because the environment holds no row.
    • Both run before any write.
    • With several slots, the body stamp no longer wins over an unattributed row, so a B-stamped body can no longer land in B's row (the ⛔ case). The 4 pins plus the ablations measure all of this.
  • must-fix M1: the widening writes a body whose package stamp names a non-holder, and on the environment's row that changes whose row it is.
    • The path. Naming no package, an unattributed version, one slot, and a restored body stamped _packageId: B for a package that holds no row: before this PR it was refused 409 not-held-by-qualifier. Now it lands, and registerRollback writes metadata: JSON.stringify(restoredData) with B's stamp kept. Nothing strips or re-stamps it, and rowToData serves it as stored.
    • When the one slot is the environment's row (package_id NULL or sys_metadata): storedRowPackageId skips the column and reads the body's stamp. After the write, rowSlots keys that row as B's slot. The environment loses its row, package B gains a name it never declared, and every later read, save, history and rollback treats the row as B's. That is the attribution-by-body this PR set out to remove, now made durable in sys_metadata.
    • When the one slot is a package's (the PR's pin, slot A): the column keeps the row A's. But the served item carries _packageId: B, and readers that take the holder from the listed item's stamp (for example plugin-webhooks' listedHolderOf) see B.
    • The fix. The simplest fix is to keep the old refusal: a restored body whose _packageId names a package other than the slot's (or names one when the slot is the environment's) is refused before any write. This drops the widening, and the arm becomes a pure narrowing. Alternatively, strip or re-stamp the body to the slot's package before the write, keep the widening, and pin the environment-slot case. Either way, add a pin with the environment's row as the one slot and assert rowSlots / storedRowPackageId of the written row afterwards.
  • nit N1 (pre-existing, not this PR). A history row attributed to A, slot A, and a body stamped B lands B's stamp in A's row, both before and after this PR. If you take the "refuse a stamp that is not the slot's" fix, one guard covers this case too. Otherwise, a follow-up.
  • The metadata-core doc change ("NULL means not attributed, never the environment") matches historyRowAttribution and the sys_metadata mark. It is doc-only.

② Semver level

@objectstack/metadata: patch and @objectstack/metadata-core: patch are right for a non-protocol change. Clause-②: yes (narrowing) with !, BREAKING and one adr-0087 marker is correct for the two narrowing arms. If M1 is fixed by dropping the widening, delete the "Widened:" sentence and the matching FROM/TO text. If it is fixed by re-stamping, keep the sentence and say the stamp is replaced.

③ Boundary flags

  • First-party reach: MetadataManager.rollback reads the addressed history first, so it answered these cases already. The narrowings reach only a direct caller of DatabaseLoader.registerRollback. The M1 path also reaches only a direct caller handing in a body whose stamp disagrees with the version's snapshot.
  • Seat 5 has the platform rule. Landing waits for that ruling and for this M1.
  • CI at this head: 36 success, 18 skipped, 1 in progress. The ACCEPT re-reads every check at the landing head, including the dogfood battery this narrowing arm owes.

④ Design judgment

"A history row that exists says whose version it is, and the caller's body never does" is the right rule, and the narrowings follow from it. The same rule implies that the caller's body must not carry a stamp into the stored row either. M1 is the half of that rule this PR does not yet apply.

Implemented-by: claude/area-E2-32717
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: FAIL

…ons land addressed or not, and the written body is stamped for its slot (red)

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
…r its unattributed and environment versions, and a rollback writes its body stamped for the slot (seat ruling 6885958183)

Drops the two one-holder "not found" narrowings in DatabaseLoader.registerRollback and widens the manager's addressed history (getHistory, getHistoryRecord, queryHistory, diff, rollback) to the versions a rollback addressing the name's one row slot restores. The history-row-over-body-stamp attribution stays. A restored body whose package stamp names another package than the slot it is written into is re-stamped for that slot (a package's) or stripped (the environment's), so the environment's row never becomes another package's and a package's row is never served as another's.

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
…seat ruling 6885958183 — the sole-slot widening at the manager's door, the stamped-body narrowing on several slots, and the body stamped for its slot

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot changed the title fix(metadata)!: the manager and loader rollback doors answer an addressed version their address does not hold "not found" alike, and a history row, never the body stamp, says whose version it is (#32717) fix(metadata)!: a sole row slot's history and rollback answer its unattributed and environment versions at every door, a history row (never the body stamp) says whose version it is, and a rollback writes its body stamped for its slot (#32717) Nov 15, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw
Request: review · area E2 · head ed9e3a9

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head ed9e3a9 · 2026-11-15T01:43Z

review#3 (delta).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ed9e3a95e2f5ba85cc9a17f16e725bbc0b547b1a
Local-runs: none
Scope: packages/metadata/src/loaders/database-loader.ts, packages/metadata/src/metadata-manager.ts, packages/metadata/src/package-slotted-store.ts, packages/metadata-core/src/objects/sys-metadata-history.object.ts, .changeset/32717-rollback-door-one-answer.md + packages/metadata/src/metadata-history.test.ts

This reviews the delta on my FAIL at 3300a61212c6dbbb5c92499df5ab1806126c35e3. It is read as the three PR commits on top of the main merge a8c08609 (cfd4f28b red pins, beeac9cc fix, ed9e3a95 changeset), so git diff a8c08609 ed9e3a95. The rest of the range is main. The platform rule is seat ruling 6885958183 (amended B), and it is applied here, not re-judged.

① Derived judgments

  • M1 closed. registerRollback now writes stampedForSlot(restoredData, slot.packageId).
    • What gets rewritten. A package stamp naming another package than the slot's is replaced by the slot's package. On the environment's package-less row the stamp is removed. That covers the envelope's packageId and the body's _packageId, which are exactly the two stamps storedItemPackageId reads, in both the wrapped and the flat shape.
    • What does not. A body naming no package, the sentinel, or the slot's own package is returned as the same object, so an agreeing snapshot is restored byte for byte.
    • What is derived from the written body. The checksum, the bundle member, the scope and the history snapshot are all computed from written.
    • The pin I asked for. It uses the environment's row as the one slot. It restores versions 1 (unattributed) and 2 (environment's own) with a B-stamped body, both bare and addressing null. It asserts that the row stays package_id NULL and unstamped, that load serves no _packageId, and that B is still refused not-held-by-qualifier naming only the environment. So the row is still the environment's slot.
  • N1 closed. Two pins (loader and manager door) restore an A-attributed version with a B-stamped snapshot into A's row. They assert it is written and filed stamped A, and that B's row is byte-identical. The manager files the body the loader returned (written !== handed), so the registry and the stored row agree.
  • Ruling B applied. isAddressedVersion(row, packageId, sole): when the addressed slot is the name's one row slot (isSoleRowSlot), a version attributed to none and the environment's own are that slot's. queryHistory, getHistoryRecord and so getHistory/diff/rollback all read through it, so an addressed read lists exactly what an addressed rollback restores. rollbackSlot drops the two "not found" throws and lands those versions addressed or bare. With several slots, an unattributed version is in no addressed history and its rollback is refused 409 ambiguous-in-environment. The 8 pins × 12 types and the ablations A0–A5 measure each arm.

② Semver level

@objectstack/metadata: patch and @objectstack/metadata-core: patch, fix(metadata)!, Clause-②: yes (narrowing), Metadata-protocol: none. That is correct for a non-protocol change.

  • The one narrowing left is declared: an addressed direct registerRollback, on a name with several slots, of an unattributed version with a stamped body now answers 409.
  • The widening (sole-slot versions at the manager's addressed doors) is in prose.
  • BREAKING names who is affected and the fix.
  • The adr-0087 marker reason ("the package stamp of the body a rollback writes from now on" moves; no row is rewritten) holds.
  • The re-stamp changes what a rollback stores, not what it accepts, so it is no Clause-② arm.

③ Boundary flags

  • The area's two carried notes are acknowledged as follow-ups and do not block this PR: an addressed /meta rollback on a multi-slot name may answer 404 rather than 409 (area E1), and a departed package's addressed history is still listed (pre-existing).
  • CI at this head: 37 success, 18 skipped, none failing. The area reports the dogfood battery green. The ACCEPT re-reads every check at the landing head.

④ Design judgment

"The history row says whose version it is, and the slot says whose row it is" is now applied at both ends: the body the caller hands in neither attributes the version nor stamps the row. Returning the written body to the manager keeps the served item and the stored row in agreement. The design holds.

Implemented-by: claude/area-E2-32717
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw
ACCEPT · area E2 (area-lease pilot #33059) · Part of #32717 · head ed9e3a9

  • Review: review#3 delta PASS at ed9e3a9 (record 6887482406), after FAIL 6885958083 at 3300a61. M1 is closed by stampedForSlot (re-stamp to the slot's package, or strip for the environment's row, before the write). N1 is closed by the loader and manager pins. Seat ruling 6885958183 (amended B) is applied through isAddressedVersion / isSoleRowSlot.
  • Declarations: fix(metadata)!, Clause-②: yes (narrowing) measured against main, with a BREAKING paragraph and an adr-0087 marker; Metadata-protocol: none (no edit under packages/metadata-protocol/src/**).
  • Follow-ups carried in the body's Acceptance notes, not part of this PR: an addressed /meta rollback on a name several slots hold may answer 404 rather than 409 (area E1, read from code only), and an addressed history read naming a departed package still lists its versions (pre-existing).
  • Governed: check-governed-merges --pr 33464 NOT governed; 432 changed lines.
  • Checks at this head: 37 success, 18 skipped, none failed or pending. The dogfood battery is green at this head (467 files passed, 3 skipped; 3950 tests passed, 42 skipped).
  • Landing: the maintainer's instruction in the area thread (15:21Z): PRs in this area that passed review go ready + auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 15, 2026 01:46
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 15, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants