fix(scripts): fail on a new bare-name registry collision (objectui#9264) - #9531
Conversation
`Registry.register` writes a bare-name fallback next to the namespaced key and the bare write is last-one-wins, so two registrations claiming one bare name under different full types let import order decide which declaration governs an authored node. A runtime warn surfaces that; nothing failed on it, and `registerLazy` does not even warn. - `check-registry-bare-name-collisions.mjs` sweeps every shipped registration in `packages/`, `apps/` and `examples/`, groups claims by the bare key each writes or declines, and fails on a contested key absent from a shrink-only ledger that carries the claimants. - `unit-registry-collision.mjs` gains `readRegistrationClaims`, the walker `readOwnRegistrations` already had, exported so a caller needing the claimant rather than the key does not write a second AST walker. Its `parse` now picks the script kind from the file name: a `.tsx` source parsed as TS loses a registration's meta object to the JSX, so a `skipFallback: true` call read as a bare-name claimant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa
The same sweep read 1633 source files before `turbo run build` and 1639 after it — `apps/site/.next`, `apps/site/.source` and a generated `plugin.d.ts`. A population that moves with whether someone has built is one no two runs agree on, which is the neighbour of the defect objectui#9256's table records (an unbuilt tree answered `any`, and `any` read as "reads neither"). `isGeneratedPath` excludes dot-segments and emitted declaration files, and the test asserts no population file is generated output. Measured after the fix: zero git-ignored files remain in the population. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa
Contract reviewServed-tier: ① Derived judgmentsNo published surface can have moved, and that is structural rather than argued. The diff is four files: The one real Clause-② risk is the shared module, and it was measured, not assumed.
⭐ The
⇒ the changed branch is unreachable for that gate's population. Its answer cannot move. ⭐ And the PR pins the premise rather than asserting it — the new test re-derives the unit shape and file list at The measurement the gate rests on carries controls in both directions, which is what makes its single finding credible: the five ⭐ A third instrument defect was found HERE and reported rather than quietly corrected — and in the direction that manufactures findings: a ② Semver levelNone. No published package changes. No changeset is owed and ③ Boundary flags⛔ None raised. Checked specifically, each against the dispatch's own prohibitions:
⭐ The ablation's honesty is load-bearing and is why the rest reads as measurement: one predicted element came out wrong and was reported as measured — Implemented-by: VERDICT: PASS Rulings on the two open questionsQ1 — the PR's Q2 — measurement and gate as ONE script ⇒ ⭐ A, and the reasoning is better than my instruction. ⭐ Consequence for objectui#9256 — a state I wrote an hour agoI set objectui#9256 to ⛔ Not written yet — the reading lives in this PR and a review may still move it. On merge, objectui#9256 returns to ⛔ Held26 of 33 checks green on
Generated by Claude Code |
Fixes #9264
ComponentRegistry.registerwrites a bare-name fallback alongside the namespaced key —its own branch is
if (meta?.namespace && !meta?.skipFallback)— and the bare write islast-one-wins. So when two registrations claim one bare name under DIFFERENT full types,
which declaration governs an authored node depends on module import order, and the symptom
is not an error: it is the wrong renderer for a node that type-checks fine. A runtime warn
already surfaces that on
register; nothing FAILED on it, andregisterLazytakes thesame branch with no guard at all.
The measured population is on the card:
#9264 (comment) — posted
before this pull request, as the dispatch required.
What lands
scripts/check-registry-bare-name-collisions.mjs— sweeps every shipped registrationin
packages/,apps/andexamples/, groups claims by the bare key each one writes ordeclines, and FAILS on a contested key absent from a shrink-only ledger.
--tableand--jsonprint the same derivation, so the measurement and the gate cannot drift apart.scripts/__tests__/registry-bare-name-collisions.test.ts— 21 cases. The sweep overthe real tree is the gate (it runs in the
unitproject, sopnpm testcarries it); thejudgement is also exercised on PLANTED populations, because a gate whose red path is only
reachable by breaking the repository is a gate nobody has seen fail.
scripts/unit-registry-collision.mjs— additive only.readRegistrationClaimsisobjectui#7134's own walker, factored out so a caller needing the CLAIMANT rather than the
key does not write a second AST walker. ⛔ No existing export changed meaning:
readOwnRegistrationsis now a projection of it at the default options, and objectui#7134'ssuite passes unchanged.
package.json—check:registry-bare-namesalias.The predicate, and why it is not "more than one registration"
⭐ A bare key is contested when its claimants name two or more DIFFERENT full types. A
console lazy stub and its plugin claiming one bare key under the SAME full type is this
repository's normal shape — the shape objectui#6416 converged
plugin-reportonto — and sucha key has one owner by construction whichever phase a host observes. Counting registrations
instead would have reported all thirty of those pairs as defects.
Three dispositions are carried rather than filtered, so "did not contest" never collapses
into "was not seen": STOOD DOWN (
skipFallback: true), GUARDED (insideif (!ComponentRegistry.get(type))), and OPEN (a registrar helper's parameter, declared withits reason and re-confirmed every run).
Measured
skipFallbackstand-downdashboardThe card's inherited "19 names, eight cross-package" does not survive re-derivation: sixteen
of the sixteen named keys are already settled (twelve by a
skipFallbackstand-down, three byclaimants that agree on one full type, one claimed by nobody), and the one key that IS
contested is on neither inherited list. Per-key detail is in the card comment linked above.
Three instrument defects, reported rather than quietly corrected
call site plus the key name, never a type's name.
any— structurally absent: no type checker, no.d.ts.running on both trees found its neighbour and it was real: 1633 files unbuilt, 1639
built (
apps/site/.next,apps/site/.source, a generatedplugin.d.ts). Fixed; thepopulation is 1622 and zero of its files are git-ignored.
.tsxsource parsed as plain TS silently loses the registration's metaobject. The inherited walker used
ScriptKind.TSfor everything; in a.tsxrenderer theJSX swallows the rest of the argument list, so a
skipFallback: truecall read as abare-name claimant. Before the fix the sweep reported THREE contested keys, two of them
(
calendar,calendar-view) pure artefacts. Inert for objectui#7134's population, whichholds zero
.tsxfiles — pinned by a test.Ablation — direction predicted in writing first, then measured
Prediction, written before the mutation: deleting the single
skipFallback: trueline inpackages/plugin-grid/src/index.tsxmakesview:grida live claimant of baregridbesideui:grid⇒ verdictsole→contested⇒new-bare-name-collision, exit 1, naminggridandboth full types.
skipFallback: truecount 1 → 0, injected marker count 0 → 1, blob5fa94c94b…→87660ea44…bare key "grid", namingui:gridandview:gridwith both files; contested count 1 → 25fa94c94b…= theHEADblob;git diff HEADis 0 bytes; marker count back to 0,skipFallbackcount back to 1standDownNames59 → 58. It stayed 59, because that count is per-KEY andgridkeeps itsfield:gridstand-down. Direction and every other predicted element held.distpreflight: this derivation reads source and is not resolved through any builtpackage, so there is no built artefact for a mutation to fail to reach. The on-disk proof
above (anchor counts plus blob hashes) is unconditional and was done.
Deliberately NOT done
skipFallback: truewas added to any registration. That decides which renderer a barename resolves to — a behaviour change needing its own ruling per collision.
register()does not throw. The gate is in CI, not in the runtime path.successor; objectui#9256 is not addressed here.
dashboardruling is not taken. The ledger entry states the question and theclaimants; choosing
view:dashboardorplugin-dashboard:dashboardis the decision thiswork makes possible, not one it makes.
Verification
pnpm exec vitest run scripts/__tests__/registry-bare-name-collisions.test.tsunit-registry-absence-collision,check-doc-component-types,known-schema-types-derivation-5115)turbo run buildcheck:entry-guard·check:control-bytes·check:node-esm-load·check:new-line-citations·check-test-path-rootstype-check:scripts·lint:root·lint:coveragecheck-changeset-presenceRoot
eslint . --no-inline-config(not a CI gate here;pnpm lintisturbo run lint) reports95 errors over 4994 files on this branch — a live scan, and zero of them in the three files
this branch touches, each of which lints with 0 errors and 0 warnings.
Acceptance notes
Noted while measuring, not filed and not touched by this branch:
dashboardcollision has the same SECOND consequence objectui#6416 recorded:register()clears the lazy stub for the type it registers (
view:dashboard), so theplugin-dashboard:dashboardstub is never cleared —hasLazystays true forever and nocomponent is ever stored under it, while the generated CLI whitelist lists all three
spellings as renderable. Read off
Registry.register's own lazy-clearing lines; notre-measured in a live registry, and not acted on here.
deriveRegistryKeysattributes an indirect collection's keys to the FILE rather than to acall site, and a loop registering N names attributes all N to one
file:line. Correct forthe key universe it exists to build; it is the reason this gate derives claimants from the
AST walker instead.
🤖 Generated with Claude Code
https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa
Generated by Claude Code