@open-mercato/cli 0.6.7-develop.6685.1.77c0a5591b → 0.6.7-develop.6687.1.46a258e592

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 (215) hide show
  1. package/.turbo/turbo-build.log +2 -3
  2. package/build.mjs +21 -5
  3. package/dist/agentic/claude-code/hooks/entity-migration-check.ts +32 -88
  4. package/dist/agentic/codex/enforcement-rules.md +2 -16
  5. package/dist/agentic/cursor/hooks/entity-migration-check.mjs +19 -50
  6. package/dist/agentic/cursor/rules/entity-guard.mdc +9 -21
  7. package/dist/agentic/cursor/rules/generated-guard.mdc +3 -5
  8. package/dist/agentic/cursor/rules/open-mercato.mdc +3 -29
  9. package/dist/agentic/guides/ai-workflows.md +76 -0
  10. package/dist/agentic/guides/architecture.md +107 -0
  11. package/dist/agentic/guides/backend-ui.md +80 -0
  12. package/dist/agentic/guides/contracts.md +92 -0
  13. package/dist/agentic/guides/extensions.md +104 -0
  14. package/dist/agentic/guides/integrations.md +81 -0
  15. package/dist/agentic/guides/module-facts.json +217 -81
  16. package/dist/agentic/guides/module-system.md +12 -127
  17. package/dist/agentic/guides/modules/ai_assistant.md +1 -1
  18. package/dist/agentic/guides/modules/api_docs.md +1 -1
  19. package/dist/agentic/guides/modules/api_keys.md +1 -1
  20. package/dist/agentic/guides/modules/attachments.md +2 -2
  21. package/dist/agentic/guides/modules/audit_logs.md +2 -2
  22. package/dist/agentic/guides/modules/auth.md +1 -1
  23. package/dist/agentic/guides/modules/business_rules.md +1 -1
  24. package/dist/agentic/guides/modules/catalog.md +5 -5
  25. package/dist/agentic/guides/modules/channel_gmail.md +1 -1
  26. package/dist/agentic/guides/modules/channel_imap.md +1 -1
  27. package/dist/agentic/guides/modules/checkout.md +1 -1
  28. package/dist/agentic/guides/modules/communication_channels.md +1 -1
  29. package/dist/agentic/guides/modules/configs.md +1 -1
  30. package/dist/agentic/guides/modules/content.md +1 -1
  31. package/dist/agentic/guides/modules/currencies.md +1 -1
  32. package/dist/agentic/guides/modules/customer_accounts.md +1 -1
  33. package/dist/agentic/guides/modules/customers.md +2 -2
  34. package/dist/agentic/guides/modules/dashboards.md +1 -1
  35. package/dist/agentic/guides/modules/data_sync.md +1 -1
  36. package/dist/agentic/guides/modules/dictionaries.md +1 -1
  37. package/dist/agentic/guides/modules/directory.md +1 -1
  38. package/dist/agentic/guides/modules/entities.md +2 -2
  39. package/dist/agentic/guides/modules/events.md +1 -1
  40. package/dist/agentic/guides/modules/feature_toggles.md +1 -1
  41. package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
  42. package/dist/agentic/guides/modules/generators.md +1 -1
  43. package/dist/agentic/guides/modules/inbox_ops.md +1 -1
  44. package/dist/agentic/guides/modules/integrations.md +1 -1
  45. package/dist/agentic/guides/modules/messages.md +1 -1
  46. package/dist/agentic/guides/modules/notifications.md +1 -1
  47. package/dist/agentic/guides/modules/onboarding.md +1 -1
  48. package/dist/agentic/guides/modules/payment_gateways.md +1 -1
  49. package/dist/agentic/guides/modules/perspectives.md +1 -1
  50. package/dist/agentic/guides/modules/planner.md +1 -1
  51. package/dist/agentic/guides/modules/portal.md +1 -1
  52. package/dist/agentic/guides/modules/progress.md +1 -1
  53. package/dist/agentic/guides/modules/query_index.md +2 -2
  54. package/dist/agentic/guides/modules/record_locks.md +1 -1
  55. package/dist/agentic/guides/modules/resources.md +1 -1
  56. package/dist/agentic/guides/modules/sales.md +17 -17
  57. package/dist/agentic/guides/modules/scheduler.md +1 -1
  58. package/dist/agentic/guides/modules/search.md +1 -1
  59. package/dist/agentic/guides/modules/security.md +1 -1
  60. package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
  61. package/dist/agentic/guides/modules/sso.md +1 -1
  62. package/dist/agentic/guides/modules/staff.md +2 -2
  63. package/dist/agentic/guides/modules/storage_s3.md +1 -1
  64. package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
  65. package/dist/agentic/guides/modules/sync_excel.md +1 -1
  66. package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
  67. package/dist/agentic/guides/modules/translations.md +1 -1
  68. package/dist/agentic/guides/modules/webhooks.md +2 -2
  69. package/dist/agentic/guides/modules/wms.md +2 -2
  70. package/dist/agentic/guides/modules/workflows.md +1 -1
  71. package/dist/agentic/guides/testing-debugging.md +74 -0
  72. package/dist/agentic/guides/upstream/AGENTS.md +297 -0
  73. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +303 -0
  74. package/dist/agentic/guides/upstream/manifest.json +8 -0
  75. package/dist/agentic/shared/AGENTS.md.template +123 -311
  76. package/dist/agentic/shared/ai/agentic.config.json +1 -0
  77. package/dist/agentic/shared/ai/harness/README.md +48 -0
  78. package/dist/agentic/shared/ai/harness/RELEASE.md +57 -0
  79. package/dist/agentic/shared/ai/harness/cases.json +13487 -0
  80. package/dist/agentic/shared/ai/harness/cases.schema.json +132 -0
  81. package/dist/agentic/shared/ai/harness/fixtures/index.json +49 -0
  82. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +162 -0
  83. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +20 -0
  84. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +43 -0
  85. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +110 -0
  86. package/dist/agentic/shared/ai/harness/release-matrix.json +133 -0
  87. package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
  88. package/dist/agentic/shared/ai/harness/result.schema.json +76 -0
  89. package/dist/agentic/shared/ai/harness/routing-response.schema.json +12 -0
  90. package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
  91. package/dist/agentic/shared/ai/harness/validators.json +78 -0
  92. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +1768 -0
  93. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
  94. package/dist/agentic/shared/ai/review-checklist.md +36 -0
  95. package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
  96. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +20 -241
  97. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +16 -0
  98. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
  99. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +13 -0
  100. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
  101. package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
  102. package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
  103. package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
  104. package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
  105. package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
  106. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
  107. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
  108. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
  109. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
  110. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +16 -601
  111. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +19 -0
  112. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +13 -0
  113. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +14 -0
  114. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
  115. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +14 -310
  116. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +17 -0
  117. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +28 -0
  118. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +89 -0
  119. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
  120. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
  121. package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
  122. package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
  123. package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
  124. package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
  125. package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +17 -0
  126. package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
  127. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +15 -156
  128. package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +12 -0
  129. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +24 -708
  130. package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
  131. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +14 -0
  132. package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
  133. package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
  134. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
  135. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +19 -0
  136. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +107 -0
  137. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +12 -0
  138. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
  139. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +21 -0
  140. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
  141. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
  142. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
  143. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +19 -843
  144. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +18 -0
  145. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +25 -0
  146. package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
  147. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +38 -0
  148. package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
  149. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
  150. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
  151. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +16 -467
  152. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +16 -0
  153. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
  154. package/dist/agentic/shared/ai/skills/tiers.json +137 -20
  155. package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
  156. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +174 -0
  157. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +2783 -0
  158. package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
  159. package/dist/agentic/shared/scripts/framework-context.mjs +894 -0
  160. package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
  161. package/dist/agentic/shared/scripts/install-skills.sh +4 -694
  162. package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
  163. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1761 -0
  164. package/dist/lib/__integration__/TC-INT-008.spec.js +87 -8
  165. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  166. package/dist/lib/agentic-init.js +13 -3
  167. package/dist/lib/agentic-init.js.map +2 -2
  168. package/dist/lib/agentic-setup.js +418 -186
  169. package/dist/lib/agentic-setup.js.map +2 -2
  170. package/dist/lib/generators/extensions/injection-widgets.js +20 -0
  171. package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
  172. package/dist/lib/generators/module-facts-discovery.js +41 -7
  173. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  174. package/dist/lib/generators/module-facts.js +65 -17
  175. package/dist/lib/generators/module-facts.js.map +2 -2
  176. package/dist/lib/generators/module-registry.js +5 -4
  177. package/dist/lib/generators/module-registry.js.map +2 -2
  178. package/dist/lib/resolver.js +2 -0
  179. package/dist/lib/resolver.js.map +2 -2
  180. package/package.json +5 -5
  181. package/src/lib/__integration__/TC-INT-008.spec.ts +103 -8
  182. package/src/lib/__tests__/agentic-init.test.ts +41 -5
  183. package/src/lib/__tests__/agentic-setup.ownership.test.ts +380 -0
  184. package/src/lib/__tests__/injection-widget-id-validation.test.ts +184 -0
  185. package/src/lib/agentic-init.ts +15 -3
  186. package/src/lib/agentic-setup.ts +526 -224
  187. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +6 -0
  188. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
  189. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
  190. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
  191. package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
  192. package/src/lib/generators/extensions/injection-widgets.ts +51 -0
  193. package/src/lib/generators/module-facts-discovery.ts +58 -7
  194. package/src/lib/generators/module-facts.ts +84 -20
  195. package/src/lib/generators/module-registry.ts +8 -4
  196. package/src/lib/resolver.ts +3 -0
  197. package/dist/agentic/guides/cache.md +0 -45
  198. package/dist/agentic/guides/core.md +0 -453
  199. package/dist/agentic/guides/events.md +0 -117
  200. package/dist/agentic/guides/queue.md +0 -53
  201. package/dist/agentic/guides/search.md +0 -115
  202. package/dist/agentic/guides/shared.md +0 -108
  203. package/dist/agentic/guides/ui.md +0 -347
  204. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
  205. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
  206. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
  207. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
  208. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
  209. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
  210. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
  211. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
  212. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
  213. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
  214. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
  215. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
@@ -35,6 +35,12 @@ describe('module-facts BC resolve guard (T2)', () => {
35
35
  describe(`${moduleId}`, () => {
36
36
  const facts = factsByModule[moduleId]
37
37
 
38
+ it('stamps the exact providing package and version without removing coreVersion', () => {
39
+ expect(facts.sourcePackage).toBe(source.from ?? null)
40
+ expect(facts.sourceVersion).toBe(source.packageVersion ?? null)
41
+ expect(facts).toHaveProperty('coreVersion')
42
+ })
43
+
38
44
  // Entity / search / host ids are colon-namespaced under the module by construction
39
45
  // and convention; drift here means the builder or a module's data model broke.
40
46
  it('colon-namespaces entity / search / host ids under the module and keeps ids unique', () => {
@@ -52,6 +52,7 @@ describe('module-facts customers fixture (T1 anti-drift guard)', () => {
52
52
  'customers.companies.list',
53
53
  'customers.deals.list',
54
54
  'customers.people.list',
55
+ 'customers.todos.list',
55
56
  ])
56
57
  expect(facts.hostTokens.entityIds).toEqual(['customers:customer_entity'])
57
58
  expect(facts.diTokens).toEqual([])
@@ -4,11 +4,13 @@ import path from 'node:path'
4
4
  import type { PackageResolver } from '../../resolver'
5
5
  import { discoverPackageModuleSources, hasReadableModuleSource } from '../module-facts-discovery'
6
6
 
7
- type FakePackage = { name: string; path: string; modulesPath: string }
7
+ type FakePackage = { name: string; version?: string | null; path: string; modulesPath: string }
8
+ type FakeModuleEntry = { id: string; from?: string }
8
9
 
9
- function makeResolver(packages: FakePackage[]): PackageResolver {
10
+ function makeResolver(packages: FakePackage[], enabledModules: FakeModuleEntry[] = []): PackageResolver {
10
11
  return {
11
12
  discoverPackages: () => packages,
13
+ loadEnabledModules: () => enabledModules,
12
14
  } as unknown as PackageResolver
13
15
  }
14
16
 
@@ -62,27 +64,69 @@ describe('module-facts discovery (T1)', () => {
62
64
  writeModule(path.join(pkgSrc, 'src', 'modules'), 'gamma', 'events.ts')
63
65
 
64
66
  const resolver = makeResolver([
65
- { name: '@open-mercato/tagged', path: pkgSrc, modulesPath: path.join(pkgSrc, 'src', 'modules') },
67
+ { name: '@open-mercato/tagged', version: '0.6.6', path: pkgSrc, modulesPath: path.join(pkgSrc, 'src', 'modules') },
66
68
  ])
67
69
 
68
70
  const [source] = discoverPackageModuleSources(resolver)
69
- expect(source).toMatchObject({ moduleId: 'gamma', from: '@open-mercato/tagged' })
71
+ expect(source).toMatchObject({
72
+ moduleId: 'gamma',
73
+ from: '@open-mercato/tagged',
74
+ packageVersion: '0.6.6',
75
+ })
70
76
  expect(source.moduleRoot).toBe(path.join(pkgSrc, 'src', 'modules', 'gamma'))
71
77
  })
72
78
 
73
- it('dedupes duplicate module ids first-wins', () => {
79
+ it('fails closed on duplicate module ids and reports every provider independent of discovery order', () => {
74
80
  const pkgA = path.join(tmp, 'dup-a')
75
81
  writeModule(path.join(pkgA, 'src', 'modules'), 'shared', 'index.ts')
76
82
  const pkgB = path.join(tmp, 'dup-b')
77
83
  writeModule(path.join(pkgB, 'src', 'modules'), 'shared', 'index.ts')
78
84
 
79
- const resolver = makeResolver([
85
+ const packages = [
80
86
  { name: '@open-mercato/dup-a', path: pkgA, modulesPath: path.join(pkgA, 'src', 'modules') },
81
87
  { name: '@open-mercato/dup-b', path: pkgB, modulesPath: path.join(pkgB, 'src', 'modules') },
82
- ])
88
+ ]
89
+ const expected = '[module-facts] duplicate module id "shared" is provided by '
90
+ + '@open-mercato/dup-a, @open-mercato/dup-b; src/modules.ts does not select a provider. '
91
+ + 'Select exactly one matching provider in src/modules.ts.'
92
+
93
+ expect(() => discoverPackageModuleSources(makeResolver(packages))).toThrow(expected)
94
+ expect(() => discoverPackageModuleSources(makeResolver([...packages].reverse()))).toThrow(expected)
95
+ })
96
+
97
+ it('uses the exact src/modules.ts provider even when package discovery order is reversed', () => {
98
+ const pkgA = path.join(tmp, 'selected-a')
99
+ writeModule(path.join(pkgA, 'src', 'modules'), 'shared', 'index.ts')
100
+ const pkgB = path.join(tmp, 'selected-b')
101
+ writeModule(path.join(pkgB, 'src', 'modules'), 'shared', 'index.ts')
102
+
103
+ const packages = [
104
+ { name: '@open-mercato/selected-a', path: pkgA, modulesPath: path.join(pkgA, 'src', 'modules') },
105
+ { name: '@open-mercato/selected-b', path: pkgB, modulesPath: path.join(pkgB, 'src', 'modules') },
106
+ ]
107
+ const enabled = [{ id: 'shared', from: '@open-mercato/selected-b' }]
108
+
109
+ for (const discoveredPackages of [packages, [...packages].reverse()]) {
110
+ const sources = discoverPackageModuleSources(makeResolver(discoveredPackages, enabled))
111
+ expect(sources).toHaveLength(1)
112
+ expect(sources[0]).toMatchObject({ moduleId: 'shared', from: '@open-mercato/selected-b' })
113
+ expect(sources[0].moduleRoot).toBe(path.join(pkgB, 'src', 'modules', 'shared'))
114
+ }
115
+ })
116
+
117
+ it('explains that an omitted provider implicitly selects core', () => {
118
+ const pkgA = path.join(tmp, 'implicit-a')
119
+ writeModule(path.join(pkgA, 'src', 'modules'), 'shared', 'index.ts')
120
+ const pkgB = path.join(tmp, 'implicit-b')
121
+ writeModule(path.join(pkgB, 'src', 'modules'), 'shared', 'index.ts')
122
+
123
+ const packages = [
124
+ { name: '@open-mercato/dup-a', path: pkgA, modulesPath: path.join(pkgA, 'src', 'modules') },
125
+ { name: '@open-mercato/dup-b', path: pkgB, modulesPath: path.join(pkgB, 'src', 'modules') },
126
+ ]
83
127
 
84
- const sources = discoverPackageModuleSources(resolver)
85
- expect(sources).toHaveLength(1)
86
- expect(sources[0].moduleRoot).toBe(path.join(pkgA, 'src', 'modules', 'shared'))
128
+ expect(() => discoverPackageModuleSources(makeResolver(packages, [{ id: 'shared' }]))).toThrow(
129
+ 'src/modules.ts omits "from", which defaults to @open-mercato/core',
130
+ )
87
131
  })
88
132
  })
@@ -0,0 +1,108 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { extractModuleFacts } from '../module-facts'
5
+
6
+ function findCoreSrcRoot(): string {
7
+ let dir = __dirname
8
+ for (let depth = 0; depth < 10; depth += 1) {
9
+ const candidate = path.join(dir, 'packages', 'core', 'src', 'modules')
10
+ if (fs.existsSync(candidate)) return candidate
11
+ dir = path.dirname(dir)
12
+ }
13
+ throw new Error('[internal] could not locate packages/core/src/modules from the test directory')
14
+ }
15
+
16
+ function write(filePath: string, source: string): void {
17
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
18
+ fs.writeFileSync(filePath, source)
19
+ }
20
+
21
+ describe('module-facts extension host extraction', () => {
22
+ const coreSrcRoot = findCoreSrcRoot()
23
+
24
+ it('resolves generated E.<module>.<entity> custom-field declarations from shipped modules', () => {
25
+ const expectedByModule: Record<string, string[]> = {
26
+ attachments: ['attachments:attachment'],
27
+ catalog: [
28
+ 'catalog:catalog_product',
29
+ 'catalog:catalog_product_variant',
30
+ 'catalog:catalog_product_price',
31
+ ],
32
+ sales: [
33
+ 'sales:sales_order',
34
+ 'sales:sales_order_line',
35
+ 'sales:sales_order_adjustment',
36
+ 'sales:sales_quote',
37
+ 'sales:sales_quote_line',
38
+ 'sales:sales_quote_adjustment',
39
+ 'sales:sales_invoice',
40
+ 'sales:sales_invoice_line',
41
+ 'sales:sales_payment',
42
+ 'sales:sales_payment_allocation',
43
+ 'sales:sales_credit_memo',
44
+ 'sales:sales_credit_memo_line',
45
+ 'sales:sales_shipment',
46
+ 'sales:sales_shipment_item',
47
+ 'sales:sales_note',
48
+ ],
49
+ staff: ['staff:staff_team_member'],
50
+ }
51
+
52
+ for (const [moduleId, expected] of Object.entries(expectedByModule)) {
53
+ const facts = extractModuleFacts({ moduleId, coreSrcRoot })
54
+ expect(facts.entities.filter((entity) => entity.customFields).map((entity) => entity.id).sort()).toEqual(
55
+ [...expected].sort(),
56
+ )
57
+ }
58
+ })
59
+
60
+ it('discovers literal and conditional DataTable IDs outside backend while excluding test sources', () => {
61
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'module-facts-extension-hosts-'))
62
+ const moduleRoot = path.join(tmpRoot, 'alpha')
63
+ try {
64
+ write(
65
+ path.join(moduleRoot, 'data', 'entities.ts'),
66
+ `@Entity({ tableName: 'alpha_records' })\nexport class AlphaRecord { updatedAt?: Date }\n`,
67
+ )
68
+ write(
69
+ path.join(moduleRoot, 'ce.ts'),
70
+ `export const entities = [{ id: E.alpha.alpha_record, fields: [] }]\n`,
71
+ )
72
+ write(
73
+ path.join(moduleRoot, 'components', 'AlphaTable.tsx'),
74
+ `export const props = {
75
+ perspective: { tableId: kind === 'one' ? 'alpha.one' : 'alpha.two' },
76
+ extensionTableId: 'alpha.extension',
77
+ }\n`,
78
+ )
79
+ write(
80
+ path.join(moduleRoot, 'components', 'Noise.test.tsx'),
81
+ `export const noise = { tableId: 'alpha.test-noise' }\n`,
82
+ )
83
+ write(
84
+ path.join(moduleRoot, 'backend', '__tests__', 'noise.tsx'),
85
+ `export const noise = { extensionTableId: 'alpha.test-directory-noise' }\n`,
86
+ )
87
+
88
+ const facts = extractModuleFacts({ moduleId: 'alpha', moduleRoot })
89
+ expect(facts.entities).toEqual([
90
+ expect.objectContaining({ id: 'alpha:alpha_record', customFields: true }),
91
+ ])
92
+ expect(facts.hostTokens.tableIds).toEqual(['alpha.extension', 'alpha.one', 'alpha.two'])
93
+ } finally {
94
+ fs.rmSync(tmpRoot, { recursive: true, force: true })
95
+ }
96
+ })
97
+
98
+ it('captures shipped component-level table hosts used by catalog and sales', () => {
99
+ const catalog = extractModuleFacts({ moduleId: 'catalog', coreSrcRoot })
100
+ expect(catalog.hostTokens.tableIds).toEqual([
101
+ 'catalog.categories.list',
102
+ 'catalog.products.list',
103
+ ])
104
+
105
+ const sales = extractModuleFacts({ moduleId: 'sales', coreSrcRoot })
106
+ expect(sales.hostTokens.tableIds).toEqual(['sales.orders', 'sales.quotes'])
107
+ })
108
+ })
@@ -702,6 +702,24 @@ export function GET() {
702
702
  expect(output).toContain('features:')
703
703
  })
704
704
 
705
+ it('normalizes singular app-owned integration exports without requiring plural properties', async () => {
706
+ touchFile(
707
+ path.join(tmpDir, 'app', 'src', 'modules', 'mailbox', 'integration.ts'),
708
+ "export const integration = { id: 'mailbox' }\nexport default integration\n",
709
+ )
710
+ const resolver = createMockResolver(tmpDir, [{ id: 'mailbox', from: '@app' }])
711
+ const result = await generateModuleRegistry({ resolver, quiet: true })
712
+
713
+ expect(result.errors).toEqual([])
714
+ for (const file of ['modules.generated.ts', 'modules.runtime.generated.ts']) {
715
+ const output = readGenerated(tmpDir, file)!
716
+ expect(output).toContain('as unknown as { integrations?:')
717
+ expect(output).toContain('.integration ? [')
718
+ expect(output).not.toMatch(/INTEGRATION_[A-Za-z0-9_]+\.integrations/)
719
+ expect(output).not.toMatch(/INTEGRATION_[A-Za-z0-9_]+\.bundles/)
720
+ }
721
+ })
722
+
705
723
  it('escapes generated module specifiers for lazy imports', async () => {
706
724
  touchFile(
707
725
  path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'safe_mod', 'subscribers', `o'hare.ts`),
@@ -183,9 +183,52 @@ function resolveScannedWidgetPath(roots: ModuleRoots, fromApp: boolean, relPath:
183
183
  return path.join(base, 'widgets', 'injection', ...relPath.split('/'))
184
184
  }
185
185
 
186
+ /**
187
+ * A widget's `metadata.id` and the injection-table entry that mounts it are two hand-typed string
188
+ * literals in two different files, and nothing links them. A typo in either fails silently: the spot
189
+ * resolves to no widget and nothing is logged, so it only surfaces as "why isn't my widget
190
+ * rendering" during manual QA.
191
+ *
192
+ * Reports table entries whose `widgetId` no scanned widget declares. Diagnostics only — generated
193
+ * output is unchanged and the build is not failed, since a table entry may legitimately point at a
194
+ * widget supplied by a module that is not enabled in this app.
195
+ *
196
+ * Deliberately one-directional. The mirror check ("a widget nothing references is never mounted")
197
+ * cannot be done reliably yet: `extractInjectionTableWidgetIds` only reads statically-analyzable
198
+ * object literals, so a table assembled by composition or computation contributes **no** visible
199
+ * ids — `example` exports `cond ? { ...a, ...b } : a` and `translations` exports
200
+ * `buildInjectionTable()`, and both would have every one of their widgets reported as unreferenced.
201
+ * Under-reporting is harmless in the direction implemented here (a reference the extractor never saw
202
+ * cannot produce a warning) but fatal in the other. Teaching the extractor to resolve spreads,
203
+ * conditionals and call expressions is the prerequisite for adding it.
204
+ */
205
+ function reportUnresolvableWidgetReferences(
206
+ widgetEntries: Map<string, InjectionWidgetEntry>,
207
+ tableWidgetRefs: Map<string, Set<string>>,
208
+ ): void {
209
+ const declaredIds = new Set<string>()
210
+ for (const entry of widgetEntries.values()) {
211
+ if (entry.widgetId) declaredIds.add(entry.widgetId)
212
+ }
213
+
214
+ for (const [widgetId, modules] of Array.from(tableWidgetRefs.entries()).sort(([a], [b]) => a.localeCompare(b))) {
215
+ if (declaredIds.has(widgetId)) continue
216
+ const owners = Array.from(modules).sort((left, right) => left.localeCompare(right)).join(', ')
217
+ console.warn(
218
+ `[generate] ⚠ Injection table in "${owners}" references widget id "${widgetId}", but no scanned ` +
219
+ 'widget declares it — that spot will render nothing. Check the id against the widget\'s ' +
220
+ 'metadata.id, and that the widget lives in a file the scanner reads ' +
221
+ '(widgets/injection/**/widget.ts|tsx — note that widget.client.tsx alone is not scanned).',
222
+ )
223
+ }
224
+ }
225
+
186
226
  export function createInjectionWidgetsExtension(): GeneratorExtension {
187
227
  const widgetEntries = new Map<string, InjectionWidgetEntry>()
188
228
  const tableEntries: InjectionTableEntry[] = []
229
+ // widgetId -> module ids whose injection table references it. Collected across every module so a
230
+ // table may legitimately reference a widget another module owns.
231
+ const tableWidgetRefs = new Map<string, Set<string>>()
189
232
 
190
233
  return {
191
234
  id: 'registry.injection-widgets',
@@ -217,6 +260,12 @@ export function createInjectionWidgetsExtension(): GeneratorExtension {
217
260
  moduleWidgetEntries[0].widgetId = tableWidgetIds[0]
218
261
  }
219
262
 
263
+ for (const widgetId of tableWidgetIds) {
264
+ const modules = tableWidgetRefs.get(widgetId) ?? new Set<string>()
265
+ modules.add(ctx.moduleId)
266
+ tableWidgetRefs.set(widgetId, modules)
267
+ }
268
+
220
269
  for (const entry of moduleWidgetEntries) {
221
270
  const existing = widgetEntries.get(entry.key)
222
271
  if (!existing || (existing.source !== 'app' && entry.source === 'app')) {
@@ -233,6 +282,8 @@ export function createInjectionWidgetsExtension(): GeneratorExtension {
233
282
  }
234
283
  },
235
284
  generateOutput() {
285
+ reportUnresolvableWidgetReferences(widgetEntries, tableWidgetRefs)
286
+
236
287
  const widgetDecls = Array.from(widgetEntries.entries())
237
288
  .sort(([left], [right]) => left.localeCompare(right))
238
289
  .map(([, entry]) =>
@@ -29,13 +29,59 @@ export function hasReadableModuleSource(moduleRoot: string): boolean {
29
29
  return false
30
30
  }
31
31
 
32
- function dedupeById(sources: ModuleFactSource[]): ModuleFactSource[] {
33
- const seen = new Set<string>()
32
+ function resolveDuplicateModuleIds(
33
+ sources: ModuleFactSource[],
34
+ resolver: PackageResolver,
35
+ ): ModuleFactSource[] {
36
+ const sourcesByModule = new Map<string, ModuleFactSource[]>()
37
+ for (const source of sources) {
38
+ const matches = sourcesByModule.get(source.moduleId) ?? []
39
+ matches.push(source)
40
+ sourcesByModule.set(source.moduleId, matches)
41
+ }
42
+
43
+ const duplicates = [...sourcesByModule.entries()].filter(([, matches]) => matches.length > 1)
44
+ if (duplicates.length === 0) return sources
45
+
46
+ const selectedProviders = new Map<string, Set<string>>()
47
+ const implicitCoreSelections = new Set<string>()
48
+ for (const entry of resolver.loadEnabledModules()) {
49
+ const providers = selectedProviders.get(entry.id) ?? new Set<string>()
50
+ providers.add(entry.from ?? '@open-mercato/core')
51
+ selectedProviders.set(entry.id, providers)
52
+ if (entry.from === undefined) implicitCoreSelections.add(entry.id)
53
+ }
54
+
55
+ const selectedSources = new Map<string, ModuleFactSource>()
56
+ for (const [moduleId, matches] of duplicates) {
57
+ const providers = [...new Set(matches.map((source) => source.from ?? '<unknown>'))]
58
+ .sort((left, right) => left.localeCompare(right))
59
+ const configuredProviders = [...(selectedProviders.get(moduleId) ?? [])]
60
+ .sort((left, right) => left.localeCompare(right))
61
+ const selected = configuredProviders.length === 1
62
+ ? matches.filter((source) => source.from === configuredProviders[0])
63
+ : []
64
+
65
+ if (selected.length !== 1) {
66
+ const configured = configuredProviders.length > 0
67
+ ? implicitCoreSelections.has(moduleId)
68
+ ? '; src/modules.ts omits "from", which defaults to @open-mercato/core'
69
+ : `; src/modules.ts selects ${configuredProviders.join(', ')}`
70
+ : '; src/modules.ts does not select a provider'
71
+ throw new Error(
72
+ `[module-facts] duplicate module id "${moduleId}" is provided by ${providers.join(', ')}${configured}. `
73
+ + 'Select exactly one matching provider in src/modules.ts.',
74
+ )
75
+ }
76
+ selectedSources.set(moduleId, selected[0])
77
+ }
78
+
79
+ const emitted = new Set<string>()
34
80
  const result: ModuleFactSource[] = []
35
81
  for (const source of sources) {
36
- if (seen.has(source.moduleId)) continue
37
- seen.add(source.moduleId)
38
- result.push(source)
82
+ if (emitted.has(source.moduleId)) continue
83
+ emitted.add(source.moduleId)
84
+ result.push(selectedSources.get(source.moduleId) ?? source)
39
85
  }
40
86
  return result
41
87
  }
@@ -51,8 +97,13 @@ export function discoverPackageModuleSources(resolver: PackageResolver): ModuleF
51
97
  for (const discovered of discoverModulesInPackage(pkg.path)) {
52
98
  const moduleRoot = path.join(pkg.modulesPath, discovered.moduleId)
53
99
  if (!hasReadableModuleSource(moduleRoot)) continue
54
- sources.push({ moduleId: discovered.moduleId, moduleRoot, from: pkg.name })
100
+ sources.push({
101
+ moduleId: discovered.moduleId,
102
+ moduleRoot,
103
+ from: pkg.name,
104
+ packageVersion: pkg.version ?? null,
105
+ })
55
106
  }
56
107
  }
57
- return dedupeById(sources)
108
+ return resolveDuplicateModuleIds(sources, resolver)
58
109
  }
@@ -40,6 +40,8 @@ export interface ModuleFacts {
40
40
  title: string | null
41
41
  description: string | null
42
42
  coreVersion: string | null
43
+ sourcePackage: string | null
44
+ sourceVersion: string | null
43
45
  entities: ModuleEntityFact[]
44
46
  events: ModuleEventFact[]
45
47
  aclFeatures: string[]
@@ -64,6 +66,8 @@ export interface ExtractModuleFactsOptions {
64
66
  /** Explicit module source directory. When set it overrides `coreSrcRoot + moduleId`. */
65
67
  moduleRoot?: string
66
68
  coreVersion?: string | null
69
+ sourcePackage?: string | null
70
+ sourceVersion?: string | null
67
71
  registryPath?: string | null
68
72
  registrySource?: string | null
69
73
  }
@@ -73,6 +77,7 @@ export interface ModuleFactSource {
73
77
  moduleId: string
74
78
  moduleRoot: string
75
79
  from?: string
80
+ packageVersion?: string | null
76
81
  }
77
82
 
78
83
  function readSourceFile(filePath: string): ts.SourceFile | null {
@@ -159,10 +164,41 @@ function collectCustomFieldEntityIds(ceFilePath: string | null): Set<string> {
159
164
  const sourceFile = readSourceFile(ceFilePath)
160
165
  if (!sourceFile) return result
161
166
 
167
+ const readEntityId = (expression: ts.Expression): string | undefined => {
168
+ const current = unwrapExpression(expression)
169
+ if (ts.isStringLiteralLike(current)) {
170
+ return current.text.includes(':') ? current.text : undefined
171
+ }
172
+
173
+ const readAccessPath = (candidate: ts.Expression): string[] | null => {
174
+ const access = unwrapExpression(candidate)
175
+ if (ts.isIdentifier(access)) return [access.text]
176
+ if (ts.isPropertyAccessExpression(access)) {
177
+ const parent = readAccessPath(access.expression)
178
+ return parent ? [...parent, access.name.text] : null
179
+ }
180
+ if (ts.isElementAccessExpression(access) && access.argumentExpression) {
181
+ const parent = readAccessPath(access.expression)
182
+ const key = unwrapExpression(access.argumentExpression)
183
+ return parent && ts.isStringLiteralLike(key) ? [...parent, key.text] : null
184
+ }
185
+ return null
186
+ }
187
+
188
+ const accessPath = readAccessPath(current)
189
+ if (accessPath?.length === 3 && accessPath[0] === 'E') {
190
+ return `${accessPath[1]}:${accessPath[2]}`
191
+ }
192
+ return undefined
193
+ }
194
+
162
195
  const visit = (node: ts.Node): void => {
163
196
  if (ts.isObjectLiteralExpression(node)) {
164
- const id = readStringPropertyInitializer(node, 'id')
165
- if (id && id.includes(':')) result.add(id)
197
+ const initializer = getObjectPropertyInitializer(node, 'id')
198
+ if (initializer) {
199
+ const id = readEntityId(initializer)
200
+ if (id) result.add(id)
201
+ }
166
202
  }
167
203
  node.forEachChild(visit)
168
204
  }
@@ -666,36 +702,50 @@ function extractCli(cliFilePath: string | null, warnings: string[]): string[] {
666
702
  function listSourceFilesRecursive(directory: string): string[] {
667
703
  const files: string[] = []
668
704
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
669
- if (entry.name === '__tests__' || entry.name === 'node_modules') continue
705
+ if (entry.name === '__tests__' || entry.name === '__mocks__' || entry.name === 'node_modules') continue
670
706
  const fullPath = path.join(directory, entry.name)
671
707
  if (entry.isDirectory()) {
672
708
  files.push(...listSourceFilesRecursive(fullPath))
673
- } else if (/\.tsx?$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
709
+ } else if (
710
+ /\.tsx?$/.test(entry.name)
711
+ && !entry.name.endsWith('.d.ts')
712
+ && !/\.(?:test|spec)\.tsx?$/.test(entry.name)
713
+ ) {
674
714
  files.push(fullPath)
675
715
  }
676
716
  }
677
717
  return files
678
718
  }
679
719
 
680
- function extractTableIds(backendDir: string): string[] {
681
- if (!fs.existsSync(backendDir)) return []
720
+ function extractTableIds(moduleRoot: string): string[] {
721
+ if (!fs.existsSync(moduleRoot)) return []
682
722
 
683
723
  const tableIds: string[] = []
684
724
  const seen = new Set<string>()
685
- for (const filePath of listSourceFilesRecursive(backendDir)) {
725
+ const collectLiteralValues = (expression: ts.Expression): string[] => {
726
+ const current = unwrapExpression(expression)
727
+ if (ts.isStringLiteralLike(current)) return [current.text]
728
+ if (ts.isConditionalExpression(current)) {
729
+ return [
730
+ ...collectLiteralValues(current.whenTrue),
731
+ ...collectLiteralValues(current.whenFalse),
732
+ ]
733
+ }
734
+ return []
735
+ }
736
+
737
+ for (const filePath of listSourceFilesRecursive(moduleRoot)) {
686
738
  const sourceFile = readSourceFile(filePath)
687
739
  if (!sourceFile) continue
688
740
  const visit = (node: ts.Node): void => {
689
741
  if (ts.isPropertyAssignment(node)) {
690
742
  const propertyName = getPropertyName(node)
691
- if (
692
- (propertyName === 'tableId' || propertyName === 'extensionTableId') &&
693
- ts.isStringLiteralLike(node.initializer)
694
- ) {
695
- const value = node.initializer.text
696
- if (value && !seen.has(value)) {
697
- seen.add(value)
698
- tableIds.push(value)
743
+ if (propertyName === 'tableId' || propertyName === 'extensionTableId') {
744
+ for (const value of collectLiteralValues(node.initializer)) {
745
+ if (value && !seen.has(value)) {
746
+ seen.add(value)
747
+ tableIds.push(value)
748
+ }
699
749
  }
700
750
  }
701
751
  }
@@ -724,7 +774,7 @@ function extractModuleMeta(indexFilePath: string | null): { title: string | null
724
774
  }
725
775
 
726
776
  export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFacts {
727
- const { moduleId, coreVersion = null } = options
777
+ const { moduleId, coreVersion = null, sourcePackage = null, sourceVersion = null } = options
728
778
  const moduleRoot = options.moduleRoot
729
779
  ?? (options.coreSrcRoot ? path.join(options.coreSrcRoot, moduleId) : null)
730
780
  if (!moduleRoot) {
@@ -743,7 +793,6 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
743
793
  const notificationsFilePath = resolveConventionFile(moduleRoot, 'notifications')
744
794
  const cliFilePath = resolveConventionFile(moduleRoot, 'cli')
745
795
  const indexFilePath = resolveConventionFile(moduleRoot, 'index')
746
- const backendDir = path.join(moduleRoot, 'backend')
747
796
 
748
797
  const warnings: string[] = []
749
798
  const { title, description } = extractModuleMeta(indexFilePath)
@@ -760,7 +809,7 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
760
809
  const cli = extractCli(cliFilePath, warnings)
761
810
  const hostTokens: ModuleHostTokens = {
762
811
  entityIds: extractHostEntityIds(entities),
763
- tableIds: extractTableIds(backendDir),
812
+ tableIds: extractTableIds(moduleRoot),
764
813
  }
765
814
 
766
815
  return {
@@ -768,6 +817,8 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
768
817
  title,
769
818
  description,
770
819
  coreVersion,
820
+ sourcePackage,
821
+ sourceVersion,
771
822
  entities,
772
823
  events,
773
824
  aclFeatures,
@@ -791,6 +842,8 @@ export interface ModuleFactsJsonEntry {
791
842
  title: string | null
792
843
  description: string | null
793
844
  coreVersion: string | null
845
+ sourcePackage: string | null
846
+ sourceVersion: string | null
794
847
  entities: ModuleEntityFact[]
795
848
  events: ModuleFactsJsonEvent[]
796
849
  aclFeatures: string[]
@@ -804,7 +857,14 @@ export interface ModuleFactsJsonEntry {
804
857
 
805
858
  const EMPTY_SECTION_MARKER = '_none_'
806
859
 
807
- function renderVersionStamp(coreVersion: string | null): string {
860
+ function renderVersionStamp(
861
+ coreVersion: string | null,
862
+ sourcePackage: string | null,
863
+ sourceVersion: string | null,
864
+ ): string {
865
+ if (sourcePackage) {
866
+ return `<!-- generated from ${sourcePackage} ${sourceVersion || '<unknown>'}; core ${coreVersion || '<unknown>'} — R1 staleness stamp -->`
867
+ }
808
868
  const version = coreVersion && coreVersion.length > 0 ? coreVersion : '<unknown>'
809
869
  return `<!-- generated from @open-mercato/core ${version} — R1 staleness stamp -->`
810
870
  }
@@ -873,7 +933,7 @@ function renderHostTokensSection(hostTokens: ModuleHostTokens): string {
873
933
  export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
874
934
  const sections = [
875
935
  `# ${facts.module} — module facts (generated, do not edit)`,
876
- renderVersionStamp(facts.coreVersion),
936
+ renderVersionStamp(facts.coreVersion, facts.sourcePackage, facts.sourceVersion),
877
937
  '',
878
938
  renderEntitiesSection(facts.entities),
879
939
  '',
@@ -902,6 +962,8 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
902
962
  title: facts.title,
903
963
  description: facts.description,
904
964
  coreVersion: facts.coreVersion,
965
+ sourcePackage: facts.sourcePackage,
966
+ sourceVersion: facts.sourceVersion,
905
967
  entities: facts.entities,
906
968
  events: facts.events.map((event) => ({ id: event.id, category: event.category, entity: event.entity })),
907
969
  aclFeatures: facts.aclFeatures,
@@ -967,6 +1029,8 @@ export function extractAllModuleFacts(options: ExtractAllModuleFactsOptions): Ex
967
1029
  moduleId: source.moduleId,
968
1030
  moduleRoot: source.moduleRoot,
969
1031
  coreVersion: options.coreVersion ?? null,
1032
+ sourcePackage: source.from ?? null,
1033
+ sourceVersion: source.packageVersion ?? null,
970
1034
  registryPath: options.registryPath ?? null,
971
1035
  registrySource: options.registrySource ?? null,
972
1036
  })
@@ -3059,6 +3059,10 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3059
3059
  if (fields) fieldsImportName = fields.importName
3060
3060
  }
3061
3061
 
3062
+ const integrationExports = integrationImportName
3063
+ ? `(${integrationImportName} as unknown as { integrations?: import('@open-mercato/shared/modules/integrations/types').IntegrationDefinition[]; integration?: import('@open-mercato/shared/modules/integrations/types').IntegrationDefinition; bundles?: import('@open-mercato/shared/modules/integrations/types').IntegrationBundle[]; bundle?: import('@open-mercato/shared/modules/integrations/types').IntegrationBundle })`
3064
+ : null
3065
+
3062
3066
  // 13. Pages: backend
3063
3067
  {
3064
3068
  const beApp = path.join(roots.appBase, 'backend')
@@ -3167,8 +3171,8 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3167
3171
  ${dashboardWidgets.length ? `dashboardWidgets: [${dashboardWidgets.join(', ')}],` : ''}
3168
3172
  ${setupImportName ? `setup: (${setupImportName}.default ?? ${setupImportName}.setup) || undefined,` : ''}
3169
3173
  ${encryptionImportName ? `defaultEncryptionMaps: ((${encryptionImportName}.default ?? ${encryptionImportName}.defaultEncryptionMaps) as import('@open-mercato/shared/modules/encryption').ModuleEncryptionMap[]) || [],` : ''}
3170
- ${integrationImportName ? `integrations: (( ${integrationImportName}.integrations ?? (${integrationImportName}.integration ? [${integrationImportName}.integration] : []) ) as import('@open-mercato/shared/modules/integrations/types').IntegrationDefinition[]),` : ''}
3171
- ${integrationImportName ? `bundles: (( ${integrationImportName}.bundles ?? (${integrationImportName}.bundle ? [${integrationImportName}.bundle] : []) ) as import('@open-mercato/shared/modules/integrations/types').IntegrationBundle[]),` : ''}
3174
+ ${integrationExports ? `integrations: ((${integrationExports}.integrations ?? (${integrationExports}.integration ? [${integrationExports}.integration] : [])) as import('@open-mercato/shared/modules/integrations/types').IntegrationDefinition[]),` : ''}
3175
+ ${integrationExports ? `bundles: ((${integrationExports}.bundles ?? (${integrationExports}.bundle ? [${integrationExports}.bundle] : [])) as import('@open-mercato/shared/modules/integrations/types').IntegrationBundle[]),` : ''}
3172
3176
  }`)
3173
3177
  runtimeModuleDecls.push(`{
3174
3178
  id: ${toLiteral(modId)},
@@ -3185,8 +3189,8 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3185
3189
  ${customEntitiesImportName ? `customEntities: ((${customEntitiesImportName}.default ?? ${customEntitiesImportName}.entities) as any) || [],` : ''}
3186
3190
  ${setupImportName ? `setup: (${setupImportName}.default ?? ${setupImportName}.setup) || undefined,` : ''}
3187
3191
  ${encryptionImportName ? `defaultEncryptionMaps: ((${encryptionImportName}.default ?? ${encryptionImportName}.defaultEncryptionMaps) as import('@open-mercato/shared/modules/encryption').ModuleEncryptionMap[]) || [],` : ''}
3188
- ${integrationImportName ? `integrations: (( ${integrationImportName}.integrations ?? (${integrationImportName}.integration ? [${integrationImportName}.integration] : []) ) as import('@open-mercato/shared/modules/integrations/types').IntegrationDefinition[]),` : ''}
3189
- ${integrationImportName ? `bundles: (( ${integrationImportName}.bundles ?? (${integrationImportName}.bundle ? [${integrationImportName}.bundle] : []) ) as import('@open-mercato/shared/modules/integrations/types').IntegrationBundle[]),` : ''}
3192
+ ${integrationExports ? `integrations: ((${integrationExports}.integrations ?? (${integrationExports}.integration ? [${integrationExports}.integration] : [])) as import('@open-mercato/shared/modules/integrations/types').IntegrationDefinition[]),` : ''}
3193
+ ${integrationExports ? `bundles: ((${integrationExports}.bundles ?? (${integrationExports}.bundle ? [${integrationExports}.bundle] : [])) as import('@open-mercato/shared/modules/integrations/types').IntegrationBundle[]),` : ''}
3190
3194
  }`)
3191
3195
  }
3192
3196