Repository navigation
fix(metadata-protocol): an orphan adoption hands each adopted object's registry owner and definition to the target package in the same request (#23646) - #23688
Conversation
…stry owner (#23646, wip) Claude-Session: https://claude.ai/code/session_01GP3comALHRcRBixS2TEnLF Co-authored-by: Claude <noreply@anthropic.com>
…s registry owner to its rebound row (#23646) reassignOrphanedMetadata's registry write-through followed view rows only, so an adopted object kept the sys_metadata sentinel as its registry owner and its pre-adoption definition until the next boot, and a save naming the target package answered success without going live. Each adopted object row now releases the sentinel's registration (SchemaRegistry.unregisterObject) and registers at its new binding through the same write-through, in the same request. Claude-Session: https://claude.ai/code/session_01GP3comALHRcRBixS2TEnLF Co-authored-by: Claude <noreply@anthropic.com>
…retired organization column (#23646) Claude-Session: https://claude.ai/code/session_01GP3comALHRcRBixS2TEnLF Co-authored-by: Claude <noreply@anthropic.com>
…TIVE row only, as the boot does (#23646) A package-less draft object row adopted into a package is rebound in storage only; a draft never reaches the runtime registry. Claude-Session: https://claude.ai/code/session_01GP3comALHRcRBixS2TEnLF Co-authored-by: Claude <noreply@anthropic.com>
…opt-orphans-object-registry
…wner, said once at error (#23646) Claude-Session: https://claude.ai/code/session_01GP3comALHRcRBixS2TEnLF Co-authored-by: Claude <noreply@anthropic.com>
…stead of erasing them (#23646) Claude-Session: https://claude.ai/code/session_01GP3comALHRcRBixS2TEnLF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 8 release-owned page(s) also name something this change touched. These are read-only:
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 1b63091d6cbdbb921e2f01ecce18a7298099acab && git checkout 1b63091d6cbdbb921e2f01ecce18a7298099acab
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 361a1c46d357de926c61a1f3d7d8b4997e5bac42 9e2c7a57801e6b4de635e41a2db3432aeac32c06 && git checkout -B drift-repro 361a1c46d357de926c61a1f3d7d8b4997e5bac42 && git merge --no-ff 9e2c7a57801e6b4de635e41a2db3432aeac32c06
node scripts/docs-audit/affected-docs.mjs --json 361a1c46d357de926c61a1f3d7d8b4997e5bac42
|
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The first line is Fixes #23646. A closing-keyword scan of the whole body finds one hit, line 1; #23514 is named only as "remains open". |
| Scope | 4 files (+492 / −10): packages/metadata-protocol/src/protocol.ts, one re-pinned fixture beside it, the new real-kernel pin file in packages/objectql/src, and one changeset. That is the claim's surface (6191415525). No registry verb was added, so the claim's conditional packages/objectql source arm was not used. No content/docs/releases/ edit, no governed path, under the 3,000-line threshold. |
Clause-② |
no, on the claim, the PR body and the changeset. The adoption's request, response and rebound row set are unchanged; the registry now follows the row it already rebinds. No accept-set narrowing, no published schema, so no contract review is owed. |
| Changeset, sentences read | One patch row for @objectstack/metadata-protocol. The title line "an orphan adoption moves each adopted object's registry owner and definition to the target package in the same request (ADR-0070 D5)", and a "What stays as before" list that matches the pinned table. |
| Seat's read of the diff | The object limb is synchronous end to end: releaseAdoptedObjectOwner unregisters the sentinel, then applyRegistryWriteThrough's object branch calls applyObjectRegistryMutation and returns before that method's first await. So the header's "no request sees the name unowned" holds on this head. Only an active row is handed over; a name another package owns or extends keeps its owner. |
| Card's first acceptance line | Met: the HTTP route was walked before and after the fix on pnpm dev:crm (400 INVALID_FIELD → 201 / 200, the server's unregister and register lines quoted in the body). |
| Negative control | Two ablation legs at 3061f6f3b, each with the marker proved in dist/: A (object limb removed) 3 red of 6, B (active gate neutralised) 1 red of 6, controls green; restore clean, 6 of 6. |
| Hot file | protocol.ts's reassignOrphanedMetadata region. PR #23683 (#23630) is in the promote doors, a different region; whichever lands second takes the other through the queue's merge. #23514 resumes in classifyObjectContribution; this PR builds no re-key primitive for it to reuse. |
| Gates | The dev's derived union at this head: 67 commands, 67 × exit 0, --ran reconciled. CI was still running at this read. Landing needs every check green or on the expected-skip roster, and main's full battery green (the lane's main-red #23679 is in flight). |
Residual, accepted. If registerObject threw after the sentinel's unregister, applyObjectRegistryMutation's best-effort warn would leave the name unregistered until a restart. The body registered is the one the sentinel already held, so no input path to that throw was found, and the boot re-registers the object under Q. No code is owed for it.
Disposition:
out_of_scope_findings[0](an adopted package-less draft view is served live by the registry): filed metadata-protocol: an orphan adoption registers a package-less DRAFT view as live — reassignOrphanedMetadata's view write-through has no state gate, so the registry serves a draft the boot never registers (found by #23646) #23690.out_of_scope_findings[1](legacy residue: a package-less row beside package R's row is rebound into Q in storage; the registry keeps R, as before): Acceptance notes. The behaviour predates this PR and is unchanged by it.deviations: the commit trailers follow AGENTS.md, accepted. The two root-level pid files the dev did not write:dropped — environment.
Generated by Claude Code
|
Landed ·
Status: #23646 is closed, and its Generated by Claude Code |
Fixes #23646
Clause-②: no
What changed
reassignOrphanedMetadatais the methodPOST /api/v1/packages/:id/adopt-orphanscalls (ADR-0070 D5). It rebinds every package-lesssys_metadatarow to the target package Q. Its registry write-through followed view rows only. So an adopted object kept two things until a restart: thesys_metadatasentinel as its registry owner, and its pre-adoption definition. Meanwhile a save naming Q answered 200 and never went live.Each adopted active object row now goes through two steps in the same request:
releaseAdoptedObjectOwner(a new private helper beside the method) removes the sentinel's registration. It calls the existingSchemaRegistry.unregisterObject, the verb the delete heal already uses to hand an object's name to a remaining row.applyRegistryWriteThrough, the same write-through a save uses.The registry holds one owner per object throughout (ADR-0029 D3). The adoption's request, its response and the set of rows it rebinds are unchanged.
packages/metadata-protocol/src/protocol.ts, as the claim expected.packages/objectql. The hand-over is the existingSchemaRegistry.unregisterObjectfollowed by the save's write-through. This PR builds no owner re-key primitive, so there is nothing new for another card to reuse. objectql/metadata-protocol: a save naming a package of a runtime object the registry holds under the sys_metadata sentinel answers 200, but the registry keeps the package-less definition, so the saved row never goes live #23514 remains open.saveMetaItem,classifyObjectContributionand the D9.9 refusal do not move.What the helper does depends on who owns the adopted name in the registry. Every row below is pinned:
unregisterObjectrefuses; the registry keeps the sentinel until the next restart; oneconsole.errornames the object, the consequence and the remedyA draft object row is rebound in storage only, because the boot registers active rows only (
loadMetaFromDb).The card's first acceptance line: the HTTP route, walked on a full composition
The route was walked over HTTP before the fix and again after it. The composition was
pnpm dev:crm -- --fresh -p 38461, the CRM example with the runtime's package service mounted, under the seeded admin's session.Before. The code was
origin/main57994c311f; branch heade8a22df1badded only the new test file and the changeset.PUT /api/v1/meta/object/qa_probe_gadget, naming no package, answered200: "Saved object 'qa_probe_gadget' (env-wide, state=active)". TwoPOST /api/v1/data/qa_probe_gadgetanswered201,201.POST /api/v1/packages/com.qa.q/adopt-orphansanswered200withreassignedCount: 1. The server log has no registry line for it.PUT /api/v1/meta/object/qa_probe_gadget?package=com.qa.q, adding a fieldserial, answered200: "Saved object … package=com.qa.q". The server log then reads:[Protocol] registerObject failed for qa_probe_gadget: Object "qa_probe_gadget" is already owned by package "sys_metadata". Package "com.qa.q" cannot claim ownership.GET /api/v1/meta/object/qa_probe_gadgetserves label "Gadget Q",_packageIdcom.qa.qand the fieldsnameandserial.POST /api/v1/data/qa_probe_gadgetwithserialanswered400 INVALID_FIELD("Unknown field 'serial'"), and so didGET /api/v1/data/qa_probe_gadget?serial=S-3.After. The head was
535cd8562, withmetadata-protocolrebuilt.Unregistered object: qa_probe_gadget (1 contribution(s), owner sys_metadata)and thenRegistered object: qa_probe_gadget (own, priority=100) from com.qa.q.… from com.qa.q, with no refusal.POSTwithserialanswered201, andGET …?serial=S-3answered200withtotal: 1.A boot after that cannot be walked over HTTP, because
--freshstorage is deleted on exit. The pins below hold the boot half at protocol level.Pins
The pins are in
packages/objectql/src/protocol-adopt-orphans-object-registry.test.ts. They run on a realObjectKernelwithObjectQLPluginand a memory driver; another kernel booted over the same store is the reboot.errornaming the object and Q.registerObjectand nounregisterObjectcall), and the view follows as before.Fixture triage. One existing case in
packages/metadata-protocol/src/protocol.object-registry-write-through-spelling.test.tspinned the defect: "an orphanedobjectsrow not at all", withregisteredObjectsempty. It now pins the fold the new object limb applies: a row stored asobjectsregisters underobject, never under the plural. TheapplyRegistryWriteThroughcall-site count stays 7, because the object limb rides the adoption's one existing call.Ablations (final code head
3061f6f3b)objectql's tests importmetadata-protocolthroughdist/. So each leg mutated the source throughscripts/ablation-replace.mjs(the anchor had to hit), rebuilt the package and proved the marker reacheddist/withscripts/ablation-dist-preflight.mjs.type !== 'object'was mutated, so objects skip again, as onmain.'view' | …union makestype === 'object'unreachable, the mutant's own signature. The preflight read "marker present in 2 built files" (index.js,index.cjs).row.state !== 'active'was changed to a comparison that never holds.3a0c26243aa1equals HEAD, andgit diff HEADon the path is empty.Tests (local)
These ran at
7d0b4d97d, the branch merged withorigin/main547702080, which includes #23659's delete heal:pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.pnpm --filter @objectstack/metadata-protocol test: 236 files passed and 3 skipped; 20563 tests passed and 19 skipped.pnpm --filter @objectstack/objectql typecheck: exit 0.check:test-typecheckreports OK; the new file adds no debt.objectqlprotocol suites, 8 files, 74 passed: the new pin file,protocol-package-lifecycle,protocol-writepath-object-ownership,protocol-runtime-owner-overlay-refusal,protocol-boot-object-package-binding,protocol-object-overlay-layer,protocol-boot-hydration-scopedandprotocol-delete-boot-hydrated-object-heal.runtimepackage-duplicate-adopt-org-scope.integration.test.ts: 4 passed.These ran at
3061f6f3b: the pin file, 6 passed, and theobjectqltypecheck, exit 0. The last commit9e2c7a578only types the pin file's data-plane read options (EngineReadOptionsin place of anas any). After it,check:query-options-erasureholds at 236 test sites, the ceiling.Gates (local)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat9e2c7a578derived 67 commands.--ranreconciliation: "67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED".check:type-check-debtran inside one verify-lock hold, because its closure build needs the lock.3061f6f3b, had two non-zero results, both superseded by the9e2c7a578run:check:query-options-erasurewas red: the test surface grew 236 → 237 through the pin file'sas anyon a read's options.9e2c7a578fixed it.check:type-check-debtansweredPREREQUISITE NOT METbecause it got no turn on the lock.Acceptance notes
ObjectKernelwithObjectQLPlugin; the view limb is byte-identical tomain.registry.getItem('view', NAME)answered nothing.state: draft.error, notwarn. By AGENTS.md's degradation question, storage and the runtime disagree while the adoption answers success. The remedy the line names is a restart.Generated by Claude Code