@open-mercato/cli 0.6.7 → 0.6.8-develop.6875.1.871a4afc94

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 (278) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/build.mjs +24 -7
  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 +121 -0
  14. package/dist/agentic/guides/framework-extension-points.md +36 -0
  15. package/dist/agentic/guides/integrations.md +81 -0
  16. package/dist/agentic/guides/module-facts.json +115537 -3951
  17. package/dist/agentic/guides/module-system.md +12 -127
  18. package/dist/agentic/guides/modules/ai_assistant.md +227 -28
  19. package/dist/agentic/guides/modules/api_docs.md +58 -2
  20. package/dist/agentic/guides/modules/api_keys.md +92 -7
  21. package/dist/agentic/guides/modules/attachments.md +119 -14
  22. package/dist/agentic/guides/modules/audit_logs.md +131 -10
  23. package/dist/agentic/guides/modules/auth.md +276 -33
  24. package/dist/agentic/guides/modules/business_rules.md +149 -14
  25. package/dist/agentic/guides/modules/catalog.md +475 -43
  26. package/dist/agentic/guides/modules/channel_gmail.md +82 -3
  27. package/dist/agentic/guides/modules/channel_imap.md +82 -3
  28. package/dist/agentic/guides/modules/checkout.md +299 -27
  29. package/dist/agentic/guides/modules/communication_channels.md +315 -33
  30. package/dist/agentic/guides/modules/configs.md +104 -8
  31. package/dist/agentic/guides/modules/content.md +57 -2
  32. package/dist/agentic/guides/modules/currencies.md +152 -17
  33. package/dist/agentic/guides/modules/customer_accounts.md +387 -45
  34. package/dist/agentic/guides/modules/customers.md +746 -85
  35. package/dist/agentic/guides/modules/dashboards.md +155 -10
  36. package/dist/agentic/guides/modules/data_sync.md +149 -16
  37. package/dist/agentic/guides/modules/design_system.md +102 -0
  38. package/dist/agentic/guides/modules/dictionaries.md +107 -12
  39. package/dist/agentic/guides/modules/directory.md +148 -15
  40. package/dist/agentic/guides/modules/entities.md +145 -13
  41. package/dist/agentic/guides/modules/events.md +59 -2
  42. package/dist/agentic/guides/modules/feature_toggles.md +119 -9
  43. package/dist/agentic/guides/modules/gateway_stripe.md +92 -4
  44. package/dist/agentic/guides/modules/generators.md +45 -2
  45. package/dist/agentic/guides/modules/inbox_ops.md +207 -27
  46. package/dist/agentic/guides/modules/integrations.md +146 -16
  47. package/dist/agentic/guides/modules/messages.md +223 -24
  48. package/dist/agentic/guides/modules/notifications.md +96 -8
  49. package/dist/agentic/guides/modules/onboarding.md +77 -6
  50. package/dist/agentic/guides/modules/payment_gateways.md +164 -21
  51. package/dist/agentic/guides/modules/perspectives.md +77 -7
  52. package/dist/agentic/guides/modules/planner.md +136 -18
  53. package/dist/agentic/guides/modules/portal.md +93 -2
  54. package/dist/agentic/guides/modules/progress.md +104 -14
  55. package/dist/agentic/guides/modules/query_index.md +154 -15
  56. package/dist/agentic/guides/modules/record_locks.md +206 -21
  57. package/dist/agentic/guides/modules/resources.md +197 -30
  58. package/dist/agentic/guides/modules/sales.md +678 -84
  59. package/dist/agentic/guides/modules/scheduler.md +133 -14
  60. package/dist/agentic/guides/modules/search.md +170 -6
  61. package/dist/agentic/guides/modules/security.md +294 -35
  62. package/dist/agentic/guides/modules/shipping_carriers.md +134 -18
  63. package/dist/agentic/guides/modules/sso.md +151 -26
  64. package/dist/agentic/guides/modules/staff.md +369 -57
  65. package/dist/agentic/guides/modules/storage_s3.md +95 -5
  66. package/dist/agentic/guides/modules/sync_akeneo.md +85 -3
  67. package/dist/agentic/guides/modules/sync_excel.md +75 -6
  68. package/dist/agentic/guides/modules/system_status_overlays.md +60 -2
  69. package/dist/agentic/guides/modules/translations.md +104 -10
  70. package/dist/agentic/guides/modules/webhooks.md +203 -24
  71. package/dist/agentic/guides/modules/wms.md +441 -46
  72. package/dist/agentic/guides/modules/workflows.md +302 -43
  73. package/dist/agentic/guides/spec-delivery.md +22 -0
  74. package/dist/agentic/guides/testing-debugging.md +80 -0
  75. package/dist/agentic/guides/upstream/AGENTS.md +299 -0
  76. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +316 -0
  77. package/dist/agentic/guides/upstream/manifest.json +8 -0
  78. package/dist/agentic/shared/AGENTS.md.template +124 -311
  79. package/dist/agentic/shared/ai/agentic.config.json +2 -0
  80. package/dist/agentic/shared/ai/harness/README.md +50 -0
  81. package/dist/agentic/shared/ai/harness/RELEASE.md +57 -0
  82. package/dist/agentic/shared/ai/harness/cases.json +13680 -0
  83. package/dist/agentic/shared/ai/harness/cases.schema.json +133 -0
  84. package/dist/agentic/shared/ai/harness/fixtures/index.json +49 -0
  85. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +162 -0
  86. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +21 -0
  87. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +96 -0
  88. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +172 -0
  89. package/dist/agentic/shared/ai/harness/release-matrix.json +158 -0
  90. package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
  91. package/dist/agentic/shared/ai/harness/result.schema.json +76 -0
  92. package/dist/agentic/shared/ai/harness/routing-response.schema.json +12 -0
  93. package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
  94. package/dist/agentic/shared/ai/harness/validators.json +78 -0
  95. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +2121 -0
  96. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
  97. package/dist/agentic/shared/ai/lessons/_template.md +16 -0
  98. package/dist/agentic/shared/ai/lessons.md +26 -3
  99. package/dist/agentic/shared/ai/review-checklist.md +36 -0
  100. package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +21 -0
  101. package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
  102. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +98 -0
  103. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +20 -241
  104. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +16 -0
  105. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
  106. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +13 -0
  107. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
  108. package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
  109. package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
  110. package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
  111. package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
  112. package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
  113. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
  114. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
  115. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
  116. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
  117. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +16 -601
  118. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +19 -0
  119. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +13 -0
  120. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +14 -0
  121. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
  122. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +14 -310
  123. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +17 -0
  124. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +29 -0
  125. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +100 -0
  126. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
  127. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
  128. package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
  129. package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
  130. package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
  131. package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
  132. package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +18 -0
  133. package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
  134. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +33 -152
  135. package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +33 -0
  136. package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +40 -0
  137. package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +27 -0
  138. package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +24 -0
  139. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +24 -708
  140. package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
  141. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +14 -0
  142. package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
  143. package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
  144. package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +34 -0
  145. package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +4 -0
  146. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +11 -0
  147. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +31 -0
  148. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +39 -0
  149. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +43 -0
  150. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +11 -0
  151. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
  152. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +19 -0
  153. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +172 -0
  154. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +12 -0
  155. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
  156. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +21 -0
  157. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
  158. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
  159. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
  160. package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +41 -0
  161. package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +19 -0
  162. package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +44 -0
  163. package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +39 -0
  164. package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +20 -0
  165. package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +57 -0
  166. package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +11 -0
  167. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +486 -0
  168. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +19 -843
  169. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +18 -0
  170. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +27 -0
  171. package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
  172. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +41 -0
  173. package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
  174. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
  175. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
  176. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +16 -467
  177. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +16 -0
  178. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
  179. package/dist/agentic/shared/ai/skills/tiers.json +143 -21
  180. package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
  181. package/dist/agentic/shared/ai/specs/README.md +4 -3
  182. package/dist/agentic/shared/ai/specs/SPEC-000-template.md +226 -10
  183. package/dist/agentic/shared/ai/trackers/github.md +108 -4
  184. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +187 -0
  185. package/dist/agentic/shared/scripts/check-lessons.mjs +178 -0
  186. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +2958 -0
  187. package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
  188. package/dist/agentic/shared/scripts/framework-context.mjs +888 -0
  189. package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
  190. package/dist/agentic/shared/scripts/install-skills.sh +4 -694
  191. package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
  192. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1774 -0
  193. package/dist/bin.js +13 -1
  194. package/dist/bin.js.map +2 -2
  195. package/dist/lib/__integration__/TC-INT-008.spec.js +88 -8
  196. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  197. package/dist/lib/agentic-init.js +13 -3
  198. package/dist/lib/agentic-init.js.map +2 -2
  199. package/dist/lib/agentic-setup.js +434 -186
  200. package/dist/lib/agentic-setup.js.map +2 -2
  201. package/dist/lib/generators/extensions/injection-widgets.js +20 -0
  202. package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
  203. package/dist/lib/generators/module-extension-facts.js +1840 -0
  204. package/dist/lib/generators/module-extension-facts.js.map +7 -0
  205. package/dist/lib/generators/module-fact-sources.js +70 -0
  206. package/dist/lib/generators/module-fact-sources.js.map +7 -0
  207. package/dist/lib/generators/module-facts-discovery.js +41 -7
  208. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  209. package/dist/lib/generators/module-facts.js +1387 -45
  210. package/dist/lib/generators/module-facts.js.map +3 -3
  211. package/dist/lib/generators/module-override-targets.js +688 -0
  212. package/dist/lib/generators/module-override-targets.js.map +7 -0
  213. package/dist/lib/generators/module-registry.js +11 -5
  214. package/dist/lib/generators/module-registry.js.map +2 -2
  215. package/dist/lib/load-env.js +24 -0
  216. package/dist/lib/load-env.js.map +7 -0
  217. package/dist/lib/resolver.js +2 -0
  218. package/dist/lib/resolver.js.map +2 -2
  219. package/dist/lib/telemetry-init.js +358 -0
  220. package/dist/lib/telemetry-init.js.map +7 -0
  221. package/dist/lib/testing/integration.js +5 -0
  222. package/dist/lib/testing/integration.js.map +2 -2
  223. package/dist/mercato.js +23 -1
  224. package/dist/mercato.js.map +2 -2
  225. package/package.json +13 -11
  226. package/src/bin.ts +33 -1
  227. package/src/lib/__integration__/TC-INT-008.spec.ts +107 -9
  228. package/src/lib/__tests__/agentic-init.test.ts +41 -5
  229. package/src/lib/__tests__/agentic-setup.ownership.test.ts +386 -0
  230. package/src/lib/__tests__/injection-widget-id-validation.test.ts +184 -0
  231. package/src/lib/__tests__/load-env.test.ts +65 -0
  232. package/src/lib/__tests__/telemetry-init.test.ts +298 -0
  233. package/src/lib/agentic-init.ts +15 -3
  234. package/src/lib/agentic-setup.ts +573 -222
  235. package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +92 -0
  236. package/src/lib/generators/__tests__/module-extension-activations.test.ts +762 -0
  237. package/src/lib/generators/__tests__/module-extension-facts.test.ts +404 -0
  238. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +88 -2
  239. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
  240. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
  241. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
  242. package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +561 -0
  243. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +119 -0
  244. package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +149 -0
  245. package/src/lib/generators/__tests__/module-override-targets.test.ts +304 -0
  246. package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
  247. package/src/lib/generators/extensions/injection-widgets.ts +51 -0
  248. package/src/lib/generators/module-extension-facts.ts +2365 -0
  249. package/src/lib/generators/module-fact-sources.ts +108 -0
  250. package/src/lib/generators/module-facts-discovery.ts +58 -7
  251. package/src/lib/generators/module-facts.ts +1851 -46
  252. package/src/lib/generators/module-override-targets.ts +863 -0
  253. package/src/lib/generators/module-registry.ts +16 -6
  254. package/src/lib/load-env.ts +30 -0
  255. package/src/lib/resolver.ts +3 -0
  256. package/src/lib/telemetry-init.ts +461 -0
  257. package/src/lib/testing/__tests__/integration.test.ts +4 -0
  258. package/src/lib/testing/integration.ts +5 -0
  259. package/src/mercato.ts +26 -1
  260. package/dist/agentic/guides/cache.md +0 -45
  261. package/dist/agentic/guides/core.md +0 -453
  262. package/dist/agentic/guides/events.md +0 -117
  263. package/dist/agentic/guides/queue.md +0 -53
  264. package/dist/agentic/guides/search.md +0 -115
  265. package/dist/agentic/guides/shared.md +0 -108
  266. package/dist/agentic/guides/ui.md +0 -347
  267. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
  268. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
  269. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
  270. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
  271. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
  272. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
  273. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
  274. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
  275. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
  276. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
  277. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
  278. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
@@ -0,0 +1,404 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import {
5
+ assertNoUnresolvedExtensionTargets,
6
+ correlateModuleExtensionFacts,
7
+ extractModuleExtensionFacts,
8
+ getFrameworkExtensionHosts,
9
+ renderFrameworkExtensionPointsMarkdown,
10
+ } from '../module-extension-facts'
11
+ import type { ModuleFactsJsonEntry } from '../module-facts'
12
+
13
+ function write(root: string, relativePath: string, source: string): void {
14
+ const filePath = path.join(root, relativePath)
15
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
16
+ fs.writeFileSync(filePath, source)
17
+ }
18
+
19
+ describe('module extension facts', () => {
20
+ let moduleRoot: string
21
+
22
+ beforeEach(() => {
23
+ moduleRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'module-extension-facts-'))
24
+ })
25
+
26
+ afterEach(() => {
27
+ fs.rmSync(moduleRoot, { recursive: true, force: true })
28
+ })
29
+
30
+ it('expands declared host families and preserves fact references and browser transports', () => {
31
+ write(moduleRoot, 'extension-points.ts', `
32
+ import { defineModuleExtensionPoints, dataTableExtensionHost, crudFormExtensionHost, injectionExtensionHost } from 'x'
33
+ export const extensionPoints = defineModuleExtensionPoints({
34
+ moduleId: 'alpha',
35
+ hosts: {
36
+ records: dataTableExtensionHost({ tableId: 'alpha.records.list', baseSpotId: 'data-table:alpha.records', source: 'Records.tsx' }),
37
+ editor: crudFormExtensionHost({ entityId: 'alpha.record', source: 'Edit.tsx' }),
38
+ detail: injectionExtensionHost({
39
+ family: 'detail', pattern: 'detail:alpha.record:{recordId}',
40
+ parameters: { recordId: { source: 'record.id', pattern: '^[a-z]+$' } },
41
+ supported: ['render-widget'], source: 'Detail.tsx', fallbacks: ['detail:alpha.record'],
42
+ }),
43
+ },
44
+ })
45
+ `)
46
+ write(moduleRoot, 'Records.tsx', 'export const tableId = extensionPoints.hosts.records.tableId')
47
+ write(moduleRoot, 'Edit.tsx', 'export const entityId = extensionPoints.hosts.editor.entityId')
48
+ write(moduleRoot, 'Detail.tsx', 'export const spotId = extensionPoints.hosts.detail.pattern')
49
+
50
+ const facts = extractModuleExtensionFacts({
51
+ moduleId: 'alpha',
52
+ moduleRoot,
53
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
54
+ entities: [{ id: 'alpha:record' }],
55
+ events: [{ id: 'alpha.record.updated', clientBroadcast: true, portalBroadcast: true }],
56
+ apiRoutes: [{ path: '/alpha/records', methods: ['GET', 'POST'] }],
57
+ searchEntities: ['alpha:record'],
58
+ })
59
+
60
+ expect(facts.hosts).toEqual(expect.arrayContaining([
61
+ expect.objectContaining({ id: 'data-table:alpha.records:header', bound: true }),
62
+ expect.objectContaining({ id: 'data-table:alpha.records.list:columns', capabilities: ['column-widget'] }),
63
+ expect.objectContaining({ id: 'data-table:alpha.records:empty-state', bound: false }),
64
+ expect.objectContaining({ id: 'crud-form:alpha.record:fields', capabilities: ['field-widget'] }),
65
+ expect.objectContaining({ id: 'detail:alpha.record:{recordId}', resolution: 'pattern' }),
66
+ expect.objectContaining({ id: 'alpha:record', resolution: 'fact-ref' }),
67
+ expect.objectContaining({
68
+ id: 'alpha.record.updated',
69
+ capabilities: ['async-subscriber', 'sync-subscriber', 'browser-client', 'browser-portal'],
70
+ }),
71
+ expect.objectContaining({ id: 'alpha.record.querying', activation: 'caller-opt-in' }),
72
+ expect.objectContaining({ id: 'alpha.record.queried', phases: ['result-transform', 'scope-reapply'] }),
73
+ ]))
74
+ expect(facts.unresolved).toEqual([])
75
+ const hostOrder = facts.hosts.map((host) => `${host.family}\u0000${host.id}`)
76
+ expect(hostOrder).toEqual([...hostOrder].sort((left, right) => left.localeCompare(right)))
77
+ })
78
+
79
+ it('extracts outgoing mechanisms with kind-specific contracts and correlates targets', () => {
80
+ write(moduleRoot, 'extension-points.ts', `
81
+ import { defineModuleExtensionPoints, dataTableExtensionHost } from 'x'
82
+ export const extensionPoints = defineModuleExtensionPoints({
83
+ moduleId: 'alpha', hosts: { records: dataTableExtensionHost({ tableId: 'alpha.records', source: 'Records.tsx' }) },
84
+ })
85
+ `)
86
+ write(moduleRoot, 'Records.tsx', 'export const tableId = extensionPoints.hosts.records.tableId')
87
+ write(moduleRoot, 'widgets/injection-table.ts', `
88
+ export const injectionTable = {
89
+ 'data-table:alpha.records:columns': [{ widgetId: 'alpha.columns', priority: 20, features: ['alpha.view'] }],
90
+ 'menu:backend.sidebar': [{ widgetId: 'alpha.menu' }],
91
+ }
92
+ `)
93
+ write(moduleRoot, 'data/enrichers.ts', `
94
+ const enricher = {
95
+ id: 'alpha.records.enricher', targetEntity: 'alpha:record', features: ['alpha.view'], timeout: 750,
96
+ fallback: {}, queryEngine: { enabled: true, engines: ['json'], applyOn: ['list'] },
97
+ async enrichOne() {}, async enrichMany() {},
98
+ }
99
+ export const enrichers = [enricher]
100
+ `)
101
+ write(moduleRoot, 'api/interceptors.ts', `
102
+ export const interceptors = [{
103
+ id: 'alpha.records.interceptor', targetRoute: 'alpha/records', methods: ['POST'],
104
+ async before() {}, async after() {},
105
+ }]
106
+ `)
107
+ write(moduleRoot, 'commands/interceptors.ts', `
108
+ export const interceptors = [{
109
+ id: 'alpha.records.command', targetCommand: 'alpha.records.update',
110
+ async beforeExecute() {}, async afterUndo() {},
111
+ }]
112
+ `)
113
+ write(moduleRoot, 'commands/update.ts', `
114
+ const updateCommand = { id: 'alpha.records.update', async execute() {} }
115
+ registerCommand(updateCommand)
116
+ `)
117
+ write(moduleRoot, 'data/guards.ts', `
118
+ const guard = { id: 'alpha.records.guard', targetEntity: 'alpha:record', operations: ['update', 'delete'], async validate() {}, async afterSuccess() {} }
119
+ export const guards = [guard]
120
+ `)
121
+ write(moduleRoot, 'data/extensions.ts', `
122
+ export const extensions = [{ base: 'alpha:record', extension: 'beta:record_meta', join: { baseKey: 'id', extensionKey: 'record_id' } }]
123
+ `)
124
+ write(moduleRoot, 'subscribers/updated.ts', `
125
+ export const metadata = { event: 'alpha.record.updated', id: 'alpha:record-updated', persistent: true, priority: 10 }
126
+ `)
127
+
128
+ const surface = extractModuleExtensionFacts({
129
+ moduleId: 'alpha',
130
+ moduleRoot,
131
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
132
+ entities: [{ id: 'alpha:record' }],
133
+ events: [{ id: 'alpha.record.updated', clientBroadcast: true }],
134
+ apiRoutes: [{ path: '/alpha/records', methods: ['POST'] }],
135
+ searchEntities: [],
136
+ })
137
+ const correlated = correlateModuleExtensionFacts({
138
+ surfacesByModule: { alpha: surface },
139
+ entityIds: new Set(['alpha:record']),
140
+ eventIds: new Set(['alpha.record.updated']),
141
+ apiRoutes: new Set(['/alpha/records']),
142
+ commandIds: new Set(['alpha.records.update']),
143
+ }).alpha
144
+
145
+ expect(correlated.contributions).toEqual(expect.arrayContaining([
146
+ expect.objectContaining({
147
+ id: 'alpha.records.enricher',
148
+ kind: 'response-enricher',
149
+ details: expect.objectContaining({
150
+ targetEntity: 'alpha:record', surfaces: ['list', 'detail'], timeoutMs: 750,
151
+ queryEngine: { engines: ['json'], applyOn: ['list'], activation: 'caller-opt-in' },
152
+ }),
153
+ }),
154
+ expect.objectContaining({
155
+ id: 'alpha.records.interceptor', kind: 'api-interceptor',
156
+ targets: [expect.objectContaining({
157
+ resolution: 'fact-ref',
158
+ factRef: { factSection: 'apiRoutes', factKey: '/alpha/records' },
159
+ })],
160
+ details: expect.objectContaining({ phases: ['before', 'after'] }),
161
+ }),
162
+ expect.objectContaining({
163
+ id: 'alpha.records.command',
164
+ targets: [expect.objectContaining({
165
+ factRef: { factSection: 'ownedContracts.command', factKey: 'alpha.records.update' },
166
+ })],
167
+ details: expect.objectContaining({ phases: ['before-execute', 'after-undo'] }),
168
+ }),
169
+ expect.objectContaining({ id: 'alpha.records.guard', details: expect.objectContaining({ optimisticLock: 'preserved' }) }),
170
+ expect.objectContaining({ kind: 'entity-extension', details: expect.objectContaining({ linkId: 'id:record_id' }) }),
171
+ expect.objectContaining({ id: 'alpha:record-updated', details: expect.objectContaining({ persistent: true, sync: false, priority: 10 }) }),
172
+ expect.objectContaining({ id: 'alpha.columns@data-table:alpha.records:columns', targets: [expect.objectContaining({ resolution: 'exact' })] }),
173
+ expect.objectContaining({ id: 'alpha.menu@menu:backend.sidebar', targets: [expect.objectContaining({ resolution: 'framework' })] }),
174
+ expect.objectContaining({ id: 'alpha.record.updated.browser', kind: 'browser-reaction' }),
175
+ ]))
176
+ expect(correlated.unresolved).toEqual([])
177
+ const contributionOrder = correlated.contributions.map((contribution) => `${contribution.kind}\u0000${contribution.id}`)
178
+ expect(contributionOrder).toEqual([...contributionOrder].sort((left, right) => left.localeCompare(right)))
179
+ })
180
+
181
+ it('renders a deterministic separate framework catalog', () => {
182
+ type ExtensionSurfacesAreOptional = {} extends Pick<ModuleFactsJsonEntry, 'extensionSurfaces'> ? true : false
183
+ const extensionSurfacesAreOptional: ExtensionSurfacesAreOptional = true
184
+ const first = renderFrameworkExtensionPointsMarkdown()
185
+ const second = renderFrameworkExtensionPointsMarkdown()
186
+ expect(second).toBe(first)
187
+ expect(first).toContain('# Framework extension points (generated, do not edit)')
188
+ expect(first).toContain('integrations.detail:{integrationId}')
189
+ expect(first).toContain('menu:backend.sidebar')
190
+ expect(extensionSurfacesAreOptional).toBe(true)
191
+ })
192
+
193
+ it('projects every wired unified override key with its supported mode', () => {
194
+ const overrides = getFrameworkExtensionHosts()
195
+ .filter((host) => host.family === 'module-override')
196
+ .map((host) => [host.id, host.operations?.[0]])
197
+
198
+ expect(overrides).toEqual([
199
+ ['module-override:acl.features', 'disable-replace'],
200
+ ['module-override:ai.agents', 'disable-replace'],
201
+ ['module-override:ai.extensions', 'additive'],
202
+ ['module-override:ai.tools', 'disable-replace'],
203
+ ['module-override:cli', 'disable-replace'],
204
+ ['module-override:commandInterceptors', 'disable-replace'],
205
+ ['module-override:di', 'disable-replace'],
206
+ ['module-override:encryption.maps', 'disable-replace'],
207
+ ['module-override:enrichers', 'disable-replace'],
208
+ ['module-override:events.subscribers', 'disable-replace'],
209
+ ['module-override:guards', 'disable-replace'],
210
+ ['module-override:interceptors', 'disable-replace'],
211
+ ['module-override:nav.groupOrder', 'additive'],
212
+ ['module-override:notifications.handlers', 'disable-replace'],
213
+ ['module-override:notifications.types', 'disable-replace'],
214
+ ['module-override:routes.api', 'disable-replace'],
215
+ ['module-override:routes.pages', 'disable-replace'],
216
+ ['module-override:setup', 'replace'],
217
+ ['module-override:widgets.components', 'disable-replace'],
218
+ ['module-override:widgets.dashboard', 'disable-replace'],
219
+ ['module-override:widgets.injection', 'disable-replace'],
220
+ ['module-override:workers', 'disable-replace'],
221
+ ])
222
+ })
223
+
224
+ it('projects dashboard, notification, integration, vector, provider, and workflow registries', () => {
225
+ write(moduleRoot, 'notifications.handlers.ts', `
226
+ export const notificationHandlers = [{
227
+ id: 'alpha.alert-toast', notificationType: 'alpha.alert', features: ['alpha.view'], priority: 80,
228
+ handle() {},
229
+ }]
230
+ `)
231
+ write(moduleRoot, 'integration.ts', `
232
+ export const integration = { id: 'alpha_gateway', category: 'payment', providerKey: 'alpha-pay' }
233
+ export const integrations = [integration]
234
+ `)
235
+ write(moduleRoot, 'vector.ts', `
236
+ export const vectorConfig = { entities: [{ entityId: 'alpha:record' }] }
237
+ export default vectorConfig
238
+ `)
239
+ write(moduleRoot, 'workflows.ts', `
240
+ const approval = defineWorkflow({ workflowId: 'alpha.approval', steps: [], transitions: [] })
241
+ export const workflowsConfig = createWorkflowsModuleConfig({ moduleId: 'alpha', workflows: [approval] })
242
+ `)
243
+ write(moduleRoot, 'di.ts', `
244
+ export function register() {
245
+ registerPaymentGatewayDescriptor({ providerKey: 'alpha-pay', label: 'Alpha Pay' })
246
+ registerShippingAdapter({ providerKey: 'alpha-ship' })
247
+ registerCurrencyProvider({ id: 'alpha-rates' })
248
+ }
249
+ `)
250
+ write(moduleRoot, 'widgets/dashboard/summary/widget.ts', `
251
+ const widget = { metadata: { id: 'alpha.dashboard.summary', features: ['alpha.view'] }, Widget() {} }
252
+ export default widget
253
+ `)
254
+
255
+ const facts = extractModuleExtensionFacts({
256
+ moduleId: 'alpha',
257
+ moduleRoot,
258
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
259
+ entities: [],
260
+ events: [],
261
+ apiRoutes: [],
262
+ searchEntities: ['alpha:record'],
263
+ notifications: ['alpha.alert'],
264
+ aiTools: [{ name: 'alpha_lookup', sourcePath: 'node_modules/pkg/src/modules/alpha/ai-tools.ts' }],
265
+ aiAgents: [{ id: 'alpha_agent', sourcePath: 'node_modules/pkg/src/modules/alpha/ai-agents.ts' }],
266
+ })
267
+
268
+ expect(facts.contributions).toEqual(expect.arrayContaining([
269
+ expect.objectContaining({
270
+ id: 'alpha.alert-toast',
271
+ kind: 'browser-reaction',
272
+ targets: [expect.objectContaining({ id: 'alpha.alert', resolution: 'fact-ref' })],
273
+ details: expect.objectContaining({ transports: ['notification-effect'], hooks: ['useNotificationEffect'] }),
274
+ }),
275
+ expect.objectContaining({ id: 'dashboard:alpha.dashboard.summary', kind: 'widget', details: expect.objectContaining({ payload: 'dashboard' }) }),
276
+ expect.objectContaining({ id: 'integration:alpha_gateway', details: expect.objectContaining({ specialistRoute: 'integrations' }) }),
277
+ expect.objectContaining({ id: 'notification:alpha.alert', details: expect.objectContaining({ registry: 'notification' }) }),
278
+ expect.objectContaining({ id: 'search:alpha:record', details: expect.objectContaining({ registry: 'search' }) }),
279
+ expect.objectContaining({ id: 'vector:alpha:record', details: expect.objectContaining({ registry: 'vector' }) }),
280
+ expect.objectContaining({ id: 'ai:alpha_lookup', details: expect.objectContaining({ specialistRoute: 'aiTools' }) }),
281
+ expect.objectContaining({ id: 'ai:alpha_agent', details: expect.objectContaining({ specialistRoute: 'aiAgents' }) }),
282
+ expect.objectContaining({ id: 'payment:alpha-pay', source: expect.objectContaining({ symbol: 'registerPaymentGatewayDescriptor' }) }),
283
+ expect.objectContaining({ id: 'shipping:alpha-ship', details: expect.objectContaining({ specialistRoute: 'shippingCarriers' }) }),
284
+ expect.objectContaining({ id: 'currency:alpha-rates', details: expect.objectContaining({ specialistRoute: 'currencies' }) }),
285
+ expect.objectContaining({ id: 'workflow:alpha.approval', details: expect.objectContaining({ specialistRoute: 'workflows' }) }),
286
+ ]))
287
+ expect(facts.contributions.filter((fact) => fact.id === 'payment:alpha-pay')).toHaveLength(1)
288
+ })
289
+
290
+ it('resolves factory-built contributions and generated entity registry references', () => {
291
+ write(moduleRoot, 'data/enrichers.ts', `
292
+ function makeEnricher(targetEntity) {
293
+ return { id: \`alpha.image:\${targetEntity}\`, targetEntity, async enrichMany(records) { return records } }
294
+ }
295
+ const registryEnricher = { id: 'alpha.registry', targetEntity: E.alpha.record, async enrichOne(record) { return record } }
296
+ export const enrichers = [makeEnricher('alpha:line'), registryEnricher]
297
+ `)
298
+
299
+ const facts = extractModuleExtensionFacts({
300
+ moduleId: 'alpha',
301
+ moduleRoot,
302
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
303
+ entities: [],
304
+ events: [],
305
+ apiRoutes: [],
306
+ searchEntities: [],
307
+ })
308
+
309
+ expect(facts.contributions).toEqual(expect.arrayContaining([
310
+ expect.objectContaining({ id: 'alpha.image:alpha:line', targets: [expect.objectContaining({ id: 'alpha:line' })] }),
311
+ expect.objectContaining({ id: 'alpha.registry', targets: [expect.objectContaining({ id: 'alpha:record' })] }),
312
+ ]))
313
+ })
314
+
315
+ it('reports declarations whose authoritative source no longer binds the host key', () => {
316
+ write(moduleRoot, 'extension-points.ts', `
317
+ export const extensionPoints = defineModuleExtensionPoints({
318
+ moduleId: 'alpha',
319
+ hosts: { records: dataTableExtensionHost({ tableId: 'alpha.records', source: 'Records.tsx' }) },
320
+ })
321
+ `)
322
+ write(moduleRoot, 'Records.tsx', "export const tableId = 'alpha.records'")
323
+
324
+ const facts = extractModuleExtensionFacts({
325
+ moduleId: 'alpha',
326
+ moduleRoot,
327
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
328
+ entities: [],
329
+ events: [],
330
+ apiRoutes: [],
331
+ searchEntities: [],
332
+ })
333
+
334
+ expect(facts.unresolved).toEqual([
335
+ expect.objectContaining({ key: 'alpha.records', reason: 'unbound-declaration' }),
336
+ ])
337
+ expect(facts.hosts.filter((host) => host.key.startsWith('records.')).every((host) => !host.bound)).toBe(true)
338
+ })
339
+
340
+ it('fails closed when a correlated first-party target remains unresolved', () => {
341
+ expect(() => assertNoUnresolvedExtensionTargets({
342
+ alpha: {
343
+ hosts: [],
344
+ contributions: [],
345
+ unresolved: [{
346
+ key: 'alpha.widget:data-table:alpha.missing',
347
+ source: { path: 'widgets/injection-table.ts' },
348
+ reason: 'unresolved-first-party-target',
349
+ }],
350
+ },
351
+ })).toThrow('alpha:alpha.widget:data-table:alpha.missing')
352
+ })
353
+
354
+ it('does not trust a claimed fact reference before matching a known first-party host', () => {
355
+ write(moduleRoot, 'data/enrichers.ts', `
356
+ export const enrichers = [{
357
+ id: 'alpha.typo', targetEntity: 'alpha.missing', async enrichOne(record) { return record },
358
+ }]
359
+ `)
360
+ const surface = extractModuleExtensionFacts({
361
+ moduleId: 'alpha',
362
+ moduleRoot,
363
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
364
+ entities: [{ id: 'alpha:record' }],
365
+ events: [],
366
+ apiRoutes: [],
367
+ searchEntities: [],
368
+ })
369
+ const correlated = correlateModuleExtensionFacts({
370
+ surfacesByModule: { alpha: surface },
371
+ entityIds: new Set(['alpha:record']),
372
+ eventIds: new Set(),
373
+ apiRoutes: new Set(),
374
+ commandIds: new Set(),
375
+ })
376
+
377
+ expect(() => assertNoUnresolvedExtensionTargets(correlated)).toThrow('alpha.typo:alpha.missing')
378
+ })
379
+
380
+ it('rejects wildcard targets that match no known first-party contract', () => {
381
+ const correlated = correlateModuleExtensionFacts({
382
+ surfacesByModule: {
383
+ alpha: {
384
+ hosts: [],
385
+ contributions: [{
386
+ id: 'alpha.typo-pattern',
387
+ kind: 'command-interceptor',
388
+ targets: [{ id: 'alpha.typo.*', resolution: 'unresolved' }],
389
+ scopeContract: 'tenant-and-organization',
390
+ source: { path: 'commands/interceptors.ts' },
391
+ details: { targetCommand: 'alpha.typo.*', phases: ['before-execute'] },
392
+ }],
393
+ unresolved: [],
394
+ },
395
+ },
396
+ entityIds: new Set(),
397
+ eventIds: new Set(),
398
+ apiRoutes: new Set(),
399
+ commandIds: new Set(['alpha.records.update']),
400
+ })
401
+
402
+ expect(() => assertNoUnresolvedExtensionTargets(correlated)).toThrow('alpha.typo-pattern:alpha.typo.*')
403
+ })
404
+ })
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { extractAllModuleFacts } from '../module-facts'
3
+ import { extractAllModuleFacts, renderModuleFactsJson } from '../module-facts'
4
4
  import { discoverPackageModuleSources } from '../module-facts-discovery'
5
5
  import { createResolver } from '../../resolver'
6
6
 
@@ -20,7 +20,77 @@ function isUnique(values: string[]): boolean {
20
20
  describe('module-facts BC resolve guard (T2)', () => {
21
21
  const repoRoot = findRepoRoot()
22
22
  const sources = discoverPackageModuleSources(createResolver(repoRoot))
23
- const { factsByModule } = extractAllModuleFacts({ sources })
23
+ const extractionStartedAt = process.cpuUsage()
24
+ const { factsByModule, markdownByModule, frameworkMarkdown } = extractAllModuleFacts({ sources })
25
+ const extractionCpuUsage = process.cpuUsage(extractionStartedAt)
26
+ const extractionCpuDurationMs = (extractionCpuUsage.user + extractionCpuUsage.system) / 1_000
27
+
28
+ it('emits complete, deterministic extension catalogs for every resolved module', () => {
29
+ const repeated = extractAllModuleFacts({ sources })
30
+ expect(renderModuleFactsJson(repeated.factsByModule)).toBe(renderModuleFactsJson(factsByModule))
31
+ expect(repeated.markdownByModule).toEqual(markdownByModule)
32
+ expect(repeated.frameworkMarkdown).toBe(frameworkMarkdown)
33
+ for (const facts of Object.values(factsByModule)) {
34
+ expect(facts.extensionSurfaces).toBeDefined()
35
+ expect(facts.extensionSurfaces?.unresolved).toEqual([])
36
+ }
37
+ })
38
+
39
+ it('keeps generated extension facts within bounded build-time and context budgets', () => {
40
+ const completeJson = renderModuleFactsJson(factsByModule)
41
+ const legacyJson = renderModuleFactsJson(Object.fromEntries(
42
+ Object.entries(factsByModule).map(([moduleId, facts]) => [moduleId, { ...facts, extensionSurfaces: undefined }]),
43
+ ))
44
+ const markdownBytes = Object.values(markdownByModule)
45
+ .reduce((total, markdown) => total + Buffer.byteLength(markdown), Buffer.byteLength(frameworkMarkdown))
46
+
47
+ // Budget raised by the bidirectional-topology spec
48
+ // (2026-08-02-module-facts-extension-activation-and-incoming-index): the
49
+ // additive `activations`, cross-module `incoming`, and per-contribution
50
+ // `contributionResolutions` layers add ~210KB of compact references (no
51
+ // contribution payloads are duplicated). Incoming rows are cross-module only;
52
+ // resolution rows are required one-per-contribution by the spec's acceptance
53
+ // criteria and are the dominant term.
54
+ //
55
+ // JSON cap raised again by the exact-override-targets spec
56
+ // (2026-08-02-module-facts-exact-override-targets): the additive per-module
57
+ // `overrideTargets` project one exact key per real override entry (acl
58
+ // features, di tokens, subscribers, pages, workers, encryption, widgets,
59
+ // notifications, cli, setup, ai, interceptors/enrichers, page guards). These
60
+ // are required exhaustively by the spec's acceptance criteria; targets carry
61
+ // only compact structured path/key/factRef/source refs (no runtime values or
62
+ // contribution payloads). The delta cap is unchanged because `overrideTargets`
63
+ // live in both the complete and legacy renders (only `extensionSurfaces` is
64
+ // stripped for the legacy comparison).
65
+ //
66
+ // JSON cap raised a third time by the uniform provenance index
67
+ // (2026-08-02-module-facts-source-provenance-and-contract-inventory): every
68
+ // proven `(kind, id)` now reaches `factSources` (~630KB across the repo), so a
69
+ // consumer resolves any fact's origin through one lookup. Entries whose
70
+ // declaration site is already serialized inline (routes, pages, CLI commands,
71
+ // AI tools/agents, owned contracts, hosts, contributions) emit a typed
72
+ // `factRef` pointer instead of a duplicated source ref, and `factKey` is
73
+ // omitted when it equals the entry `id` — so the index costs references, never
74
+ // copied provenance payloads. The cap also covers the newly reachable
75
+ // framework-host activations (dashboard/menu/notification contributions now
76
+ // resolve as bound instead of silently falling back to capability-only).
77
+ // This is a blow-up detector, not a performance target. It measures CPU time
78
+ // for a whole-repo extraction, and CPU time for fixed work varies with the
79
+ // machine: the same extraction measures ~7.3s on a developer workstation and
80
+ // ~30.0s on a CI runner. At the previous 30s cap CI sat exactly on the line
81
+ // (an observed failure at 30,052.8ms), so the guard could not tell a genuine
82
+ // pathological regression from ordinary hardware variance and failed
83
+ // unrelated PRs at random. 90s keeps it meaningful — a real blow-up here is
84
+ // multiplicative, not a few percent — while leaving CI roughly 3x headroom.
85
+ expect(extractionCpuDurationMs).toBeLessThan(90_000)
86
+ expect(Buffer.byteLength(completeJson)).toBeLessThan(3_500_000)
87
+ expect(Buffer.byteLength(completeJson) - Buffer.byteLength(legacyJson)).toBeLessThan(1_800_000)
88
+ // Markdown cap raised with the source-link contract: entities, events, ACL
89
+ // features, DI tokens, search entities, notifications, UMES hosts and UMES
90
+ // contributions all render a resolved Source cell, and contribution
91
+ // resolutions render as their own source-linked section.
92
+ expect(markdownBytes).toBeLessThan(1_550_000)
93
+ })
24
94
 
25
95
  it('discovers a superset of the historical core modules', () => {
26
96
  const discovered = new Set(Object.keys(factsByModule))
@@ -30,11 +100,27 @@ describe('module-facts BC resolve guard (T2)', () => {
30
100
  expect(discovered.size).toBeGreaterThan(9)
31
101
  })
32
102
 
103
+ it('keeps factory-built and generated-registry enricher contributions visible', () => {
104
+ expect(factsByModule.sales.extensionSurfaces?.contributions).toEqual(expect.arrayContaining([
105
+ expect.objectContaining({ id: 'sales.catalog-image:sales:sales_quote_line', kind: 'response-enricher' }),
106
+ expect.objectContaining({ id: 'sales.catalog-image:sales:sales_order_line', kind: 'response-enricher' }),
107
+ ]))
108
+ expect(factsByModule.wms.extensionSurfaces?.contributions).toEqual(expect.arrayContaining([
109
+ expect.objectContaining({ id: 'wms.sales-order-inventory', kind: 'response-enricher' }),
110
+ ]))
111
+ })
112
+
33
113
  for (const source of sources) {
34
114
  const moduleId = source.moduleId
35
115
  describe(`${moduleId}`, () => {
36
116
  const facts = factsByModule[moduleId]
37
117
 
118
+ it('stamps the exact providing package and version without removing coreVersion', () => {
119
+ expect(facts.sourcePackage).toBe(source.from ?? null)
120
+ expect(facts.sourceVersion).toBe(source.packageVersion ?? null)
121
+ expect(facts).toHaveProperty('coreVersion')
122
+ })
123
+
38
124
  // Entity / search / host ids are colon-namespaced under the module by construction
39
125
  // and convention; drift here means the builder or a module's data model broke.
40
126
  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
  })