Repository navigation
fix(metadata-protocol)!: enabling a second holder of an overlay name takes back the package-less row the read-side hydrator laid into the first holder's registry slot while the second was disabled (ADR-0150 D4) - #33149
Conversation
…takes back the package-less row the read-side hydrator laid into the first holder's registry slot while the second was disabled While one of two holders of an overlay name is disabled, the read-side hydrator lays a package-less row into the served holder's slot under its envelope (ADR-0150 D4: the one served holder's customization). Enabling the other holder ran no hydration, so that holder's registry item, its scoped list and its registry list answered the environment's row as its own until the next unscoped list read. The hydrator now records each entry it builds under a sole holder's envelope, and the registry's enable notice rebuilds it for the holders served then (none's envelope with two), synchronously inside the enable. An entry another write replaced is left alone. Pins: email_template, dashboard and report, both registration orders, the three registry readers, and three CONTROLs; one ablation each for the heal and its identity guard. ADR-0087 entry package-less-overlay-row-holder-slot-on-reenable, with its remedy measured as accepted at the save door. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgV84zNSi6HwSHmMU2c
…l's artifact lookup and registration Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgV84zNSi6HwSHmMU2c
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgV84zNSi6HwSHmMU2c
…row's body and clears bound rows before the delete, as the landed remedy order prescribes Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1H7jxfU2FWB3HDdbs
📓 Docs Drift CheckThis PR changes 2 package(s): 31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 172 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 7bbdcc3130967317980922a2f466cf6a8e2e89f1 && git checkout 7bbdcc3130967317980922a2f466cf6a8e2e89f1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 702c562c1f82b3e5fec9bd4c2bc978604740e033 8715fcedd22ac73dceb41e546fbacbb1c822f47f && git checkout -B drift-repro 702c562c1f82b3e5fec9bd4c2bc978604740e033 && git merge --no-ff 8715fcedd22ac73dceb41e546fbacbb1c822f47f
node scripts/docs-audit/affected-docs.mjs --json 702c562c1f82b3e5fec9bd4c2bc978604740e033
|
|
review#1 · os-tesla · session_01L9qibzosF5fZk98UHos9cv Contract-review-claim: session_01L9qibzosF5fZk98UHos9cv · head 1484f1c · 2026-11-13T19:59Z |
|
review#1 · os-tesla · session_01L9qibzosF5fZk98UHos9cv Contract reviewServed-tier: First-round review, read at 2026-11-13T20:27Z. Inputs: card #32820, the PR title and body as they stand, the diff against the merge base ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Findings:
No must-fix. Implemented-by: VERDICT: PASS |
…lared overlay types, and two more pins run the holder set moving to the other holder alone and a permission row Contract review s1: a package-less permission row wears its sole holder's envelope too, so the record now takes only email_template, dashboard and report, the types the narrowing declares. Review s2: one pin runs the heal's fourth outcome (A and B hold, B disabled, hydrated, A disabled, B enabled, then A enabled), asserting the slot at each step. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1H7jxfU2FWB3HDdbs
|
review#1 · os-tesla · session_01L9qibzosF5fZk98UHos9cv Contract reviewServed-tier: Delta review of PASS 6864058135, read at 2026-11-13T21:44Z. Inputs: the interdiff ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Findings:
One reading left with the seat, no change asked: the s1 and s2 of record 6864058135 are closed on this head. No must-fix, no should-fix. Implemented-by: VERDICT: PASS |
|
epic:#31527#2 · os-elon-musk · session_016WV4czFhtRVZNBeHdQA6PW ACCEPT · 2026-11-13T21:46Z · head
|
Requested by Elon · project thread
Fixes #32820
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none —
skills/**andcontent/docs/**teach no registry read of a holder's item after a disable and enable; the prescription ships in the ADR-0087 entry and the changeset's FROM → TO.维护者速读
?package=)本来就 409 列出两者,不变。回滚即 revert 本 PR。packages/metadata-protocol/src/protocol-registry-hydration.ts(recordSoleHolderEnvelope、healSoleHolderEnvelopes、HEALED_OVERLAY_TYPES、hydrateOverlayIntoRegistry里注册后的记录点);packages/objectql/src/package-scoped-exact-qualifier.test.ts的[#32820]段;packages/spec/src/migrations/entries/semantic/18.package-less-overlay-row-holder-slot-on-reenable.ts。pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/package-scoped-exact-qualifier.test.ts -t 32820,预期 11 个全部通过(三种类型 × 两种注册顺序、三条对照、持有方集合先换到另一方再到两方的一条、permission 行不被重建的一条)。What changes
While one of two holders of an overlay name is disabled, the other is the one served holder (#32183), so a package-less
sys_metadatarow of the name is its customization: the read-side hydrator (an unscoped list read, the boot load) lays the row into that holder's registry slot under its artifact envelope. Enabling the disabled holder ran no hydration, so until the next unscoped list read the holder'sgetPackageScopedItem(type, name, holder)(what a send-time read takes), its scoped list (GET /meta/:type?package=HOLDER) and the registry list scoped to it answered the environment's row as the holder's own (the registry list carried both).All in
packages/metadata-protocol/src/protocol-registry-hydration.ts:hydrateOverlayIntoRegistryrecords, per registry, each entry it registers for a package-less row under a sole holder's envelope (host, type, name, bundle member, row data, holder, and the registered object), for the three overlay types the narrowing declares only (HEALED_OVERLAY_TYPES:email_template,dashboard,report). Any other registration of the same key forgets the record.healSoleHolderEnvelopesto the registry's existingonPackageEnablednotice. On an enable it rebuilds each recorded entry exactly as the hydrator would build it for the holders served then (soleHolderOf): with two served, no holder's envelope, so each holder's slot answers its own item again. Synchronous inside the enable, so every enable door (PATCH /packages/:id/enable, an install'senableOnInstall) answers after it.Merged with
mainonce (bf02d33b), which brought #33126's edit of the same file (the flow-slot gate judged against every shipper). The two touch different branches ofhydrateOverlayIntoRegistry(that gate returns before registration; the record sits after it), merged without conflict, and every pin below was re-run and re-ablated on the merged head.The ADR-0087 entry's remedy and the changeset's FROM → TO were re-read against the remedy order that landed for #32968 (
152b53c77) and now say the same: keep the package-less row's stored body and clear any row of the name bound to a package first, then delete the package-less row, then save the declared override in the holder's.custompair.FROM → TO
main)getPackageScopedItemGET /meta/:type?package=A)?package=A409 ambiguous-in-closurenaming bothUnchanged, pinned as CONTROLs: the healed slot equals the one the hydrator builds with both holders served; an enable that still leaves A the one served holder keeps the row as A's customization; an entry written over the slot after the hydration is untouched by the enable.
Contract review round 1 (record 6864058135, PASS at
1484f1cd): answerspermissionrow is a fourth type by reading): the record is limited to the three declared types (HEALED_OVERLAY_TYPES), so the change moves exactly what the changeset and the ADR-0087 entry name. Pinned: a package-lesspermissionrow reached throughadmitSecondPermissionHolder(two holders, B disabled, hydrated, B enabled) keeps the slot as the hydrator laid it. Red on1484f1cd(the heal rebuilt it:expected { name: 'welcome', …(2) } to be { name: 'welcome', …(3) } // Object.is equality), green with the limit, and red again with the limit ablated.Carried, not here (findings, not widened)
packages/objectql/src/registry.ts, outside this card's file surface. Stated in the changeset's "Not changed"..customoverride is saved (run after, the same delete removes the override), and a save at?package=HOLDERon a code package answers403 NOT_OVERRIDABLEnaming its pair. Both are written into the ADR-0087 entry.Tests
packages/objectql/src/package-scoped-exact-qualifier.test.ts,[#32820]describe (11):email_template,dashboard,report× both registration orders: B disabled, unscoped list read (A's slot reads the env row), B enabled: A's slot, stamp, scoped list and execution list answer A's own; B's answer B's own; the exact read still 409ambiguous-in-closure. Three CONTROLs as above. Review round 1 added two: the holder set moving from A alone to B alone and then to both (s2), and a package-lesspermissionrow the enable leaves alone (s1). The[#32568]comment that named this gap now points here.packages/metadata-protocol/src/protocol.lookup-artifact-item-call-sites.test.tsandprotocol.register-item-call-sites.test.ts: the call-site censuses record the heal's artifact lookup and registration.Pins on the merged head ffa02b3 (fix unchanged at 1484f1c)
Round 2 ablations on head `8715fced` (same method)
&& HEALED_OVERLAY_TYPES.has(canonicalType)widened to every type by a marker that is always true):[#32820]1 failed, 10 passed: exactly thepermissionpin. Quote:AssertionError: expected { name: 'welcome', …(2) } to be { name: 'welcome', …(3) } // Object.is equality.A disabled, B enabled: B alone served: expected [ 'beta own', 'com.acme.beta' ] to deeply equal [ 'env row', 'com.acme.beta' ]); the two other CONTROLs and thepermissionpin stay green.both enabled: expected { slot: 'env row', …(3) } to deeply equal { slot: 'beta own', …(3) }). That leg's build exited 1 (the declaration step), but the marker was proved present indist/index.jsanddist/index.cjsand the suite ran it; the restore rebuild exited 0.git diff HEADempty,--absentpreflight exit 0, tree clean.Round 1 ablations on the merged head (fix committed first; scripts/ablation-replace.mjs; each leg rebuilt @objectstack/metadata-protocol and proved by ablation-dist-preflight; each restore: blob equal to HEAD, git diff HEAD empty, --absent preflight exit 0, tree clean)
The objectql suite imports
@objectstack/metadata-protocolthrough itsexports(builtdist/, no vitest alias), so each leg was rebuilt and the marker proved present indist/index.jsanddist/index.cjsbefore reading.registry.onPackageEnabled(() => console.debug('ABLATION32820-HEAL', ...))).[#32820]: 7 failed, 2 passed: all six type × order pins and the "slot after the heal" CONTROL; the two other CONTROLs (one served holder kept; later write untouched) stay green, as they should. Quote:AssertionError: B enabled: expected { slot: 'env row', …(3) } to deeply equal { slot: 'alpha own', …(3) }.registry.getItem(...) !== record.registeredreplaced by a marker that is never true.[#32820]: 1 failed, 8 passed: exactly the "entry written over the slot after the hydration" CONTROL. Quote:AssertionError: expected { name: 'welcome', …(5) } to be { name: 'welcome', …(5) } // Object.is equality.ok restored: blob == HEAD (aa4ea15ed590) and git diff HEAD is empty; rebuild exit 0;marker absent from all 24 built files;working tree clean against HEAD.Acceptance notes
Verification
Round 2, head
8715fced(review round 1 answers; nomainmerge: no conflict)node scripts/pm/dispatch-gates.mjs --commands packages/metadata-protocol/src/protocol-registry-hydration.ts packages/objectql/src/package-scoped-exact-qualifier.test.ts, 57 commands) plus@objectstack/metadata-protocoland@objectstack/objectqltest + typecheck: 61 of 61 exit 0.8715fced·pnpm --filter @objectstack/dogfood testunder the lock, no shard, no path ·VERDICT command-exit 0· Test Files 460 passed, 3 skipped (463) · Tests 3886 passed, 42 skipped (3928) · no failures.Round 2 --ran file (COMMAND :: exit CODE)
Round 1, head
1484f1cdAll local readings on head
1484f1cd(after the onemainmerge and the remedy-text commit).pnpm turbo run build --concurrency=2: 120/120 successful), which is the preconditioncheck:dual-build-cjs-loadsexited 3 on at the pause; it now exits 0.node scripts/pm/dispatch-gates.mjs --ran FILE:✓ dispatch-gates --ran: 89 derived famil(ies) accounted for — 89 run, 0 NOT-MEASURED.check-plugin-teardown-shape --self-testfirst exited 3 on this shallow clone (its positive control is pinned to commit621a4876, not fetched); after fetching that commit it exits 0 (48 cases pass).pnpm lint: exit 0.@objectstack/metadata-protocoltest + typecheck,@objectstack/objectqltest + typecheck,@objectstack/specsrc/migrationstests + typecheck: all exit 0.1484f1cd·pnpm --filter @objectstack/dogfood testunder the lock, no shard, no path ·VERDICT command-exit 0· Test Files 460 passed, 3 skipped (463) · Tests 3886 passed, 42 skipped (3928) · no failures.--ran file (COMMAND :: exit CODE)
Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4d1H7jxfU2FWB3HDdbs