@happyvertical/smrt-core 0.51.24 → 0.51.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/AGENTS.md +17 -0
  2. package/agents/schema-paths.md +7 -0
  3. package/dist/index.js +1 -1
  4. package/dist/manifest/manifest-loader.d.ts +12 -0
  5. package/dist/manifest/manifest-loader.d.ts.map +1 -1
  6. package/dist/manifest/manifest-loader.js +54 -10
  7. package/dist/manifest/manifest-loader.js.map +1 -1
  8. package/dist/manifest/static-manifest.js +1 -1
  9. package/dist/manifest/static-manifest.js.map +1 -1
  10. package/dist/manifest/store.d.ts.map +1 -1
  11. package/dist/manifest/store.js +7 -2
  12. package/dist/manifest/store.js.map +1 -1
  13. package/dist/manifest.json +1 -1
  14. package/dist/migrations/differ.d.ts +6 -3
  15. package/dist/migrations/differ.d.ts.map +1 -1
  16. package/dist/migrations/differ.js +11 -5
  17. package/dist/migrations/differ.js.map +1 -1
  18. package/dist/object.d.ts.map +1 -1
  19. package/dist/object.js +26 -26
  20. package/dist/object.js.map +1 -1
  21. package/dist/registry/__tests__/helpers/consumer-workspace.d.ts +42 -0
  22. package/dist/registry/__tests__/helpers/consumer-workspace.d.ts.map +1 -0
  23. package/dist/registry/class-registration.d.ts +15 -0
  24. package/dist/registry/class-registration.d.ts.map +1 -1
  25. package/dist/registry/class-registration.js +171 -21
  26. package/dist/registry/class-registration.js.map +1 -1
  27. package/dist/registry/collection-resolution.d.ts +8 -2
  28. package/dist/registry/collection-resolution.d.ts.map +1 -1
  29. package/dist/registry/collection-resolution.js +12 -2
  30. package/dist/registry/collection-resolution.js.map +1 -1
  31. package/dist/registry/collision-policy.d.ts +10 -0
  32. package/dist/registry/collision-policy.d.ts.map +1 -1
  33. package/dist/registry/collision-policy.js +7 -1
  34. package/dist/registry/collision-policy.js.map +1 -1
  35. package/dist/registry/inheritance-resolver.js +1 -1
  36. package/dist/registry/name-resolver.d.ts.map +1 -1
  37. package/dist/registry/name-resolver.js +11 -0
  38. package/dist/registry/name-resolver.js.map +1 -1
  39. package/dist/registry/schema-builder.d.ts.map +1 -1
  40. package/dist/registry/schema-builder.js +71 -31
  41. package/dist/registry/schema-builder.js.map +1 -1
  42. package/dist/registry/shared-state.d.ts.map +1 -1
  43. package/dist/registry/shared-state.js +3 -4
  44. package/dist/registry/shared-state.js.map +1 -1
  45. package/dist/registry/types.d.ts +3 -1
  46. package/dist/registry/types.d.ts.map +1 -1
  47. package/dist/registry.d.ts.map +1 -1
  48. package/dist/registry.js +25 -8
  49. package/dist/registry.js.map +1 -1
  50. package/dist/scanner/manifest-generator.js +1 -1
  51. package/dist/smrt-knowledge.json +7 -7
  52. package/dist/testing/database.js +1 -1
  53. package/dist/utils/stack-frames.d.ts +26 -0
  54. package/dist/utils/stack-frames.d.ts.map +1 -1
  55. package/dist/utils/stack-frames.js +79 -2
  56. package/dist/utils/stack-frames.js.map +1 -1
  57. package/package.json +4 -4
@@ -19,8 +19,14 @@ export interface CollectionRegistrationLookup {
19
19
  * creates a base-columns-only table, dropping the junction's FK columns
20
20
  * (#1342 — `place_assets` vs `profile_assets` asymmetry).
21
21
  *
22
+ * `SmrtReportCollection` (`@happyvertical/smrt-reports`) is the same case: a
23
+ * framework collection base that is never registered, so a report collection
24
+ * (`class XCollection extends SmrtReportCollection<X>`) was classified as a
25
+ * table-bearing class and claimed its item's table. The schema planner then
26
+ * rejected the pair as two unrelated owners of one table (#3110).
27
+ *
22
28
  * Keep in sync with the collection bases that extend `SmrtCollection` in
23
- * `packages/core/src/junction.ts`. (`SmrtHierarchical` and
29
+ * `packages/core/src/junction.ts` and `packages/reports/src/report.ts`. (`SmrtHierarchical` and
24
30
  * `SmrtPolymorphicAssociation` extend `SmrtObject`, not `SmrtCollection`, so
25
31
  * they deliberately do NOT belong here.)
26
32
  *
@@ -36,7 +42,7 @@ export interface CollectionRegistrationLookup {
36
42
  * duplicate detector is exactly how the #1342 junction schema-drop bug
37
43
  * half-survived the original core fix.
38
44
  */
39
- export declare const SMRT_COLLECTION_BASE_NAMES: readonly ["SmrtCollection", "SmrtJunction"];
45
+ export declare const SMRT_COLLECTION_BASE_NAMES: readonly ["SmrtCollection", "SmrtJunction", "SmrtReportCollection"];
40
46
  export declare function isSmrtCollectionExtendsName(extendsName: string | undefined): boolean;
41
47
  export declare function resolveRelatedRegistration(className: string, origin: RegisteredClass, lookup: CollectionRegistrationLookup): RegisteredClass | undefined;
42
48
  export declare function getCollectionChainRegistrations(className: string, registered: RegisteredClass, lookup: CollectionRegistrationLookup): RegisteredClass[];
@@ -1 +1 @@
1
- {"version":3,"file":"collection-resolution.d.ts","sourceRoot":"","sources":["../../src/registry/collection-resolution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,GAAG,SAAS,CAAC;IACzD,kBAAkB,CAAC,EAAE,CACnB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,KACd,eAAe,GAAG,SAAS,CAAC;IACjC,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;CACtD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,0BAA0B,6CAG7B,CAAC;AAEX,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAOT;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,4BAA4B,GACnC,eAAe,GAAG,SAAS,CAa7B;AAED,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,4BAA4B,GACnC,eAAe,EAAE,CAiBnB;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,4BAA4B,GACnC,OAAO,CAQT;AA+DD,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,4BAA4B,GACnC,MAAM,GAAG,SAAS,CAmCpB"}
1
+ {"version":3,"file":"collection-resolution.d.ts","sourceRoot":"","sources":["../../src/registry/collection-resolution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,GAAG,SAAS,CAAC;IACzD,kBAAkB,CAAC,EAAE,CACnB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,KACd,eAAe,GAAG,SAAS,CAAC;IACjC,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;CACtD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,0BAA0B,qEAI7B,CAAC;AAEX,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAOT;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,4BAA4B,GACnC,eAAe,GAAG,SAAS,CAa7B;AAED,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,4BAA4B,GACnC,eAAe,EAAE,CAiBnB;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,4BAA4B,GACnC,OAAO,CAQT;AA+DD,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,4BAA4B,GACnC,MAAM,GAAG,SAAS,CAmCpB"}
@@ -14,8 +14,14 @@
14
14
  * creates a base-columns-only table, dropping the junction's FK columns
15
15
  * (#1342 — `place_assets` vs `profile_assets` asymmetry).
16
16
  *
17
+ * `SmrtReportCollection` (`@happyvertical/smrt-reports`) is the same case: a
18
+ * framework collection base that is never registered, so a report collection
19
+ * (`class XCollection extends SmrtReportCollection<X>`) was classified as a
20
+ * table-bearing class and claimed its item's table. The schema planner then
21
+ * rejected the pair as two unrelated owners of one table (#3110).
22
+ *
17
23
  * Keep in sync with the collection bases that extend `SmrtCollection` in
18
- * `packages/core/src/junction.ts`. (`SmrtHierarchical` and
24
+ * `packages/core/src/junction.ts` and `packages/reports/src/report.ts`. (`SmrtHierarchical` and
19
25
  * `SmrtPolymorphicAssociation` extend `SmrtObject`, not `SmrtCollection`, so
20
26
  * they deliberately do NOT belong here.)
21
27
  *
@@ -31,7 +37,11 @@
31
37
  * duplicate detector is exactly how the #1342 junction schema-drop bug
32
38
  * half-survived the original core fix.
33
39
  */
34
- var SMRT_COLLECTION_BASE_NAMES = ["SmrtCollection", "SmrtJunction"];
40
+ var SMRT_COLLECTION_BASE_NAMES = [
41
+ "SmrtCollection",
42
+ "SmrtJunction",
43
+ "SmrtReportCollection"
44
+ ];
35
45
  function isSmrtCollectionExtendsName(extendsName) {
36
46
  if (!extendsName) return false;
37
47
  return SMRT_COLLECTION_BASE_NAMES.some((base) => extendsName === base || extendsName.endsWith(`:${base}`));
@@ -1 +1 @@
1
- {"version":3,"file":"collection-resolution.js","names":[],"sources":["../../src/registry/collection-resolution.ts"],"sourcesContent":["import type { RegisteredClass } from './types';\n\nexport interface CollectionRegistrationLookup {\n findClass: (name: string) => RegisteredClass | undefined;\n findClassInPackage?: (\n packageName: string,\n className: string,\n ) => RegisteredClass | undefined;\n getInheritanceChain: (className: string) => string[];\n}\n\n/**\n * Framework base classes that ARE collections (extend `SmrtCollection`).\n *\n * `SmrtJunction` is an abstract collection base — junction collections declare\n * `extends SmrtJunction<Item>`, and the manifest records that *direct* parent.\n * `SmrtJunction` is itself never registered in a consumer's registry, so the\n * inheritance-chain walk in `isCollectionRegistration()` cannot reach\n * `SmrtCollection` through it. Recognizing the junction base by name here is\n * what keeps a junction Collection classified as a collection (and therefore\n * redirected to its item class for schema generation) regardless of\n * registration order. Without it, a junction Collection that is iterated\n * before its item class is mistaken for a regular table-bearing class and\n * creates a base-columns-only table, dropping the junction's FK columns\n * (#1342 — `place_assets` vs `profile_assets` asymmetry).\n *\n * Keep in sync with the collection bases that extend `SmrtCollection` in\n * `packages/core/src/junction.ts`. (`SmrtHierarchical` and\n * `SmrtPolymorphicAssociation` extend `SmrtObject`, not `SmrtCollection`, so\n * they deliberately do NOT belong here.)\n *\n * A new framework collection base must also be added to the other parallel\n * framework-base lists so it is recognized everywhere it matters:\n * - `FRAMEWORK_BASE_CLASSES` in\n * `packages/scanner/src/inheritance-resolver.ts` (scanner chain termination).\n * - `FRAMEWORK_ABSTRACT_BASE_NAMES` in\n * `packages/core/src/scanner/manifest-generator.ts` (field-merge skipping).\n *\n * Downstream tooling that needs this detector (e.g. @happyvertical/smrt-vitest)\n * should IMPORT it from `@happyvertical/smrt-core` rather than keep a copy — a\n * duplicate detector is exactly how the #1342 junction schema-drop bug\n * half-survived the original core fix.\n */\nexport const SMRT_COLLECTION_BASE_NAMES = [\n 'SmrtCollection',\n 'SmrtJunction',\n] as const;\n\nexport function isSmrtCollectionExtendsName(\n extendsName: string | undefined,\n): boolean {\n if (!extendsName) {\n return false;\n }\n return SMRT_COLLECTION_BASE_NAMES.some(\n (base) => extendsName === base || extendsName.endsWith(`:${base}`),\n );\n}\n\nexport function resolveRelatedRegistration(\n className: string,\n origin: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): RegisteredClass | undefined {\n if (className.includes(':')) {\n return lookup.findClass(className);\n }\n\n if (origin.packageName && lookup.findClassInPackage) {\n return (\n lookup.findClassInPackage(origin.packageName, className) ||\n lookup.findClass(className)\n );\n }\n\n return lookup.findClass(className);\n}\n\nexport function getCollectionChainRegistrations(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): RegisteredClass[] {\n const lookupName = registered.qualifiedName || registered.name || className;\n const chain = lookup.getInheritanceChain(lookupName);\n const registrations: RegisteredClass[] = [];\n\n for (const chainName of chain) {\n const chainEntry = resolveRelatedRegistration(\n chainName,\n registered,\n lookup,\n );\n if (chainEntry) {\n registrations.push(chainEntry);\n }\n }\n\n return registrations;\n}\n\nexport function isCollectionRegistration(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): boolean {\n if (isSmrtCollectionExtendsName(registered.extends)) {\n return true;\n }\n\n return getCollectionChainRegistrations(className, registered, lookup).some(\n (chainEntry) => isSmrtCollectionExtendsName(chainEntry.extends),\n );\n}\n\nfunction getStaticItemClassName(\n registered: RegisteredClass,\n): string | undefined {\n const itemClass = (\n registered.constructor as unknown as {\n _itemClass?: { name?: string };\n }\n )._itemClass;\n return itemClass?.name;\n}\n\nfunction getCollectionItemNameCandidates(className: string): string[] {\n const candidates: string[] = [];\n const simpleName = className.includes(':')\n ? className.slice(className.lastIndexOf(':') + 1)\n : className;\n\n const addCandidate = (candidate: string): void => {\n if (\n candidate &&\n candidate !== simpleName &&\n !candidates.includes(candidate)\n ) {\n candidates.push(candidate);\n }\n };\n\n if (simpleName.endsWith('Collection')) {\n addCandidate(simpleName.slice(0, -'Collection'.length));\n }\n\n if (simpleName.endsWith('ies')) {\n addCandidate(`${simpleName.slice(0, -3)}y`);\n } else if (simpleName.endsWith('s')) {\n addCandidate(simpleName.slice(0, -1));\n }\n\n return candidates;\n}\n\nfunction inferCollectionItemClassName(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): string | undefined {\n const lookupName = registered.name || className;\n\n for (const candidate of getCollectionItemNameCandidates(lookupName)) {\n const candidateRegistration = resolveRelatedRegistration(\n candidate,\n registered,\n lookup,\n );\n if (candidateRegistration) {\n return candidate;\n }\n }\n\n return undefined;\n}\n\nexport function resolveCollectionItemClassName(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): string | undefined {\n const staticItemClassName = getStaticItemClassName(registered);\n if (staticItemClassName) {\n return staticItemClassName;\n }\n\n if (registered.extendsTypeArg) {\n return registered.extendsTypeArg;\n }\n\n const inferredItemClassName = inferCollectionItemClassName(\n className,\n registered,\n lookup,\n );\n if (inferredItemClassName) {\n return inferredItemClassName;\n }\n\n const chain = getCollectionChainRegistrations(\n className,\n registered,\n lookup,\n ).reverse();\n\n for (const chainEntry of chain) {\n if (chainEntry === registered) {\n continue;\n }\n if (chainEntry.extendsTypeArg) {\n return chainEntry.extendsTypeArg;\n }\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,IAAa,6BAA6B,CACxC,kBACA,cACF;AAEA,SAAgB,4BACd,aACS;CACT,IAAI,CAAC,aACH,OAAO;CAET,OAAO,2BAA2B,MAC/B,SAAS,gBAAgB,QAAQ,YAAY,SAAS,IAAI,MAAM,CACnE;AACF;AAEA,SAAgB,2BACd,WACA,QACA,QAC6B;CAC7B,IAAI,UAAU,SAAS,GAAG,GACxB,OAAO,OAAO,UAAU,SAAS;CAGnC,IAAI,OAAO,eAAe,OAAO,oBAC/B,OACE,OAAO,mBAAmB,OAAO,aAAa,SAAS,KACvD,OAAO,UAAU,SAAS;CAI9B,OAAO,OAAO,UAAU,SAAS;AACnC;AAEA,SAAgB,gCACd,WACA,YACA,QACmB;CACnB,MAAM,aAAa,WAAW,iBAAiB,WAAW,QAAQ;CAClE,MAAM,QAAQ,OAAO,oBAAoB,UAAU;CACnD,MAAM,gBAAmC,CAAC;CAE1C,KAAK,MAAM,aAAa,OAAO;EAC7B,MAAM,aAAa,2BACjB,WACA,YACA,MACF;EACA,IAAI,YACF,cAAc,KAAK,UAAU;CAEjC;CAEA,OAAO;AACT;AAEA,SAAgB,yBACd,WACA,YACA,QACS;CACT,IAAI,4BAA4B,WAAW,OAAO,GAChD,OAAO;CAGT,OAAO,gCAAgC,WAAW,YAAY,MAAM,CAAC,CAAC,MACnE,eAAe,4BAA4B,WAAW,OAAO,CAChE;AACF;AAEA,SAAS,uBACP,YACoB;CAMpB,OAJE,WAAW,YAGX,YACgB;AACpB;AAEA,SAAS,gCAAgC,WAA6B;CACpE,MAAM,aAAuB,CAAC;CAC9B,MAAM,aAAa,UAAU,SAAS,GAAG,IACrC,UAAU,MAAM,UAAU,YAAY,GAAG,IAAI,CAAC,IAC9C;CAEJ,MAAM,gBAAgB,cAA4B;EAChD,IACE,aACA,cAAc,cACd,CAAC,WAAW,SAAS,SAAS,GAE9B,WAAW,KAAK,SAAS;CAE7B;CAEA,IAAI,WAAW,SAAS,YAAY,GAClC,aAAa,WAAW,MAAM,GAAG,GAAoB,CAAC;CAGxD,IAAI,WAAW,SAAS,KAAK,GAC3B,aAAa,GAAG,WAAW,MAAM,GAAG,EAAE,EAAE,EAAE;MACrC,IAAI,WAAW,SAAS,GAAG,GAChC,aAAa,WAAW,MAAM,GAAG,EAAE,CAAC;CAGtC,OAAO;AACT;AAEA,SAAS,6BACP,WACA,YACA,QACoB;CACpB,MAAM,aAAa,WAAW,QAAQ;CAEtC,KAAK,MAAM,aAAa,gCAAgC,UAAU,GAMhE,IAL8B,2BAC5B,WACA,YACA,MAEE,GACF,OAAO;AAKb;AAEA,SAAgB,+BACd,WACA,YACA,QACoB;CACpB,MAAM,sBAAsB,uBAAuB,UAAU;CAC7D,IAAI,qBACF,OAAO;CAGT,IAAI,WAAW,gBACb,OAAO,WAAW;CAGpB,MAAM,wBAAwB,6BAC5B,WACA,YACA,MACF;CACA,IAAI,uBACF,OAAO;CAGT,MAAM,QAAQ,gCACZ,WACA,YACA,MACF,CAAC,CAAC,QAAQ;CAEV,KAAK,MAAM,cAAc,OAAO;EAC9B,IAAI,eAAe,YACjB;EAEF,IAAI,WAAW,gBACb,OAAO,WAAW;CAEtB;AAGF"}
1
+ {"version":3,"file":"collection-resolution.js","names":[],"sources":["../../src/registry/collection-resolution.ts"],"sourcesContent":["import type { RegisteredClass } from './types';\n\nexport interface CollectionRegistrationLookup {\n findClass: (name: string) => RegisteredClass | undefined;\n findClassInPackage?: (\n packageName: string,\n className: string,\n ) => RegisteredClass | undefined;\n getInheritanceChain: (className: string) => string[];\n}\n\n/**\n * Framework base classes that ARE collections (extend `SmrtCollection`).\n *\n * `SmrtJunction` is an abstract collection base — junction collections declare\n * `extends SmrtJunction<Item>`, and the manifest records that *direct* parent.\n * `SmrtJunction` is itself never registered in a consumer's registry, so the\n * inheritance-chain walk in `isCollectionRegistration()` cannot reach\n * `SmrtCollection` through it. Recognizing the junction base by name here is\n * what keeps a junction Collection classified as a collection (and therefore\n * redirected to its item class for schema generation) regardless of\n * registration order. Without it, a junction Collection that is iterated\n * before its item class is mistaken for a regular table-bearing class and\n * creates a base-columns-only table, dropping the junction's FK columns\n * (#1342 — `place_assets` vs `profile_assets` asymmetry).\n *\n * `SmrtReportCollection` (`@happyvertical/smrt-reports`) is the same case: a\n * framework collection base that is never registered, so a report collection\n * (`class XCollection extends SmrtReportCollection<X>`) was classified as a\n * table-bearing class and claimed its item's table. The schema planner then\n * rejected the pair as two unrelated owners of one table (#3110).\n *\n * Keep in sync with the collection bases that extend `SmrtCollection` in\n * `packages/core/src/junction.ts` and `packages/reports/src/report.ts`. (`SmrtHierarchical` and\n * `SmrtPolymorphicAssociation` extend `SmrtObject`, not `SmrtCollection`, so\n * they deliberately do NOT belong here.)\n *\n * A new framework collection base must also be added to the other parallel\n * framework-base lists so it is recognized everywhere it matters:\n * - `FRAMEWORK_BASE_CLASSES` in\n * `packages/scanner/src/inheritance-resolver.ts` (scanner chain termination).\n * - `FRAMEWORK_ABSTRACT_BASE_NAMES` in\n * `packages/core/src/scanner/manifest-generator.ts` (field-merge skipping).\n *\n * Downstream tooling that needs this detector (e.g. @happyvertical/smrt-vitest)\n * should IMPORT it from `@happyvertical/smrt-core` rather than keep a copy — a\n * duplicate detector is exactly how the #1342 junction schema-drop bug\n * half-survived the original core fix.\n */\nexport const SMRT_COLLECTION_BASE_NAMES = [\n 'SmrtCollection',\n 'SmrtJunction',\n 'SmrtReportCollection',\n] as const;\n\nexport function isSmrtCollectionExtendsName(\n extendsName: string | undefined,\n): boolean {\n if (!extendsName) {\n return false;\n }\n return SMRT_COLLECTION_BASE_NAMES.some(\n (base) => extendsName === base || extendsName.endsWith(`:${base}`),\n );\n}\n\nexport function resolveRelatedRegistration(\n className: string,\n origin: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): RegisteredClass | undefined {\n if (className.includes(':')) {\n return lookup.findClass(className);\n }\n\n if (origin.packageName && lookup.findClassInPackage) {\n return (\n lookup.findClassInPackage(origin.packageName, className) ||\n lookup.findClass(className)\n );\n }\n\n return lookup.findClass(className);\n}\n\nexport function getCollectionChainRegistrations(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): RegisteredClass[] {\n const lookupName = registered.qualifiedName || registered.name || className;\n const chain = lookup.getInheritanceChain(lookupName);\n const registrations: RegisteredClass[] = [];\n\n for (const chainName of chain) {\n const chainEntry = resolveRelatedRegistration(\n chainName,\n registered,\n lookup,\n );\n if (chainEntry) {\n registrations.push(chainEntry);\n }\n }\n\n return registrations;\n}\n\nexport function isCollectionRegistration(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): boolean {\n if (isSmrtCollectionExtendsName(registered.extends)) {\n return true;\n }\n\n return getCollectionChainRegistrations(className, registered, lookup).some(\n (chainEntry) => isSmrtCollectionExtendsName(chainEntry.extends),\n );\n}\n\nfunction getStaticItemClassName(\n registered: RegisteredClass,\n): string | undefined {\n const itemClass = (\n registered.constructor as unknown as {\n _itemClass?: { name?: string };\n }\n )._itemClass;\n return itemClass?.name;\n}\n\nfunction getCollectionItemNameCandidates(className: string): string[] {\n const candidates: string[] = [];\n const simpleName = className.includes(':')\n ? className.slice(className.lastIndexOf(':') + 1)\n : className;\n\n const addCandidate = (candidate: string): void => {\n if (\n candidate &&\n candidate !== simpleName &&\n !candidates.includes(candidate)\n ) {\n candidates.push(candidate);\n }\n };\n\n if (simpleName.endsWith('Collection')) {\n addCandidate(simpleName.slice(0, -'Collection'.length));\n }\n\n if (simpleName.endsWith('ies')) {\n addCandidate(`${simpleName.slice(0, -3)}y`);\n } else if (simpleName.endsWith('s')) {\n addCandidate(simpleName.slice(0, -1));\n }\n\n return candidates;\n}\n\nfunction inferCollectionItemClassName(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): string | undefined {\n const lookupName = registered.name || className;\n\n for (const candidate of getCollectionItemNameCandidates(lookupName)) {\n const candidateRegistration = resolveRelatedRegistration(\n candidate,\n registered,\n lookup,\n );\n if (candidateRegistration) {\n return candidate;\n }\n }\n\n return undefined;\n}\n\nexport function resolveCollectionItemClassName(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): string | undefined {\n const staticItemClassName = getStaticItemClassName(registered);\n if (staticItemClassName) {\n return staticItemClassName;\n }\n\n if (registered.extendsTypeArg) {\n return registered.extendsTypeArg;\n }\n\n const inferredItemClassName = inferCollectionItemClassName(\n className,\n registered,\n lookup,\n );\n if (inferredItemClassName) {\n return inferredItemClassName;\n }\n\n const chain = getCollectionChainRegistrations(\n className,\n registered,\n lookup,\n ).reverse();\n\n for (const chainEntry of chain) {\n if (chainEntry === registered) {\n continue;\n }\n if (chainEntry.extendsTypeArg) {\n return chainEntry.extendsTypeArg;\n }\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,IAAa,6BAA6B;CACxC;CACA;CACA;AACF;AAEA,SAAgB,4BACd,aACS;CACT,IAAI,CAAC,aACH,OAAO;CAET,OAAO,2BAA2B,MAC/B,SAAS,gBAAgB,QAAQ,YAAY,SAAS,IAAI,MAAM,CACnE;AACF;AAEA,SAAgB,2BACd,WACA,QACA,QAC6B;CAC7B,IAAI,UAAU,SAAS,GAAG,GACxB,OAAO,OAAO,UAAU,SAAS;CAGnC,IAAI,OAAO,eAAe,OAAO,oBAC/B,OACE,OAAO,mBAAmB,OAAO,aAAa,SAAS,KACvD,OAAO,UAAU,SAAS;CAI9B,OAAO,OAAO,UAAU,SAAS;AACnC;AAEA,SAAgB,gCACd,WACA,YACA,QACmB;CACnB,MAAM,aAAa,WAAW,iBAAiB,WAAW,QAAQ;CAClE,MAAM,QAAQ,OAAO,oBAAoB,UAAU;CACnD,MAAM,gBAAmC,CAAC;CAE1C,KAAK,MAAM,aAAa,OAAO;EAC7B,MAAM,aAAa,2BACjB,WACA,YACA,MACF;EACA,IAAI,YACF,cAAc,KAAK,UAAU;CAEjC;CAEA,OAAO;AACT;AAEA,SAAgB,yBACd,WACA,YACA,QACS;CACT,IAAI,4BAA4B,WAAW,OAAO,GAChD,OAAO;CAGT,OAAO,gCAAgC,WAAW,YAAY,MAAM,CAAC,CAAC,MACnE,eAAe,4BAA4B,WAAW,OAAO,CAChE;AACF;AAEA,SAAS,uBACP,YACoB;CAMpB,OAJE,WAAW,YAGX,YACgB;AACpB;AAEA,SAAS,gCAAgC,WAA6B;CACpE,MAAM,aAAuB,CAAC;CAC9B,MAAM,aAAa,UAAU,SAAS,GAAG,IACrC,UAAU,MAAM,UAAU,YAAY,GAAG,IAAI,CAAC,IAC9C;CAEJ,MAAM,gBAAgB,cAA4B;EAChD,IACE,aACA,cAAc,cACd,CAAC,WAAW,SAAS,SAAS,GAE9B,WAAW,KAAK,SAAS;CAE7B;CAEA,IAAI,WAAW,SAAS,YAAY,GAClC,aAAa,WAAW,MAAM,GAAG,GAAoB,CAAC;CAGxD,IAAI,WAAW,SAAS,KAAK,GAC3B,aAAa,GAAG,WAAW,MAAM,GAAG,EAAE,EAAE,EAAE;MACrC,IAAI,WAAW,SAAS,GAAG,GAChC,aAAa,WAAW,MAAM,GAAG,EAAE,CAAC;CAGtC,OAAO;AACT;AAEA,SAAS,6BACP,WACA,YACA,QACoB;CACpB,MAAM,aAAa,WAAW,QAAQ;CAEtC,KAAK,MAAM,aAAa,gCAAgC,UAAU,GAMhE,IAL8B,2BAC5B,WACA,YACA,MAEE,GACF,OAAO;AAKb;AAEA,SAAgB,+BACd,WACA,YACA,QACoB;CACpB,MAAM,sBAAsB,uBAAuB,UAAU;CAC7D,IAAI,qBACF,OAAO;CAGT,IAAI,WAAW,gBACb,OAAO,WAAW;CAGpB,MAAM,wBAAwB,6BAC5B,WACA,YACA,MACF;CACA,IAAI,uBACF,OAAO;CAGT,MAAM,QAAQ,gCACZ,WACA,YACA,MACF,CAAC,CAAC,QAAQ;CAEV,KAAK,MAAM,cAAc,OAAO;EAC9B,IAAI,eAAe,YACjB;EAEF,IAAI,WAAW,gBACb,OAAO,WAAW;CAEtB;AAGF"}
@@ -153,6 +153,16 @@ export interface CollisionInputs {
153
153
  * treats it as promotion rather than collision.
154
154
  */
155
155
  readonly existingHasNoPackage: boolean;
156
+ /**
157
+ * Decorator-origin only: the table the `@smrt()` decorator resolved for the
158
+ * new class (its explicit `tableName`, its STI parent's table, or the name
159
+ * derived from the class) differs from the existing entry's table. A
160
+ * duplicate of one class — pnpm copy, inlined chunk — resolves the same
161
+ * table, so this is positive evidence of a different class (#3106): ergot's
162
+ * `LicenseSale` resolves `license_sales` where smrt-commerce's STI subtype
163
+ * resolves `contracts`.
164
+ */
165
+ readonly declaresDifferentTable: boolean;
156
166
  }
157
167
  export interface PolicyDecision {
158
168
  readonly policy: Policy;
@@ -1 +1 @@
1
- {"version":3,"file":"collision-policy.d.ts","sourceRoot":"","sources":["../../src/registry/collision-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,UAAU,CAAC;AAE1D;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,kBAAkB,GAAG,gBAAgB,CAAC;AAE5E,MAAM,MAAM,MAAM;AAChB,yEAAyE;AACvE,QAAQ;AACV,kDAAkD;GAChD,SAAS;AACX;;;;;;GAMG;GACD,gBAAgB;AAClB;;;;GAIG;GACD,mBAAmB;AACrB,2DAA2D;GACzD,MAAM;AACR,0DAA0D;GACxD,OAAO,CAAC;AAEZ;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAE9B,qFAAqF;IACrF,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAElC,iEAAiE;IACjE,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IAEjC,uEAAuE;IACvE,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;IAEvC,uDAAuD;IACvD,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;IAEzC;;;OAGG;IACH,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IAEtC;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAErC,+CAA+C;IAC/C,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAErC,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAE9B;;;;;OAKG;IACH,QAAQ,CAAC,0BAA0B,EAAE,OAAO,CAAC;IAE7C;;;;;;OAMG;IACH,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAC;IAExC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAE3B;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAErC;;;;OAIG;IACH,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;IAEzC;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAErC;;;;;OAKG;IACH,QAAQ,CAAC,qCAAqC,EAAE,OAAO,CAAC;IAExD;;;;;;OAMG;IACH,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,GAAG;IACX,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC;IACrD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,MAAM,CAAC;CACtD;AA+MD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,CAc7E;AAED,2EAA2E;AAC3E,eAAO,MAAM,sBAAsB,EAAE,SAAS,GAAG,EAAS,CAAC"}
1
+ {"version":3,"file":"collision-policy.d.ts","sourceRoot":"","sources":["../../src/registry/collision-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,UAAU,CAAC;AAE1D;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,kBAAkB,GAAG,gBAAgB,CAAC;AAE5E,MAAM,MAAM,MAAM;AAChB,yEAAyE;AACvE,QAAQ;AACV,kDAAkD;GAChD,SAAS;AACX;;;;;;GAMG;GACD,gBAAgB;AAClB;;;;GAIG;GACD,mBAAmB;AACrB,2DAA2D;GACzD,MAAM;AACR,0DAA0D;GACxD,OAAO,CAAC;AAEZ;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAE9B,qFAAqF;IACrF,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAElC,iEAAiE;IACjE,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IAEjC,uEAAuE;IACvE,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;IAEvC,uDAAuD;IACvD,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;IAEzC;;;OAGG;IACH,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IAEtC;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAErC,+CAA+C;IAC/C,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAErC,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAE9B;;;;;OAKG;IACH,QAAQ,CAAC,0BAA0B,EAAE,OAAO,CAAC;IAE7C;;;;;;OAMG;IACH,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAC;IAExC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAE3B;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAErC;;;;OAIG;IACH,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;IAEzC;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAErC;;;;;OAKG;IACH,QAAQ,CAAC,qCAAqC,EAAE,OAAO,CAAC;IAExD;;;;;;OAMG;IACH,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;IAEvC;;;;;;;;OAQG;IACH,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,GAAG;IACX,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC;IACrD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,MAAM,CAAC;CACtD;AAwPD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,CAc7E;AAED,2EAA2E;AAC3E,eAAO,MAAM,sBAAsB,EAAE,SAAS,GAAG,EAAS,CAAC"}
@@ -17,7 +17,7 @@ var ROWS = [
17
17
  },
18
18
  {
19
19
  scenario: "manifest-stub-replacement",
20
- match: (i) => i.origin === "decorator" && i.existingIsManifestStub && !i.sameConstructor,
20
+ match: (i) => i.origin === "decorator" && i.existingIsManifestStub && !i.sameConstructor && !(i.hasNewQualifiedKey && i.existingHasAnyPackage && !i.samePackage && !i.sameSourceFile && (i.bothSourceFilesKnown && !i.newInBundledContext || i.declaresDifferentTable)),
21
21
  policy: "replace",
22
22
  reason: () => "Existing entry is a manifest stub placeholder; the real implementation takes over."
23
23
  },
@@ -69,6 +69,12 @@ var ROWS = [
69
69
  policy: "accept",
70
70
  reason: () => "pnpm store duplicated the same package; different physical constructors but semantically identical."
71
71
  },
72
+ {
73
+ scenario: "decorator-different-packages-qualified-coexist",
74
+ match: (i) => i.origin === "decorator" && i.hasNewQualifiedKey && i.existingHasAnyPackage && !i.samePackage && !i.sameSourceFile && !i.newExtendsExisting && !i.existingExtendsNew && (!i.newInBundledContext || i.declaresDifferentTable),
75
+ policy: "coexist-qualified",
76
+ reason: () => "Different packages declare the same simple name; both registrations live under their qualified keys (issue #3106)."
77
+ },
72
78
  {
73
79
  scenario: "decorator-bundled-context-known-package",
74
80
  match: (i) => i.origin === "decorator" && i.newInBundledContext && i.existingIsPackageQualified,
@@ -1 +1 @@
1
- {"version":3,"file":"collision-policy.js","names":[],"sources":["../../src/registry/collision-policy.ts"],"sourcesContent":["/**\n * Release C (#1134) — unified collision policy for class registration.\n *\n * `register()` and `registerFromManifest()` both need to decide what to do\n * when a new registration lands on an existing class with the same (or a\n * case-insensitive-matching) name. Before Release C the decision logic was\n * a ~300-line if/else tree duplicated across three code paths:\n *\n * - `register()` exact-match branch (class-registration.ts:159-275)\n * - `register()` case-insensitive branch (class-registration.ts:286-391)\n * - `resolveManifestCollision()` (class-registration.ts:876-929)\n *\n * This module collapses all three onto a single decision table. Each row\n * names a scenario that exists in the current test suite; the row order\n * encodes specificity (first match wins).\n *\n * # Invocation contract\n *\n * `decideCollisionPolicy(inputs)` is ONLY called when the caller has already\n * identified exactly one colliding candidate for the new registration. Two\n * cases MUST be handled upstream without consulting the table:\n *\n * 1. No colliding candidate exists → register fresh.\n * 2. Ambiguous simple name matches >1 existing entry across different\n * packages (`getCanonicalClassName()` returns undefined) → fall through\n * to the exact-key path and allow coexistence under qualified keys.\n * See issue #951 for the semantics being preserved.\n *\n * Feeding the ambiguous case through the table would lose the\n * `coexist-qualified` behavior that's crucial for multi-package apps.\n *\n * @see https://github.com/happyvertical/smrt/issues/1134\n */\n\nexport type RegistrationOrigin = 'decorator' | 'manifest';\n\n/**\n * How the colliding candidate was identified.\n *\n * - `exact-key` : `classes.get(name)` returned the candidate.\n * `existingKey === name`.\n * - `case-insensitive` : Iteration found a case-insensitive match on\n * either the registry key or the entry's `name`\n * property. `existingKey !== name`.\n * - `canonical-name` : Looked up via `getCanonicalClassName(simpleName)`\n * — unambiguous single match under a lowered\n * simple name. Used by the manifest path.\n */\nexport type MatchKind = 'exact-key' | 'case-insensitive' | 'canonical-name';\n\nexport type Policy =\n /** Keep existing identity, update metadata via `upsertExistingEntry`. */\n | 'accept'\n /** Drop existing, register new as replacement. */\n | 'replace'\n /**\n * Fold manifest fields into existing registration. The **caller MUST**:\n * (1) call `mergeManifestIntoExistingRegistration(existing, objectDef, packageName)`\n * (2) if `registrationKey !== existingCanonical`, alias the existing entry\n * under `registrationKey` via `classes.set()` + `constructorIndex.set()`\n * Both steps together preserve the `issue-951:144-200` contract.\n */\n | 'merge-manifest'\n /**\n * Different packages, different qualified keys. Both registrations stay\n * in the classes Map under their respective qualified keys. Ambiguity is\n * surfaced at `findClass` time. Preserves issue #951.\n */\n | 'coexist-qualified'\n /** Existing wins; silently ignore the new registration. */\n | 'skip'\n /** Unresolvable collision — raise a user-facing error. */\n | 'throw';\n\n/**\n * Every signal the decision table can read. Computed by the caller from\n * inspection of the new and existing registrations.\n */\nexport interface CollisionInputs {\n readonly origin: RegistrationOrigin;\n readonly matchKind: MatchKind;\n\n /** `new.ctor === existing.constructor` (decorator path only; false for manifest). */\n readonly sameConstructor: boolean;\n\n /** `new.sourceFile === existing.sourceFilePath`, both truthy. */\n readonly sameSourceFile: boolean;\n\n /** Both the new and existing registrations have known source files. */\n readonly bothSourceFilesKnown: boolean;\n\n /** `existing.constructor._isManifestStub === true`. */\n readonly existingIsManifestStub: boolean;\n\n /**\n * New registration's source file lives in a bundled output directory\n * (`.svelte-kit/output/`, `/dist/`, `/build/`, `.next/`, `.nuxt/`).\n */\n readonly newInBundledContext: boolean;\n\n /**\n * New class extends existing.\n * - decorator path: `new.ctor.prototype instanceof existing.constructor`\n * - manifest path : `new.objectDef.extends` names existing\n */\n readonly newExtendsExisting: boolean;\n\n /** Inverse direction: existing extends new. */\n readonly existingExtendsNew: boolean;\n\n /** `new.packageName === existing.packageName` (both truthy). */\n readonly samePackage: boolean;\n\n /**\n * `existing.packageName?.startsWith('@')` — existing entry came from a\n * **scoped** package manifest (e.g. `@happyvertical/smrt-core`). Used by\n * the external-package-replace (#584) and bundled-context scenarios\n * that were gated on `@` in pre-Release-C code.\n */\n readonly existingIsPackageQualified: boolean;\n\n /**\n * Existing entry carries any packageName, scoped or not. Broader than\n * `existingIsPackageQualified`. Used for the different-packages\n * coexistence scenario where pre-Release-C code only required both\n * packageNames to be present and different (unscoped manifests like\n * `smrt-profiles` count here).\n */\n readonly existingHasAnyPackage: boolean;\n\n /**\n * Case-sensitive equality between `new.name` and `existing.name`.\n * Used to distinguish a pnpm-duplicate (same exact name) from a genuine\n * case-insensitive collision (different names that lower to the same string).\n */\n readonly sameName: boolean;\n\n /**\n * New registration has (or can derive) a qualified key `@pkg:Name`.\n * Paired with `existingIsPackageQualified` to drive the different-packages\n * coexistence behavior from issue #951.\n */\n readonly hasNewQualifiedKey: boolean;\n\n /**\n * Existing registry key contains a `:` separator. Needed alongside\n * `existingIsPackageQualified` to identify valid pnpm-dup candidates\n * (old code required both).\n */\n readonly existingKeyIsQualified: boolean;\n\n /**\n * Manifest-origin only: the incoming `objectDef` carries at least one of\n * `fields`, `methods`, `schema`, or `decoratorConfig`. Without that, the\n * merge path has nothing to fold in and the caller should skip the\n * merge-manifest policy entirely.\n */\n readonly hasManifestContent: boolean;\n\n /**\n * Manifest-origin only: the new registration's qualified key differs\n * from the existing entry's canonical key. When true and the policy is\n * `merge-manifest`, the caller MUST also alias the existing entry under\n * the new qualified key (step 2 of the merge-manifest contract).\n */\n readonly registrationKeyDiffersFromExistingKey: boolean;\n\n /**\n * Existing registration has no `packageName` set. Distinct from\n * `!samePackage`: this specifically means the existing entry is a bare\n * source registration waiting for a package manifest to promote it. When\n * true and the new registration carries a packageName, merge-manifest\n * treats it as promotion rather than collision.\n */\n readonly existingHasNoPackage: boolean;\n}\n\nexport interface PolicyDecision {\n readonly policy: Policy;\n /** Stable id. Matches the scenario name in the test suite 1-to-1. */\n readonly scenario: string;\n /** Human-readable explanation. Used by verboseLog + the error message. */\n readonly reason: string;\n}\n\ninterface Row {\n readonly scenario: string;\n readonly match: (inputs: CollisionInputs) => boolean;\n readonly policy: Policy;\n readonly reason: (inputs: CollisionInputs) => string;\n}\n\n/**\n * Ordered decision table. First row whose `match` returns true wins.\n * Order reflects specificity — scenarios that narrow down earlier\n * scenarios come before them.\n *\n * Each row is named to match a scenario in the test suite so a reviewer\n * can bisect one-to-one between \"new class extends existing\" (the code)\n * and \"sti-child-wins\" (the test row and the verbose-log output).\n */\nconst ROWS: readonly Row[] = [\n // --- Cross-origin: constructor identity and stub replacement ----------\n\n {\n scenario: 'same-constructor-reregistration',\n match: (i) => i.sameConstructor,\n policy: 'accept',\n reason: () =>\n 'Same constructor identity — module re-evaluation. Update metadata only.',\n },\n\n {\n // Decorator-only: the decorator fires after a manifest builder\n // pre-registered a stub placeholder under this name. The real class\n // takes over. Manifest-origin stub-to-stub arrivals fall through to\n // the source-file / STI rows instead.\n scenario: 'manifest-stub-replacement',\n match: (i) =>\n i.origin === 'decorator' &&\n i.existingIsManifestStub &&\n !i.sameConstructor,\n policy: 'replace',\n reason: () =>\n 'Existing entry is a manifest stub placeholder; the real implementation takes over.',\n },\n\n // --- Manifest-origin rows (checked before decorator-origin) -----------\n\n {\n scenario: 'manifest-same-source-file',\n match: (i) => i.origin === 'manifest' && i.sameSourceFile,\n policy: 'skip',\n reason: () =>\n 'Same source file — consumer manifest re-export, not a real collision.',\n },\n\n {\n scenario: 'manifest-sti-child-wins',\n match: (i) =>\n i.origin === 'manifest' && i.newExtendsExisting && !i.existingExtendsNew,\n policy: 'replace',\n reason: () =>\n 'Manifest entry extends existing — STI child wins over parent.',\n },\n\n {\n scenario: 'manifest-sti-parent-skip',\n match: (i) =>\n i.origin === 'manifest' && i.existingExtendsNew && !i.newExtendsExisting,\n policy: 'skip',\n reason: () =>\n 'Existing entry is the STI child of the incoming manifest parent; keep the child.',\n },\n\n {\n // Uses `existingHasAnyPackage` not `existingIsPackageQualified`: the\n // pre-Release-C `allowCoexistingQualifiedRegistration` check only\n // required both packages to be present and differ, regardless of\n // whether they were scoped (`@scope/pkg`) or unscoped (`smrt-profiles`).\n // Narrowing to scoped would silently drop coexistence for unscoped\n // manifests. See PR #1140 review (willgriffin P2 at line 251).\n scenario: 'manifest-different-packages-qualified-coexist',\n match: (i) =>\n i.origin === 'manifest' &&\n !i.samePackage &&\n i.hasNewQualifiedKey &&\n i.existingHasAnyPackage,\n policy: 'coexist-qualified',\n reason: () =>\n 'Different packages share a simple name; both registrations live under their qualified keys (issue #951).',\n },\n\n {\n // Covers three related scenarios:\n // (a) `samePackage` — decorator + manifest from the same\n // package land on the same class; merge fields (issue #1000).\n // (b) `existingHasNoPackage` — source registration hasn't been\n // promoted to a qualified key yet and a manifest now provides\n // the package identity (issue #951:144-200). The merge runs\n // AND the caller aliases the existing entry under the new\n // qualified key.\n // (c) The same scenarios when the existing entry is a manifest\n // stub from an earlier unqualified/local manifest registration\n // — the pre-Release-C code merged whenever\n // `!existing.packageName || packageName === existing.packageName`\n // regardless of stub status. Release C preserves that: removing\n // the `!existingIsManifestStub` gate lets a later\n // package-qualified manifest upgrade a stub's fields + alias\n // under the qualified key. See PR #1140 review (willgriffin P1\n // at line 270).\n scenario: 'manifest-same-package-merge',\n match: (i) =>\n i.origin === 'manifest' &&\n (i.samePackage || (i.existingHasNoPackage && i.hasNewQualifiedKey)) &&\n i.hasManifestContent,\n policy: 'merge-manifest',\n reason: () =>\n 'Manifest arriving for an existing same-package or unqualified source registration; fold fields in and alias the qualified key if keys differ.',\n },\n\n {\n scenario: 'manifest-default-skip',\n match: (i) => i.origin === 'manifest',\n policy: 'skip',\n reason: () =>\n 'Manifest collision with no inheritance relationship; keep existing entry (pre-Release-C resolveManifestCollision default).',\n },\n\n // --- Decorator-origin rows --------------------------------------------\n\n {\n scenario: 'decorator-exact-match-same-package-external',\n match: (i) =>\n i.origin === 'decorator' &&\n i.matchKind === 'exact-key' &&\n i.existingIsPackageQualified &&\n i.samePackage &&\n !i.sameConstructor,\n policy: 'accept',\n reason: () =>\n 'External package manifest entry being replaced by its own decorator-registered class (issue #584).',\n },\n\n {\n // pnpm-dup allows unscoped package names (old code didn't gate on\n // scoped-only). `samePackage` already requires both sides to have a\n // packageName and match, so no extra existingHasAnyPackage check\n // is needed.\n scenario: 'decorator-case-insensitive-pnpm-duplicate',\n match: (i) =>\n i.origin === 'decorator' &&\n i.matchKind === 'case-insensitive' &&\n i.existingKeyIsQualified &&\n i.samePackage &&\n i.sameName &&\n !i.sameConstructor,\n policy: 'accept',\n reason: () =>\n 'pnpm store duplicated the same package; different physical constructors but semantically identical.',\n },\n\n {\n scenario: 'decorator-bundled-context-known-package',\n match: (i) =>\n i.origin === 'decorator' &&\n i.newInBundledContext &&\n i.existingIsPackageQualified,\n policy: 'accept',\n reason: () =>\n 'Bundled output duplicated a known package class across chunks; treat as re-registration.',\n },\n\n {\n scenario: 'decorator-same-source-file',\n match: (i) => i.origin === 'decorator' && i.sameSourceFile,\n policy: 'accept',\n reason: () =>\n 'Same source file — module re-evaluated (vitest test-isolation or HMR).',\n },\n\n {\n scenario: 'decorator-unknown-source-fallback',\n match: (i) => i.origin === 'decorator' && !i.bothSourceFilesKnown,\n policy: 'accept',\n reason: () =>\n 'Source file unavailable for comparison; allow re-registration rather than block legitimate updates.',\n },\n\n // --- Cross-origin STI fallbacks (used by decorator path; manifest\n // path has its own explicit rows above that come first) ------------\n\n {\n scenario: 'sti-child-wins',\n match: (i) => i.newExtendsExisting && !i.existingExtendsNew,\n policy: 'replace',\n reason: () => 'New class extends existing — STI child wins over parent.',\n },\n\n {\n scenario: 'sti-parent-skip',\n match: (i) => i.existingExtendsNew && !i.newExtendsExisting,\n policy: 'skip',\n reason: () => 'Existing entry extends new — STI child already registered.',\n },\n\n // --- Default: unresolvable decorator collision ------------------------\n\n {\n scenario: 'true-collision',\n match: () => true,\n policy: 'throw',\n reason: () =>\n 'Different constructors, different source files, no inheritance relationship. Unresolvable collision.',\n },\n];\n\n/**\n * Resolve a collision to a policy. See module docstring for the invocation\n * contract — callers MUST NOT invoke this for the no-candidate or\n * ambiguous-simple-name cases.\n */\nexport function decideCollisionPolicy(inputs: CollisionInputs): PolicyDecision {\n for (const row of ROWS) {\n if (row.match(inputs)) {\n return {\n policy: row.policy,\n scenario: row.scenario,\n reason: row.reason(inputs),\n };\n }\n }\n // Unreachable — `true-collision` is the final catch-all.\n throw new Error(\n `decideCollisionPolicy: no row matched (should be unreachable). Inputs: ${JSON.stringify(inputs)}`,\n );\n}\n\n/** Test-only: expose the row list so integration tests can scenario-id. */\nexport const __POLICY_ROWS_FOR_TEST: readonly Row[] = ROWS;\n"],"mappings":";;;;;;;;;;AAyMA,IAAM,OAAuB;CAG3B;EACE,UAAU;EACV,QAAQ,MAAM,EAAE;EAChB,QAAQ;EACR,cACE;CACJ;CAEA;EAKE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,eACb,EAAE,0BACF,CAAC,EAAE;EACL,QAAQ;EACR,cACE;CACJ;CAIA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,WAAW,cAAc,EAAE;EAC3C,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,cAAc,EAAE,sBAAsB,CAAC,EAAE;EACxD,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,cAAc,EAAE,sBAAsB,CAAC,EAAE;EACxD,QAAQ;EACR,cACE;CACJ;CAEA;EAOE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,cACb,CAAC,EAAE,eACH,EAAE,sBACF,EAAE;EACJ,QAAQ;EACR,cACE;CACJ;CAEA;EAkBE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,eACZ,EAAE,eAAgB,EAAE,wBAAwB,EAAE,uBAC/C,EAAE;EACJ,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,WAAW;EAC3B,QAAQ;EACR,cACE;CACJ;CAIA;EACE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,eACb,EAAE,cAAc,eAChB,EAAE,8BACF,EAAE,eACF,CAAC,EAAE;EACL,QAAQ;EACR,cACE;CACJ;CAEA;EAKE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,eACb,EAAE,cAAc,sBAChB,EAAE,0BACF,EAAE,eACF,EAAE,YACF,CAAC,EAAE;EACL,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,eACb,EAAE,uBACF,EAAE;EACJ,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,WAAW,eAAe,EAAE;EAC5C,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,WAAW,eAAe,CAAC,EAAE;EAC7C,QAAQ;EACR,cACE;CACJ;CAKA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,sBAAsB,CAAC,EAAE;EACzC,QAAQ;EACR,cAAc;CAChB;CAEA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,sBAAsB,CAAC,EAAE;EACzC,QAAQ;EACR,cAAc;CAChB;CAIA;EACE,UAAU;EACV,aAAa;EACb,QAAQ;EACR,cACE;CACJ;AACF;;;;;;AAOA,SAAgB,sBAAsB,QAAyC;CAC7E,KAAK,MAAM,OAAO,MAChB,IAAI,IAAI,MAAM,MAAM,GAClB,OAAO;EACL,QAAQ,IAAI;EACZ,UAAU,IAAI;EACd,QAAQ,IAAI,OAAO,MAAM;CAC3B;CAIJ,MAAM,IAAI,MACR,0EAA0E,KAAK,UAAU,MAAM,GACjG;AACF"}
1
+ {"version":3,"file":"collision-policy.js","names":[],"sources":["../../src/registry/collision-policy.ts"],"sourcesContent":["/**\n * Release C (#1134) — unified collision policy for class registration.\n *\n * `register()` and `registerFromManifest()` both need to decide what to do\n * when a new registration lands on an existing class with the same (or a\n * case-insensitive-matching) name. Before Release C the decision logic was\n * a ~300-line if/else tree duplicated across three code paths:\n *\n * - `register()` exact-match branch (class-registration.ts:159-275)\n * - `register()` case-insensitive branch (class-registration.ts:286-391)\n * - `resolveManifestCollision()` (class-registration.ts:876-929)\n *\n * This module collapses all three onto a single decision table. Each row\n * names a scenario that exists in the current test suite; the row order\n * encodes specificity (first match wins).\n *\n * # Invocation contract\n *\n * `decideCollisionPolicy(inputs)` is ONLY called when the caller has already\n * identified exactly one colliding candidate for the new registration. Two\n * cases MUST be handled upstream without consulting the table:\n *\n * 1. No colliding candidate exists → register fresh.\n * 2. Ambiguous simple name matches >1 existing entry across different\n * packages (`getCanonicalClassName()` returns undefined) → fall through\n * to the exact-key path and allow coexistence under qualified keys.\n * See issue #951 for the semantics being preserved.\n *\n * Feeding the ambiguous case through the table would lose the\n * `coexist-qualified` behavior that's crucial for multi-package apps.\n *\n * @see https://github.com/happyvertical/smrt/issues/1134\n */\n\nexport type RegistrationOrigin = 'decorator' | 'manifest';\n\n/**\n * How the colliding candidate was identified.\n *\n * - `exact-key` : `classes.get(name)` returned the candidate.\n * `existingKey === name`.\n * - `case-insensitive` : Iteration found a case-insensitive match on\n * either the registry key or the entry's `name`\n * property. `existingKey !== name`.\n * - `canonical-name` : Looked up via `getCanonicalClassName(simpleName)`\n * — unambiguous single match under a lowered\n * simple name. Used by the manifest path.\n */\nexport type MatchKind = 'exact-key' | 'case-insensitive' | 'canonical-name';\n\nexport type Policy =\n /** Keep existing identity, update metadata via `upsertExistingEntry`. */\n | 'accept'\n /** Drop existing, register new as replacement. */\n | 'replace'\n /**\n * Fold manifest fields into existing registration. The **caller MUST**:\n * (1) call `mergeManifestIntoExistingRegistration(existing, objectDef, packageName)`\n * (2) if `registrationKey !== existingCanonical`, alias the existing entry\n * under `registrationKey` via `classes.set()` + `constructorIndex.set()`\n * Both steps together preserve the `issue-951:144-200` contract.\n */\n | 'merge-manifest'\n /**\n * Different packages, different qualified keys. Both registrations stay\n * in the classes Map under their respective qualified keys. Ambiguity is\n * surfaced at `findClass` time. Preserves issue #951.\n */\n | 'coexist-qualified'\n /** Existing wins; silently ignore the new registration. */\n | 'skip'\n /** Unresolvable collision — raise a user-facing error. */\n | 'throw';\n\n/**\n * Every signal the decision table can read. Computed by the caller from\n * inspection of the new and existing registrations.\n */\nexport interface CollisionInputs {\n readonly origin: RegistrationOrigin;\n readonly matchKind: MatchKind;\n\n /** `new.ctor === existing.constructor` (decorator path only; false for manifest). */\n readonly sameConstructor: boolean;\n\n /** `new.sourceFile === existing.sourceFilePath`, both truthy. */\n readonly sameSourceFile: boolean;\n\n /** Both the new and existing registrations have known source files. */\n readonly bothSourceFilesKnown: boolean;\n\n /** `existing.constructor._isManifestStub === true`. */\n readonly existingIsManifestStub: boolean;\n\n /**\n * New registration's source file lives in a bundled output directory\n * (`.svelte-kit/output/`, `/dist/`, `/build/`, `.next/`, `.nuxt/`).\n */\n readonly newInBundledContext: boolean;\n\n /**\n * New class extends existing.\n * - decorator path: `new.ctor.prototype instanceof existing.constructor`\n * - manifest path : `new.objectDef.extends` names existing\n */\n readonly newExtendsExisting: boolean;\n\n /** Inverse direction: existing extends new. */\n readonly existingExtendsNew: boolean;\n\n /** `new.packageName === existing.packageName` (both truthy). */\n readonly samePackage: boolean;\n\n /**\n * `existing.packageName?.startsWith('@')` — existing entry came from a\n * **scoped** package manifest (e.g. `@happyvertical/smrt-core`). Used by\n * the external-package-replace (#584) and bundled-context scenarios\n * that were gated on `@` in pre-Release-C code.\n */\n readonly existingIsPackageQualified: boolean;\n\n /**\n * Existing entry carries any packageName, scoped or not. Broader than\n * `existingIsPackageQualified`. Used for the different-packages\n * coexistence scenario where pre-Release-C code only required both\n * packageNames to be present and different (unscoped manifests like\n * `smrt-profiles` count here).\n */\n readonly existingHasAnyPackage: boolean;\n\n /**\n * Case-sensitive equality between `new.name` and `existing.name`.\n * Used to distinguish a pnpm-duplicate (same exact name) from a genuine\n * case-insensitive collision (different names that lower to the same string).\n */\n readonly sameName: boolean;\n\n /**\n * New registration has (or can derive) a qualified key `@pkg:Name`.\n * Paired with `existingIsPackageQualified` to drive the different-packages\n * coexistence behavior from issue #951.\n */\n readonly hasNewQualifiedKey: boolean;\n\n /**\n * Existing registry key contains a `:` separator. Needed alongside\n * `existingIsPackageQualified` to identify valid pnpm-dup candidates\n * (old code required both).\n */\n readonly existingKeyIsQualified: boolean;\n\n /**\n * Manifest-origin only: the incoming `objectDef` carries at least one of\n * `fields`, `methods`, `schema`, or `decoratorConfig`. Without that, the\n * merge path has nothing to fold in and the caller should skip the\n * merge-manifest policy entirely.\n */\n readonly hasManifestContent: boolean;\n\n /**\n * Manifest-origin only: the new registration's qualified key differs\n * from the existing entry's canonical key. When true and the policy is\n * `merge-manifest`, the caller MUST also alias the existing entry under\n * the new qualified key (step 2 of the merge-manifest contract).\n */\n readonly registrationKeyDiffersFromExistingKey: boolean;\n\n /**\n * Existing registration has no `packageName` set. Distinct from\n * `!samePackage`: this specifically means the existing entry is a bare\n * source registration waiting for a package manifest to promote it. When\n * true and the new registration carries a packageName, merge-manifest\n * treats it as promotion rather than collision.\n */\n readonly existingHasNoPackage: boolean;\n\n /**\n * Decorator-origin only: the table the `@smrt()` decorator resolved for the\n * new class (its explicit `tableName`, its STI parent's table, or the name\n * derived from the class) differs from the existing entry's table. A\n * duplicate of one class — pnpm copy, inlined chunk — resolves the same\n * table, so this is positive evidence of a different class (#3106): ergot's\n * `LicenseSale` resolves `license_sales` where smrt-commerce's STI subtype\n * resolves `contracts`.\n */\n readonly declaresDifferentTable: boolean;\n}\n\nexport interface PolicyDecision {\n readonly policy: Policy;\n /** Stable id. Matches the scenario name in the test suite 1-to-1. */\n readonly scenario: string;\n /** Human-readable explanation. Used by verboseLog + the error message. */\n readonly reason: string;\n}\n\ninterface Row {\n readonly scenario: string;\n readonly match: (inputs: CollisionInputs) => boolean;\n readonly policy: Policy;\n readonly reason: (inputs: CollisionInputs) => string;\n}\n\n/**\n * Ordered decision table. First row whose `match` returns true wins.\n * Order reflects specificity — scenarios that narrow down earlier\n * scenarios come before them.\n *\n * Each row is named to match a scenario in the test suite so a reviewer\n * can bisect one-to-one between \"new class extends existing\" (the code)\n * and \"sti-child-wins\" (the test row and the verbose-log output).\n */\nconst ROWS: readonly Row[] = [\n // --- Cross-origin: constructor identity and stub replacement ----------\n\n {\n scenario: 'same-constructor-reregistration',\n match: (i) => i.sameConstructor,\n policy: 'accept',\n reason: () =>\n 'Same constructor identity — module re-evaluation. Update metadata only.',\n },\n\n {\n // Decorator-only: the decorator fires after a manifest builder\n // pre-registered a stub placeholder under this name. The real class\n // takes over. Manifest-origin stub-to-stub arrivals fall through to\n // the source-file / STI rows instead.\n // A stub another package's manifest registered is that package's class:\n // a class from another package that is known not to be the stub's — its\n // source file differs (outside bundled output, whose stack package is the\n // bundle's, not the declaring package's) or it declares another table —\n // is a different class and coexists instead (#3106).\n scenario: 'manifest-stub-replacement',\n match: (i) =>\n i.origin === 'decorator' &&\n i.existingIsManifestStub &&\n !i.sameConstructor &&\n !(\n i.hasNewQualifiedKey &&\n i.existingHasAnyPackage &&\n !i.samePackage &&\n !i.sameSourceFile &&\n ((i.bothSourceFilesKnown && !i.newInBundledContext) ||\n i.declaresDifferentTable)\n ),\n policy: 'replace',\n reason: () =>\n 'Existing entry is a manifest stub placeholder; the real implementation takes over.',\n },\n\n // --- Manifest-origin rows (checked before decorator-origin) -----------\n\n {\n scenario: 'manifest-same-source-file',\n match: (i) => i.origin === 'manifest' && i.sameSourceFile,\n policy: 'skip',\n reason: () =>\n 'Same source file — consumer manifest re-export, not a real collision.',\n },\n\n {\n scenario: 'manifest-sti-child-wins',\n match: (i) =>\n i.origin === 'manifest' && i.newExtendsExisting && !i.existingExtendsNew,\n policy: 'replace',\n reason: () =>\n 'Manifest entry extends existing — STI child wins over parent.',\n },\n\n {\n scenario: 'manifest-sti-parent-skip',\n match: (i) =>\n i.origin === 'manifest' && i.existingExtendsNew && !i.newExtendsExisting,\n policy: 'skip',\n reason: () =>\n 'Existing entry is the STI child of the incoming manifest parent; keep the child.',\n },\n\n {\n // Uses `existingHasAnyPackage` not `existingIsPackageQualified`: the\n // pre-Release-C `allowCoexistingQualifiedRegistration` check only\n // required both packages to be present and differ, regardless of\n // whether they were scoped (`@scope/pkg`) or unscoped (`smrt-profiles`).\n // Narrowing to scoped would silently drop coexistence for unscoped\n // manifests. See PR #1140 review (willgriffin P2 at line 251).\n scenario: 'manifest-different-packages-qualified-coexist',\n match: (i) =>\n i.origin === 'manifest' &&\n !i.samePackage &&\n i.hasNewQualifiedKey &&\n i.existingHasAnyPackage,\n policy: 'coexist-qualified',\n reason: () =>\n 'Different packages share a simple name; both registrations live under their qualified keys (issue #951).',\n },\n\n {\n // Covers three related scenarios:\n // (a) `samePackage` — decorator + manifest from the same\n // package land on the same class; merge fields (issue #1000).\n // (b) `existingHasNoPackage` — source registration hasn't been\n // promoted to a qualified key yet and a manifest now provides\n // the package identity (issue #951:144-200). The merge runs\n // AND the caller aliases the existing entry under the new\n // qualified key.\n // (c) The same scenarios when the existing entry is a manifest\n // stub from an earlier unqualified/local manifest registration\n // — the pre-Release-C code merged whenever\n // `!existing.packageName || packageName === existing.packageName`\n // regardless of stub status. Release C preserves that: removing\n // the `!existingIsManifestStub` gate lets a later\n // package-qualified manifest upgrade a stub's fields + alias\n // under the qualified key. See PR #1140 review (willgriffin P1\n // at line 270).\n scenario: 'manifest-same-package-merge',\n match: (i) =>\n i.origin === 'manifest' &&\n (i.samePackage || (i.existingHasNoPackage && i.hasNewQualifiedKey)) &&\n i.hasManifestContent,\n policy: 'merge-manifest',\n reason: () =>\n 'Manifest arriving for an existing same-package or unqualified source registration; fold fields in and alias the qualified key if keys differ.',\n },\n\n {\n scenario: 'manifest-default-skip',\n match: (i) => i.origin === 'manifest',\n policy: 'skip',\n reason: () =>\n 'Manifest collision with no inheritance relationship; keep existing entry (pre-Release-C resolveManifestCollision default).',\n },\n\n // --- Decorator-origin rows --------------------------------------------\n\n {\n scenario: 'decorator-exact-match-same-package-external',\n match: (i) =>\n i.origin === 'decorator' &&\n i.matchKind === 'exact-key' &&\n i.existingIsPackageQualified &&\n i.samePackage &&\n !i.sameConstructor,\n policy: 'accept',\n reason: () =>\n 'External package manifest entry being replaced by its own decorator-registered class (issue #584).',\n },\n\n {\n // pnpm-dup allows unscoped package names (old code didn't gate on\n // scoped-only). `samePackage` already requires both sides to have a\n // packageName and match, so no extra existingHasAnyPackage check\n // is needed.\n scenario: 'decorator-case-insensitive-pnpm-duplicate',\n match: (i) =>\n i.origin === 'decorator' &&\n i.matchKind === 'case-insensitive' &&\n i.existingKeyIsQualified &&\n i.samePackage &&\n i.sameName &&\n !i.sameConstructor,\n policy: 'accept',\n reason: () =>\n 'pnpm store duplicated the same package; different physical constructors but semantically identical.',\n },\n\n {\n // The decorator-path counterpart of\n // `manifest-different-packages-qualified-coexist` (#3106): a class whose\n // own package is known and differs from the existing entry's package is a\n // different class, not a duplicate — e.g. a consumer's `LicenseSale` next\n // to smrt-commerce's. Both live under their qualified keys. Inheritance\n // between them is left to the STI rows. In bundled output the declaring\n // package is only trusted when the new class declares another table: a\n // consumer bundle that inlined a dependency reports the consumer's\n // package for the dependency's classes, and the same class may also load\n // from the installed dependency (a SvelteKit build's analysis step does),\n // so neither the package nor the file location tells a duplicate apart.\n // Those duplicates are what the next row accepts.\n scenario: 'decorator-different-packages-qualified-coexist',\n match: (i) =>\n i.origin === 'decorator' &&\n i.hasNewQualifiedKey &&\n i.existingHasAnyPackage &&\n !i.samePackage &&\n !i.sameSourceFile &&\n !i.newExtendsExisting &&\n !i.existingExtendsNew &&\n (!i.newInBundledContext || i.declaresDifferentTable),\n policy: 'coexist-qualified',\n reason: () =>\n 'Different packages declare the same simple name; both registrations live under their qualified keys (issue #3106).',\n },\n\n {\n scenario: 'decorator-bundled-context-known-package',\n match: (i) =>\n i.origin === 'decorator' &&\n i.newInBundledContext &&\n i.existingIsPackageQualified,\n policy: 'accept',\n reason: () =>\n 'Bundled output duplicated a known package class across chunks; treat as re-registration.',\n },\n\n {\n scenario: 'decorator-same-source-file',\n match: (i) => i.origin === 'decorator' && i.sameSourceFile,\n policy: 'accept',\n reason: () =>\n 'Same source file — module re-evaluated (vitest test-isolation or HMR).',\n },\n\n {\n scenario: 'decorator-unknown-source-fallback',\n match: (i) => i.origin === 'decorator' && !i.bothSourceFilesKnown,\n policy: 'accept',\n reason: () =>\n 'Source file unavailable for comparison; allow re-registration rather than block legitimate updates.',\n },\n\n // --- Cross-origin STI fallbacks (used by decorator path; manifest\n // path has its own explicit rows above that come first) ------------\n\n {\n scenario: 'sti-child-wins',\n match: (i) => i.newExtendsExisting && !i.existingExtendsNew,\n policy: 'replace',\n reason: () => 'New class extends existing — STI child wins over parent.',\n },\n\n {\n scenario: 'sti-parent-skip',\n match: (i) => i.existingExtendsNew && !i.newExtendsExisting,\n policy: 'skip',\n reason: () => 'Existing entry extends new — STI child already registered.',\n },\n\n // --- Default: unresolvable decorator collision ------------------------\n\n {\n scenario: 'true-collision',\n match: () => true,\n policy: 'throw',\n reason: () =>\n 'Different constructors, different source files, no inheritance relationship. Unresolvable collision.',\n },\n];\n\n/**\n * Resolve a collision to a policy. See module docstring for the invocation\n * contract — callers MUST NOT invoke this for the no-candidate or\n * ambiguous-simple-name cases.\n */\nexport function decideCollisionPolicy(inputs: CollisionInputs): PolicyDecision {\n for (const row of ROWS) {\n if (row.match(inputs)) {\n return {\n policy: row.policy,\n scenario: row.scenario,\n reason: row.reason(inputs),\n };\n }\n }\n // Unreachable — `true-collision` is the final catch-all.\n throw new Error(\n `decideCollisionPolicy: no row matched (should be unreachable). Inputs: ${JSON.stringify(inputs)}`,\n );\n}\n\n/** Test-only: expose the row list so integration tests can scenario-id. */\nexport const __POLICY_ROWS_FOR_TEST: readonly Row[] = ROWS;\n"],"mappings":";;;;;;;;;;AAoNA,IAAM,OAAuB;CAG3B;EACE,UAAU;EACV,QAAQ,MAAM,EAAE;EAChB,QAAQ;EACR,cACE;CACJ;CAEA;EAUE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,eACb,EAAE,0BACF,CAAC,EAAE,mBACH,EACE,EAAE,sBACF,EAAE,yBACF,CAAC,EAAE,eACH,CAAC,EAAE,mBACD,EAAE,wBAAwB,CAAC,EAAE,uBAC7B,EAAE;EAER,QAAQ;EACR,cACE;CACJ;CAIA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,WAAW,cAAc,EAAE;EAC3C,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,cAAc,EAAE,sBAAsB,CAAC,EAAE;EACxD,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,cAAc,EAAE,sBAAsB,CAAC,EAAE;EACxD,QAAQ;EACR,cACE;CACJ;CAEA;EAOE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,cACb,CAAC,EAAE,eACH,EAAE,sBACF,EAAE;EACJ,QAAQ;EACR,cACE;CACJ;CAEA;EAkBE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,eACZ,EAAE,eAAgB,EAAE,wBAAwB,EAAE,uBAC/C,EAAE;EACJ,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,WAAW;EAC3B,QAAQ;EACR,cACE;CACJ;CAIA;EACE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,eACb,EAAE,cAAc,eAChB,EAAE,8BACF,EAAE,eACF,CAAC,EAAE;EACL,QAAQ;EACR,cACE;CACJ;CAEA;EAKE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,eACb,EAAE,cAAc,sBAChB,EAAE,0BACF,EAAE,eACF,EAAE,YACF,CAAC,EAAE;EACL,QAAQ;EACR,cACE;CACJ;CAEA;EAaE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,eACb,EAAE,sBACF,EAAE,yBACF,CAAC,EAAE,eACH,CAAC,EAAE,kBACH,CAAC,EAAE,sBACH,CAAC,EAAE,uBACF,CAAC,EAAE,uBAAuB,EAAE;EAC/B,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MACN,EAAE,WAAW,eACb,EAAE,uBACF,EAAE;EACJ,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,WAAW,eAAe,EAAE;EAC5C,QAAQ;EACR,cACE;CACJ;CAEA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,WAAW,eAAe,CAAC,EAAE;EAC7C,QAAQ;EACR,cACE;CACJ;CAKA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,sBAAsB,CAAC,EAAE;EACzC,QAAQ;EACR,cAAc;CAChB;CAEA;EACE,UAAU;EACV,QAAQ,MAAM,EAAE,sBAAsB,CAAC,EAAE;EACzC,QAAQ;EACR,cAAc;CAChB;CAIA;EACE,UAAU;EACV,aAAa;EACb,QAAQ;EACR,cACE;CACJ;AACF;;;;;;AAOA,SAAgB,sBAAsB,QAAyC;CAC7E,KAAK,MAAM,OAAO,MAChB,IAAI,IAAI,MAAM,MAAM,GAClB,OAAO;EACL,QAAQ,IAAI;EACZ,UAAU,IAAI;EACd,QAAQ,IAAI,OAAO,MAAM;CAC3B;CAIJ,MAAM,IAAI,MACR,0EAA0E,KAAK,UAAU,MAAM,GACjG;AACF"}
@@ -1,6 +1,6 @@
1
1
  import { ConfigurationError } from "../errors.js";
2
- import { prependSmrtSystemFields } from "../system-fields.js";
3
2
  import { getInheritanceCache, getInheritanceConfig, verboseLog } from "./shared-state.js";
3
+ import { prependSmrtSystemFields } from "../system-fields.js";
4
4
  import { findClass, findClassStrict } from "./name-resolver.js";
5
5
  import { ObjectRegistry } from "../registry.js";
6
6
  import { createLogger } from "@happyvertical/logger";
@@ -1 +1 @@
1
- {"version":3,"file":"name-resolver.d.ts","sourceRoot":"","sources":["../../src/registry/name-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAQ9E,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAyFtE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKtE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAuDnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,MAAM,GACnB,eAAe,GAAG,SAAS,CAyC7B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,cAAc,EAAE,MAAM,GACrB,MAAM,CAkCR;AAID;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAElE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,qBAAqB,GAC1B,eAAe,GAAG,SAAS,CAM7B;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,MAAM,GACpB,eAAe,GAAG,SAAS,CAE7B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,eAAe,GAAG,SAAS,CAG7B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CActE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,CAgCjE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,GAClB,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAU9B;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,cAAc,GACzB,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAW9B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAE/D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAE5D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAGxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAqBjD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE9C"}
1
+ {"version":3,"file":"name-resolver.d.ts","sourceRoot":"","sources":["../../src/registry/name-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAQ9E,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAyFtE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKtE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAuDnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,MAAM,GACnB,eAAe,GAAG,SAAS,CAyC7B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,cAAc,EAAE,MAAM,GACrB,MAAM,CAyDR;AAID;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAElE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,qBAAqB,GAC1B,eAAe,GAAG,SAAS,CAM7B;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,MAAM,GACpB,eAAe,GAAG,SAAS,CAE7B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,eAAe,GAAG,SAAS,CAG7B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CActE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,CAgCjE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,GAClB,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAU9B;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,cAAc,GACzB,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAW9B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAE/D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAE5D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAGxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAqBjD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE9C"}
@@ -2,6 +2,7 @@ import { ConfigurationError } from "../errors.js";
2
2
  import { getRegistryGeneration } from "./generation.js";
3
3
  import { createQualifiedName, isQualifiedName, parseQualifiedName } from "../utils/qualified-names.js";
4
4
  import { getClasses, getConstructorIndex, verboseLog } from "./shared-state.js";
5
+ import { getLocalTestManifestCache, getManifestCache, getTestManifestCache } from "../manifest/store.js";
5
6
  //#region src/registry/name-resolver.ts
6
7
  /**
7
8
  * Name resolution module for the SMRT ObjectRegistry.
@@ -176,6 +177,16 @@ function qualifyExtendsName(extendsValue, currentPackage) {
176
177
  if (extendsValue === "SmrtObject" || extendsValue === "SmrtClass" || extendsValue === "SmrtCollection") return extendsValue;
177
178
  const samePackageQualified = createQualifiedName(currentPackage, extendsValue);
178
179
  if (classes.has(samePackageQualified)) return samePackageQualified;
180
+ if ([
181
+ getManifestCache().get(currentPackage),
182
+ getLocalTestManifestCache(),
183
+ getTestManifestCache()
184
+ ].some((manifest) => {
185
+ if (manifest?.packageName !== currentPackage) return false;
186
+ const entry = manifest.objects?.[samePackageQualified];
187
+ const entryParent = entry?.extends?.split(":").pop();
188
+ return !!entry && entryParent !== extendsValue;
189
+ })) return samePackageQualified;
179
190
  const keys = registryKeysBySimpleName(extendsValue);
180
191
  if (keys.length === 1) return keys[0];
181
192
  return extendsValue;
@@ -1 +1 @@
1
- {"version":3,"file":"name-resolver.js","names":[],"sources":["../../src/registry/name-resolver.ts"],"sourcesContent":["/**\n * Name resolution module for the SMRT ObjectRegistry.\n *\n * Handles class lookup, disambiguation, and qualified name resolution.\n * All functions operate on the shared globalThis state.\n *\n * Release B (#1133): classNameMap is gone — the eagerly-maintained\n * lowercase-simple-name → qualified-key[] index was removed in favor of\n * on-demand iteration over the `classes` Map. Production SMRT apps carry\n * a few hundred classes at most, so the linear scan is negligible and\n * removes an entire class of cache-sync bugs (#584, #847, #951).\n *\n * #3047: hydration resolves names per row, so the scan now runs once per\n * registry generation into a derived (never hand-maintained) index; see\n * `getSimpleNameIndex()` and `registry/generation.ts`.\n *\n * Extracted from registry.ts as part of issue #1006.\n * @see https://github.com/happyvertical/smrt/issues/1006\n * @see https://github.com/happyvertical/smrt/issues/1133\n */\n\nimport { ConfigurationError } from '../errors';\nimport type { QualifiedClassName, SmrtVisibility } from '../scanner/types.js';\nimport {\n createQualifiedName,\n isQualifiedName,\n parseQualifiedName,\n} from '../utils/qualified-names.js';\nimport { getRegistryGeneration } from './generation';\nimport { getClasses, getConstructorIndex, verboseLog } from './shared-state';\nimport type { RegisteredClass, SmrtObjectConstructor } from './types';\n\n// ── Simple-name iteration (replaces classNameMap reads) ─────────────\n\n/**\n * Return every registry key whose registered class has the given simple\n * name (case-insensitive). Replaces the old eagerly-maintained\n * `__smrtRegistryClassNameMap` lookup: we iterate `classes` instead.\n *\n * Cost is O(n) over the classes map; production SMRT apps hold low-\n * hundreds of entries so this is trivially fast. Returned keys preserve\n * insertion order, which makes the \"first match wins\" behavior of the old\n * map-based lookup deterministic in the same way.\n *\n * De-duplicates by RegisteredClass object identity so a class registered\n * under both a simple key and a promoted qualified key (a transitional\n * state during manifest merge) counts as a single entry — matching the\n * old classNameMap's behavior of storing each canonical qualified key once.\n * When that happens, the qualified key wins over the simple one.\n */\nfunction registryKeysBySimpleName(simpleName: string): string[] {\n const keys = getSimpleNameIndex().get(simpleName.toLowerCase());\n return keys ? [...keys] : [];\n}\n\n/**\n * Lower-cased simple name → registry keys, rebuilt once per registry\n * generation (#3047).\n *\n * #1133 removed an eagerly-maintained name index because every writer had to\n * keep it in sync. This one is derived, not maintained: it is rebuilt from\n * the `classes` map whenever the registry generation moves, and every\n * registry mutation bumps the generation (the map itself is\n * generation-tracked; in-place identity changes bump explicitly). Hydration\n * resolves names several times per row, so the O(registered classes) scan\n * per lookup dominated large result sets.\n */\nlet simpleNameIndex:\n | {\n generation: number;\n classes: Map<string, RegisteredClass>;\n keysByName: Map<string, string[]>;\n }\n | undefined;\n\nfunction getSimpleNameIndex(): Map<string, string[]> {\n const classes = getClasses();\n const generation = getRegistryGeneration();\n if (\n simpleNameIndex &&\n simpleNameIndex.generation === generation &&\n simpleNameIndex.classes === classes\n ) {\n return simpleNameIndex.keysByName;\n }\n\n const byName = new Map<string, Map<RegisteredClass, string>>();\n for (const [key, value] of classes.entries()) {\n const lower = value.name?.toLowerCase();\n if (!lower) continue;\n\n let seen = byName.get(lower);\n if (!seen) {\n seen = new Map<RegisteredClass, string>();\n byName.set(lower, seen);\n }\n\n const existing = seen.get(value);\n if (!existing) {\n seen.set(value, key);\n continue;\n }\n\n // Two keys for the same object: prefer the qualified form.\n if (key.includes(':') && !existing.includes(':')) {\n seen.set(value, key);\n }\n }\n\n const keysByName = new Map<string, string[]>();\n for (const [lower, seen] of byName) {\n keysByName.set(lower, [...seen.values()]);\n }\n simpleNameIndex = { generation, classes, keysByName };\n return keysByName;\n}\n\n// ── Lookup functions ────────────────────────────────────────\n\n/**\n * Check if a class is already registered (case-insensitive).\n * Returns the canonical name if found, undefined otherwise. Returns\n * undefined if the simple name is ambiguous across multiple packages.\n */\nexport function getCanonicalClassName(name: string): string | undefined {\n const keys = registryKeysBySimpleName(name);\n if (keys.length === 1) return keys[0];\n // Zero matches, or ambiguous with >1 entries\n return undefined;\n}\n\n/**\n * Check if a class exists by name (case-insensitive).\n */\nexport function hasClassCaseInsensitive(name: string): boolean {\n return getSimpleNameIndex().has(name.toLowerCase());\n}\n\n/**\n * Helper for class lookup with qualified name support.\n *\n * Lookup priority:\n * 1. Direct hit on classes map (works for qualified names as keys)\n * 2. If input contains ':', prefer direct qualified lookup, then fall back\n * to an exact/simple registration when runtime source imports registered\n * the class before package-qualified promotion happened\n * 3. Simple-name iteration by lowercase\n * - Unambiguous (1 match) → return it\n * - Ambiguous (>1 matches) → log warning, return first\n */\nexport function findClass(name: string): RegisteredClass | undefined {\n const classes = getClasses();\n\n // 1. Direct hit on classes map (fast path, works for qualified keys)\n const registered = classes.get(name);\n if (registered) {\n return registered;\n }\n\n // 2. Qualified lookup fallback for source-registered classes.\n // In workspace/dev mode a package can be imported from source before a\n // manifest-promoted qualified key exists. If we already have exactly the\n // requested package or a single unqualified registration for this class\n // name, treat it as the same class instead of forcing node_modules manifest\n // discovery.\n if (isQualifiedName(name)) {\n const { packageName, className } = parseQualifiedName(name);\n const keys = registryKeysBySimpleName(className);\n\n if (keys.length > 0) {\n const exactPackageMatch = keys\n .map((key) => classes.get(key))\n .find((candidate) => candidate?.packageName === packageName);\n\n if (exactPackageMatch) {\n return exactPackageMatch;\n }\n\n if (keys.length === 1) {\n const fallback = classes.get(keys[0]);\n if (fallback && !fallback.packageName) {\n return fallback;\n }\n }\n }\n\n return undefined;\n }\n\n // 3. Simple-name lookup\n const keys = registryKeysBySimpleName(name);\n if (keys.length > 0) {\n if (keys.length === 1) {\n return classes.get(keys[0]);\n }\n // Ambiguous — multiple packages define this class name\n // Return first match but log a warning (use resolveType() for strict behavior)\n verboseLog(\n `[registry] findClass(\"${name}\") is ambiguous — ${keys.length} matches. ` +\n `Use qualified name (e.g., ${keys[0]}) for precision.`,\n );\n return classes.get(keys[0]);\n }\n\n return undefined;\n}\n\n/**\n * Strict class lookup with package-aware disambiguation.\n *\n * Unlike `findClass()` which silently returns the first match when a simple\n * name is ambiguous, this method throws a `ConfigurationError` — making it\n * safe for inheritance-critical paths.\n *\n * @throws {ConfigurationError} When simple name is ambiguous and no package context resolves it\n * @see https://github.com/happyvertical/smrt/issues/1005\n */\nexport function findClassStrict(\n name: string,\n fromPackage?: string,\n): RegisteredClass | undefined {\n const classes = getClasses();\n\n // 1. Direct hit on classes map (fast path, works for qualified keys)\n const registered = classes.get(name);\n if (registered) {\n return registered;\n }\n\n // 2. If input is a qualified name, no fallback — it's not found\n if (isQualifiedName(name)) {\n return undefined;\n }\n\n // 3. If fromPackage provided, try constructing qualified name for direct lookup\n if (fromPackage) {\n const qualifiedAttempt = createQualifiedName(fromPackage, name);\n const byQualified = classes.get(qualifiedAttempt);\n if (byQualified) {\n return byQualified;\n }\n }\n\n // 4. Simple-name lookup\n const keys = registryKeysBySimpleName(name);\n if (keys.length > 0) {\n if (keys.length === 1) {\n return classes.get(keys[0]);\n }\n // Ambiguous — multiple packages define this class name\n // In strict mode, throw instead of silently returning first match\n throw new ConfigurationError(\n `Ambiguous class name \"${name}\" — found in ${keys.length} packages: ` +\n `${keys.join(', ')}. ` +\n `Use a qualified name (e.g., ${keys[0]}) to disambiguate.`,\n 'CONFIG_AMBIGUOUS_CLASS',\n { className: name, candidates: keys },\n );\n }\n\n return undefined;\n}\n\n/**\n * Qualify an `extends` value with the parent class's package name.\n *\n * @see https://github.com/happyvertical/smrt/issues/1004\n */\nexport function qualifyExtendsName(\n extendsValue: string,\n currentPackage: string,\n): string {\n const classes = getClasses();\n\n // Already qualified → pass through\n if (isQualifiedName(extendsValue)) {\n return extendsValue;\n }\n\n // Skip framework base classes (never registered with qualified names)\n if (\n extendsValue === 'SmrtObject' ||\n extendsValue === 'SmrtClass' ||\n extendsValue === 'SmrtCollection'\n ) {\n return extendsValue;\n }\n\n // Try same-package first (common case: child and parent in same package)\n const samePackageQualified = createQualifiedName(\n currentPackage,\n extendsValue,\n );\n if (classes.has(samePackageQualified)) {\n return samePackageQualified;\n }\n\n // Try to find the parent in any registered package\n const keys = registryKeysBySimpleName(extendsValue);\n if (keys.length === 1) {\n return keys[0];\n }\n\n // Fallback: return unmodified (backward compat)\n return extendsValue;\n}\n\n// ── Public lookup functions ─────────────────────────────────\n\n/**\n * Get a registered class by name (case-insensitive).\n */\nexport function getClass(name: string): RegisteredClass | undefined {\n return findClass(name);\n}\n\n/**\n * Get a registered class by its constructor reference (O(1) WeakMap lookup).\n */\nexport function getClassByConstructor(\n ctor: SmrtObjectConstructor,\n): RegisteredClass | undefined {\n const registeredName = getConstructorIndex().get(ctor);\n if (registeredName) {\n return getClasses().get(registeredName);\n }\n return undefined;\n}\n\n/**\n * Get a registered class by its qualified name (O(1) direct lookup).\n */\nexport function getClassByQualifiedName(\n qualifiedName: string,\n): RegisteredClass | undefined {\n return getClasses().get(qualifiedName);\n}\n\n/**\n * Get a registered class by package name and class name.\n */\nexport function getClassInPackage(\n packageName: string,\n className: string,\n): RegisteredClass | undefined {\n const qualifiedName = createQualifiedName(packageName, className);\n return getClasses().get(qualifiedName);\n}\n\n/**\n * Find all registered classes with a given simple class name.\n */\nexport function findClassesByName(className: string): RegisteredClass[] {\n // A source registration can retain its simple key while manifest hydration\n // adds its canonical qualified key. Both aliases designate one class, so\n // ambiguity means distinct RegisteredClass identities — which is what the\n // index's per-name keys already are (one key per identity).\n const classes = getClasses();\n const matches: RegisteredClass[] = [];\n for (const key of getSimpleNameIndex().get(className.toLowerCase()) ?? []) {\n const registered = classes.get(key);\n if (registered) {\n matches.push(registered);\n }\n }\n return matches;\n}\n\n/**\n * Resolve a short class name to its qualified name.\n * @throws {Error} If no class or ambiguous classes registered\n */\nexport function resolveType(shortName: string): QualifiedClassName {\n // If already qualified, validate and return\n if (shortName.includes(':') && shortName.startsWith('@')) {\n const registered = getClassByQualifiedName(shortName as QualifiedClassName);\n if (!registered) {\n throw new Error(\n `Class \"${shortName}\" is not registered. ` +\n `Make sure the package is installed and the class is decorated with @smrt().`,\n );\n }\n return shortName as QualifiedClassName;\n }\n\n // Find all classes with this short name\n const matches = findClassesByName(shortName);\n\n if (matches.length === 0) {\n throw new Error(\n `Class \"${shortName}\" is not registered. ` +\n `Make sure the package is installed and the class is decorated with @smrt().`,\n );\n }\n\n if (matches.length > 1) {\n const packageList = matches.map((m) => ` - ${m.qualifiedName}`).join('\\n');\n throw new Error(\n `\"${shortName}\" is ambiguous. Found in multiple packages:\\n${packageList}\\n` +\n `Use the fully qualified name instead.`,\n );\n }\n\n return matches[0].qualifiedName as QualifiedClassName;\n}\n\n/**\n * Get all registered classes from a specific package.\n */\nexport function getClassesByPackage(\n packageName: string,\n): Map<string, RegisteredClass> {\n const result = new Map<string, RegisteredClass>();\n\n for (const [name, registered] of getClasses().entries()) {\n if (registered.packageName === packageName) {\n result.set(name, registered);\n }\n }\n\n return result;\n}\n\n/**\n * Get all registered classes with a specific visibility level.\n */\nexport function getClassesByVisibility(\n visibility: SmrtVisibility,\n): Map<string, RegisteredClass> {\n const result = new Map<string, RegisteredClass>();\n\n for (const [name, registered] of getClasses().entries()) {\n const classVisibility = registered.visibility || 'public';\n if (classVisibility === visibility) {\n result.set(name, registered);\n }\n }\n\n return result;\n}\n\n/**\n * Get all public registered classes.\n */\nexport function getPublicClasses(): Map<string, RegisteredClass> {\n return getClassesByVisibility('public');\n}\n\n/**\n * Get all registered classes.\n */\nexport function getAllClasses(): Map<string, RegisteredClass> {\n return new Map(getClasses());\n}\n\n/**\n * Get class names (simple names, deduplicated).\n */\nexport function getClassNames(): string[] {\n const names = Array.from(getClasses().values()).map((entry) => entry.name);\n return Array.from(new Set(names));\n}\n\n/**\n * Get lookup names for every registered class without collapsing\n * cross-package simple-name collisions.\n *\n * Prefer each registration's qualified name when available, fall back to the\n * registry key, then to the simple class name for package-less registrations.\n * De-duplicates aliases that point to the same RegisteredClass object while\n * preserving distinct packages that intentionally share a simple class name.\n */\nexport function getQualifiedClassNames(): string[] {\n const names: string[] = [];\n const seenRegistrations = new Set<RegisteredClass>();\n const seenNames = new Set<string>();\n\n for (const [key, entry] of getClasses().entries()) {\n if (seenRegistrations.has(entry)) {\n continue;\n }\n seenRegistrations.add(entry);\n\n const lookupName = entry.qualifiedName || key || entry.name;\n if (seenNames.has(lookupName)) {\n continue;\n }\n\n seenNames.add(lookupName);\n names.push(lookupName);\n }\n\n return names;\n}\n\n/**\n * Check if a class is registered (case-insensitive).\n */\nexport function hasClass(name: string): boolean {\n return findClass(name) !== undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,SAAS,yBAAyB,YAA8B;CAC9D,MAAM,OAAO,mBAAmB,CAAC,CAAC,IAAI,WAAW,YAAY,CAAC;CAC9D,OAAO,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;AAC7B;;;;;;;;;;;;;AAcA,IAAI;AAQJ,SAAS,qBAA4C;CACnD,MAAM,UAAU,WAAW;CAC3B,MAAM,aAAa,sBAAsB;CACzC,IACE,mBACA,gBAAgB,eAAe,cAC/B,gBAAgB,YAAY,SAE5B,OAAO,gBAAgB;CAGzB,MAAM,yBAAS,IAAI,IAA0C;CAC7D,KAAK,MAAM,CAAC,KAAK,UAAU,QAAQ,QAAQ,GAAG;EAC5C,MAAM,QAAQ,MAAM,MAAM,YAAY;EACtC,IAAI,CAAC,OAAO;EAEZ,IAAI,OAAO,OAAO,IAAI,KAAK;EAC3B,IAAI,CAAC,MAAM;GACT,uBAAO,IAAI,IAA6B;GACxC,OAAO,IAAI,OAAO,IAAI;EACxB;EAEA,MAAM,WAAW,KAAK,IAAI,KAAK;EAC/B,IAAI,CAAC,UAAU;GACb,KAAK,IAAI,OAAO,GAAG;GACnB;EACF;EAGA,IAAI,IAAI,SAAS,GAAG,KAAK,CAAC,SAAS,SAAS,GAAG,GAC7C,KAAK,IAAI,OAAO,GAAG;CAEvB;CAEA,MAAM,6BAAa,IAAI,IAAsB;CAC7C,KAAK,MAAM,CAAC,OAAO,SAAS,QAC1B,WAAW,IAAI,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC;CAE1C,kBAAkB;EAAE;EAAY;EAAS;CAAW;CACpD,OAAO;AACT;;;;;;AASA,SAAgB,sBAAsB,MAAkC;CACtE,MAAM,OAAO,yBAAyB,IAAI;CAC1C,IAAI,KAAK,WAAW,GAAG,OAAO,KAAK;AAGrC;;;;AAKA,SAAgB,wBAAwB,MAAuB;CAC7D,OAAO,mBAAmB,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC;AACpD;;;;;;;;;;;;;AAcA,SAAgB,UAAU,MAA2C;CACnE,MAAM,UAAU,WAAW;CAG3B,MAAM,aAAa,QAAQ,IAAI,IAAI;CACnC,IAAI,YACF,OAAO;CAST,IAAI,gBAAgB,IAAI,GAAG;EACzB,MAAM,EAAE,aAAa,cAAc,mBAAmB,IAAI;EAC1D,MAAM,OAAO,yBAAyB,SAAS;EAE/C,IAAI,KAAK,SAAS,GAAG;GACnB,MAAM,oBAAoB,KACvB,KAAK,QAAQ,QAAQ,IAAI,GAAG,CAAC,CAAC,CAC9B,MAAM,cAAc,WAAW,gBAAgB,WAAW;GAE7D,IAAI,mBACF,OAAO;GAGT,IAAI,KAAK,WAAW,GAAG;IACrB,MAAM,WAAW,QAAQ,IAAI,KAAK,EAAE;IACpC,IAAI,YAAY,CAAC,SAAS,aACxB,OAAO;GAEX;EACF;EAEA;CACF;CAGA,MAAM,OAAO,yBAAyB,IAAI;CAC1C,IAAI,KAAK,SAAS,GAAG;EACnB,IAAI,KAAK,WAAW,GAClB,OAAO,QAAQ,IAAI,KAAK,EAAE;EAI5B,WACE,yBAAyB,KAAK,oBAAoB,KAAK,OAAO,sCAC/B,KAAK,GAAG,iBACzC;EACA,OAAO,QAAQ,IAAI,KAAK,EAAE;CAC5B;AAGF;;;;;;;;;;;AAYA,SAAgB,gBACd,MACA,aAC6B;CAC7B,MAAM,UAAU,WAAW;CAG3B,MAAM,aAAa,QAAQ,IAAI,IAAI;CACnC,IAAI,YACF,OAAO;CAIT,IAAI,gBAAgB,IAAI,GACtB;CAIF,IAAI,aAAa;EACf,MAAM,mBAAmB,oBAAoB,aAAa,IAAI;EAC9D,MAAM,cAAc,QAAQ,IAAI,gBAAgB;EAChD,IAAI,aACF,OAAO;CAEX;CAGA,MAAM,OAAO,yBAAyB,IAAI;CAC1C,IAAI,KAAK,SAAS,GAAG;EACnB,IAAI,KAAK,WAAW,GAClB,OAAO,QAAQ,IAAI,KAAK,EAAE;EAI5B,MAAM,IAAI,mBACR,yBAAyB,KAAK,eAAe,KAAK,OAAO,aACpD,KAAK,KAAK,IAAI,EAAE,gCACY,KAAK,GAAG,qBACzC,0BACA;GAAE,WAAW;GAAM,YAAY;EAAK,CACtC;CACF;AAGF;;;;;;AAOA,SAAgB,mBACd,cACA,gBACQ;CACR,MAAM,UAAU,WAAW;CAG3B,IAAI,gBAAgB,YAAY,GAC9B,OAAO;CAIT,IACE,iBAAiB,gBACjB,iBAAiB,eACjB,iBAAiB,kBAEjB,OAAO;CAIT,MAAM,uBAAuB,oBAC3B,gBACA,YACF;CACA,IAAI,QAAQ,IAAI,oBAAoB,GAClC,OAAO;CAIT,MAAM,OAAO,yBAAyB,YAAY;CAClD,IAAI,KAAK,WAAW,GAClB,OAAO,KAAK;CAId,OAAO;AACT;;;;AAOA,SAAgB,SAAS,MAA2C;CAClE,OAAO,UAAU,IAAI;AACvB;;;;AAKA,SAAgB,sBACd,MAC6B;CAC7B,MAAM,iBAAiB,oBAAoB,CAAC,CAAC,IAAI,IAAI;CACrD,IAAI,gBACF,OAAO,WAAW,CAAC,CAAC,IAAI,cAAc;AAG1C;;;;AAKA,SAAgB,wBACd,eAC6B;CAC7B,OAAO,WAAW,CAAC,CAAC,IAAI,aAAa;AACvC;;;;AAKA,SAAgB,kBACd,aACA,WAC6B;CAC7B,MAAM,gBAAgB,oBAAoB,aAAa,SAAS;CAChE,OAAO,WAAW,CAAC,CAAC,IAAI,aAAa;AACvC;;;;AAKA,SAAgB,kBAAkB,WAAsC;CAKtE,MAAM,UAAU,WAAW;CAC3B,MAAM,UAA6B,CAAC;CACpC,KAAK,MAAM,OAAO,mBAAmB,CAAC,CAAC,IAAI,UAAU,YAAY,CAAC,KAAK,CAAC,GAAG;EACzE,MAAM,aAAa,QAAQ,IAAI,GAAG;EAClC,IAAI,YACF,QAAQ,KAAK,UAAU;CAE3B;CACA,OAAO;AACT;;;;;AAMA,SAAgB,YAAY,WAAuC;CAEjE,IAAI,UAAU,SAAS,GAAG,KAAK,UAAU,WAAW,GAAG,GAAG;EAExD,IAAI,CADe,wBAAwB,SACtC,GACH,MAAM,IAAI,MACR,UAAU,UAAU,iGAEtB;EAEF,OAAO;CACT;CAGA,MAAM,UAAU,kBAAkB,SAAS;CAE3C,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,MACR,UAAU,UAAU,iGAEtB;CAGF,IAAI,QAAQ,SAAS,GAAG;EACtB,MAAM,cAAc,QAAQ,KAAK,MAAM,OAAO,EAAE,eAAe,CAAC,CAAC,KAAK,IAAI;EAC1E,MAAM,IAAI,MACR,IAAI,UAAU,+CAA+C,YAAY,wCAE3E;CACF;CAEA,OAAO,QAAQ,EAAE,CAAC;AACpB;;;;AAKA,SAAgB,oBACd,aAC8B;CAC9B,MAAM,yBAAS,IAAI,IAA6B;CAEhD,KAAK,MAAM,CAAC,MAAM,eAAe,WAAW,CAAC,CAAC,QAAQ,GACpD,IAAI,WAAW,gBAAgB,aAC7B,OAAO,IAAI,MAAM,UAAU;CAI/B,OAAO;AACT;;;;AAKA,SAAgB,uBACd,YAC8B;CAC9B,MAAM,yBAAS,IAAI,IAA6B;CAEhD,KAAK,MAAM,CAAC,MAAM,eAAe,WAAW,CAAC,CAAC,QAAQ,GAEpD,KADwB,WAAW,cAAc,cACzB,YACtB,OAAO,IAAI,MAAM,UAAU;CAI/B,OAAO;AACT;;;;AAKA,SAAgB,mBAAiD;CAC/D,OAAO,uBAAuB,QAAQ;AACxC;;;;AAKA,SAAgB,gBAA8C;CAC5D,OAAO,IAAI,IAAI,WAAW,CAAC;AAC7B;;;;AAKA,SAAgB,gBAA0B;CACxC,MAAM,QAAQ,MAAM,KAAK,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,UAAU,MAAM,IAAI;CACzE,OAAO,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC;AAClC;;;;;;;;;;AAWA,SAAgB,yBAAmC;CACjD,MAAM,QAAkB,CAAC;CACzB,MAAM,oCAAoB,IAAI,IAAqB;CACnD,MAAM,4BAAY,IAAI,IAAY;CAElC,KAAK,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,CAAC,QAAQ,GAAG;EACjD,IAAI,kBAAkB,IAAI,KAAK,GAC7B;EAEF,kBAAkB,IAAI,KAAK;EAE3B,MAAM,aAAa,MAAM,iBAAiB,OAAO,MAAM;EACvD,IAAI,UAAU,IAAI,UAAU,GAC1B;EAGF,UAAU,IAAI,UAAU;EACxB,MAAM,KAAK,UAAU;CACvB;CAEA,OAAO;AACT;;;;AAKA,SAAgB,SAAS,MAAuB;CAC9C,OAAO,UAAU,IAAI,MAAM,KAAA;AAC7B"}
1
+ {"version":3,"file":"name-resolver.js","names":[],"sources":["../../src/registry/name-resolver.ts"],"sourcesContent":["/**\n * Name resolution module for the SMRT ObjectRegistry.\n *\n * Handles class lookup, disambiguation, and qualified name resolution.\n * All functions operate on the shared globalThis state.\n *\n * Release B (#1133): classNameMap is gone — the eagerly-maintained\n * lowercase-simple-name → qualified-key[] index was removed in favor of\n * on-demand iteration over the `classes` Map. Production SMRT apps carry\n * a few hundred classes at most, so the linear scan is negligible and\n * removes an entire class of cache-sync bugs (#584, #847, #951).\n *\n * #3047: hydration resolves names per row, so the scan now runs once per\n * registry generation into a derived (never hand-maintained) index; see\n * `getSimpleNameIndex()` and `registry/generation.ts`.\n *\n * Extracted from registry.ts as part of issue #1006.\n * @see https://github.com/happyvertical/smrt/issues/1006\n * @see https://github.com/happyvertical/smrt/issues/1133\n */\n\nimport { ConfigurationError } from '../errors';\nimport {\n getLocalTestManifestCache,\n getManifestCache,\n getTestManifestCache,\n} from '../manifest/store.js';\nimport type { QualifiedClassName, SmrtVisibility } from '../scanner/types.js';\nimport {\n createQualifiedName,\n isQualifiedName,\n parseQualifiedName,\n} from '../utils/qualified-names.js';\nimport { getRegistryGeneration } from './generation';\nimport { getClasses, getConstructorIndex, verboseLog } from './shared-state';\nimport type { RegisteredClass, SmrtObjectConstructor } from './types';\n\n// ── Simple-name iteration (replaces classNameMap reads) ─────────────\n\n/**\n * Return every registry key whose registered class has the given simple\n * name (case-insensitive). Replaces the old eagerly-maintained\n * `__smrtRegistryClassNameMap` lookup: we iterate `classes` instead.\n *\n * Cost is O(n) over the classes map; production SMRT apps hold low-\n * hundreds of entries so this is trivially fast. Returned keys preserve\n * insertion order, which makes the \"first match wins\" behavior of the old\n * map-based lookup deterministic in the same way.\n *\n * De-duplicates by RegisteredClass object identity so a class registered\n * under both a simple key and a promoted qualified key (a transitional\n * state during manifest merge) counts as a single entry — matching the\n * old classNameMap's behavior of storing each canonical qualified key once.\n * When that happens, the qualified key wins over the simple one.\n */\nfunction registryKeysBySimpleName(simpleName: string): string[] {\n const keys = getSimpleNameIndex().get(simpleName.toLowerCase());\n return keys ? [...keys] : [];\n}\n\n/**\n * Lower-cased simple name → registry keys, rebuilt once per registry\n * generation (#3047).\n *\n * #1133 removed an eagerly-maintained name index because every writer had to\n * keep it in sync. This one is derived, not maintained: it is rebuilt from\n * the `classes` map whenever the registry generation moves, and every\n * registry mutation bumps the generation (the map itself is\n * generation-tracked; in-place identity changes bump explicitly). Hydration\n * resolves names several times per row, so the O(registered classes) scan\n * per lookup dominated large result sets.\n */\nlet simpleNameIndex:\n | {\n generation: number;\n classes: Map<string, RegisteredClass>;\n keysByName: Map<string, string[]>;\n }\n | undefined;\n\nfunction getSimpleNameIndex(): Map<string, string[]> {\n const classes = getClasses();\n const generation = getRegistryGeneration();\n if (\n simpleNameIndex &&\n simpleNameIndex.generation === generation &&\n simpleNameIndex.classes === classes\n ) {\n return simpleNameIndex.keysByName;\n }\n\n const byName = new Map<string, Map<RegisteredClass, string>>();\n for (const [key, value] of classes.entries()) {\n const lower = value.name?.toLowerCase();\n if (!lower) continue;\n\n let seen = byName.get(lower);\n if (!seen) {\n seen = new Map<RegisteredClass, string>();\n byName.set(lower, seen);\n }\n\n const existing = seen.get(value);\n if (!existing) {\n seen.set(value, key);\n continue;\n }\n\n // Two keys for the same object: prefer the qualified form.\n if (key.includes(':') && !existing.includes(':')) {\n seen.set(value, key);\n }\n }\n\n const keysByName = new Map<string, string[]>();\n for (const [lower, seen] of byName) {\n keysByName.set(lower, [...seen.values()]);\n }\n simpleNameIndex = { generation, classes, keysByName };\n return keysByName;\n}\n\n// ── Lookup functions ────────────────────────────────────────\n\n/**\n * Check if a class is already registered (case-insensitive).\n * Returns the canonical name if found, undefined otherwise. Returns\n * undefined if the simple name is ambiguous across multiple packages.\n */\nexport function getCanonicalClassName(name: string): string | undefined {\n const keys = registryKeysBySimpleName(name);\n if (keys.length === 1) return keys[0];\n // Zero matches, or ambiguous with >1 entries\n return undefined;\n}\n\n/**\n * Check if a class exists by name (case-insensitive).\n */\nexport function hasClassCaseInsensitive(name: string): boolean {\n return getSimpleNameIndex().has(name.toLowerCase());\n}\n\n/**\n * Helper for class lookup with qualified name support.\n *\n * Lookup priority:\n * 1. Direct hit on classes map (works for qualified names as keys)\n * 2. If input contains ':', prefer direct qualified lookup, then fall back\n * to an exact/simple registration when runtime source imports registered\n * the class before package-qualified promotion happened\n * 3. Simple-name iteration by lowercase\n * - Unambiguous (1 match) → return it\n * - Ambiguous (>1 matches) → log warning, return first\n */\nexport function findClass(name: string): RegisteredClass | undefined {\n const classes = getClasses();\n\n // 1. Direct hit on classes map (fast path, works for qualified keys)\n const registered = classes.get(name);\n if (registered) {\n return registered;\n }\n\n // 2. Qualified lookup fallback for source-registered classes.\n // In workspace/dev mode a package can be imported from source before a\n // manifest-promoted qualified key exists. If we already have exactly the\n // requested package or a single unqualified registration for this class\n // name, treat it as the same class instead of forcing node_modules manifest\n // discovery.\n if (isQualifiedName(name)) {\n const { packageName, className } = parseQualifiedName(name);\n const keys = registryKeysBySimpleName(className);\n\n if (keys.length > 0) {\n const exactPackageMatch = keys\n .map((key) => classes.get(key))\n .find((candidate) => candidate?.packageName === packageName);\n\n if (exactPackageMatch) {\n return exactPackageMatch;\n }\n\n if (keys.length === 1) {\n const fallback = classes.get(keys[0]);\n if (fallback && !fallback.packageName) {\n return fallback;\n }\n }\n }\n\n return undefined;\n }\n\n // 3. Simple-name lookup\n const keys = registryKeysBySimpleName(name);\n if (keys.length > 0) {\n if (keys.length === 1) {\n return classes.get(keys[0]);\n }\n // Ambiguous — multiple packages define this class name\n // Return first match but log a warning (use resolveType() for strict behavior)\n verboseLog(\n `[registry] findClass(\"${name}\") is ambiguous — ${keys.length} matches. ` +\n `Use qualified name (e.g., ${keys[0]}) for precision.`,\n );\n return classes.get(keys[0]);\n }\n\n return undefined;\n}\n\n/**\n * Strict class lookup with package-aware disambiguation.\n *\n * Unlike `findClass()` which silently returns the first match when a simple\n * name is ambiguous, this method throws a `ConfigurationError` — making it\n * safe for inheritance-critical paths.\n *\n * @throws {ConfigurationError} When simple name is ambiguous and no package context resolves it\n * @see https://github.com/happyvertical/smrt/issues/1005\n */\nexport function findClassStrict(\n name: string,\n fromPackage?: string,\n): RegisteredClass | undefined {\n const classes = getClasses();\n\n // 1. Direct hit on classes map (fast path, works for qualified keys)\n const registered = classes.get(name);\n if (registered) {\n return registered;\n }\n\n // 2. If input is a qualified name, no fallback — it's not found\n if (isQualifiedName(name)) {\n return undefined;\n }\n\n // 3. If fromPackage provided, try constructing qualified name for direct lookup\n if (fromPackage) {\n const qualifiedAttempt = createQualifiedName(fromPackage, name);\n const byQualified = classes.get(qualifiedAttempt);\n if (byQualified) {\n return byQualified;\n }\n }\n\n // 4. Simple-name lookup\n const keys = registryKeysBySimpleName(name);\n if (keys.length > 0) {\n if (keys.length === 1) {\n return classes.get(keys[0]);\n }\n // Ambiguous — multiple packages define this class name\n // In strict mode, throw instead of silently returning first match\n throw new ConfigurationError(\n `Ambiguous class name \"${name}\" — found in ${keys.length} packages: ` +\n `${keys.join(', ')}. ` +\n `Use a qualified name (e.g., ${keys[0]}) to disambiguate.`,\n 'CONFIG_AMBIGUOUS_CLASS',\n { className: name, candidates: keys },\n );\n }\n\n return undefined;\n}\n\n/**\n * Qualify an `extends` value with the parent class's package name.\n *\n * @see https://github.com/happyvertical/smrt/issues/1004\n */\nexport function qualifyExtendsName(\n extendsValue: string,\n currentPackage: string,\n): string {\n const classes = getClasses();\n\n // Already qualified → pass through\n if (isQualifiedName(extendsValue)) {\n return extendsValue;\n }\n\n // Skip framework base classes (never registered with qualified names)\n if (\n extendsValue === 'SmrtObject' ||\n extendsValue === 'SmrtClass' ||\n extendsValue === 'SmrtCollection'\n ) {\n return extendsValue;\n }\n\n // Try same-package first (common case: child and parent in same package)\n const samePackageQualified = createQualifiedName(\n currentPackage,\n extendsValue,\n );\n if (classes.has(samePackageQualified)) {\n return samePackageQualified;\n }\n\n // The package's own manifest declares the parent but it is not registered\n // yet (manifest entries register in file order, so a subclass can precede\n // its base). It is still this package's class: another package's\n // same-named registration must not become the parent (#3098).\n const ownManifests = [\n getManifestCache().get(currentPackage),\n getLocalTestManifestCache(),\n getTestManifestCache(),\n ];\n if (\n ownManifests.some((manifest) => {\n if (manifest?.packageName !== currentPackage) return false;\n const entry = manifest.objects?.[samePackageQualified];\n // An entry that itself extends this simple name is the subclass being\n // registered (a same-named child of another package's base, e.g.\n // support's ServiceTimeEntry), not a base of its own package.\n const entryParent = entry?.extends?.split(':').pop();\n return !!entry && entryParent !== extendsValue;\n })\n ) {\n return samePackageQualified;\n }\n\n // Try to find the parent in any registered package\n const keys = registryKeysBySimpleName(extendsValue);\n if (keys.length === 1) {\n return keys[0];\n }\n\n // Fallback: return unmodified (backward compat)\n return extendsValue;\n}\n\n// ── Public lookup functions ─────────────────────────────────\n\n/**\n * Get a registered class by name (case-insensitive).\n */\nexport function getClass(name: string): RegisteredClass | undefined {\n return findClass(name);\n}\n\n/**\n * Get a registered class by its constructor reference (O(1) WeakMap lookup).\n */\nexport function getClassByConstructor(\n ctor: SmrtObjectConstructor,\n): RegisteredClass | undefined {\n const registeredName = getConstructorIndex().get(ctor);\n if (registeredName) {\n return getClasses().get(registeredName);\n }\n return undefined;\n}\n\n/**\n * Get a registered class by its qualified name (O(1) direct lookup).\n */\nexport function getClassByQualifiedName(\n qualifiedName: string,\n): RegisteredClass | undefined {\n return getClasses().get(qualifiedName);\n}\n\n/**\n * Get a registered class by package name and class name.\n */\nexport function getClassInPackage(\n packageName: string,\n className: string,\n): RegisteredClass | undefined {\n const qualifiedName = createQualifiedName(packageName, className);\n return getClasses().get(qualifiedName);\n}\n\n/**\n * Find all registered classes with a given simple class name.\n */\nexport function findClassesByName(className: string): RegisteredClass[] {\n // A source registration can retain its simple key while manifest hydration\n // adds its canonical qualified key. Both aliases designate one class, so\n // ambiguity means distinct RegisteredClass identities — which is what the\n // index's per-name keys already are (one key per identity).\n const classes = getClasses();\n const matches: RegisteredClass[] = [];\n for (const key of getSimpleNameIndex().get(className.toLowerCase()) ?? []) {\n const registered = classes.get(key);\n if (registered) {\n matches.push(registered);\n }\n }\n return matches;\n}\n\n/**\n * Resolve a short class name to its qualified name.\n * @throws {Error} If no class or ambiguous classes registered\n */\nexport function resolveType(shortName: string): QualifiedClassName {\n // If already qualified, validate and return\n if (shortName.includes(':') && shortName.startsWith('@')) {\n const registered = getClassByQualifiedName(shortName as QualifiedClassName);\n if (!registered) {\n throw new Error(\n `Class \"${shortName}\" is not registered. ` +\n `Make sure the package is installed and the class is decorated with @smrt().`,\n );\n }\n return shortName as QualifiedClassName;\n }\n\n // Find all classes with this short name\n const matches = findClassesByName(shortName);\n\n if (matches.length === 0) {\n throw new Error(\n `Class \"${shortName}\" is not registered. ` +\n `Make sure the package is installed and the class is decorated with @smrt().`,\n );\n }\n\n if (matches.length > 1) {\n const packageList = matches.map((m) => ` - ${m.qualifiedName}`).join('\\n');\n throw new Error(\n `\"${shortName}\" is ambiguous. Found in multiple packages:\\n${packageList}\\n` +\n `Use the fully qualified name instead.`,\n );\n }\n\n return matches[0].qualifiedName as QualifiedClassName;\n}\n\n/**\n * Get all registered classes from a specific package.\n */\nexport function getClassesByPackage(\n packageName: string,\n): Map<string, RegisteredClass> {\n const result = new Map<string, RegisteredClass>();\n\n for (const [name, registered] of getClasses().entries()) {\n if (registered.packageName === packageName) {\n result.set(name, registered);\n }\n }\n\n return result;\n}\n\n/**\n * Get all registered classes with a specific visibility level.\n */\nexport function getClassesByVisibility(\n visibility: SmrtVisibility,\n): Map<string, RegisteredClass> {\n const result = new Map<string, RegisteredClass>();\n\n for (const [name, registered] of getClasses().entries()) {\n const classVisibility = registered.visibility || 'public';\n if (classVisibility === visibility) {\n result.set(name, registered);\n }\n }\n\n return result;\n}\n\n/**\n * Get all public registered classes.\n */\nexport function getPublicClasses(): Map<string, RegisteredClass> {\n return getClassesByVisibility('public');\n}\n\n/**\n * Get all registered classes.\n */\nexport function getAllClasses(): Map<string, RegisteredClass> {\n return new Map(getClasses());\n}\n\n/**\n * Get class names (simple names, deduplicated).\n */\nexport function getClassNames(): string[] {\n const names = Array.from(getClasses().values()).map((entry) => entry.name);\n return Array.from(new Set(names));\n}\n\n/**\n * Get lookup names for every registered class without collapsing\n * cross-package simple-name collisions.\n *\n * Prefer each registration's qualified name when available, fall back to the\n * registry key, then to the simple class name for package-less registrations.\n * De-duplicates aliases that point to the same RegisteredClass object while\n * preserving distinct packages that intentionally share a simple class name.\n */\nexport function getQualifiedClassNames(): string[] {\n const names: string[] = [];\n const seenRegistrations = new Set<RegisteredClass>();\n const seenNames = new Set<string>();\n\n for (const [key, entry] of getClasses().entries()) {\n if (seenRegistrations.has(entry)) {\n continue;\n }\n seenRegistrations.add(entry);\n\n const lookupName = entry.qualifiedName || key || entry.name;\n if (seenNames.has(lookupName)) {\n continue;\n }\n\n seenNames.add(lookupName);\n names.push(lookupName);\n }\n\n return names;\n}\n\n/**\n * Check if a class is registered (case-insensitive).\n */\nexport function hasClass(name: string): boolean {\n return findClass(name) !== undefined;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"schema-builder.d.ts","sourceRoot":"","sources":["../../src/registry/schema-builder.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAO3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAc7D,OAAO,KAAK,EACV,gBAAgB,EAEhB,gBAAgB,EAChB,WAAW,EACZ,MAAM,oBAAoB,CAAC;AAwL5B;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAIpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,GAAG,SAAS,CAWpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAkB7D;AAwaD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAuFxE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CA4C7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACzC,KAAK,UAAQ,EACb,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CA2DR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EACpC,OAAO,CAAC,EAAE,sBAAsB,GAC/B,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAsHlC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,CAAC,MAAM,CAAC,GACjC,WAAW,CAqBb"}
1
+ {"version":3,"file":"schema-builder.d.ts","sourceRoot":"","sources":["../../src/registry/schema-builder.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAO3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAc7D,OAAO,KAAK,EACV,gBAAgB,EAEhB,gBAAgB,EAChB,WAAW,EACZ,MAAM,oBAAoB,CAAC;AA4L5B;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAIpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,GAAG,SAAS,CAWpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAkB7D;AAydD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAuFxE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CA4C7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACzC,KAAK,UAAQ,EACb,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CA2DR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EACpC,OAAO,CAAC,EAAE,sBAAsB,GAC/B,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAsHlC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,CAAC,MAAM,CAAC,GACjC,WAAW,CAqBb"}
@@ -1,13 +1,15 @@
1
+ import { ConfigurationError } from "../errors.js";
1
2
  import { requireForeignKeyAction, resolveForeignKeyDeleteAction } from "../schema/foreign-key-policy.js";
2
3
  import { classnameToTablename, toSnakeCase } from "../utils/naming.js";
3
4
  import { formatDefaultValue as formatDefaultValue$1, quoteIdentifier } from "../schema/sql-identifiers.js";
4
5
  import { shortenIdentifier } from "../schema/index-utils.js";
5
6
  import { renderForeignKeyConstraint, schemaForeignKeys } from "../schema/foreign-key-ddl.js";
6
7
  import { getDDLStrategy } from "../schema/ddl/index.js";
8
+ import { isQualifiedName, parseQualifiedName } from "../utils/qualified-names.js";
7
9
  import { getClasses, getCollectionTableNames } from "./shared-state.js";
8
- import { findClass } from "./name-resolver.js";
9
10
  import { conflictIndexName, nullableConflictIdentity } from "../schema/conflict-target.js";
10
11
  import { normalizeBackfill } from "../schema/backfill.js";
12
+ import { findClass } from "./name-resolver.js";
11
13
  import "../utils.js";
12
14
  import { isFrameworkBaseClass } from "./framework-base-classes.js";
13
15
  import { readFieldAttribute } from "./manifest-field-merge.js";
@@ -311,6 +313,41 @@ function resolveContributorTable(registered, fallbackName) {
311
313
  };
312
314
  }
313
315
  /**
316
+ * Reject a table claimed by two unrelated classes (#3098).
317
+ *
318
+ * Classes share a physical table only as one single-table-inheritance family,
319
+ * identified by its STI base. Anything else — two packages that both declare
320
+ * `Account` and both derive `accounts`, or an explicit `tableName` reused by an
321
+ * unrelated class — would be merged into one union table whose rows each
322
+ * package's unscoped queries then return as its own. An unqualified (source)
323
+ * registration and a package-qualified one of the same class are the same
324
+ * family, not a conflict.
325
+ */
326
+ function assertSingleTableFamily(tableName, contributors) {
327
+ const families = /* @__PURE__ */ new Map();
328
+ for (const contributor of contributors) {
329
+ const familyKey = contributor.conflictKey;
330
+ if (families.has(familyKey)) continue;
331
+ families.set(familyKey, isQualifiedName(familyKey) ? (() => {
332
+ const parsed = parseQualifiedName(familyKey);
333
+ return {
334
+ className: parsed.className,
335
+ pkg: parsed.packageName
336
+ };
337
+ })() : { className: familyKey });
338
+ }
339
+ const distinct = [...families.entries()];
340
+ for (let i = 0; i < distinct.length; i++) for (let j = i + 1; j < distinct.length; j++) {
341
+ const [leftKey, left] = distinct[i];
342
+ const [rightKey, right] = distinct[j];
343
+ if (left.className === right.className && (!left.pkg || !right.pkg || left.pkg === right.pkg)) continue;
344
+ throw new ConfigurationError(`Table '${tableName}' is claimed by unrelated classes ${leftKey} and ${rightKey}. Classes share a table only as one single-table-inheritance family; give one of them its own @smrt({ tableName }).`, "CONFIG_TABLE_NAME_COLLISION", {
345
+ tableName,
346
+ classes: [leftKey, rightKey]
347
+ });
348
+ }
349
+ }
350
+ /**
314
351
  * Order the classes that share one table deterministically.
315
352
  *
316
353
  * The first contributor seeds the table: it supplies the base columns, the
@@ -349,37 +386,40 @@ function buildMergedTableSchemas() {
349
386
  else contributorsByTable.set(resolved.tableName, [resolved.contributor]);
350
387
  }
351
388
  const tableSchemas = {};
352
- for (const [tableName, contributors] of contributorsByTable) for (const contributor of sortTableContributors(contributors)) {
353
- const { registered, simpleName, isSTI } = contributor;
354
- const conflictColumns = ObjectRegistry.getConflictColumns(contributor.conflictKey);
355
- const columnsToUse = mergeRuntimeFieldColumns(simpleName, registered.schema?.columns, registered.fields, {
356
- stiUnionColumns: isSTI,
357
- conflictColumns
358
- });
359
- let tableSchema = tableSchemas[tableName];
360
- if (!tableSchema) {
361
- tableSchema = {
362
- tableName,
363
- columns: {
364
- ...createBaseColumns(registered, isSTI),
365
- ...columnsToUse
366
- },
367
- indexes: [],
368
- ddl: registered.schema?.ddl || "",
369
- isSTI,
389
+ for (const [tableName, contributors] of contributorsByTable) {
390
+ assertSingleTableFamily(tableName, contributors);
391
+ for (const contributor of sortTableContributors(contributors)) {
392
+ const { registered, simpleName, isSTI } = contributor;
393
+ const conflictColumns = ObjectRegistry.getConflictColumns(contributor.conflictKey);
394
+ const columnsToUse = mergeRuntimeFieldColumns(simpleName, registered.schema?.columns, registered.fields, {
395
+ stiUnionColumns: isSTI,
370
396
  conflictColumns
371
- };
372
- tableSchemas[tableName] = tableSchema;
373
- } else for (const [colName, colDef] of Object.entries(columnsToUse)) if (!tableSchema.columns[colName]) tableSchema.columns[colName] = colDef;
374
- applyContributorForeignKeys(tableSchema, contributor);
375
- const schemaIndexes = registered.schema?.indexes;
376
- if (schemaIndexes && schemaIndexes.length > 0) {
377
- const existingNames = new Set(tableSchema.indexes.map((index) => index.name));
378
- for (const index of schemaIndexes) {
379
- if (typeof index === "string") continue;
380
- if (!existingNames.has(index.name)) {
381
- tableSchema.indexes.push(index);
382
- existingNames.add(index.name);
397
+ });
398
+ let tableSchema = tableSchemas[tableName];
399
+ if (!tableSchema) {
400
+ tableSchema = {
401
+ tableName,
402
+ columns: {
403
+ ...createBaseColumns(registered, isSTI),
404
+ ...columnsToUse
405
+ },
406
+ indexes: [],
407
+ ddl: registered.schema?.ddl || "",
408
+ isSTI,
409
+ conflictColumns
410
+ };
411
+ tableSchemas[tableName] = tableSchema;
412
+ } else for (const [colName, colDef] of Object.entries(columnsToUse)) if (!tableSchema.columns[colName]) tableSchema.columns[colName] = colDef;
413
+ applyContributorForeignKeys(tableSchema, contributor);
414
+ const schemaIndexes = registered.schema?.indexes;
415
+ if (schemaIndexes && schemaIndexes.length > 0) {
416
+ const existingNames = new Set(tableSchema.indexes.map((index) => index.name));
417
+ for (const index of schemaIndexes) {
418
+ if (typeof index === "string") continue;
419
+ if (!existingNames.has(index.name)) {
420
+ tableSchema.indexes.push(index);
421
+ existingNames.add(index.name);
422
+ }
383
423
  }
384
424
  }
385
425
  }