Repository navigation
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
objectstack-fleet[bot] wants to merge 10 commits into
Conversation
…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>
Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…mment in its dist Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw |
|
Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head 3300a61 · 2026-11-14T23:33Z review#3. |
Contract reviewServed-tier: This reviews the mechanics only. The platform rule ( ① Derived judgments
② Semver level
③ Boundary flags
④ 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: VERDICT: FAIL |
Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw Co-authored-by: Claude <noreply@anthropic.com>
…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>
|
domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw |
|
Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head ed9e3a9 · 2026-11-15T01:43Z review#3 (delta). |
Contract reviewServed-tier: This reviews the delta on my FAIL at ① Derived judgments
② Semver level
③ Boundary flags
④ 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: VERDICT: PASS |
|
domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw
|
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 norollbackline underskills/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
维护者速读
/meta)。一个名字只有一个行槽时,未归属的旧版本和环境自己的版本都算这个槽的:点名包或不点名都能列出、diff、回滚落地,manager 的点名历史读也列出它们。同时保留“版本归属只看历史行,不看调用方 body 上的包戳”。回滚写入的 body 一律改成该槽的包戳:包槽重盖为该包,环境槽去掉外来包戳。SysMetadataRepository仍在写未归属行的现状、以及控制台ResourceHistoryPage在历史和回滚都带?package=的用法都冲突,席位裁决改为放宽 manager。review#3 指出放宽后 body 上的外来包戳会被原样写入:写进环境行会把该行变成那个包的槽,写进 A 的行会以 B 的身份对外服务。本版在写入前把包戳改成槽主人的。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):DatabaseLoader.registerRollbackare 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.getHistoryRecordandqueryHistory, and soMetadataManager.getHistory,diffandrollback, addressed to the name's one row slot, now list those versions (isAddressedVersion,isSoleRowSlot). This is the/metadoors'isVersionOfrule, 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.409 ambiguous-in-environmentnaming every holder, addressed or bare.registerRollbackaddressing 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 answer409 not-held-by-qualifier. It now answers409 ambiguous-in-environment.registerRollbackwrites the restored body stamped for the row slot it lands in (stampedForSlot). A package stamp (_packageId, or the envelope'spackageId) 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 maderowSlotskey 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.registerRollbacknow resolves to the body it wrote (it resolved to nothing before; an additive change), andMetadataManager.rollbackfiles and returns that body, so the registry and the row agree.sys_metadata_history.package_iddoc comment (metadata-core, shipped in itsdist) 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'sversions(type, { packageId: A })changes from[3, 1]to[3, 2, 1].getHistory,getHistoryRecord,diff) and rolls back addressed or bare, at the manager and the loader.not-held-by-qualifier.null, and the rollback is409 ambiguous-in-environmentat both doors for a body stamped for either holder._packageId, and leaves the row the environment's. A B-addressed rollback is refusednot-held-by-qualifierwith candidates[environment], and the environment's addressed history reads the new version.manager.get(A)serves it stamped A, andmanager.get(B)is undefined.Red first. The pins were committed (
cfd4f28b16) before the fix:3300a61212code):Tests 84 failed | 218 passed (302), 7 pins x12. The two-slot pin was already green there.origin/maincode (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.302 passed (302).Ablations. Each one ran through
scripts/ablation-replace.mjs: the anchor hit 1 -> 0, and the restore was proven byblob == HEADand an emptygit diff HEAD. The suite imports the loader from source, so there is nodistleg.refuseUnattributedVersionreturns at oncestampedForSlotreturns the body unchangedAcceptance notes
/metawas not edited (area E1). ItsisVersionOfalready 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=Pto an unattributed version on a name several row slots hold reachesrestoreVersionand answers404 VERSION_NOT_FOUND, which says the version belongs to no package and several hold the name. The manager and loader answer409 ambiguous-in-environmentthere, as ruling item 2 states. The bare/metarollback refuses409first. This goes to the seat as a note.409 not-held-by-qualifier(S8a)./metarefuses the read itself. This divergence predates the PR and is outside the ruling./metaaddress 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.cfd4f28b16is the red-first pin commit, and CI at it is red by construction. CI atbeeac9ccb8showsLint & Repo GatesandTypeScript Type Checkfailed only because every lane wascancelledby the superseding push ofed9e3a95e2(check-typecheck-lanes/check-lint-lanes: "concludedcancelled-- expectedsuccess"). 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.typecheckfor metadata, metadata-core and metadata-protocol: exit 0 each.eslint --no-inline-configon the five changed source and test files: exit 0.Derived gate families (dispatch-gates)
--commandsagainst merge base811b976aa: 66 families. 64 exit 0. Two exit 3 (PREREQUISITE NOT MET, full build absent):check:dual-build-cjs-loadsandcheck:lean-entry-closure. Both are NOT MEASURED locally and left to CI.--ranreconciliation:66 derived, 64 run, 2 NOT-MEASURED, 0 UNRUN.Dogfood battery (narrowing arm)
ed9e3a95e2· closure build thenpnpm --filter @objectstack/dogfood testunderos-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 ChangesetandDogfood Regression Gateall succeeded.Generated by Claude Code