Problem
ObjectRegistry.getClassNames() returns class names deduplicated by simple name. When two packages register classes with the same simple name (e.g. both ship an Event), getClassNames() returns the name once and downstream callers — including schema-bootstrap loops in test helpers and tooling — resolve it via getClass(simpleName) → findClass's multi-strategy lookup, which picks one arbitrary registration. The other colliding class never gets iterated, never gets its schema prepared, never participates in the loop.
This is a pre-existing architectural seam that R5-canon's qualified-name canonicalization made more visible. R5-canon (#1274) made getInheritanceChain / getSTIBase / getDescendants return qualified names internally so call-site comparisons are unambiguous, but iteration over getClassNames() itself stays simple-name-keyed.
Concrete failure mode (from #1274 Copilot review)
packages/core/src/testing/database.ts:216 — getTestDatabase iterates ObjectRegistry.getClassNames() to bootstrap schema. The R5-canon fixup at that line computes the qualifiedClassName for the comparison, but the loop input is already deduplicated. In a collision, only one of two same-simple-name classes is iterated; the other's table is never created.
Other likely affected sites worth auditing:
packages/cli/src/commands/utilities.ts — db:setup / dependency-order printer also iterate getClassNames() / getInitializationOrder() and apply per-class schema work.
packages/core/src/registry/schema-builder.ts — getAllSchemas iterates getClasses() directly (the underlying Map keyed by registrationKey, which IS qualified). So this path is collision-safe already; just confirming.
- Any other consumer that calls
getClassNames() and treats each entry as a unique identity.
Proposed direction
-
Add a new registry API for qualified-name iteration:
/**
* Iterate every registered class by qualified name (or simple name
* when the registration has no package context). Unlike
* `getClassNames()`, this preserves cross-package collisions —
* two packages shipping the same simple name appear as two
* separate entries.
*/
static getQualifiedClassNames(): string[];
Underlying iteration is over getClasses().keys() (or getClasses().values() mapping to qualifiedName ?? name).
-
Migrate collision-sensitive callers to the new API. At minimum:
testing/database.ts:getTestDatabase schema bootstrap loop.
cli/commands/utilities.ts schema-related iterations.
- Audit the rest for ones that should switch.
-
Keep getClassNames() for backward compatibility (the dedup-by-simple-name behavior is fine for display / discovery contexts where collisions are reported separately). Document the trade-off.
-
Add a regression test that registers two same-simple-name classes under different packages, iterates via the new API, and confirms both get their tables prepared.
Why this wasn't fixed in #1274
R5-canon's PR was already a ~20-file refactor of the registry's core lookup invariants. Adding a new public API + migrating multiple call sites is a separate concern with its own scope and review surface. Filed here so it doesn't get lost.
References
Problem
ObjectRegistry.getClassNames()returns class names deduplicated by simple name. When two packages register classes with the same simple name (e.g. both ship anEvent),getClassNames()returns the name once and downstream callers — including schema-bootstrap loops in test helpers and tooling — resolve it viagetClass(simpleName)→findClass's multi-strategy lookup, which picks one arbitrary registration. The other colliding class never gets iterated, never gets its schema prepared, never participates in the loop.This is a pre-existing architectural seam that R5-canon's qualified-name canonicalization made more visible. R5-canon (#1274) made
getInheritanceChain/getSTIBase/getDescendantsreturn qualified names internally so call-site comparisons are unambiguous, but iteration overgetClassNames()itself stays simple-name-keyed.Concrete failure mode (from #1274 Copilot review)
packages/core/src/testing/database.ts:216—getTestDatabaseiteratesObjectRegistry.getClassNames()to bootstrap schema. The R5-canon fixup at that line computes thequalifiedClassNamefor the comparison, but the loop input is already deduplicated. In a collision, only one of two same-simple-name classes is iterated; the other's table is never created.Other likely affected sites worth auditing:
packages/cli/src/commands/utilities.ts—db:setup/ dependency-order printer also iterategetClassNames()/getInitializationOrder()and apply per-class schema work.packages/core/src/registry/schema-builder.ts—getAllSchemasiteratesgetClasses()directly (the underlying Map keyed byregistrationKey, which IS qualified). So this path is collision-safe already; just confirming.getClassNames()and treats each entry as a unique identity.Proposed direction
Add a new registry API for qualified-name iteration:
Underlying iteration is over
getClasses().keys()(orgetClasses().values()mapping toqualifiedName ?? name).Migrate collision-sensitive callers to the new API. At minimum:
testing/database.ts:getTestDatabaseschema bootstrap loop.cli/commands/utilities.tsschema-related iterations.Keep
getClassNames()for backward compatibility (the dedup-by-simple-name behavior is fine for display / discovery contexts where collisions are reported separately). Document the trade-off.Add a regression test that registers two same-simple-name classes under different packages, iterates via the new API, and confirms both get their tables prepared.
Why this wasn't fixed in #1274
R5-canon's PR was already a ~20-file refactor of the registry's core lookup invariants. Adding a new public API + migrating multiple call sites is a separate concern with its own scope and review surface. Filed here so it doesn't get lost.
References
testing/database.ts:216flagged the original concern