@open-mercato/cli 0.6.7-develop.6677.1.beabb7ca12 → 0.6.7-develop.6686.1.bdda5ed397

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 (211) hide show
  1. package/.turbo/turbo-build.log +0 -1
  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/module-facts-discovery.js +41 -7
  171. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  172. package/dist/lib/generators/module-facts.js +65 -17
  173. package/dist/lib/generators/module-facts.js.map +2 -2
  174. package/dist/lib/generators/module-registry.js +5 -4
  175. package/dist/lib/generators/module-registry.js.map +2 -2
  176. package/dist/lib/resolver.js +2 -0
  177. package/dist/lib/resolver.js.map +2 -2
  178. package/package.json +5 -5
  179. package/src/lib/__integration__/TC-INT-008.spec.ts +103 -8
  180. package/src/lib/__tests__/agentic-init.test.ts +41 -5
  181. package/src/lib/__tests__/agentic-setup.ownership.test.ts +380 -0
  182. package/src/lib/agentic-init.ts +15 -3
  183. package/src/lib/agentic-setup.ts +526 -224
  184. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +6 -0
  185. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
  186. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
  187. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
  188. package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
  189. package/src/lib/generators/module-facts-discovery.ts +58 -7
  190. package/src/lib/generators/module-facts.ts +84 -20
  191. package/src/lib/generators/module-registry.ts +8 -4
  192. package/src/lib/resolver.ts +3 -0
  193. package/dist/agentic/guides/cache.md +0 -45
  194. package/dist/agentic/guides/core.md +0 -453
  195. package/dist/agentic/guides/events.md +0 -117
  196. package/dist/agentic/guides/queue.md +0 -53
  197. package/dist/agentic/guides/search.md +0 -115
  198. package/dist/agentic/guides/shared.md +0 -108
  199. package/dist/agentic/guides/ui.md +0 -347
  200. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
  201. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
  202. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
  203. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
  204. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
  205. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
  206. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
  207. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
  208. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
  209. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
  210. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
  211. 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`),
@@ -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
 
@@ -29,6 +29,7 @@ export type ModuleEntry = {
29
29
 
30
30
  export type PackageInfo = {
31
31
  name: string
32
+ version?: string | null
32
33
  path: string
33
34
  modulesPath: string
34
35
  }
@@ -356,6 +357,7 @@ function discoverPackagesInMonorepo(rootDir: string): PackageInfo[] {
356
357
  if (fs.existsSync(modulesPath)) {
357
358
  packages.push({
358
359
  name: pkgJson.name || `@open-mercato/${entry.name}`,
360
+ version: typeof pkgJson.version === 'string' ? pkgJson.version : null,
359
361
  path: pkgPath,
360
362
  modulesPath,
361
363
  })
@@ -391,6 +393,7 @@ function discoverPackagesInNodeModules(rootDir: string): PackageInfo[] {
391
393
  if (fs.existsSync(modulesPath)) {
392
394
  packages.push({
393
395
  name: pkgJson.name || `@open-mercato/${entry.name}`,
396
+ version: typeof pkgJson.version === 'string' ? pkgJson.version : null,
394
397
  path: pkgPath,
395
398
  modulesPath,
396
399
  })
@@ -1,45 +0,0 @@
1
- # Cache Package — Standalone Developer Guide
2
-
3
- `@open-mercato/cache` provides tenant-scoped caching with tag-based invalidation. MUST NOT use raw Redis, SQLite, or in-memory caching directly.
4
-
5
- ## Strategy Selection
6
-
7
- | Strategy | When | Config |
8
- |----------|------|--------|
9
- | Memory | Development, single-process | Default (no config) |
10
- | SQLite | Single-server production | `CACHE_STRATEGY=sqlite` |
11
- | Redis | Multi-server, shared cache | `CACHE_STRATEGY=redis` |
12
-
13
- ## Usage
14
-
15
- Always resolve via DI:
16
-
17
- ```typescript
18
- const cacheService = container.resolve('cacheService')
19
-
20
- // Set with tags for targeted invalidation
21
- await cacheService.set(`${tenantId}:my_module:stats`, value, {
22
- tags: [`tenant:${tenantId}`, 'my_module'],
23
- })
24
-
25
- // Get
26
- const cached = await cacheService.get(`${tenantId}:my_module:stats`)
27
-
28
- // Invalidate by tag (clears all entries with that tag)
29
- await cacheService.invalidateTag('my_module')
30
- ```
31
-
32
- ## MUST Rules
33
-
34
- 1. **MUST resolve via DI** — `container.resolve('cacheService')`, never instantiate directly
35
- 2. **MUST scope to tenant** — include `tenantId` in cache keys
36
- 3. **MUST use tag-based invalidation** for CRUD side effects
37
- 4. **MUST NOT cache sensitive data** without encryption
38
-
39
- ## Adding Cache to a Module
40
-
41
- 1. Resolve `cacheService` from DI in your service or route handler
42
- 2. Define cache keys: `${tenantId}:${module}:${identifier}`
43
- 3. Tag entries: `{ tags: ['tenant:123', 'my_module'] }`
44
- 4. Add cache invalidation to CRUD side effects (`emitCrudSideEffects` with `cacheAliases`)
45
- 5. Test with `CACHE_STRATEGY=memory` (default in dev)