Repository navigation
fix(core): report a bare-name collision at both registry doors, across both tables - #9838
Conversation
…s both tables `Registry.registerLazy` took the same `namespace && !skipFallback` bare-name fallback branch as `register` with no collision check at all, and `register`'s own check read only `this.components`. The two doors write different tables, so a contest that spans them was reported by neither: replaying this repository's 425 declared registration claims against a real registry emitted zero collision warnings in every order, including for the one genuinely contested bare key. Both doors now consult both tables and key on the declared full type, so the contest is reported whichever order the declarations arrive in. The predicate was derived from the census rather than chosen: of the 31 bare keys a lazy stub claims, 30 name exactly one full type across both tables (the stub-then-real lifecycle, which stays silent), and nine of those are stubbed twice with different loader closures, which is why loader identity is not part of the key. The two doors give different advice on purpose. `registerLazy` names `skipFallback: true`, which settles it there. `register` does not, because it clears the bare stub whether or not the fallback is taken, so that opt-out would leave the bare key resolving to nothing at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Re:
|
| pending changeset | what its body claims about this file | still true? |
|---|---|---|
6067-component-meta-derive-from-canonical.md |
the shape of Registry.ts's own ComponentMeta declaration — its thirteen keys, the four registry-only ones (tier, namespace, skipFallback, labelling), and tags / description being absent |
⭐ yes |
componentinput-reexport-4972.md |
ComponentInput being a re-export |
⭐ yes |
default-children-retired-5051.md |
defaultChildren being gone from all three declarations |
⭐ yes |
Why, measured rather than recalled. This change adds 83 lines and removes none in packages/core/src/registry/Registry.ts, in two places: a lazyStubFullType helper beside LazyEntry, and two diagnostic blocks inside register and registerLazy. Grepping only the added lines for the three bodies' subjects (ComponentMeta, ComponentInput, defaultChildren) returns 0, with a control in the same command shape returning 11 on the subjects this diff does move (lazyEntries, skipFallback, fullType). No import type line is added or removed. ⇒ an admissible zero, ⛔ not a bare one.
6067 names skipFallback, and this diff mentions skipFallback repeatedly. It names it as one of the four registry-only keys on the ComponentMeta declaration. This change neither adds nor removes any key on that declaration — it only reads meta?.skipFallback inside two guard conditions. The sentence is about the declaration's key set, and the key set did not move. ⇒ the claim stands as written.
⛔ No changeset body needs correcting, and none was edited.
domain:ui#2 execution seat · session_018HrVaotisyhgmot9o2MLRq · gate answer · readings taken 2026-09-18T08:40Z
Generated by Claude Code
Fixes #9821
Clause-②: no
Registry.registerLazytook the samenamespace && !skipFallbackbare-name fallback branch asregisterwith no collision check at all. The repair is not the obvious one, and the census is what decided its shape.1. The falsification came first, and triage's inference SURVIVED it
The dispatch's first task: read objectui#9533's reproduction and determine whether its two lazy declarations are the two console files, a third-party package, or a fixture.
They are the two console files. Three sources agree, and I took the third myself:
apps/console/src/preview-gallery.tsx:36andapps/console/src/register-plugins.ts:65, bothregisterLazy, bothplugin-dashboard:dashboard; the third claimant ispackages/plugin-dashboard/src/index.tsx:79,register,view:dashboardRegistryfed the package's real declared metadata, labelled=== fresh Registry, stub then eager (what the console does) ===, ending=== collision warnings seen === 0. The probe file itself was deleted by that delivery, so the transcript is the durable artefactorigin/main26ac5036, 2026-09-18T08:15Znode scripts/check-registry-bare-name-collisions.mjs --jsonreports thedashboardgroup with exactly those three claimants, at those files and lines⇒ triage's inference holds: copying the existing guard into
registerLazywould not have caught the card that produced this one. Both lazy claimants declare ONE full type, so a lazy-vs-lazy comparison excuses them, and the contest is against the other table.2. The cross-table census — taken first, because it decides which table the predicate keys on
All readings on
origin/main26ac5036, taken 2026-09-18T08:15Z. PR objectui#9803 had NOT landed (re-checked at 08:31Z: open, draft, unmerged, head588e4595), so thedashboardcontest is still live on the tree these numbers describe. That PR does not touchpackages/core/src/registry/Registry.ts; it does touch the collision gate I used as an instrument, which is why the ref and time are on every row.register/ 40registerLazy)dashboardThe dynamic half, replaying those 425 declared claims against a real
Registry(temporary probe, since deleted):skipFallback: trueThe
beforezeros are admissible because the control in the same command hits: the existing guard is alive and fires on the shape it was written for, and it still never fires anywhere on this repository's own declared population. The singleafterwarning is thedashboardcontest — no other key moved.3. The predicate, derived rather than chosen
Both tables, keyed on the declared full type.
lazyEntriesagainst a loaded registration incomponents. A same-table check sees neither side of it.⇒ the dispatch's live stop condition is not met: no first-party correct sequence depends on the silence, and the predicate that excludes the correct ones is exactly the one the census names.
4. Why
registerchanged too, and why that is this card and not anotherregister's existing guard as well as adding one toregisterLazy. Both are inside the declared file surface, both are diagnostic-only, and the reason is measured, not stylistic.In the order this repository actually boots — stubs at module scope, plugin chunks later — the silent overwrite happens at
register, whose guard readsthis.componentsonly and therefore sees nothing while a stub holds the bare key. Ablation leg 2 below is the proof: with theregisterLazyhalf in place and theregisterhalf removed, the test named for objectui#9533's own order goes red. A fix that added only the lazy-door check would leave this card's own measured instance emitting zero warnings at boot, and would report the contest only in the order this repository does not start in — the failure modereport-bare-key-ownership.test.tsandtimeline-bare-key-ownership.test.tsreplay both orders to prevent.⛔ Nothing about what the registry ACCEPTS changed, and
skipFallback's behaviour is untouched — only what the registry SAYS. That is theClause-②: nothe claim declared, and the census did not push against it.5. The warning text — written last, and the two doors deliberately differ
The filing seat flagged that
register's existing wording prescribesskipFallbackas the remedy and that this may be wrong advice before a module has loaded. It is wrong advice — measurably, and only on one of the two doors:registerLazy,skipFallback: truereally does settle it: the stub then claims onlynamespace:typeand leaves the bare key alone. The new message says so.register, it does not. That door clears the bare stub outside the fallback branch, so it deletes the stub whether or not it takes the bare key — passskipFallback: truethere and the bare key ends up claimed by nobody at all. The new message asks for the two declarations to agree on one full type instead, and says why the opt-out does not settle this one.skipFallback on the EAGER door does not preserve the stub's bare claimpins that behaviour, so the advice rests on an assertion rather than on prose.Tests
All readings at final head
f502af2d.pnpm exec vitest run packages/core/src/registry/__tests__/Registry.test.ts—Test Files 1 passed (1) / Tests 49 passed (49); the 8 new cases were confirmed by name in--reporter=verbose, not inferred from the count.packages/core/ packages/fields/plusregistration-uniqueness,html-page-lazy-blocks,react-page-scope,react-page-state,report-bare-key-ownership,timeline-bare-key-ownership,plugin-map/src/index.registration,unfulfilled-chart-stubs-retired-8760,node-slot-registered-arms-8499,phase2-schemas,registry-bare-name-collisions,unit-registry-absence-collision—Test Files 333 passed (333) / Tests 6306 passed (6306).apps/consoleregistry parity and chart-stub tests,plugin-calendarandplugin-kanbankey tests, all ofpackages/plugin-dashboard/—Test Files 124 passed (124) / Tests 1393 passed (1393).pnpm --filter @object-ui/core type-checkexit 0, afterpnpm --filter '@object-ui/core^...' build(exit 0). The script runstsc --noEmitandtsc -p tsconfig.test.json, so the new tests are inside the typechecked set by construction.check:registry-bare-names(237 bare key(s) claimed, 59 declined with skipFallback; 1 contested; ledger holds 1— unchanged by this diff, as it must be: no registration site moved),check:changeset-presence,check:changeset-no-major,check:changeset-claims,check:pending-changeset-literals,check:control-bytes,check:new-line-citations(0 new citations),check:test-path-roots,check:doc-types.eslint --no-inline-configon the two edited files gives 0 errors and 6 warnings; linting the BASE copy of each file gives the same 0 errors and 6 warnings, allno-explicit-anyat lines that predate this branch. ⇒ 0 new. The repository-wide run belongs to CI.Ablation — two legs, both against the committed fix, direction predicted and observed: TURNS RED. Each leg proved its mutation reached disk (anchor matched exactly once, injected marker counted 1, blob hash differs from the HEAD blob) and proved its restore by an empty
git diff HEADplus a hash equal to the HEAD blob, with an empty hash treated as failure and an EXIT/INT/TERM trap holding an absolute repository path. No build is in this path:vitest.config.mtsaliases@object-ui/coretopackages/core/src, and the test imports../Registrydirectly, so there is no stale-distchannel that could keep a broken assertion green.registerLazycollision guard deletedregister's guard deletedwarns when a LOADED registration takes a bare key a pending stub claims (the objectui#9533 order)⭐ Leg 2's red set is what makes the second half of the fix a measurement rather than an opinion: leg 1 does not cover that case, so the lazy door alone leaves the boot-order instance unreported.
Acceptance notes
ledger holds 1. Preserved from the filing seat and passed on unchanged.dashboardcontest itself. objectui#9533 is ruled and in flight as PR objectui#9803 (open, draft at 08:31Z). This card adds the detector; that card converges the key. If objectui#9803 lands, the single warning measured above goes away and the detector reports nothing on this tree — which is the correct end state for a detector, and the 30 silent keys plus the ablation legs are what keep it non-vacuous.needs:contract-reviewis the seat's to hang or remove; this PR neither carries nor requests it.Generated by Claude Code