@happyvertical/smrt-core 0.54.2 → 0.54.3

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 (138) hide show
  1. package/AGENTS.md +6 -1
  2. package/README.md +8 -0
  3. package/agents/change-feed.md +7 -12
  4. package/agents/generators.md +13 -0
  5. package/agents/object-runtime.md +8 -0
  6. package/agents/schema-paths.md +14 -0
  7. package/dist/audit-context.d.ts +4 -0
  8. package/dist/audit-context.d.ts.map +1 -0
  9. package/dist/audit-context.js +16 -0
  10. package/dist/audit-context.js.map +1 -0
  11. package/dist/audit.d.ts +34 -0
  12. package/dist/audit.d.ts.map +1 -0
  13. package/dist/audit.js +63 -0
  14. package/dist/audit.js.map +1 -0
  15. package/dist/browser.d.ts +2 -1
  16. package/dist/browser.d.ts.map +1 -1
  17. package/dist/browser.js +4 -3
  18. package/dist/cascade.d.ts +1 -1
  19. package/dist/cascade.d.ts.map +1 -1
  20. package/dist/cascade.js +10 -7
  21. package/dist/cascade.js.map +1 -1
  22. package/dist/change-feed.js +1 -1
  23. package/dist/collection.d.ts +9 -2
  24. package/dist/collection.d.ts.map +1 -1
  25. package/dist/collection.js +59 -4
  26. package/dist/collection.js.map +1 -1
  27. package/dist/embedded-write-queue.d.ts +2 -0
  28. package/dist/embedded-write-queue.d.ts.map +1 -1
  29. package/dist/embedded-write-queue.js +12 -5
  30. package/dist/embedded-write-queue.js.map +1 -1
  31. package/dist/generators/changes-route.js +1 -1
  32. package/dist/generators/mcp-runtime-template.d.ts +2 -0
  33. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  34. package/dist/generators/mcp-runtime-template.js +18 -16
  35. package/dist/generators/mcp-runtime-template.js.map +1 -1
  36. package/dist/generators/mcp-tool-name.d.ts +6 -0
  37. package/dist/generators/mcp-tool-name.d.ts.map +1 -1
  38. package/dist/generators/mcp-tool-name.js.map +1 -1
  39. package/dist/generators/mcp.d.ts +34 -1
  40. package/dist/generators/mcp.d.ts.map +1 -1
  41. package/dist/generators/mcp.js +148 -110
  42. package/dist/generators/mcp.js.map +1 -1
  43. package/dist/generators/rest.d.ts.map +1 -1
  44. package/dist/generators/rest.js +16 -7
  45. package/dist/generators/rest.js.map +1 -1
  46. package/dist/index.d.ts +3 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +9 -7
  49. package/dist/junction.d.ts +29 -10
  50. package/dist/junction.d.ts.map +1 -1
  51. package/dist/junction.js +31 -12
  52. package/dist/junction.js.map +1 -1
  53. package/dist/knowledge.js +1 -1
  54. package/dist/knowledge.js.map +1 -1
  55. package/dist/manifest/manifest-loader.d.ts.map +1 -1
  56. package/dist/manifest/manifest-loader.js +1 -1
  57. package/dist/manifest/manifest-loader.js.map +1 -1
  58. package/dist/manifest/static-manifest.d.ts.map +1 -1
  59. package/dist/manifest/static-manifest.js +106 -9
  60. package/dist/manifest/static-manifest.js.map +1 -1
  61. package/dist/manifest/store.d.ts +8 -0
  62. package/dist/manifest/store.d.ts.map +1 -1
  63. package/dist/manifest/store.js +12 -2
  64. package/dist/manifest/store.js.map +1 -1
  65. package/dist/manifest.json +119 -9
  66. package/dist/migrations/differ.d.ts +3 -3
  67. package/dist/migrations/differ.d.ts.map +1 -1
  68. package/dist/migrations/differ.js +8 -4
  69. package/dist/migrations/differ.js.map +1 -1
  70. package/dist/object.d.ts +1 -0
  71. package/dist/object.d.ts.map +1 -1
  72. package/dist/object.js +19 -7
  73. package/dist/object.js.map +1 -1
  74. package/dist/registry/class-registration.d.ts.map +1 -1
  75. package/dist/registry/class-registration.js +75 -10
  76. package/dist/registry/class-registration.js.map +1 -1
  77. package/dist/registry/collection-resolution.d.ts +2 -2
  78. package/dist/registry/collection-resolution.d.ts.map +1 -1
  79. package/dist/registry/collection-resolution.js +2 -1
  80. package/dist/registry/collection-resolution.js.map +1 -1
  81. package/dist/registry/collision-policy.d.ts +8 -0
  82. package/dist/registry/collision-policy.d.ts.map +1 -1
  83. package/dist/registry/collision-policy.js +2 -2
  84. package/dist/registry/collision-policy.js.map +1 -1
  85. package/dist/registry/framework-base-classes.d.ts.map +1 -1
  86. package/dist/registry/framework-base-classes.js +2 -1
  87. package/dist/registry/framework-base-classes.js.map +1 -1
  88. package/dist/registry/schema-builder.d.ts.map +1 -1
  89. package/dist/registry/schema-builder.js +33 -4
  90. package/dist/registry/schema-builder.js.map +1 -1
  91. package/dist/registry/shared-state.d.ts +6 -0
  92. package/dist/registry/shared-state.d.ts.map +1 -1
  93. package/dist/registry/shared-state.js +12 -1
  94. package/dist/registry/shared-state.js.map +1 -1
  95. package/dist/registry/types.d.ts +7 -1
  96. package/dist/registry/types.d.ts.map +1 -1
  97. package/dist/registry.d.ts +7 -1
  98. package/dist/registry.d.ts.map +1 -1
  99. package/dist/registry.js +25 -3
  100. package/dist/registry.js.map +1 -1
  101. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  102. package/dist/scanner/manifest-generator.js +5 -1
  103. package/dist/scanner/manifest-generator.js.map +1 -1
  104. package/dist/scanner/types.d.ts +8 -0
  105. package/dist/scanner/types.d.ts.map +1 -1
  106. package/dist/scanner/types.js.map +1 -1
  107. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  108. package/dist/schema/ddl/duckdb-strategy.js +1 -0
  109. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  110. package/dist/schema/ddl/null-equal-index.d.ts.map +1 -1
  111. package/dist/schema/ddl/null-equal-index.js +4 -2
  112. package/dist/schema/ddl/null-equal-index.js.map +1 -1
  113. package/dist/schema/generator.d.ts +3 -0
  114. package/dist/schema/generator.d.ts.map +1 -1
  115. package/dist/schema/generator.js +38 -0
  116. package/dist/schema/generator.js.map +1 -1
  117. package/dist/smrt-knowledge.json +74 -20
  118. package/dist/sync/apply.d.ts +2 -0
  119. package/dist/sync/apply.d.ts.map +1 -1
  120. package/dist/sync/apply.js +7 -3
  121. package/dist/sync/apply.js.map +1 -1
  122. package/dist/utils/scanner-module.d.ts +9 -0
  123. package/dist/utils/scanner-module.d.ts.map +1 -1
  124. package/dist/vite-plugin/index.d.ts +14 -0
  125. package/dist/vite-plugin/index.d.ts.map +1 -1
  126. package/dist/vite-plugin/index.js +58 -18
  127. package/dist/vite-plugin/index.js.map +1 -1
  128. package/dist/vite-plugin/package-stamp.d.ts +41 -0
  129. package/dist/vite-plugin/package-stamp.d.ts.map +1 -0
  130. package/dist/vite-plugin/package-stamp.js +33 -0
  131. package/dist/vite-plugin/package-stamp.js.map +1 -0
  132. package/dist/vite-plugin/sveltekit-generator.js +5 -3
  133. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  134. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  135. package/dist/vite-plugin/sync-apply-route.js +10 -0
  136. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  137. package/dist/vite-plugin.js +2 -2
  138. package/package.json +10 -10
@@ -7,7 +7,7 @@ export interface CollectionRegistrationLookup {
7
7
  /**
8
8
  * Framework base classes that ARE collections (extend `SmrtCollection`).
9
9
  *
10
- * `SmrtJunction` is an abstract collection base — junction collections declare
10
+ * `SmrtJunction` and `SmrtJunctionBase` are abstract collection bases — junction collections declare
11
11
  * `extends SmrtJunction<Item>`, and the manifest records that *direct* parent.
12
12
  * `SmrtJunction` is itself never registered in a consumer's registry, so the
13
13
  * inheritance-chain walk in `isCollectionRegistration()` cannot reach
@@ -42,7 +42,7 @@ export interface CollectionRegistrationLookup {
42
42
  * duplicate detector is exactly how the #1342 junction schema-drop bug
43
43
  * half-survived the original core fix.
44
44
  */
45
- export declare const SMRT_COLLECTION_BASE_NAMES: readonly ["SmrtCollection", "SmrtJunction", "SmrtReportCollection"];
45
+ export declare const SMRT_COLLECTION_BASE_NAMES: readonly ["SmrtCollection", "SmrtJunction", "SmrtJunctionBase", "SmrtReportCollection"];
46
46
  export declare function isSmrtCollectionExtendsName(extendsName: string | undefined): boolean;
47
47
  export declare function resolveRelatedRegistration(className: string, origin: RegisteredClass, lookup: CollectionRegistrationLookup): RegisteredClass | undefined;
48
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
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,yFAK7B,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"}
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Framework base classes that ARE collections (extend `SmrtCollection`).
4
4
  *
5
- * `SmrtJunction` is an abstract collection base — junction collections declare
5
+ * `SmrtJunction` and `SmrtJunctionBase` are abstract collection bases — junction collections declare
6
6
  * `extends SmrtJunction<Item>`, and the manifest records that *direct* parent.
7
7
  * `SmrtJunction` is itself never registered in a consumer's registry, so the
8
8
  * inheritance-chain walk in `isCollectionRegistration()` cannot reach
@@ -40,6 +40,7 @@
40
40
  var SMRT_COLLECTION_BASE_NAMES = [
41
41
  "SmrtCollection",
42
42
  "SmrtJunction",
43
+ "SmrtJunctionBase",
43
44
  "SmrtReportCollection"
44
45
  ];
45
46
  function isSmrtCollectionExtendsName(extendsName) {
@@ -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 * `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"}
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` and `SmrtJunctionBase` are abstract collection bases — 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 'SmrtJunctionBase',\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;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"}
@@ -163,6 +163,14 @@ export interface CollisionInputs {
163
163
  * resolves `contracts`.
164
164
  */
165
165
  readonly declaresDifferentTable: boolean;
166
+ /**
167
+ * Decorator-origin only: the existing entry's package stamps every class it
168
+ * declares with its package name (#3490). The new class is unstamped or
169
+ * stamped with another package, so it is provably not that package's class:
170
+ * in bundled output too, where the stack names the bundle's package, it
171
+ * coexists rather than adopting the entry by name and table.
172
+ */
173
+ readonly existingPackageStampsConstructors: boolean;
166
174
  }
167
175
  export interface PolicyDecision {
168
176
  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;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"}
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;IAEzC;;;;;;OAMG;IACH,QAAQ,CAAC,iCAAiC,EAAE,OAAO,CAAC;CACrD;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;AA2PD;;;;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 && !(i.hasNewQualifiedKey && i.existingHasAnyPackage && !i.samePackage && !i.sameSourceFile && (i.bothSourceFilesKnown && !i.newInBundledContext || i.declaresDifferentTable)),
20
+ match: (i) => i.origin === "decorator" && i.existingIsManifestStub && !i.sameConstructor && !(i.hasNewQualifiedKey && i.existingHasAnyPackage && !i.samePackage && !i.sameSourceFile && (i.bothSourceFilesKnown && !i.newInBundledContext || i.declaresDifferentTable || i.existingPackageStampsConstructors)),
21
21
  policy: "replace",
22
22
  reason: () => "Existing entry is a manifest stub placeholder; the real implementation takes over."
23
23
  },
@@ -71,7 +71,7 @@ var ROWS = [
71
71
  },
72
72
  {
73
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),
74
+ match: (i) => i.origin === "decorator" && i.hasNewQualifiedKey && i.existingHasAnyPackage && !i.samePackage && !i.sameSourceFile && !i.newExtendsExisting && !i.existingExtendsNew && (!i.newInBundledContext || i.declaresDifferentTable || i.existingPackageStampsConstructors),
75
75
  policy: "coexist-qualified",
76
76
  reason: () => "Different packages declare the same simple name; both registrations live under their qualified keys (issue #3106)."
77
77
  },
@@ -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 /**\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
+ {"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 /**\n * Decorator-origin only: the existing entry's package stamps every class it\n * declares with its package name (#3490). The new class is unstamped or\n * stamped with another package, so it is provably not that package's class:\n * in bundled output too, where the stack names the bundle's package, it\n * coexists rather than adopting the entry by name and table.\n */\n readonly existingPackageStampsConstructors: 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 i.existingPackageStampsConstructors)\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 ||\n i.declaresDifferentTable ||\n i.existingPackageStampsConstructors),\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":";;;;;;;;;;AA6NA,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,0BACF,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,uBACF,EAAE,0BACF,EAAE;EACN,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 +1 @@
1
- {"version":3,"file":"framework-base-classes.d.ts","sourceRoot":"","sources":["../../src/registry/framework-base-classes.ts"],"names":[],"mappings":"AAgFA;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAMT"}
1
+ {"version":3,"file":"framework-base-classes.d.ts","sourceRoot":"","sources":["../../src/registry/framework-base-classes.ts"],"names":[],"mappings":"AAiFA;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAMT"}
@@ -47,7 +47,7 @@
47
47
  * answering a different question — not "is this class a resource?" but
48
48
  * "should this ancestor's METHODS merge into a subclass that doesn't declare
49
49
  * them?" `SmrtReportCollection` and `SmrtJunction` are deliberately in this
50
- * 8-name resource set but NOT in that 3-name method-merge set: their declared
50
+ * resource set but NOT in that 3-name method-merge set: their declared
51
51
  * methods (`list`/`get` overrides, `attach`/`detach`/`byLeft`/`byRight`/
52
52
  * `setLinks`) are real, subclass-inherited API, not generic object-lifecycle
53
53
  * plumbing. Unifying the two sets — applying this one to method merging too —
@@ -73,6 +73,7 @@ var FRAMEWORK_BASE_CLASS_PACKAGES = /* @__PURE__ */ new Map([
73
73
  ["SmrtClass", "@happyvertical/smrt-core"],
74
74
  ["SmrtCollection", "@happyvertical/smrt-core"],
75
75
  ["SmrtJunction", "@happyvertical/smrt-core"],
76
+ ["SmrtJunctionBase", "@happyvertical/smrt-core"],
76
77
  ["SmrtHierarchical", "@happyvertical/smrt-core"],
77
78
  ["SmrtPolymorphicAssociation", "@happyvertical/smrt-core"],
78
79
  ["SmrtReport", "@happyvertical/smrt-reports"],
@@ -1 +1 @@
1
- {"version":3,"file":"framework-base-classes.js","names":[],"sources":["../../src/registry/framework-base-classes.ts"],"sourcesContent":["/**\n * The framework's own abstract base classes, keyed by simple class name to\n * their declaring package.\n *\n * These are scaffolding every application model descends from — they carry\n * no `@smrt()` decorator and have no independent existence as a resource.\n * A foundation package like `@happyvertical/smrt-core` declares them as real\n * local classes (there is no other package for them to live in), so\n * `loadAllManifests()` registers them exactly like any genuine domain class\n * when a consumer installs `@happyvertical/smrt-core` — with no decoration\n * or framework-base filter of its own (#2642). The registration criterion\n * this framework was designed around is descent from `SmrtObject`, not the\n * presence of a decorator — undecorated domain classes (e.g. a hand-written\n * `SmrtCollection` subclass) keep their surfaces; only the framework's own\n * bases are excluded here, checked by class identity rather than decoration.\n *\n * This is the SINGLE shared source of truth for that identity check within\n * `@happyvertical/smrt-core` and its consumers — imported by:\n * - `./schema-builder.ts` (skips table/DDL generation for these classes)\n * - `../knowledge.ts` (the `.smrt/smrt-knowledge.json` surface projection)\n * - `../generators/mcp.ts` (`MCPGenerator.generateTools()`)\n * - `../generators/rest.ts` (`APIGenerator.findClassByCollectionSegment()`)\n * - `../vite-plugin/sveltekit-generator.ts` (REST route generation)\n * - `../vite-plugin/sync-apply-route.ts` (`collectSyncApplyTargets()`)\n * - `../vite-plugin/web-collections.ts` (the `smrt-virt-web` client's\n * collection and WebMCP tool selectors)\n * - `@happyvertical/smrt-cli`'s `cli-generator.ts` (the live local `smrt`\n * binary's object command generation), via core's public export\n * - `../testing/database.ts` (`getTestDatabase()` skips table creation for\n * these classes, but only for an implicit `classes` list — an explicit\n * `classes: [...]` caller naming a framework base class is still honored,\n * unlike every unconditional filter above) (#2645)\n *\n * Do not add another hardcoded copy of this list at a new call site — import\n * from here instead.\n *\n * `packages/scanner/src/inheritance-resolver.ts` keeps its own separate\n * `FRAMEWORK_BASE_CLASSES` set: that package is a lower-level AST layer this\n * one has no reason to otherwise depend on, and that set controls a\n * different concern (scanner-level extends-chain termination and stub\n * resolution for packages that don't declare these classes locally), not\n * resource exposure.\n *\n * `scanner/manifest-generator.ts`'s `FRAMEWORK_METHOD_BASE_NAMES` is a\n * DIFFERENT, narrower (3-name: `SmrtObject`/`SmrtClass`/`SmrtCollection`) set\n * answering a different question — not \"is this class a resource?\" but\n * \"should this ancestor's METHODS merge into a subclass that doesn't declare\n * them?\" `SmrtReportCollection` and `SmrtJunction` are deliberately in this\n * 8-name resource set but NOT in that 3-name method-merge set: their declared\n * methods (`list`/`get` overrides, `attach`/`detach`/`byLeft`/`byRight`/\n * `setLinks`) are real, subclass-inherited API, not generic object-lifecycle\n * plumbing. Unifying the two sets — applying this one to method merging too —\n * strips those methods from every subclass; that regression actually\n * happened during #2624 and was caught only by a\n * `packages/template-sveltekit` snapshot test. Do not merge these lists.\n *\n * Most of these live in `@happyvertical/smrt-core` itself, but\n * `SmrtReport`/`SmrtReportCollection` are declared in\n * `@happyvertical/smrt-reports` — the owning package is per-name, not a\n * single blanket package check. A new framework base must be added here AND\n * to the scanner's `FRAMEWORK_BASE_CLASSES`.\n *\n * Deliberately NOT exported: only `isFrameworkBaseClass()` below needs to be\n * public. A `Map` survives TypeScript's `ReadonlyMap` typing only at the\n * type-check layer — a plain-JS or `as Map` caller could still `.set()`/\n * `.clear()` an exported instance at runtime and change what the framework\n * considers a base class. Keeping the map itself module-private closes that\n * off entirely rather than relying on callers not to do that.\n */\nconst FRAMEWORK_BASE_CLASS_PACKAGES: ReadonlyMap<string, string> = new Map([\n ['SmrtObject', '@happyvertical/smrt-core'],\n ['SmrtClass', '@happyvertical/smrt-core'],\n ['SmrtCollection', '@happyvertical/smrt-core'],\n ['SmrtJunction', '@happyvertical/smrt-core'],\n ['SmrtHierarchical', '@happyvertical/smrt-core'],\n ['SmrtPolymorphicAssociation', '@happyvertical/smrt-core'],\n ['SmrtReport', '@happyvertical/smrt-reports'],\n ['SmrtReportCollection', '@happyvertical/smrt-reports'],\n]);\n\n/**\n * True when `className`/`packageName` identify one of the framework's own\n * abstract base classes rather than a genuine resource.\n *\n * Both a class name AND its declaring package must match: a consuming\n * package is free to declare its own local class named e.g. `SmrtReport`\n * without colliding with the framework base of the same name.\n */\nexport function isFrameworkBaseClass(\n className: string | undefined,\n packageName: string | undefined,\n): boolean {\n return (\n className !== undefined &&\n packageName !== undefined &&\n FRAMEWORK_BASE_CLASS_PACKAGES.get(className) === packageName\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqEA,IAAM,gDAA6D,IAAI,IAAI;CACzE,CAAC,cAAc,0BAA0B;CACzC,CAAC,aAAa,0BAA0B;CACxC,CAAC,kBAAkB,0BAA0B;CAC7C,CAAC,gBAAgB,0BAA0B;CAC3C,CAAC,oBAAoB,0BAA0B;CAC/C,CAAC,8BAA8B,0BAA0B;CACzD,CAAC,cAAc,6BAA6B;CAC5C,CAAC,wBAAwB,6BAA6B;AACxD,CAAC;;;;;;;;;AAUD,SAAgB,qBACd,WACA,aACS;CACT,OACE,cAAc,KAAA,KACd,gBAAgB,KAAA,KAChB,8BAA8B,IAAI,SAAS,MAAM;AAErD"}
1
+ {"version":3,"file":"framework-base-classes.js","names":[],"sources":["../../src/registry/framework-base-classes.ts"],"sourcesContent":["/**\n * The framework's own abstract base classes, keyed by simple class name to\n * their declaring package.\n *\n * These are scaffolding every application model descends from — they carry\n * no `@smrt()` decorator and have no independent existence as a resource.\n * A foundation package like `@happyvertical/smrt-core` declares them as real\n * local classes (there is no other package for them to live in), so\n * `loadAllManifests()` registers them exactly like any genuine domain class\n * when a consumer installs `@happyvertical/smrt-core` — with no decoration\n * or framework-base filter of its own (#2642). The registration criterion\n * this framework was designed around is descent from `SmrtObject`, not the\n * presence of a decorator — undecorated domain classes (e.g. a hand-written\n * `SmrtCollection` subclass) keep their surfaces; only the framework's own\n * bases are excluded here, checked by class identity rather than decoration.\n *\n * This is the SINGLE shared source of truth for that identity check within\n * `@happyvertical/smrt-core` and its consumers — imported by:\n * - `./schema-builder.ts` (skips table/DDL generation for these classes)\n * - `../knowledge.ts` (the `.smrt/smrt-knowledge.json` surface projection)\n * - `../generators/mcp.ts` (`MCPGenerator.generateTools()`)\n * - `../generators/rest.ts` (`APIGenerator.findClassByCollectionSegment()`)\n * - `../vite-plugin/sveltekit-generator.ts` (REST route generation)\n * - `../vite-plugin/sync-apply-route.ts` (`collectSyncApplyTargets()`)\n * - `../vite-plugin/web-collections.ts` (the `smrt-virt-web` client's\n * collection and WebMCP tool selectors)\n * - `@happyvertical/smrt-cli`'s `cli-generator.ts` (the live local `smrt`\n * binary's object command generation), via core's public export\n * - `../testing/database.ts` (`getTestDatabase()` skips table creation for\n * these classes, but only for an implicit `classes` list — an explicit\n * `classes: [...]` caller naming a framework base class is still honored,\n * unlike every unconditional filter above) (#2645)\n *\n * Do not add another hardcoded copy of this list at a new call site — import\n * from here instead.\n *\n * `packages/scanner/src/inheritance-resolver.ts` keeps its own separate\n * `FRAMEWORK_BASE_CLASSES` set: that package is a lower-level AST layer this\n * one has no reason to otherwise depend on, and that set controls a\n * different concern (scanner-level extends-chain termination and stub\n * resolution for packages that don't declare these classes locally), not\n * resource exposure.\n *\n * `scanner/manifest-generator.ts`'s `FRAMEWORK_METHOD_BASE_NAMES` is a\n * DIFFERENT, narrower (3-name: `SmrtObject`/`SmrtClass`/`SmrtCollection`) set\n * answering a different question — not \"is this class a resource?\" but\n * \"should this ancestor's METHODS merge into a subclass that doesn't declare\n * them?\" `SmrtReportCollection` and `SmrtJunction` are deliberately in this\n * resource set but NOT in that 3-name method-merge set: their declared\n * methods (`list`/`get` overrides, `attach`/`detach`/`byLeft`/`byRight`/\n * `setLinks`) are real, subclass-inherited API, not generic object-lifecycle\n * plumbing. Unifying the two sets — applying this one to method merging too —\n * strips those methods from every subclass; that regression actually\n * happened during #2624 and was caught only by a\n * `packages/template-sveltekit` snapshot test. Do not merge these lists.\n *\n * Most of these live in `@happyvertical/smrt-core` itself, but\n * `SmrtReport`/`SmrtReportCollection` are declared in\n * `@happyvertical/smrt-reports` — the owning package is per-name, not a\n * single blanket package check. A new framework base must be added here AND\n * to the scanner's `FRAMEWORK_BASE_CLASSES`.\n *\n * Deliberately NOT exported: only `isFrameworkBaseClass()` below needs to be\n * public. A `Map` survives TypeScript's `ReadonlyMap` typing only at the\n * type-check layer — a plain-JS or `as Map` caller could still `.set()`/\n * `.clear()` an exported instance at runtime and change what the framework\n * considers a base class. Keeping the map itself module-private closes that\n * off entirely rather than relying on callers not to do that.\n */\nconst FRAMEWORK_BASE_CLASS_PACKAGES: ReadonlyMap<string, string> = new Map([\n ['SmrtObject', '@happyvertical/smrt-core'],\n ['SmrtClass', '@happyvertical/smrt-core'],\n ['SmrtCollection', '@happyvertical/smrt-core'],\n ['SmrtJunction', '@happyvertical/smrt-core'],\n ['SmrtJunctionBase', '@happyvertical/smrt-core'],\n ['SmrtHierarchical', '@happyvertical/smrt-core'],\n ['SmrtPolymorphicAssociation', '@happyvertical/smrt-core'],\n ['SmrtReport', '@happyvertical/smrt-reports'],\n ['SmrtReportCollection', '@happyvertical/smrt-reports'],\n]);\n\n/**\n * True when `className`/`packageName` identify one of the framework's own\n * abstract base classes rather than a genuine resource.\n *\n * Both a class name AND its declaring package must match: a consuming\n * package is free to declare its own local class named e.g. `SmrtReport`\n * without colliding with the framework base of the same name.\n */\nexport function isFrameworkBaseClass(\n className: string | undefined,\n packageName: string | undefined,\n): boolean {\n return (\n className !== undefined &&\n packageName !== undefined &&\n FRAMEWORK_BASE_CLASS_PACKAGES.get(className) === packageName\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqEA,IAAM,gDAA6D,IAAI,IAAI;CACzE,CAAC,cAAc,0BAA0B;CACzC,CAAC,aAAa,0BAA0B;CACxC,CAAC,kBAAkB,0BAA0B;CAC7C,CAAC,gBAAgB,0BAA0B;CAC3C,CAAC,oBAAoB,0BAA0B;CAC/C,CAAC,oBAAoB,0BAA0B;CAC/C,CAAC,8BAA8B,0BAA0B;CACzD,CAAC,cAAc,6BAA6B;CAC5C,CAAC,wBAAwB,6BAA6B;AACxD,CAAC;;;;;;;;;AAUD,SAAgB,qBACd,WACA,aACS;CACT,OACE,cAAc,KAAA,KACd,gBAAgB,KAAA,KAChB,8BAA8B,IAAI,SAAS,MAAM;AAErD"}
@@ -1 +1 @@
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;AA6L5B;;;;;;;;;;;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;AA6dD;;;;;;;;;;;;;;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,CAiHlC;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;AAKH,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;AA6L5B;;;;;;;;;;;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;AA2hBD;;;;;;;;;;;;;;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,CAiHlC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,CAAC,MAAM,CAAC,GACjC,WAAW,CAqBb"}
@@ -6,15 +6,16 @@ import { formatDefaultValue as formatDefaultValue$1, quoteIdentifier } from "../
6
6
  import { shortenIdentifier } from "../schema/index-utils.js";
7
7
  import { renderForeignKeyConstraint, schemaForeignKeys } from "../schema/foreign-key-ddl.js";
8
8
  import { getDDLStrategy } from "../schema/ddl/index.js";
9
+ import "../utils.js";
9
10
  import { isQualifiedName, parseQualifiedName } from "../utils/qualified-names.js";
10
11
  import { getClasses, getCollectionTableNames } from "./shared-state.js";
11
12
  import { normalizeBackfill } from "../schema/backfill.js";
12
13
  import { findClass } from "./name-resolver.js";
13
- import "../utils.js";
14
14
  import { isFrameworkBaseClass } from "./framework-base-classes.js";
15
15
  import { readFieldAttribute } from "./manifest-field-merge.js";
16
16
  import { isCollectionRegistration } from "./collection-resolution.js";
17
17
  import { ObjectRegistry } from "../registry.js";
18
+ import { createHash } from "node:crypto";
18
19
  //#region src/registry/schema-builder.ts
19
20
  /**
20
21
  * Schema building logic for the SMRT ObjectRegistry.
@@ -182,17 +183,25 @@ function getTableName(name) {
182
183
  if (stiBase && stiBase !== qualifiedName) return getSchema(stiBase)?.tableName;
183
184
  return getSchema(name)?.tableName;
184
185
  }
185
- function withConflictIndex(tableName, columns, indexes, conflictColumns) {
186
+ function withConflictIndex(tableName, columns, indexes, conflictColumns, conflictPredicate) {
186
187
  if (conflictColumns.length === 0 || !conflictColumns.every((column) => columns[column])) return indexes;
187
188
  const primaryKeyColumns = Object.entries(columns).filter(([, column]) => column.primaryKey === true).map(([name]) => name);
188
189
  if (primaryKeyColumns.length > 0 && primaryKeyColumns.length === conflictColumns.length && primaryKeyColumns.every((column) => conflictColumns.includes(column))) return indexes;
189
190
  const tenantColumn = Object.entries(columns).find(([, column]) => column.referenceKind === "tenantId")?.[0];
190
191
  const name = shortenIdentifier(conflictIndexName(tableName, conflictColumns, tenantColumn));
191
- if (!indexes.find((index) => index.name === name) && indexes.some((index) => index.unique === true && !index.where && !index.jsonPath && index.columns.length === conflictColumns.length && index.columns.every((column) => conflictColumns.includes(column)))) return indexes;
192
+ const canonical = indexes.find((index) => index.name === name);
193
+ if (!canonical && indexes.some((index) => index.unique === true && !index.where && !index.jsonPath && index.columns.length === conflictColumns.length && index.columns.every((column) => conflictColumns.includes(column)))) return indexes;
192
194
  const conflictIndex = {
193
195
  name,
194
196
  columns: conflictColumns,
195
197
  unique: true,
198
+ ...conflictPredicate ? {
199
+ where: conflictPredicate,
200
+ description: "STI subclass conflict target"
201
+ } : canonical?.description === "STI subclass conflict target" ? {
202
+ where: canonical.where,
203
+ description: canonical.description
204
+ } : {},
196
205
  ...nullableConflictIdentity(conflictColumns, columns) ? { nullsNotDistinct: true } : {}
197
206
  };
198
207
  if (indexes.some((index) => index.name === name)) return indexes.map((index) => index.name === name ? conflictIndex : index);
@@ -389,7 +398,8 @@ function buildMergedTableSchemas() {
389
398
  const tableSchemas = {};
390
399
  for (const [tableName, contributors] of contributorsByTable) {
391
400
  assertSingleTableFamily(tableName, contributors);
392
- for (const contributor of sortTableContributors(contributors)) {
401
+ const orderedContributors = sortTableContributors(contributors);
402
+ for (const contributor of orderedContributors) {
393
403
  const { registered, simpleName, isSTI } = contributor;
394
404
  const conflictColumns = ObjectRegistry.getConflictColumns(contributor.conflictKey);
395
405
  const columnsToUse = mergeRuntimeFieldColumns(simpleName, registered.schema?.columns, registered.fields, {
@@ -424,6 +434,25 @@ function buildMergedTableSchemas() {
424
434
  }
425
435
  }
426
436
  }
437
+ const tableSchema = tableSchemas[tableName];
438
+ tableSchema.indexes = withConflictIndex(tableName, tableSchema.columns, tableSchema.indexes, tableSchema.conflictColumns, tableSchema.isSTI ? ObjectRegistry.getConflictPredicate(orderedContributors[0].conflictKey) : void 0);
439
+ if (tableSchema.isSTI) for (const className of ObjectRegistry.getSTIConflictOverrides(orderedContributors[0].conflictKey)) {
440
+ const conflictColumns = ObjectRegistry.getConflictColumns(className);
441
+ const primaryKeys = Object.entries(tableSchema.columns).filter(([, column]) => column.primaryKey).map(([name]) => name);
442
+ if (primaryKeys.length === conflictColumns.length && primaryKeys.every((name) => conflictColumns.includes(name))) continue;
443
+ const name = shortenIdentifier(`${tableName}_${createHash("sha256").update(className).digest("hex").slice(0, 8)}_conflict_idx`);
444
+ const childIndex = {
445
+ name,
446
+ columns: conflictColumns,
447
+ unique: true,
448
+ description: "STI subclass conflict target",
449
+ where: ObjectRegistry.getConflictPredicate(className),
450
+ ...nullableConflictIdentity(conflictColumns, tableSchema.columns) ? { nullsNotDistinct: true } : {}
451
+ };
452
+ const existing = tableSchema.indexes.findIndex((index) => index.name === name);
453
+ if (existing >= 0) tableSchema.indexes[existing] = childIndex;
454
+ else tableSchema.indexes.push(childIndex);
455
+ }
427
456
  }
428
457
  return tableSchemas;
429
458
  }