Repository navigation
fix(metadata): a rewrite of an environment row the boot pass stamped keeps it the environment's, so unregistering the host package keeps it - #33036
Conversation
… own object, view, app, hook and dashboard rows The boot priming pass stamps a package-less stored row with the host's package. For a type not stored per (package, name) the stamp stays, and unregisterPackage(host) took it as membership, deleting the environment's own rows from the registry and storage. The removal now leaves an item a stored-row loader handed on naming no package whose only claim to the package is that stamp. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgThzreKNjha83MypKs
…ce at its erasure ceiling Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgThzreKNjha83MypKs
…y is still unregistered with it; state the mark's limit Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgThzreKNjha83MypKs
…keeps it the environment's, and stores it without the host stamp A copy that replaces such a row and still names the host by the stamp alone (ObjectQL's late-manifest bridge and its restore, its object-copy follow, publishPackage / revertPackage, importMetadata's merge) keeps the row's mark, and register stores the row without the host stamp, so the next boot reads it as the environment's own again. unregisterPackage(host) keeps it in process and after a reboot; the host's own rows are removed as before. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
…d entry, is the host's; store only the envelope body unstamped Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
… mark when it keeps the stamp; pin a copy stamped with another package Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
…environment's object, and unregistering the host keeps it Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
…-k9j7az # Conflicts: # packages/metadata/src/loaders/database-loader.test.ts # packages/metadata/src/metadata-manager.ts
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 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 7d00df958ba7e4346b2f2a123f54fa1fecb8839a && git checkout 7d00df958ba7e4346b2f2a123f54fa1fecb8839a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 157051ceb48d2d9ed289f21f7018a39ce7249178 83947aaf59c1d779154afe7904346be05a4a0111 && git checkout -B drift-repro 157051ceb48d2d9ed289f21f7018a39ce7249178 && git merge --no-ff 83947aaf59c1d779154afe7904346be05a4a0111
node scripts/docs-audit/affected-docs.mjs --json 157051ceb48d2d9ed289f21f7018a39ce7249178
|
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract-review-claim: session_01Q4EjjMnyLqJdzuktEmRPXH · head f64ffdc · 2026-11-13T05:40Z Taken on the request: p0 #33003 (18.4.0 blocker). The face reads as declaration ( |
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract reviewServed-tier: PR #33036, card #33003 (p0 for the 18.4.0 window). Inputs: the card and its four comments, the PR body, file list and net diff at this head, the head's check-runs. Read, never run: the head's Findings: 0 must-fix, 3 should-fix, 2 nits. ① Derived judgmentsThe four writes the brief names, judged on the tree:
② Semver level
③ Boundary flagsThe open question (comment 6845180542): option A. The declaration asks whether a published package's accept set or public surface widens or narrows (CONTRIBUTING.md item 5). Nothing a caller can pass or call changes. What moves is one private provenance key in the stored body of one class of rows. Reader by reader:
The stored-body difference is disclosed in the PR body and the changeset says where the stamp is no longer stored. That is enough for Should-fix:
Author: PR ④ Design judgment
Implemented-by: VERDICT: PASS Generated by Claude Code |
…he host stamp, and an unstamped replacement keeps the row's mark and host Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
…carry, and the rows already stored stamped Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract reviewServed-tier: Delta review of PR #33036 (card #33003) over this reviewer's PASS on Findings at this head: 0 must-fix, 0 should-fix, 1 new nit. The earlier S1 to S3, N1 and N2 are closed as below. ① Derived judgmentsEach earlier finding against the hop:
Read on the tree:
② Semver levelUnchanged: ③ Boundary flags
Author: PR ④ Design judgment
Implemented-by: VERDICT: PASS Generated by Claude Code |
Conflict in packages/metadata/src/metadata-manager.ts with #33006, which retired the storedRowsOverOwnItems set: kept main's removal and this branch's environmentRowHosts field and carryEnvironmentRowMark call. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
|
epic:#31527#2 · os-elon-musk · session_016WV4czFhtRVZNBeHdQA6PW ACCEPT · 2026-11-13T07:50Z · head
|
Requested by Elon · project thread
Fixes #33003
Clause-②: no
Metadata-protocol: none
维护者速读
packageId/package指名宿主的拷贝、带别的包印记的拷贝、被别的写入方改存为宿主的行,都照旧被卸载。唯一的存储差异是被改写过的环境行在数据库里不再带_packageId宿主印记(评审已裁定 Clause-② no)。已经被 18.3.0 及更早版本连印记一起存进库的旧行,库里分不出来,仍按宿主自己的行处理,升级路径归 metadata: on a database upgraded from 18.3.0, the host stamp 18.3.0 persisted into environment rows makes unregisterPackage(host) still delete them under #32198 (upgrade path of #32986 / #33003) #33043。回滚即 revert 本 PR。packages/metadata/src/metadata-manager.ts(writeRegistryEntry里新加的一行、register与rollback写库前去掉印记的几行、carryEnvironmentRowMark);其次packages/objectql/src/plugin-late-bridge-environment-row.test.ts。pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 src/loaders/database-loader.test.ts -t 33003,预期 26 条全部通过。What changes
PR #32999 (card #32986) keeps an environment row the boot pass stamped with the host when the host package is unregistered, by marking the registry object the pass registered (
storedEnvironmentRows). Triage's contract review of #32999 (should-fix S1, record 6842932691) found where that stops: a write that replaces the object with a copy still stamped_packageId: HOSTdrops the mark, andDatabaseLoader.savestores the stamp in the row's body. The row then reads as the host's own, in this process and after a restart, andunregisterPackage(HOST)deletes it.Writers measured (every caller that registers a copy of a stamped item, on #32999's head
a43fc250):MetadataManagerownPreviousCopyarm (plugin.tsbridgeManifestObjectsToMetadataService)register('object', name, merged, { notify: false }),merged._packageId= the owner (HOST)restoreExtendedObjectCopiesregister('object', name, served, { notify: false }), stamped with the ownerputObjectCopy)registerInMemory('object', name, body), stamped with the ownerpublishPackage(HOST)overcollectPackageMembersregister(type, name, { ...item.data, publishedDefinition, … })revertPackage(HOST)register(type, name, { ...item.data, metadata: publishedDefinition, … }): the stamp also lands in the envelope'smetadatabodyimportMetadatawithconflictResolution: 'merge'(not named on the card)register(type, name, { ...existing, ...item })rollback(type, name, version)to a version stored with the host stamp (review S2)dbLoader.registerRollback(…), thenwriteRegistryEntry(type, name, restored): not throughregisterimportMetadatawithoutmerge,bulkRegister,rollbackto an authored version), then the object-copy follow stamped with the owner (review S3)register(type, name, body)with no stamp, thenregisterInMemory('object', name, { …, _packageId: HOST })The boot bridge (
bridgeObjectsToMetadataService) skips a name the service already holds, and the protocol's authoring writes register an unstamped body, so neither replaces a marked row with a stamped copy.Route: both halves, at the one choke point every writer above goes through.
writeRegistryEntry(underregister,registerInMemoryandrollback): a copy that replaces a marked environment row, was not handed out by a loader, and carries the row's host stamp or no stamp joinsstoredEnvironmentRows(carryEnvironmentRowMark). The host stamp is remembered per row (environmentRowHosts), so a copy stamped with the host after an unstamped replacement is still the row (review S3). A copy stamped with another package is that package's.isStampedEnvironmentRowstill reads every key at each use, so a copy whose ownpackageId/packagenames the package stays the package's item.register, androllbackbeforeregisterRollback(review S2): a marked row is stored without the host stamp, at the top and in the publish envelope'smetadatabody (withoutEnvironmentRowStamp). The registry entry keeps the stamp, so ObjectQL's owner does not move.Why not "membership reads something the rewrite keeps" alone: once the stamp is persisted, a rewritten environment row and the host's own row the pass seeded are the same bytes in
sys_metadata(body_packageId: HOST, emptypackage_idcolumn for a non-slotted type), so no rule read at the next boot can tell them apart. The reboot pin needs the stored body to stay package-less, which is the state of a row nothing has rewritten.FROM → TO
HOST, an environment row (stored package-less) the boot pass stamped, after…a43fc250)ownPreviousCopy)unregisterPackage(HOST)deletes it, in process and after a restartrestoreExtendedObjectCopiesregisterInMemory)publishPackage(HOST)publishPackage(HOST)thenrevertPackage(HOST)importMetadatamergerollbackto a version stored with the host stampimportMetadataoverwrite,bulkRegister), then the object-copy follow stamped with the hostUnchanged, pinned as CONTROLs: the host's own rows (stored body carrying its stamp) after each of the six writes, in process and after a restart, and after a
rollback; a copy naming the host by its ownpackageId; a copy stamped with another package (removed by that package's unregister); a stored row another writer re-saved naming the host, read again by the pass over the marked entry; the extender's unregister. Also pinned (review nits N1/N2 of #32999): astate: 'draft'row behaves likeactive, and the rule holds forpageandreport. The card'sflowexample is stored per(package, name)(PACKAGE_SLOTTED_STORE_TYPES), outside this rule, sopageandreportstand in for it.Clause-②:
no, re-derived after measuring.@objectstack/metadata@18.3.0'sunregisterPackageselected bypackageId/packageonly, so it never removed an item named by the_packageIdstamp alone, and these rows were kept there too. This PR removes nothing 18.3.0 kept and keeps nothing 18.3.0 removed. The registry entry keeps its stamp, so every in-process reader answers as before. Metadata-protocol:none: no authorable key, value or type moves, and nopackages/metadata-protocolfile is touched.Acceptance notes
_packageId. A rewritten environment row is now stored without_packageId: HOST(18.3.0 andmainstored it with the stamp). Only that key is removed: the pass's other two stamps,_packageVersionand_provenance: 'package', stay in the stored body of a rewritten row, so it is not byte-identical to a row nothing rewrote (review N1). No reader moves on them:applyProtectionfills only unset keys, and the protocol overwrites provenance on read. Triage ruled the open question A on the contract review (6845366666):Clause-②: nostands.skipSourceLoader, so it stored the stamp in every row it stamped; so did any rewrite before this change. Such a row reads as the host's own (the CONTROLs pin exactly that byte state as removed), and no boot rule can tell it from a row the host seeded. metadata: unregisterPackage of the host package deletes the environment's own object, view, app, hook and dashboard rows the boot pass stamped with the host (follow-up of #32198) #32986 and this PR protect a fresh database and rows written after the upgrade; the upgrade path is metadata: on a database upgraded from 18.3.0, the host stamp 18.3.0 persisted into environment rows makes unregisterPackage(host) still delete them under #32198 (upgrade path of #32986 / #33003) #33043. The changeset says so.mainalready overwrites the environment's body, and it errs toward keeping.a43fc250, the maintainer's word on the seat's decision card (an exception to the no-stacking rule for this p0).origin/mainmerged once after fix(metadata): unregistering the host package keeps the environment's own object, view, app, hook and dashboard rows #32999 landed.Tests
packages/metadata/src/loaders/database-loader.test.ts,[#33003]describe (26 cases). The real boot pass (MetadataPlugin._loadFromFileSystem) over aDatabaseLoaderon the file's in-memory driver, host declaringHOST, rows ofobject(oneactive, onedraft),view,app,hook,dashboard,page,report. For each of the six writers in rows 1 to 6, driven through the call it makes on the manager: (1)unregisterPackage(HOST)in the same process keeps every rewritten row, registry entry and stored row, the stamp still on the entry; (2) a second boot pass over the same store, thenunregisterPackage(HOST), keeps them; (3) CONTROL: the host's own rows (stored body stamped) are removed in both processes. Review follow-ups:rollbackto a version stored with the host stamp, kept in process and after a reboot (S2), with its CONTROL on the host's own row; an unstamped replacement byimportMetadataoverwrite and bybulkRegister, then the object-copy follow stamped with the host, kept (S3). Plus four CONTROLs: a copy naming the host by its ownpackageId; a copy stamped with another package; a stored row another writer re-saved naming the host, read again by the pass over the marked entry; the extender's unregister moves nothing.packages/objectql/src/plugin-late-bridge-environment-row.test.ts(new, 3 cases): the plugin's ownbridgeManifestObjectsToMetadataServiceandrestoreExtendedObjectCopies, over a realObjectQLregistry (the environment object owned byHOST, an extender addingext_score), into a realMetadataManagerprimed by the real boot pass over aDatabaseLoader; a second pass over the same store is the restart. Bridged: kept in process and after a restart. Restored: kept, both. CONTROL: the host's own object, bridged and restored, is removed. The suite reads@objectstack/metadatafromdist/, so its ablations rebuilt it.Ablations (fix committed first; scripts/ablation-replace.mjs; each restore verified: blob equal to HEAD, git diff HEAD empty)
On
metadata-manager.tsblob7e2109f16965(head012671ee), the[#33003]metadata cases (26):carryEnvironmentRowMarkcall inwriteRegistryEntry: 14 failed, 12 passed. Red: every in-process pin (6), the reboot pin of every persisting writer (5), both S3 chains and the S2 rollback pin. Green: the CONTROLs and theregisterInMemoryreboot case (that writer persists nothing).loader.save(type, name, data)in place of the unstamped body: 5 failed, 21 passed. Red: exactly the five reboot pins of the persisting writers.metadatabody strip: 1 failed, 25 passed. Red: publish then revert, after a reboot.this.loadedFrom.has(data)from the carry: 1 failed, 25 passed. Red: the CONTROL of a stored row another writer re-saved naming the host.registerRollbackgiven the restored body as is: 1 failed, 25 passed. Red: the rollback pin.On the objectql pin (3 cases, at
f64ffdce; the mutated lines are unchanged in purpose), each leg rebuilt@objectstack/metadataand the suite was read after it; the restore leg rebuilt again andablation-dist-preflight --absentreported the tree clean: carry disabled 2 failed, 1 passed; stamped body saved 2 failed, 1 passed; after restore 3 passed.Verification
On head
012671ee(branchclaude/project-thread-k9j7az; stacked on #32999a43fc250; after #32999 merged at4db19924,origin/maine728d7a1merged once, by merge, atf64ffdce; two commits since, answering the contract review 6845366666).Local runs on 012671e
pnpm --filter @objectstack/metadata build(throughscripts/pm/os-verify-lock.sh): VERDICT command-exit 0, over the dependency closure built atf64ffdce(turbo run build --filter='@objectstack/dogfood^...' --filter='@objectstack/runtime^...', VERDICT command-exit 0).node scripts/pm/dispatch-gates.mjs --commands: 65 families, the same list at4bfa8c2f,f64ffdceand012671ee; every one run on012671ee, each recorded with its exit code: all 0.--ranreconciliation: 65 derived, 65 run, 0 NOT-MEASURED (a derived zero). On4bfa8c2f,check-plugin-teardown-shape --self-testfirst answered exit 3 (shallow clone, its pinned fixture commit missing) and passed after fetching it.pnpm --filter @objectstack/metadata test: Test Files 76 passed (76), Tests 2115 passed (2115).pnpm --filter @objectstack/objectql test: Test Files 591 passed (591), Tests 12742 passed (12742).pnpm --filter @objectstack/metadata typecheck && pnpm --filter @objectstack/objectql typecheck: VERDICT command-exit 0.pnpm lint: exit 0.!in the title, no(narrowing)arm).CI on this head: see the check runs.
Human Minimum
packages/specholds no runtime logic (not touched).pnpm lintpasses.mainmerged once after it landed).@objectstack/metadatapatch), and both declaration lines are filled in here and in the changeset.packages/specnot touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
Generated by Claude Code