Repository navigation
fix(metadata-protocol): the orphan adoption keeps overlays of packaged items, and the registry follows each view row it rebinds (#23198) - #23264
Conversation
…d items, and the registry follows each view row it rebinds ADR-0070 keeps an overlay of an artifact item bound to the item it customises, so reassignOrphanedMetadata skips a package-less row for which isArtifactBacked holds, the predicate saveMetaItem sets override-artifact by. Each view row it rebinds goes through the write-through with the binding it left, so reconcileViewExpansions moves the row's record and registers its expansion at once. The closing check's bypass row is gone. Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…pansions, not the container body the boot hydrates as stored Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…n adoption, which writes views through and objects not at all Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…phan-adoption-reconcile
📓 Docs Drift CheckThis PR changes 1 package(s): 31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 11 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 31c294cd0a11f14e5ca6687255c88389125f719d && git checkout 31c294cd0a11f14e5ca6687255c88389125f719d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6d7f4df3c91c0d4e7d0a40c3b5dc796290e90a2d dc2e53693853285aca7029c671a3913796ae2348 && git checkout -B drift-repro 6d7f4df3c91c0d4e7d0a40c3b5dc796290e90a2d && git merge --no-ff dc2e53693853285aca7029c671a3913796ae2348
node scripts/docs-audit/affected-docs.mjs --json 6d7f4df3c91c0d4e7d0a40c3b5dc796290e90a2d
|
Fixes #23198
Clause-②: no
This is the second half of #23198, the closing card for the registry-heal family. The first half landed as #23234.
The orphan adoption (
POST /api/v1/packages/ID/adopt-orphans→reassignOrphanedMetadata) now leaves a package-less overlay of a packaged item as it is stored. Each view row it does rebind is written through to the registry with the binding it left, so the one reconcile moves that row's record at once. This was the family's last stored-row write that did not reach the reconcile. The closing check'sbypassrow is gone, and the census no longer has an exception kind.Direction: PM comment 6144955760 on #23198, which chose option B1 of the
needs_decisionreport 6144921283. It is execution under ADR-0070's kernel section: "Overlays of artifact items (intent = override-artifact,allowOrgOverride) are unchanged — they keep their binding to the packaged item they customise."What was wrong (measured before the change)
Measured over the real
SchemaRegistryand the built protocol on a control-plane kernel, at6f4c518b01, in an uncommitted scratch inpackages/objectql/src. A "restart" means a fresh registry with the package registered, thenloadMetaFromDbover the same rows.showcase_task(All Tasks (X), 6 columns). Adoption rebound it to the base.showcase_task.defaultstill servedAll Tasks (X), and two new views were listed next to it.?object=list and the by-name read all servedshowcase_task.defaultas the packagedAll Tasks(7 columns). The row expanded under its own name in the base instead, asshowcase_task.showcase_taskandshowcase_task.showcase_task.in_progress.showcase_task.in_progress. This is the shape of the record list's View Config Save,PUT /meta/view/NAME?mode=draftwith no package. After adoption and a restart, the list served two entries under that one name.rt_ticketon an object no package owns. Its names and content were unchanged; only_packageIdmoved.The change (
packages/metadata-protocol/src/protocol.ts)reassignOrphanedMetadataskips a package-less row for whichisArtifactBacked(type, name)holds. That is the predicatesaveMetaItemuses to setintent: 'override-artifact', with the type folded the same way the save folds it (PLURAL_TO_SINGULAR). There is no second derivation.view, the adoption prepares the row the way the list read does (storedOverlayEntries, at the new binding). It then callsapplyRegistryWriteThroughwithreboundFrom: { packageId: OLD_PACKAGE_ID }.warn, as the write-through itself does, and moves on to the next row.reboundFrompasses throughhydrateOverlayIntoRegistryandhydrateExpandedViewItems(they now share aHydrationOptionstype) intoreconcileViewExpansions. Its binding form gains a rebind arm: the record row of the binding the row left is removed, and its names count as the row's old names. Names that the new expansion lacks are healed by the usual per-name walk.reassignOrphanedMetadata, the reconcile, the write-through andViewContainerWritenow say all of this.No signature, export or schema changed: every changed member is
private, andHydrationOptionsis module-local.The spec JSDoc stays as it is
packages/spec/src/api/package-lifecycle.zod.tsreads: "bulk-rebind package-less (orphaned) metadata into this base (ADR-0070 D5)". That is still true after B1:No spec path is touched, so no cross-lane declaration is owed.
Pins
In
protocol.view-container-reset-expansions.test.ts, a new block. The harness'sbootnow takes an existing row store, so a restart isboot({ rows })plusloadMetaFromDb.reassignedCount: 0), and the row is still package-less. In the same process and after a restart, every door servesAll Tasks (X)/6, andshowcase_task.showcase_taskis served by none.In Progress (X)/2, once.rt_ticketis adopted. Before any read, the registry entries of the container and of each expanded view carry the base's_packageId. Every door serves the views in the same process and after a restart, and the overlay keeps serving.rt_ticket.openandrt_ticket.closedstay registered).Elsewhere:
protocol.view-expansion-reconcile-call-sites.test.ts: the adoption's write is recorded with its route (sameMember: applyRegistryWriteThrough). Thebypassroute kind and its test are removed, so a test that would now iterate over nothing does not remain.protocol.object-registry-write-through-spelling.test.ts(Thirdtype === 'objects'tolerance in protocol.ts:applyObjectRegistryMutationCONSUMES the tolerant spelling rather than only guarding on it #8862): the caller set grows from five to six. The header trace gains route 6, and one case shows that a storedviewsrow is written through asviewand an orphanedobjectsrow is not written through at all.Ablations
One-shot runs, not kept as files. The fix was committed first. Each leg used
scripts/ablation-replace.mjsin WRAP mode (the anchor had to hit, and the blob was proven changed) and was restored withblob == HEADand an emptygit diff HEAD.isArtifactBackedskip replaced bytrueviewgate always continues)this.engine.update('sys_metadata', …)inrollBackUnmanagedImportthe writes in the source are exactly the recorded onesred, namingrollBackUnmanagedImport: this.engine.update('sys_metadata'protocol.tsrestored to main'safbf960c26viewgate also admitsobjectThe first attempt at leg 4 was refused by the tool, because the replacement still contained the anchor; nothing ran. The second attempt ran.
Tests and gates
All of these were read at head
dc2e536938(the commit that mergesmainat4f5679c261).dc2e536938: the reset-expansions file, the census, the Thirdtype === 'objects'tolerance in protocol.ts:applyObjectRegistryMutationCONSUMES the tolerant spelling rather than only guarding on it #8862 spelling file, the finding(metadata-protocol): on an unscoped kernel, a package-bound stored row of a view name two packages ship is hydrated into the registry's bare slot, so a by-name read naming the other package serves that row's body under its own _packageId #22057 register-item call sites and thehydrateOverlayIntoRegistrymints registry entries under the RAWtypewith no fold and no assertion — the last unfolded seam of theobjectstolerance family #9111 hydrator pin. 5 files, 55 tests passed (VERDICT command-exit 0).@objectstack/metadata-protocolsuite ran in two halves before the merge (the merge touchesplugin-sharingonly):type === 'objects'tolerance in protocol.ts:applyObjectRegistryMutationCONSUMES the tolerant spelling rather than only guarding on it #8862 caller-count pin, which this PR updates. That file was rerun after the update: 12 of 12 passed.pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.reassignedCount 0, andAll Tasks (X)/6 on every door in the same process and after a restart;reassigned [], andIn Progress (X)/2 once, in the same process and after a restart;app.home.dispatch-gates --commands --repo objectstack-ai/objectstackatdc2e536938derives 65 commands.--ranreports 65 run, 0 NOT-MEASURED and 0 UNRUN.check:dts-closurefirst exited 1, namingplugin-auth's.d.ts. That run raced my own parallel gate lane, which was rebuilding packages at the time. Rerun on the settled tree, it exited 0 (73 packages, 173 of 173 declarations present).check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET). Rerun after the builds, it exited 0 (108 entry points across 67 packages load)..tsfiles are ineslint.config.mjs's population:--print-configresolves a config for each.eslint --no-inline-config --format jsonreports 4 files, 0 errors and 0 warnings. Type-aware linting is off (noparserOptions.projectorprojectServicein the resolved config), so the verdicts on untouched files cannot move. The fullpnpm lintrun is CI's.Size: 5 files, +282 / −35 against the merge base
4f5679c26.Acceptance notes
reassignedCount: 0andsuccess: false. The response shape is unchanged, and the changeset says this.loadMetaFromDb) registers a package-bound container's own body without the_packageIdthat the list read's hydration stamps on it. So after a restart, the container's bare entry carries no package, while its expanded views do. This PR does not change that difference between the two callers. The control D pin asserts the expanded views after a restart, not the container body.showcase_task.in_progresswithpackageId app.home). The list then serves two bodies under that name, and both wear the shipping package's_packageId. Adoption no longer produces that row. It was measured at the protocol layer only, with no other producer measured.type === 'objects'tolerance in protocol.ts:applyObjectRegistryMutationCONSUMES the tolerant spelling rather than only guarding on it #8862 caller count with its header trace, and the census header.Generated by Claude Code