@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
@@ -1,7 +1,33 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import ts from 'typescript-js'
4
+ import type {
5
+ ModuleExtensionContributionFact,
6
+ ModuleExtensionHostFact,
7
+ ModuleExtensionSurfaceFacts,
8
+ ModuleExtensionTargetRef,
9
+ } from '@open-mercato/shared/modules/widgets/extension-points'
4
10
  import { toSnake } from '../utils'
11
+ import { extractCommandIdsFromSource } from './module-registry'
12
+ import { MODULE_CODE_EXTENSIONS } from './scanner'
13
+ import type { ApiRouteInterceptorBridge } from './module-extension-facts'
14
+ import {
15
+ assertNoUnresolvedExtensionTargets,
16
+ correlateIncomingExtensions,
17
+ correlateModuleExtensionFacts,
18
+ extractActivationTargetOwners,
19
+ extractKnownApiRouteIds,
20
+ extractKnownCommandIds,
21
+ extractModuleExtensionFacts,
22
+ renderFrameworkExtensionPointsMarkdown,
23
+ withModuleExtensionFactExtractionCache,
24
+ } from './module-extension-facts'
25
+ import {
26
+ collectModuleOverrideTargets,
27
+ type ModuleOverrideTarget,
28
+ type ModuleOverrideTargetDiagnostic,
29
+ } from './module-override-targets'
30
+ import { buildFactSourceLookup, type FactSourceLookup } from './module-fact-sources'
5
31
 
6
32
  export interface ModuleEntityFact {
7
33
  id: string
@@ -21,6 +47,49 @@ export interface ModuleApiRouteFact {
21
47
  path: string
22
48
  methods: string[]
23
49
  auth: Record<string, ApiRouteAuthRule>
50
+ sourcePath: string | null
51
+ }
52
+
53
+ /**
54
+ * Statically declared page metadata that affects extensibility or access, using the
55
+ * runtime `PageMetadata` contract keys and the same alias precedence as
56
+ * `resolvePageRouteMetadata` (`pageGroup ?? group`, `pageGroupKey ?? groupKey`,
57
+ * `pageOrder ?? order`). Executable guards, icons, titles and translation results are
58
+ * never serialized.
59
+ */
60
+ export interface ModulePageMetadata {
61
+ requireAuth?: boolean
62
+ requireCustomerAuth?: boolean
63
+ requireFeatures?: string[]
64
+ requireCustomerFeatures?: string[]
65
+ navHidden?: boolean
66
+ navGroup?: string
67
+ navGroupKey?: string
68
+ navOrder?: number
69
+ pageContext?: 'main' | 'admin' | 'settings' | 'profile'
70
+ }
71
+
72
+ export interface ModulePageFact {
73
+ path: string
74
+ sourcePath: string
75
+ /** Companion metadata file (`page.meta.*` / `meta.*`) when runtime reads metadata from one. */
76
+ metadataSourcePath?: string
77
+ metadata?: ModulePageMetadata
78
+ }
79
+
80
+ export interface ModuleCliCommandFact {
81
+ command: string
82
+ sourcePath: string
83
+ }
84
+
85
+ export interface ModuleAiToolFact {
86
+ name: string
87
+ sourcePath: string
88
+ }
89
+
90
+ export interface ModuleAiAgentFact {
91
+ id: string
92
+ sourcePath: string
24
93
  }
25
94
 
26
95
  export interface ModuleEventFact {
@@ -28,6 +97,8 @@ export interface ModuleEventFact {
28
97
  label?: string
29
98
  category: string | null
30
99
  entity: string | null
100
+ clientBroadcast?: boolean
101
+ portalBroadcast?: boolean
31
102
  }
32
103
 
33
104
  export interface ModuleHostTokens {
@@ -35,11 +106,116 @@ export interface ModuleHostTokens {
35
106
  tableIds: string[]
36
107
  }
37
108
 
109
+ export type ModuleFactSourceRef = {
110
+ sourcePath: string
111
+ exportName?: string
112
+ line?: number
113
+ }
114
+
115
+ export type ModuleFactRef = {
116
+ factSection: string
117
+ factKey: string
118
+ }
119
+
120
+ export type ModuleFactSourceKind =
121
+ | 'module-metadata'
122
+ | 'entity'
123
+ | 'event'
124
+ | 'acl-feature'
125
+ | 'api-route'
126
+ | 'di-registration'
127
+ | 'search'
128
+ | 'vector'
129
+ | 'notification'
130
+ | 'cli-command'
131
+ | 'backend-page'
132
+ | 'frontend-page'
133
+ | 'ai-tool'
134
+ | 'ai-agent'
135
+ | 'ai-extension'
136
+ | 'command'
137
+ | 'subscriber'
138
+ | 'worker'
139
+ | 'page-middleware'
140
+ | 'setup'
141
+ | 'encryption'
142
+ | 'custom-entity'
143
+ | 'integration'
144
+ | 'generator-plugin'
145
+ | 'extension-host'
146
+ | 'extension-contribution'
147
+
148
+ export type ModuleOwnedContractKind =
149
+ | 'module-metadata'
150
+ | 'command'
151
+ | 'worker'
152
+ | 'page-middleware'
153
+ | 'setup'
154
+ | 'encryption'
155
+ | 'di-registration'
156
+ | 'custom-entity'
157
+ | 'ai-extension'
158
+ | 'generator-plugin'
159
+
160
+ export type ModuleFactSafeScalar = string | number | boolean | null
161
+
162
+ export type ModuleOwnedContractFact = {
163
+ kind: ModuleOwnedContractKind
164
+ id: string
165
+ source: ModuleFactSourceRef
166
+ metadata?: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]>
167
+ }
168
+
169
+ export type ModuleOwnedContracts = Partial<
170
+ Record<ModuleOwnedContractKind, ModuleOwnedContractFact[]>
171
+ >
172
+
173
+ /**
174
+ * Provenance pointer into a fact section that already serializes the declaration site.
175
+ * `factKey` is omitted when it equals the owning entry's `id`, which is the common case.
176
+ */
177
+ export type ModuleFactIndexRef = {
178
+ factSection: string
179
+ factKey?: string
180
+ }
181
+
182
+ /**
183
+ * One provenance entry per proven `(kind, id)` in the module. Exactly one of `source`
184
+ * (the declaration site, for facts that have no other home in the JSON) or `factRef`
185
+ * (a deterministic pointer into a fact section that already carries the source inline)
186
+ * is present, so a consumer resolves any fact's origin through this single index.
187
+ */
188
+ export type ModuleFactSourceEntry = {
189
+ kind: ModuleFactSourceKind
190
+ id: string
191
+ source?: ModuleFactSourceRef
192
+ factRef?: ModuleFactIndexRef
193
+ }
194
+
195
+ export type ModuleFactDiagnostic = {
196
+ code: 'duplicate-source' | 'unresolved-static-contract'
197
+ kind: ModuleFactSourceKind
198
+ id: string
199
+ source?: ModuleFactSourceRef
200
+ }
201
+
202
+ export type ModuleDiRegistrationFact = {
203
+ token: string
204
+ registrationKind: 'function' | 'class' | 'value' | 'alias'
205
+ providerSymbol?: string
206
+ lifetime?: 'singleton' | 'scoped' | 'transient'
207
+ injectionMode?: 'classic' | 'proxy'
208
+ source: ModuleFactSourceRef
209
+ }
210
+
38
211
  export interface ModuleFacts {
39
212
  module: string
40
213
  title: string | null
41
214
  description: string | null
42
215
  coreVersion: string | null
216
+ sourcePackage: string | null
217
+ sourceVersion: string | null
218
+ sourceRoot: string
43
219
  entities: ModuleEntityFact[]
44
220
  events: ModuleEventFact[]
45
221
  aclFeatures: string[]
@@ -49,6 +225,17 @@ export interface ModuleFacts {
49
225
  hostTokens: ModuleHostTokens
50
226
  notifications: string[]
51
227
  cli: string[]
228
+ backendPages: ModulePageFact[]
229
+ frontendPages: ModulePageFact[]
230
+ cliCommands: ModuleCliCommandFact[]
231
+ aiTools: ModuleAiToolFact[]
232
+ aiAgents: ModuleAiAgentFact[]
233
+ extensionSurfaces?: ModuleExtensionSurfaceFacts
234
+ factSources?: ModuleFactSourceEntry[]
235
+ ownedContracts?: ModuleOwnedContracts
236
+ factDiagnostics?: ModuleFactDiagnostic[]
237
+ overrideTargets?: ModuleOverrideTarget[]
238
+ overrideTargetDiagnostics?: ModuleOverrideTargetDiagnostic[]
52
239
  warnings: string[]
53
240
  }
54
241
 
@@ -64,6 +251,8 @@ export interface ExtractModuleFactsOptions {
64
251
  /** Explicit module source directory. When set it overrides `coreSrcRoot + moduleId`. */
65
252
  moduleRoot?: string
66
253
  coreVersion?: string | null
254
+ sourcePackage?: string | null
255
+ sourceVersion?: string | null
67
256
  registryPath?: string | null
68
257
  registrySource?: string | null
69
258
  }
@@ -73,6 +262,7 @@ export interface ModuleFactSource {
73
262
  moduleId: string
74
263
  moduleRoot: string
75
264
  from?: string
265
+ packageVersion?: string | null
76
266
  }
77
267
 
78
268
  function readSourceFile(filePath: string): ts.SourceFile | null {
@@ -159,10 +349,41 @@ function collectCustomFieldEntityIds(ceFilePath: string | null): Set<string> {
159
349
  const sourceFile = readSourceFile(ceFilePath)
160
350
  if (!sourceFile) return result
161
351
 
352
+ const readEntityId = (expression: ts.Expression): string | undefined => {
353
+ const current = unwrapExpression(expression)
354
+ if (ts.isStringLiteralLike(current)) {
355
+ return current.text.includes(':') ? current.text : undefined
356
+ }
357
+
358
+ const readAccessPath = (candidate: ts.Expression): string[] | null => {
359
+ const access = unwrapExpression(candidate)
360
+ if (ts.isIdentifier(access)) return [access.text]
361
+ if (ts.isPropertyAccessExpression(access)) {
362
+ const parent = readAccessPath(access.expression)
363
+ return parent ? [...parent, access.name.text] : null
364
+ }
365
+ if (ts.isElementAccessExpression(access) && access.argumentExpression) {
366
+ const parent = readAccessPath(access.expression)
367
+ const key = unwrapExpression(access.argumentExpression)
368
+ return parent && ts.isStringLiteralLike(key) ? [...parent, key.text] : null
369
+ }
370
+ return null
371
+ }
372
+
373
+ const accessPath = readAccessPath(current)
374
+ if (accessPath?.length === 3 && accessPath[0] === 'E') {
375
+ return `${accessPath[1]}:${accessPath[2]}`
376
+ }
377
+ return undefined
378
+ }
379
+
162
380
  const visit = (node: ts.Node): void => {
163
381
  if (ts.isObjectLiteralExpression(node)) {
164
- const id = readStringPropertyInitializer(node, 'id')
165
- if (id && id.includes(':')) result.add(id)
382
+ const initializer = getObjectPropertyInitializer(node, 'id')
383
+ if (initializer) {
384
+ const id = readEntityId(initializer)
385
+ if (id) result.add(id)
386
+ }
166
387
  }
167
388
  node.forEachChild(visit)
168
389
  }
@@ -351,7 +572,11 @@ function extractEvents(eventsFilePath: string | null): ModuleEventFact[] {
351
572
  category: category ?? null,
352
573
  entity: entity ?? null,
353
574
  }
575
+ const clientBroadcast = readBooleanPropertyInitializer(element, 'clientBroadcast')
576
+ const portalBroadcast = readBooleanPropertyInitializer(element, 'portalBroadcast')
354
577
  if (label !== undefined) fact.label = label
578
+ if (clientBroadcast !== undefined) fact.clientBroadcast = clientBroadcast
579
+ if (portalBroadcast !== undefined) fact.portalBroadcast = portalBroadcast
355
580
  facts.push(fact)
356
581
  }
357
582
 
@@ -459,13 +684,14 @@ function parseApiRouteEntry(entryLiteral: ts.ObjectLiteralExpression): ModuleApi
459
684
  }
460
685
  }
461
686
 
462
- return { path: routePath, methods, auth }
687
+ return { path: routePath, methods, auth, sourcePath: null }
463
688
  }
464
689
 
465
690
  function extractApiRoutes(
466
691
  moduleId: string,
467
692
  registrySource: string | null,
468
693
  registryDescription: string,
694
+ routeSourcePaths: ReadonlyMap<string, string>,
469
695
  warnings: string[],
470
696
  ): ModuleApiRouteFact[] {
471
697
  if (registrySource == null) {
@@ -493,7 +719,7 @@ function extractApiRoutes(
493
719
  const route = parseApiRouteEntry(element)
494
720
  if (route && !seenPaths.has(route.path)) {
495
721
  seenPaths.add(route.path)
496
- routes.push(route)
722
+ routes.push({ ...route, sourcePath: routeSourcePaths.get(route.path) ?? null })
497
723
  }
498
724
  }
499
725
  }
@@ -666,36 +892,241 @@ function extractCli(cliFilePath: string | null, warnings: string[]): string[] {
666
892
  function listSourceFilesRecursive(directory: string): string[] {
667
893
  const files: string[] = []
668
894
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
669
- if (entry.name === '__tests__' || entry.name === 'node_modules') continue
895
+ if (entry.name === '__tests__' || entry.name === '__mocks__' || entry.name === 'node_modules') continue
670
896
  const fullPath = path.join(directory, entry.name)
671
897
  if (entry.isDirectory()) {
672
898
  files.push(...listSourceFilesRecursive(fullPath))
673
- } else if (/\.tsx?$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
899
+ } else if (
900
+ /\.tsx?$/.test(entry.name)
901
+ && !entry.name.endsWith('.d.ts')
902
+ && !/\.(?:test|spec)\.tsx?$/.test(entry.name)
903
+ ) {
674
904
  files.push(fullPath)
675
905
  }
676
906
  }
677
907
  return files
678
908
  }
679
909
 
680
- function extractTableIds(backendDir: string): string[] {
681
- if (!fs.existsSync(backendDir)) return []
910
+ function toPortableSourceRoot(moduleId: string, sourcePackage: string | null): string {
911
+ return path.posix.join('node_modules', sourcePackage ?? '@open-mercato/core', 'src', 'modules', moduleId)
912
+ }
913
+
914
+ function toPortableSourcePath(moduleRoot: string, sourceRoot: string, filePath: string): string {
915
+ const relativePath = path.relative(moduleRoot, filePath).split(path.sep).join('/')
916
+ return path.posix.join(sourceRoot, relativePath)
917
+ }
918
+
919
+ function sourceHasDefaultExport(sourceFile: ts.SourceFile): boolean {
920
+ return sourceFile.statements.some((statement) => {
921
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals) return true
922
+ if (!ts.isFunctionDeclaration(statement) && !ts.isClassDeclaration(statement)) return false
923
+ return statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
924
+ })
925
+ }
926
+
927
+ function readRouteMetadataPath(sourceFile: ts.SourceFile): string | null {
928
+ const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
929
+ return metadata ? readStringPropertyInitializer(metadata, 'path') ?? null : null
930
+ }
931
+
932
+ function extractApiRouteSourcePaths(
933
+ moduleId: string,
934
+ moduleRoot: string,
935
+ sourceRoot: string,
936
+ ): Map<string, string> {
937
+ const apiRoot = path.join(moduleRoot, 'api')
938
+ const sources = new Map<string, string>()
939
+ if (!fs.existsSync(apiRoot)) return sources
940
+ const methodDirectories = new Set(['get', 'post', 'put', 'patch', 'delete'])
941
+
942
+ for (const filePath of listSourceFilesRecursive(apiRoot)) {
943
+ const relativePath = path.relative(apiRoot, filePath).split(path.sep).join('/')
944
+ const segments = relativePath.split('/')
945
+ const fileName = segments.pop() as string
946
+ const fileStem = fileName.replace(/\.tsx?$/, '')
947
+ let routeSegments: string[]
948
+ if (fileStem === 'route') {
949
+ routeSegments = segments
950
+ } else if (segments[0] && methodDirectories.has(segments[0].toLowerCase())) {
951
+ routeSegments = [...segments.slice(1), fileStem]
952
+ } else {
953
+ routeSegments = [...segments, fileStem]
954
+ }
955
+ const defaultPath = `/${[moduleId, ...routeSegments].filter(Boolean).join('/')}`
956
+ const sourceFile = readSourceFile(filePath)
957
+ const routePath = sourceFile ? readRouteMetadataPath(sourceFile) ?? defaultPath : defaultPath
958
+ sources.set(routePath, toPortableSourcePath(moduleRoot, sourceRoot, filePath))
959
+ }
960
+ return sources
961
+ }
962
+
963
+ function extractModulePages(
964
+ moduleId: string,
965
+ moduleRoot: string,
966
+ sourceRoot: string,
967
+ surface: 'backend' | 'frontend',
968
+ ): ModulePageFact[] {
969
+ const pageRoot = path.join(moduleRoot, surface)
970
+ if (!fs.existsSync(pageRoot)) return []
971
+ const pages: ModulePageFact[] = []
972
+ const seen = new Set<string>()
973
+
974
+ for (const filePath of listSourceFilesRecursive(pageRoot)) {
975
+ if (!filePath.endsWith('.tsx')) continue
976
+ const relativePath = path.relative(pageRoot, filePath).split(path.sep).join('/')
977
+ const segments = relativePath.split('/')
978
+ const fileName = segments.pop() as string
979
+ const fileStem = fileName.replace(/\.tsx$/, '')
980
+ const isModernPage = fileStem === 'page'
981
+ if (!isModernPage && (fileStem === 'meta' || fileStem.endsWith('.meta') || /^[A-Z]/.test(fileStem))) continue
982
+ const sourceFile = readSourceFile(filePath)
983
+ if (!sourceFile || !sourceHasDefaultExport(sourceFile)) continue
984
+ const routeSegments = isModernPage ? segments : [...segments, fileStem]
985
+ const routePath = surface === 'frontend'
986
+ ? `/${routeSegments.filter(Boolean).join('/')}`
987
+ : isModernPage
988
+ ? `/backend/${routeSegments.join('/') || moduleId}`
989
+ : `/backend/${routeSegments[0] === moduleId
990
+ ? routeSegments.filter(Boolean).join('/')
991
+ : [moduleId, ...routeSegments].filter(Boolean).join('/')}`
992
+ if (seen.has(routePath)) continue
993
+ seen.add(routePath)
994
+ const metadataFilePath = resolvePageMetadataFile(path.dirname(filePath))
995
+ const metadataSourceFile = metadataFilePath ? readSourceFile(metadataFilePath) : sourceFile
996
+ const metadata = metadataSourceFile ? readPageMetadata(metadataSourceFile) : undefined
997
+ pages.push({
998
+ path: routePath,
999
+ sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, filePath),
1000
+ ...(metadataFilePath
1001
+ ? { metadataSourcePath: toPortableSourcePath(moduleRoot, sourceRoot, metadataFilePath) }
1002
+ : {}),
1003
+ ...(metadata ? { metadata } : {}),
1004
+ })
1005
+ }
1006
+ return pages.sort((left, right) => left.path.localeCompare(right.path))
1007
+ }
1008
+
1009
+ function findAncestorVariableDeclaration(node: ts.Node): ts.VariableDeclaration | null {
1010
+ let current: ts.Node | undefined = node.parent
1011
+ while (current) {
1012
+ if (ts.isVariableDeclaration(current)) return current
1013
+ current = current.parent
1014
+ }
1015
+ return null
1016
+ }
1017
+
1018
+ function findAncestorCallExpression(node: ts.Node): ts.CallExpression | null {
1019
+ let current: ts.Node | undefined = node.parent
1020
+ while (current) {
1021
+ if (ts.isCallExpression(current)) return current
1022
+ if (ts.isVariableDeclaration(current) || ts.isStatement(current)) return null
1023
+ current = current.parent
1024
+ }
1025
+ return null
1026
+ }
1027
+
1028
+ function extractNamedAiFacts(
1029
+ filePaths: readonly string[],
1030
+ moduleRoot: string,
1031
+ sourceRoot: string,
1032
+ propertyName: 'name' | 'id',
1033
+ ): Array<{ value: string; sourcePath: string }> {
1034
+ const facts: Array<{ value: string; sourcePath: string }> = []
1035
+ const seen = new Set<string>()
1036
+ for (const filePath of filePaths) {
1037
+ const sourceFile = readSourceFile(filePath)
1038
+ if (!sourceFile) continue
1039
+ const initializers = buildVariableInitializerMap(sourceFile)
1040
+ const readDefinitionValue = (objectLiteral: ts.ObjectLiteralExpression): string | undefined => {
1041
+ const initializer = getObjectPropertyInitializer(objectLiteral, propertyName)
1042
+ if (!initializer) return undefined
1043
+ const resolved = unwrapExpression(initializer)
1044
+ if (ts.isStringLiteralLike(resolved)) return resolved.text
1045
+ if (ts.isIdentifier(resolved)) {
1046
+ const declarationInitializer = initializers.get(resolved.text)
1047
+ if (declarationInitializer) {
1048
+ const declarationValue = unwrapExpression(declarationInitializer)
1049
+ if (ts.isStringLiteralLike(declarationValue)) return declarationValue.text
1050
+ }
1051
+ }
1052
+ return undefined
1053
+ }
1054
+ const visit = (node: ts.Node): void => {
1055
+ if (ts.isObjectLiteralExpression(node)) {
1056
+ const value = readDefinitionValue(node)
1057
+ if (value && !seen.has(value)) {
1058
+ const declaration = findAncestorVariableDeclaration(node)
1059
+ const declarationName = declaration && ts.isIdentifier(declaration.name) ? declaration.name.text : ''
1060
+ const declarationType = declaration?.type?.getText(sourceFile) ?? ''
1061
+ const call = findAncestorCallExpression(node)
1062
+ const callee = call?.expression.getText(sourceFile) ?? ''
1063
+ const isDefinition = propertyName === 'name'
1064
+ ? /AiTool/.test(declarationType) || /tool$/i.test(declarationName) || /AiTool/.test(callee)
1065
+ : /AiAgentDefinition/.test(declarationType) || /agent$/i.test(declarationName) || /defineAiAgent/.test(callee)
1066
+ if (isDefinition) {
1067
+ seen.add(value)
1068
+ facts.push({
1069
+ value,
1070
+ sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, filePath),
1071
+ })
1072
+ }
1073
+ }
1074
+ }
1075
+ node.forEachChild(visit)
1076
+ }
1077
+ sourceFile.forEachChild(visit)
1078
+ }
1079
+ return facts.sort((left, right) => left.value.localeCompare(right.value))
1080
+ }
1081
+
1082
+ function extractAiTools(moduleRoot: string, sourceRoot: string): ModuleAiToolFact[] {
1083
+ const files = new Set<string>()
1084
+ const rootFile = resolveConventionFile(moduleRoot, 'ai-tools')
1085
+ if (rootFile) files.add(rootFile)
1086
+ const toolsDirectory = path.join(moduleRoot, 'ai-tools')
1087
+ if (fs.existsSync(toolsDirectory)) {
1088
+ for (const filePath of listSourceFilesRecursive(toolsDirectory)) files.add(filePath)
1089
+ }
1090
+ return extractNamedAiFacts([...files], moduleRoot, sourceRoot, 'name')
1091
+ .map((fact) => ({ name: fact.value, sourcePath: fact.sourcePath }))
1092
+ }
1093
+
1094
+ function extractAiAgents(moduleRoot: string, sourceRoot: string): ModuleAiAgentFact[] {
1095
+ const agentsFile = resolveConventionFile(moduleRoot, 'ai-agents')
1096
+ if (!agentsFile) return []
1097
+ return extractNamedAiFacts([agentsFile], moduleRoot, sourceRoot, 'id')
1098
+ .map((fact) => ({ id: fact.value, sourcePath: fact.sourcePath }))
1099
+ }
1100
+
1101
+ function extractTableIds(moduleRoot: string): string[] {
1102
+ if (!fs.existsSync(moduleRoot)) return []
682
1103
 
683
1104
  const tableIds: string[] = []
684
1105
  const seen = new Set<string>()
685
- for (const filePath of listSourceFilesRecursive(backendDir)) {
1106
+ const collectLiteralValues = (expression: ts.Expression): string[] => {
1107
+ const current = unwrapExpression(expression)
1108
+ if (ts.isStringLiteralLike(current)) return [current.text]
1109
+ if (ts.isConditionalExpression(current)) {
1110
+ return [
1111
+ ...collectLiteralValues(current.whenTrue),
1112
+ ...collectLiteralValues(current.whenFalse),
1113
+ ]
1114
+ }
1115
+ return []
1116
+ }
1117
+
1118
+ for (const filePath of listSourceFilesRecursive(moduleRoot)) {
686
1119
  const sourceFile = readSourceFile(filePath)
687
1120
  if (!sourceFile) continue
688
1121
  const visit = (node: ts.Node): void => {
689
1122
  if (ts.isPropertyAssignment(node)) {
690
1123
  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)
1124
+ if (propertyName === 'tableId' || propertyName === 'extensionTableId') {
1125
+ for (const value of collectLiteralValues(node.initializer)) {
1126
+ if (value && !seen.has(value)) {
1127
+ seen.add(value)
1128
+ tableIds.push(value)
1129
+ }
699
1130
  }
700
1131
  }
701
1132
  }
@@ -723,13 +1154,750 @@ function extractModuleMeta(indexFilePath: string | null): { title: string | null
723
1154
  }
724
1155
  }
725
1156
 
1157
+ function nodeLine(sourceFile: ts.SourceFile, node: ts.Node): number {
1158
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1
1159
+ }
1160
+
1161
+ function readPropertyNameText(name: ts.PropertyName): string | undefined {
1162
+ if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text
1163
+ return undefined
1164
+ }
1165
+
1166
+ function readMethodBearingPropertyNames(objectLiteral: ts.ObjectLiteralExpression): string[] {
1167
+ const names: string[] = []
1168
+ for (const property of objectLiteral.properties) {
1169
+ if (ts.isMethodDeclaration(property)) {
1170
+ const name = readPropertyNameText(property.name)
1171
+ if (name) names.push(name)
1172
+ continue
1173
+ }
1174
+ if (ts.isPropertyAssignment(property)) {
1175
+ const name = readPropertyNameText(property.name)
1176
+ if (!name) continue
1177
+ const initializer = unwrapExpression(property.initializer)
1178
+ if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) names.push(name)
1179
+ }
1180
+ }
1181
+ return names.sort((left, right) => left.localeCompare(right))
1182
+ }
1183
+
1184
+ function readObjectKeyNames(objectLiteral: ts.ObjectLiteralExpression): string[] {
1185
+ const names: string[] = []
1186
+ for (const property of objectLiteral.properties) {
1187
+ const name = getPropertyName(property)
1188
+ if (name) names.push(name)
1189
+ }
1190
+ return names.sort((left, right) => left.localeCompare(right))
1191
+ }
1192
+
1193
+ function mapDiRegistrationKind(base: string | null): ModuleDiRegistrationFact['registrationKind'] | null {
1194
+ switch (base) {
1195
+ case 'asFunction':
1196
+ return 'function'
1197
+ case 'asClass':
1198
+ return 'class'
1199
+ case 'asValue':
1200
+ return 'value'
1201
+ case 'aliasTo':
1202
+ return 'alias'
1203
+ default:
1204
+ return null
1205
+ }
1206
+ }
1207
+
1208
+ function readDiChainMetadata(initializer: ts.Expression): {
1209
+ lifetime?: ModuleDiRegistrationFact['lifetime']
1210
+ injectionMode?: ModuleDiRegistrationFact['injectionMode']
1211
+ } {
1212
+ const result: {
1213
+ lifetime?: ModuleDiRegistrationFact['lifetime']
1214
+ injectionMode?: ModuleDiRegistrationFact['injectionMode']
1215
+ } = {}
1216
+ let current: ts.Expression = unwrapExpression(initializer)
1217
+ while (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
1218
+ const method = current.expression.name.text
1219
+ if ((method === 'singleton' || method === 'scoped' || method === 'transient') && !result.lifetime) {
1220
+ result.lifetime = method
1221
+ } else if (method === 'proxy' && !result.injectionMode) {
1222
+ result.injectionMode = 'proxy'
1223
+ } else if (method === 'classic' && !result.injectionMode) {
1224
+ result.injectionMode = 'classic'
1225
+ }
1226
+ current = unwrapExpression(current.expression.expression)
1227
+ }
1228
+ return result
1229
+ }
1230
+
1231
+ function readDiProviderSymbol(
1232
+ initializer: ts.Expression,
1233
+ kind: ModuleDiRegistrationFact['registrationKind'],
1234
+ ): string | undefined {
1235
+ let current: ts.Expression = unwrapExpression(initializer)
1236
+ while (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
1237
+ current = unwrapExpression(current.expression.expression)
1238
+ }
1239
+ if (!ts.isCallExpression(current)) return undefined
1240
+ const argument = current.arguments[0]
1241
+ if (!argument) return undefined
1242
+ if (kind === 'value') return undefined
1243
+ if (kind === 'alias' && ts.isStringLiteralLike(argument)) return argument.text
1244
+ if ((kind === 'function' || kind === 'class') && ts.isIdentifier(argument)) return argument.text
1245
+ return undefined
1246
+ }
1247
+
1248
+ function extractDiRegistrationFacts(
1249
+ diFilePath: string | null,
1250
+ sourcePath: string,
1251
+ ): { facts: ModuleDiRegistrationFact[]; unresolvedTokens: string[] } {
1252
+ if (!diFilePath) return { facts: [], unresolvedTokens: [] }
1253
+ const sourceFile = readSourceFile(diFilePath)
1254
+ if (!sourceFile) return { facts: [], unresolvedTokens: [] }
1255
+
1256
+ const facts: ModuleDiRegistrationFact[] = []
1257
+ const unresolvedTokens: string[] = []
1258
+ const visit = (node: ts.Node): void => {
1259
+ if (
1260
+ ts.isCallExpression(node) &&
1261
+ ts.isPropertyAccessExpression(node.expression) &&
1262
+ node.expression.name.text === 'register'
1263
+ ) {
1264
+ const argument = node.arguments[0]
1265
+ if (argument && ts.isObjectLiteralExpression(argument)) {
1266
+ for (const property of argument.properties) {
1267
+ if (!ts.isPropertyAssignment(property)) continue
1268
+ const token = getPropertyName(property)
1269
+ if (!token) continue
1270
+ const kind = mapDiRegistrationKind(detectAwilixRegistrationKind(property.initializer))
1271
+ if (!kind) {
1272
+ if (!unresolvedTokens.includes(token)) unresolvedTokens.push(token)
1273
+ continue
1274
+ }
1275
+ const chain = readDiChainMetadata(property.initializer)
1276
+ const providerSymbol = readDiProviderSymbol(property.initializer, kind)
1277
+ facts.push({
1278
+ token,
1279
+ registrationKind: kind,
1280
+ ...(providerSymbol ? { providerSymbol } : {}),
1281
+ ...(chain.lifetime ? { lifetime: chain.lifetime } : {}),
1282
+ ...(chain.injectionMode ? { injectionMode: chain.injectionMode } : {}),
1283
+ source: { sourcePath, exportName: token, line: nodeLine(sourceFile, property) },
1284
+ })
1285
+ }
1286
+ }
1287
+ }
1288
+ node.forEachChild(visit)
1289
+ }
1290
+ sourceFile.forEachChild(visit)
1291
+ return { facts, unresolvedTokens }
1292
+ }
1293
+
1294
+ function deriveLegacyDiTokens(facts: readonly ModuleDiRegistrationFact[]): string[] {
1295
+ const tokens: string[] = []
1296
+ const seen = new Set<string>()
1297
+ for (const fact of facts) {
1298
+ if (fact.registrationKind !== 'function' && fact.registrationKind !== 'class') continue
1299
+ if (seen.has(fact.token)) continue
1300
+ seen.add(fact.token)
1301
+ tokens.push(fact.token)
1302
+ }
1303
+ return tokens
1304
+ }
1305
+
1306
+ function dedupeOwnedContractFacts(facts: readonly ModuleOwnedContractFact[]): ModuleOwnedContractFact[] {
1307
+ const byId = new Map<string, ModuleOwnedContractFact>()
1308
+ for (const fact of facts) {
1309
+ const existing = byId.get(fact.id)
1310
+ if (!existing) {
1311
+ byId.set(fact.id, fact)
1312
+ continue
1313
+ }
1314
+ if (fact.source.sourcePath.localeCompare(existing.source.sourcePath) < 0) byId.set(fact.id, fact)
1315
+ }
1316
+ return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id))
1317
+ }
1318
+
1319
+ const PAGE_CONTEXT_VALUES = new Set(['main', 'admin', 'settings', 'profile'])
1320
+
1321
+ function readAliasedStringProperty(
1322
+ metadata: ts.ObjectLiteralExpression,
1323
+ preferred: string,
1324
+ fallback: string,
1325
+ ): string | undefined {
1326
+ return readStringPropertyInitializer(metadata, preferred) ?? readStringPropertyInitializer(metadata, fallback)
1327
+ }
1328
+
1329
+ function readNumericProperty(metadata: ts.ObjectLiteralExpression, propertyName: string): number | undefined {
1330
+ const initializer = getObjectPropertyInitializer(metadata, propertyName)
1331
+ if (!initializer || !ts.isNumericLiteral(initializer)) return undefined
1332
+ return Number(initializer.text)
1333
+ }
1334
+
1335
+ function readPageMetadata(sourceFile: ts.SourceFile): ModulePageMetadata | undefined {
1336
+ const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
1337
+ if (!metadata) return undefined
1338
+ const result: ModulePageMetadata = {}
1339
+ const requireAuth = readBooleanPropertyInitializer(metadata, 'requireAuth')
1340
+ if (requireAuth !== undefined) result.requireAuth = requireAuth
1341
+ const requireCustomerAuth = readBooleanPropertyInitializer(metadata, 'requireCustomerAuth')
1342
+ if (requireCustomerAuth !== undefined) result.requireCustomerAuth = requireCustomerAuth
1343
+ const requireFeatures = readStringArrayPropertyInitializer(metadata, 'requireFeatures')
1344
+ if (requireFeatures && requireFeatures.length > 0) result.requireFeatures = requireFeatures
1345
+ const requireCustomerFeatures = readStringArrayPropertyInitializer(metadata, 'requireCustomerFeatures')
1346
+ if (requireCustomerFeatures && requireCustomerFeatures.length > 0) {
1347
+ result.requireCustomerFeatures = requireCustomerFeatures
1348
+ }
1349
+ const navHidden = readBooleanPropertyInitializer(metadata, 'navHidden')
1350
+ if (navHidden !== undefined) result.navHidden = navHidden
1351
+ const navGroup = readAliasedStringProperty(metadata, 'pageGroup', 'group')
1352
+ if (navGroup) result.navGroup = navGroup
1353
+ const navGroupKey = readAliasedStringProperty(metadata, 'pageGroupKey', 'groupKey')
1354
+ if (navGroupKey) result.navGroupKey = navGroupKey
1355
+ const navOrder = readNumericProperty(metadata, 'pageOrder') ?? readNumericProperty(metadata, 'order')
1356
+ if (navOrder !== undefined) result.navOrder = navOrder
1357
+ const pageContext = readStringPropertyInitializer(metadata, 'pageContext')
1358
+ if (pageContext && PAGE_CONTEXT_VALUES.has(pageContext)) {
1359
+ result.pageContext = pageContext as ModulePageMetadata['pageContext']
1360
+ }
1361
+ return Object.keys(result).length > 0 ? result : undefined
1362
+ }
1363
+
1364
+ /**
1365
+ * Mirrors the runtime companion-file convention: a page's metadata comes from
1366
+ * `page.meta.*` (or `meta.*`) in the page directory when one exists, and only
1367
+ * otherwise from the page component itself.
1368
+ */
1369
+ function resolvePageMetadataFile(pageDirectory: string): string | null {
1370
+ for (const baseName of ['page.meta', 'meta']) {
1371
+ for (const extension of MODULE_CODE_EXTENSIONS) {
1372
+ const candidate = path.join(pageDirectory, `${baseName}${extension}`)
1373
+ if (fs.existsSync(candidate)) return candidate
1374
+ }
1375
+ }
1376
+ return null
1377
+ }
1378
+
1379
+ function extractModuleMetadataContract(
1380
+ indexFilePath: string | null,
1381
+ moduleId: string,
1382
+ sourcePath: string | null,
1383
+ ): ModuleOwnedContractFact | null {
1384
+ if (!indexFilePath || !sourcePath) return null
1385
+ const sourceFile = readSourceFile(indexFilePath)
1386
+ if (!sourceFile) return null
1387
+ const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
1388
+ if (!metadata) return null
1389
+ const facts: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
1390
+ const name = readStringPropertyInitializer(metadata, 'name')
1391
+ if (name) facts.name = name
1392
+ const version = readStringPropertyInitializer(metadata, 'version')
1393
+ if (version) facts.version = version
1394
+ const requires = readStringArrayPropertyInitializer(metadata, 'requires')
1395
+ if (requires && requires.length > 0) facts.requires = requires
1396
+ const ejectable = readBooleanPropertyInitializer(metadata, 'ejectable')
1397
+ if (ejectable !== undefined) facts.ejectable = ejectable
1398
+ return {
1399
+ kind: 'module-metadata',
1400
+ id: moduleId,
1401
+ source: { sourcePath, exportName: 'metadata', line: nodeLine(sourceFile, metadata) },
1402
+ ...(Object.keys(facts).length > 0 ? { metadata: facts } : {}),
1403
+ }
1404
+ }
1405
+
1406
+ function extractCommandContractCandidates(
1407
+ moduleId: string,
1408
+ moduleRoot: string,
1409
+ sourceRoot: string,
1410
+ ): Array<{ id: string; sourcePath: string }> {
1411
+ const commandsRoot = path.join(moduleRoot, 'commands')
1412
+ if (!fs.existsSync(commandsRoot)) return []
1413
+ const candidates: Array<{ id: string; sourcePath: string }> = []
1414
+ for (const filePath of listSourceFilesRecursive(commandsRoot)) {
1415
+ if (path.basename(filePath).replace(/\.tsx?$/, '') === 'interceptors') continue
1416
+ const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
1417
+ for (const id of extractCommandIdsFromSource(filePath)) candidates.push({ id, sourcePath })
1418
+ }
1419
+ return candidates
1420
+ }
1421
+
1422
+ /**
1423
+ * Runtime worker id: `metadata.id` when declared, otherwise the path-derived
1424
+ * `<moduleId>:workers:<...segments>:<fileStem>` that `discoverWorkers` builds.
1425
+ */
1426
+ function deriveWorkerId(moduleId: string, workersRoot: string, filePath: string): string {
1427
+ const relativePath = path.relative(workersRoot, filePath).split(path.sep).join('/')
1428
+ const segments = relativePath.split('/')
1429
+ const fileName = segments.pop() as string
1430
+ const fileStem = fileName.replace(/\.tsx?$/, '')
1431
+ return [moduleId, 'workers', ...segments, fileStem].filter(Boolean).join(':')
1432
+ }
1433
+
1434
+ /** Resolves a string property directly or through a local `const` in the same file. */
1435
+ function readResolvedStringProperty(
1436
+ objectLiteral: ts.ObjectLiteralExpression,
1437
+ propertyName: string,
1438
+ initializers: Map<string, ts.Expression>,
1439
+ ): string | undefined {
1440
+ const initializer = getObjectPropertyInitializer(objectLiteral, propertyName)
1441
+ if (!initializer) return undefined
1442
+ const resolved = unwrapExpression(initializer)
1443
+ if (ts.isStringLiteralLike(resolved)) return resolved.text
1444
+ if (ts.isIdentifier(resolved)) {
1445
+ const declaration = initializers.get(resolved.text)
1446
+ if (declaration) {
1447
+ const declarationValue = unwrapExpression(declaration)
1448
+ if (ts.isStringLiteralLike(declarationValue)) return declarationValue.text
1449
+ }
1450
+ }
1451
+ return undefined
1452
+ }
1453
+
1454
+ function extractWorkerContracts(
1455
+ moduleId: string,
1456
+ moduleRoot: string,
1457
+ sourceRoot: string,
1458
+ ): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
1459
+ const workersRoot = path.join(moduleRoot, 'workers')
1460
+ if (!fs.existsSync(workersRoot)) return { facts: [], unresolved: [] }
1461
+ const facts: ModuleOwnedContractFact[] = []
1462
+ const unresolved: ModuleFactDiagnostic[] = []
1463
+ for (const filePath of listSourceFilesRecursive(workersRoot)) {
1464
+ const sourceFile = readSourceFile(filePath)
1465
+ if (!sourceFile) continue
1466
+ const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
1467
+ if (!metadata) continue
1468
+ const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
1469
+ const source = { sourcePath, exportName: 'metadata', line: nodeLine(sourceFile, metadata) }
1470
+ const initializers = buildVariableInitializerMap(sourceFile)
1471
+ const workerId = readResolvedStringProperty(metadata, 'id', initializers)
1472
+ ?? deriveWorkerId(moduleId, workersRoot, filePath)
1473
+
1474
+ // Runtime skips any worker without a queue, so a worker missing the required
1475
+ // property is not a contract; a present-but-dynamic queue is reported instead.
1476
+ const hasQueueProperty = getObjectPropertyInitializer(metadata, 'queue') !== undefined
1477
+ const queue = readResolvedStringProperty(metadata, 'queue', initializers)
1478
+ if (!hasQueueProperty) {
1479
+ unresolved.push({ code: 'unresolved-static-contract', kind: 'worker', id: workerId, source })
1480
+ continue
1481
+ }
1482
+
1483
+ const workerMetadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
1484
+ if (queue) workerMetadata.queue = queue
1485
+ else unresolved.push({ code: 'unresolved-static-contract', kind: 'worker', id: workerId, source })
1486
+ const name = readResolvedStringProperty(metadata, 'name', initializers)
1487
+ if (name) workerMetadata.name = name
1488
+ const concurrencyInitializer = getObjectPropertyInitializer(metadata, 'concurrency')
1489
+ if (concurrencyInitializer && ts.isNumericLiteral(concurrencyInitializer)) {
1490
+ workerMetadata.concurrency = Number(concurrencyInitializer.text)
1491
+ }
1492
+ facts.push({
1493
+ kind: 'worker',
1494
+ id: workerId,
1495
+ source,
1496
+ ...(Object.keys(workerMetadata).length > 0 ? { metadata: workerMetadata } : {}),
1497
+ })
1498
+ }
1499
+ return { facts: dedupeOwnedContractFacts(facts), unresolved }
1500
+ }
1501
+
1502
+ function extractPageMiddlewareContracts(
1503
+ moduleRoot: string,
1504
+ sourceRoot: string,
1505
+ ): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
1506
+ const facts: ModuleOwnedContractFact[] = []
1507
+ const unresolved: ModuleFactDiagnostic[] = []
1508
+ for (const surface of ['backend', 'frontend'] as const) {
1509
+ const filePath = resolveConventionFile(path.join(moduleRoot, surface), 'middleware')
1510
+ if (!filePath) continue
1511
+ const sourceFile = readSourceFile(filePath)
1512
+ if (!sourceFile) continue
1513
+ const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
1514
+ const middlewareArray =
1515
+ findArrayLiteralDeclaration(sourceFile, 'middleware') ??
1516
+ (() => {
1517
+ const defaultExport = findDefaultExportExpression(sourceFile)
1518
+ return defaultExport ? unwrapArrayLiteral(defaultExport) : null
1519
+ })()
1520
+ if (!middlewareArray) continue
1521
+ let index = 0
1522
+ for (const element of middlewareArray.elements) {
1523
+ if (!ts.isObjectLiteralExpression(element)) continue
1524
+ const middlewareId = readStringPropertyInitializer(element, 'id')
1525
+ if (!middlewareId) {
1526
+ unresolved.push({
1527
+ code: 'unresolved-static-contract',
1528
+ kind: 'page-middleware',
1529
+ id: `${sourcePath}#${index}`,
1530
+ source: { sourcePath, line: nodeLine(sourceFile, element) },
1531
+ })
1532
+ index += 1
1533
+ continue
1534
+ }
1535
+ const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = { surface }
1536
+ const mode = readStringPropertyInitializer(element, 'mode')
1537
+ if (mode) metadata.mode = mode
1538
+ const priorityInitializer = getObjectPropertyInitializer(element, 'priority')
1539
+ if (priorityInitializer && ts.isNumericLiteral(priorityInitializer)) {
1540
+ metadata.priority = Number(priorityInitializer.text)
1541
+ }
1542
+ facts.push({
1543
+ kind: 'page-middleware',
1544
+ id: middlewareId,
1545
+ source: { sourcePath, exportName: 'middleware', line: nodeLine(sourceFile, element) },
1546
+ metadata,
1547
+ })
1548
+ index += 1
1549
+ }
1550
+ }
1551
+ return { facts: dedupeOwnedContractFacts(facts), unresolved }
1552
+ }
1553
+
1554
+ function extractSetupContract(
1555
+ moduleId: string,
1556
+ setupFilePath: string | null,
1557
+ sourcePath: string | null,
1558
+ ): ModuleOwnedContractFact | null {
1559
+ if (!setupFilePath || !sourcePath) return null
1560
+ const sourceFile = readSourceFile(setupFilePath)
1561
+ if (!sourceFile) return null
1562
+ const setupObject =
1563
+ findObjectLiteralDeclaration(sourceFile, 'setup') ??
1564
+ (() => {
1565
+ const defaultExport = findDefaultExportExpression(sourceFile)
1566
+ if (!defaultExport) return null
1567
+ const unwrapped = unwrapExpression(defaultExport)
1568
+ return ts.isObjectLiteralExpression(unwrapped) ? unwrapped : null
1569
+ })()
1570
+ if (!setupObject) return null
1571
+ const hooks = readMethodBearingPropertyNames(setupObject)
1572
+ const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
1573
+ if (hooks.length > 0) metadata.hooks = hooks
1574
+ const roleFeatures = getObjectPropertyInitializer(setupObject, 'defaultRoleFeatures')
1575
+ if (roleFeatures && ts.isObjectLiteralExpression(roleFeatures)) {
1576
+ const roles = readObjectKeyNames(roleFeatures)
1577
+ if (roles.length > 0) metadata.roles = roles
1578
+ }
1579
+ const customerRoleFeatures = getObjectPropertyInitializer(setupObject, 'defaultCustomerRoleFeatures')
1580
+ if (customerRoleFeatures && ts.isObjectLiteralExpression(customerRoleFeatures)) {
1581
+ const customerRoles = readObjectKeyNames(customerRoleFeatures)
1582
+ if (customerRoles.length > 0) metadata.customerRoles = customerRoles
1583
+ }
1584
+ return {
1585
+ kind: 'setup',
1586
+ id: `${moduleId}:setup`,
1587
+ source: { sourcePath, exportName: 'setup', line: nodeLine(sourceFile, setupObject) },
1588
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
1589
+ }
1590
+ }
1591
+
1592
+ function extractEncryptionContracts(
1593
+ encryptionFilePath: string | null,
1594
+ sourcePath: string | null,
1595
+ ): ModuleOwnedContractFact[] {
1596
+ if (!encryptionFilePath || !sourcePath) return []
1597
+ const sourceFile = readSourceFile(encryptionFilePath)
1598
+ if (!sourceFile) return []
1599
+ const mapsArray =
1600
+ findArrayLiteralDeclaration(sourceFile, 'defaultEncryptionMaps') ??
1601
+ findArrayLiteralDeclaration(sourceFile, 'encryptionMaps')
1602
+ if (!mapsArray) return []
1603
+ const facts: ModuleOwnedContractFact[] = []
1604
+ for (const element of mapsArray.elements) {
1605
+ if (!ts.isObjectLiteralExpression(element)) continue
1606
+ const entityId = readStringPropertyInitializer(element, 'entityId')
1607
+ if (!entityId) continue
1608
+ const fieldsInitializer = getObjectPropertyInitializer(element, 'fields')
1609
+ const fields: string[] = []
1610
+ if (fieldsInitializer) {
1611
+ const fieldsArray = unwrapArrayLiteral(fieldsInitializer)
1612
+ if (fieldsArray) {
1613
+ for (const fieldElement of fieldsArray.elements) {
1614
+ if (!ts.isObjectLiteralExpression(fieldElement)) continue
1615
+ const field = readStringPropertyInitializer(fieldElement, 'field')
1616
+ if (field) fields.push(field)
1617
+ }
1618
+ }
1619
+ }
1620
+ facts.push({
1621
+ kind: 'encryption',
1622
+ id: entityId,
1623
+ source: { sourcePath, exportName: 'defaultEncryptionMaps', line: nodeLine(sourceFile, element) },
1624
+ ...(fields.length > 0 ? { metadata: { fields: fields.sort((left, right) => left.localeCompare(right)) } } : {}),
1625
+ })
1626
+ }
1627
+ return dedupeOwnedContractFacts(facts)
1628
+ }
1629
+
1630
+ function extractCustomEntityContracts(
1631
+ ceFilePath: string | null,
1632
+ sourcePath: string | null,
1633
+ ): ModuleOwnedContractFact[] {
1634
+ if (!ceFilePath || !sourcePath) return []
1635
+ const entityIds = [...collectCustomFieldEntityIds(ceFilePath)].sort((left, right) => left.localeCompare(right))
1636
+ return entityIds.map((id) => ({
1637
+ kind: 'custom-entity' as const,
1638
+ id,
1639
+ source: { sourcePath, exportName: 'default' },
1640
+ }))
1641
+ }
1642
+
1643
+ /**
1644
+ * Keyed AI override exports. Agent *extensions* (`aiAgentExtensions`) patch an agent
1645
+ * additively and belong to the `ai.extensions` array; these two replace or disable an
1646
+ * entry by key and map to exact `ai.agents.<id>` / `ai.tools.<id>` override targets.
1647
+ */
1648
+ const AI_OVERRIDE_EXPORTS = [
1649
+ { exportName: 'aiAgentOverrides', target: 'agent' },
1650
+ { exportName: 'aiToolOverrides', target: 'tool' },
1651
+ ] as const
1652
+
1653
+ function extractAiExtensionContracts(
1654
+ moduleRoot: string,
1655
+ sourceRoot: string,
1656
+ ): ModuleOwnedContractFact[] {
1657
+ const facts: ModuleOwnedContractFact[] = []
1658
+ const sources: Array<{ basename: string; relativeDir: string }> = [
1659
+ { basename: 'ai-agents', relativeDir: '.' },
1660
+ { basename: 'ai-tools', relativeDir: '.' },
1661
+ { basename: 'ai-agents.extensions', relativeDir: '.' },
1662
+ { basename: 'ai-tools.extensions', relativeDir: '.' },
1663
+ ]
1664
+ for (const entry of sources) {
1665
+ const filePath = resolveConventionFile(path.join(moduleRoot, entry.relativeDir), entry.basename)
1666
+ if (!filePath) continue
1667
+ const sourceFile = readSourceFile(filePath)
1668
+ if (!sourceFile) continue
1669
+ const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
1670
+ const extensionsArray = findArrayLiteralDeclaration(sourceFile, 'aiAgentExtensions')
1671
+ if (extensionsArray) {
1672
+ for (const element of extensionsArray.elements) {
1673
+ const objectLiteral = ts.isObjectLiteralExpression(element)
1674
+ ? element
1675
+ : ts.isCallExpression(element) && element.arguments[0] && ts.isObjectLiteralExpression(element.arguments[0])
1676
+ ? element.arguments[0]
1677
+ : null
1678
+ if (!objectLiteral) continue
1679
+ const targetAgentId = readStringPropertyInitializer(objectLiteral, 'targetAgentId')
1680
+ if (!targetAgentId) continue
1681
+ facts.push({
1682
+ kind: 'ai-extension',
1683
+ id: `agent-extension:${targetAgentId}`,
1684
+ source: { sourcePath, exportName: 'aiAgentExtensions', line: nodeLine(sourceFile, objectLiteral) },
1685
+ metadata: { target: 'agent', mode: 'extension', targetAgentId },
1686
+ })
1687
+ }
1688
+ }
1689
+ // `aiAgentOverrides` / `aiToolOverrides` are keyed replace-or-disable maps; only
1690
+ // their keys are contract facts. Values (agent/tool definitions, `null` disables)
1691
+ // are never read or serialized.
1692
+ for (const { exportName, target } of AI_OVERRIDE_EXPORTS) {
1693
+ const overridesObject = findObjectLiteralDeclaration(sourceFile, exportName)
1694
+ if (!overridesObject) continue
1695
+ for (const property of overridesObject.properties) {
1696
+ const overrideId = getPropertyName(property)
1697
+ if (!overrideId) continue
1698
+ facts.push({
1699
+ kind: 'ai-extension',
1700
+ id: `${target}-override:${overrideId}`,
1701
+ source: { sourcePath, exportName, line: nodeLine(sourceFile, property) },
1702
+ metadata: { target, mode: 'override', overrideKey: overrideId },
1703
+ })
1704
+ }
1705
+ }
1706
+ }
1707
+ return dedupeOwnedContractFacts(facts)
1708
+ }
1709
+
1710
+ function extractGeneratorPluginContracts(
1711
+ moduleRoot: string,
1712
+ sourceRoot: string,
1713
+ ): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
1714
+ const filePath = resolveConventionFile(moduleRoot, 'generators')
1715
+ if (!filePath) return { facts: [], unresolved: [] }
1716
+ const sourceFile = readSourceFile(filePath)
1717
+ if (!sourceFile) return { facts: [], unresolved: [] }
1718
+ const pluginsArray = findArrayLiteralDeclaration(sourceFile, 'generatorPlugins')
1719
+ if (!pluginsArray) return { facts: [], unresolved: [] }
1720
+ const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
1721
+ const initializers = buildVariableInitializerMap(sourceFile)
1722
+ const facts: ModuleOwnedContractFact[] = []
1723
+ const unresolved: ModuleFactDiagnostic[] = []
1724
+ let index = 0
1725
+ for (const element of pluginsArray.elements) {
1726
+ const objectLiteral = resolveToObjectLiteral(element, initializers)
1727
+ if (!objectLiteral) {
1728
+ index += 1
1729
+ continue
1730
+ }
1731
+ const pluginId = readStringPropertyInitializer(objectLiteral, 'id')
1732
+ if (!pluginId) {
1733
+ unresolved.push({
1734
+ code: 'unresolved-static-contract',
1735
+ kind: 'generator-plugin',
1736
+ id: `${sourcePath}#${index}`,
1737
+ source: { sourcePath, exportName: 'generatorPlugins', line: nodeLine(sourceFile, objectLiteral) },
1738
+ })
1739
+ index += 1
1740
+ continue
1741
+ }
1742
+ const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
1743
+ const conventionFile = readStringPropertyInitializer(objectLiteral, 'conventionFile')
1744
+ if (conventionFile) metadata.conventionFile = conventionFile
1745
+ const outputFileName = readStringPropertyInitializer(objectLiteral, 'outputFileName')
1746
+ if (outputFileName) metadata.outputFileName = outputFileName
1747
+ facts.push({
1748
+ kind: 'generator-plugin',
1749
+ id: pluginId,
1750
+ source: { sourcePath, exportName: 'generatorPlugins', line: nodeLine(sourceFile, objectLiteral) },
1751
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
1752
+ })
1753
+ index += 1
1754
+ }
1755
+ return { facts: dedupeOwnedContractFacts(facts), unresolved }
1756
+ }
1757
+
1758
+ function compareSourceRefs(left: ModuleFactSourceRef, right: ModuleFactSourceRef): number {
1759
+ return (
1760
+ left.sourcePath.localeCompare(right.sourcePath) ||
1761
+ (left.line ?? 0) - (right.line ?? 0) ||
1762
+ (left.exportName ?? '').localeCompare(right.exportName ?? '')
1763
+ )
1764
+ }
1765
+
1766
+ function sameSourceRef(left: ModuleFactSourceRef, right: ModuleFactSourceRef): boolean {
1767
+ return (
1768
+ left.sourcePath === right.sourcePath &&
1769
+ (left.line ?? null) === (right.line ?? null) &&
1770
+ (left.exportName ?? null) === (right.exportName ?? null)
1771
+ )
1772
+ }
1773
+
1774
+ // Every proven `(kind, id)` reaches `factSources`. Facts whose declaration site is already
1775
+ // serialized inline (pages, CLI commands, routes, AI tools/agents, reused contributions) and
1776
+ // the owned families (which carry `source` inside `ownedContracts`) are emitted as a typed
1777
+ // `factRef` pointer instead of a duplicated source ref, so the index stays uniform without
1778
+ // copying provenance payloads past the determinism byte guard. Duplicate detection runs over
1779
+ // ALL candidates regardless of which projection they take.
1780
+ type ProvenanceCandidate = {
1781
+ kind: ModuleFactSourceKind
1782
+ id: string
1783
+ source?: ModuleFactSourceRef
1784
+ factRef?: ModuleFactRef
1785
+ }
1786
+
1787
+ function provenanceCandidateOrderKey(candidate: ProvenanceCandidate): string {
1788
+ if (candidate.source) {
1789
+ return JSON.stringify([
1790
+ 0,
1791
+ candidate.source.sourcePath,
1792
+ candidate.source.line ?? 0,
1793
+ candidate.source.exportName ?? '',
1794
+ ])
1795
+ }
1796
+ return JSON.stringify([1, candidate.factRef?.factSection ?? '', candidate.factRef?.factKey ?? ''])
1797
+ }
1798
+
1799
+ function buildProvenanceIndex(candidates: readonly ProvenanceCandidate[]): {
1800
+ factSources: ModuleFactSourceEntry[]
1801
+ diagnostics: ModuleFactDiagnostic[]
1802
+ } {
1803
+ const grouped = new Map<string, ProvenanceCandidate[]>()
1804
+ for (const candidate of candidates) {
1805
+ const key = JSON.stringify([candidate.kind, candidate.id])
1806
+ const group = grouped.get(key) ?? []
1807
+ group.push(candidate)
1808
+ grouped.set(key, group)
1809
+ }
1810
+ // Keep the provenance index compact: sourcePath is the provenance and (kind,id) the
1811
+ // stable identity, so exportName and line (recoverable from source) are dropped here.
1812
+ // Owned families retain the richer source ref inside `ownedContracts`.
1813
+ const factSources: ModuleFactSourceEntry[] = []
1814
+ const diagnostics: ModuleFactDiagnostic[] = []
1815
+ for (const group of grouped.values()) {
1816
+ const sorted = [...group].sort(
1817
+ (left, right) => provenanceCandidateOrderKey(left).localeCompare(provenanceCandidateOrderKey(right)),
1818
+ )
1819
+ const canonical = sorted[0]
1820
+ factSources.push(
1821
+ canonical.factRef
1822
+ ? {
1823
+ kind: canonical.kind,
1824
+ id: canonical.id,
1825
+ factRef: {
1826
+ factSection: canonical.factRef.factSection,
1827
+ ...(canonical.factRef.factKey === canonical.id
1828
+ ? {}
1829
+ : { factKey: canonical.factRef.factKey }),
1830
+ },
1831
+ }
1832
+ : {
1833
+ kind: canonical.kind,
1834
+ id: canonical.id,
1835
+ source: { sourcePath: (canonical.source as ModuleFactSourceRef).sourcePath },
1836
+ },
1837
+ )
1838
+ const canonicalKey = provenanceCandidateOrderKey(canonical)
1839
+ for (const rejected of sorted.slice(1)) {
1840
+ if (provenanceCandidateOrderKey(rejected) === canonicalKey) continue
1841
+ diagnostics.push({
1842
+ code: 'duplicate-source',
1843
+ kind: rejected.kind,
1844
+ id: rejected.id,
1845
+ ...(rejected.source ? { source: rejected.source } : {}),
1846
+ })
1847
+ }
1848
+ }
1849
+ factSources.sort(
1850
+ (left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id),
1851
+ )
1852
+ diagnostics.sort(
1853
+ (left, right) =>
1854
+ left.code.localeCompare(right.code) ||
1855
+ left.kind.localeCompare(right.kind) ||
1856
+ left.id.localeCompare(right.id) ||
1857
+ compareSourceRefs(left.source ?? { sourcePath: '' }, right.source ?? { sourcePath: '' }),
1858
+ )
1859
+ return { factSources, diagnostics }
1860
+ }
1861
+
1862
+ function buildOwnedContracts(
1863
+ families: ReadonlyArray<[ModuleOwnedContractKind, ModuleOwnedContractFact[]]>,
1864
+ ): ModuleOwnedContracts | undefined {
1865
+ const result: ModuleOwnedContracts = {}
1866
+ for (const [kind, facts] of families) {
1867
+ if (facts.length === 0) continue
1868
+ // Drop the (constant/redundant) exportName from the emitted source ref; the line
1869
+ // anchor plus sourcePath already pin the declaration and keep the JSON compact.
1870
+ result[kind] = facts.map((fact) => ({
1871
+ ...fact,
1872
+ source: {
1873
+ sourcePath: fact.source.sourcePath,
1874
+ ...(fact.source.line ? { line: fact.source.line } : {}),
1875
+ },
1876
+ }))
1877
+ }
1878
+ return Object.keys(result).length > 0 ? result : undefined
1879
+ }
1880
+
1881
+ const CONTRIBUTION_KIND_TO_SOURCE_KIND: Partial<Record<string, ModuleFactSourceKind>> = {
1882
+ subscriber: 'subscriber',
1883
+ }
1884
+
1885
+ const REGISTRY_TO_SOURCE_KIND: Partial<Record<string, ModuleFactSourceKind>> = {
1886
+ vector: 'vector',
1887
+ integration: 'integration',
1888
+ payment: 'integration',
1889
+ shipping: 'integration',
1890
+ currency: 'integration',
1891
+ }
1892
+
726
1893
  export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFacts {
727
- const { moduleId, coreVersion = null } = options
1894
+ const { moduleId, coreVersion = null, sourcePackage = null, sourceVersion = null } = options
728
1895
  const moduleRoot = options.moduleRoot
729
1896
  ?? (options.coreSrcRoot ? path.join(options.coreSrcRoot, moduleId) : null)
730
1897
  if (!moduleRoot) {
731
1898
  throw new Error(`[internal] extractModuleFacts requires moduleRoot or coreSrcRoot for module "${moduleId}"`)
732
1899
  }
1900
+ const sourceRoot = toPortableSourceRoot(moduleId, sourcePackage)
733
1901
 
734
1902
  const entitiesFilePath =
735
1903
  resolveConventionFile(path.join(moduleRoot, 'data'), 'entities') ??
@@ -743,7 +1911,11 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
743
1911
  const notificationsFilePath = resolveConventionFile(moduleRoot, 'notifications')
744
1912
  const cliFilePath = resolveConventionFile(moduleRoot, 'cli')
745
1913
  const indexFilePath = resolveConventionFile(moduleRoot, 'index')
746
- const backendDir = path.join(moduleRoot, 'backend')
1914
+ const setupFilePath = resolveConventionFile(moduleRoot, 'setup')
1915
+ const encryptionFilePath = resolveConventionFile(moduleRoot, 'encryption')
1916
+
1917
+ const portableOf = (filePath: string | null): string | null =>
1918
+ filePath ? toPortableSourcePath(moduleRoot, sourceRoot, filePath) : null
747
1919
 
748
1920
  const warnings: string[] = []
749
1921
  const { title, description } = extractModuleMeta(indexFilePath)
@@ -753,21 +1925,247 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
753
1925
  const aclFeatures = extractAclFeatures(aclFilePath)
754
1926
 
755
1927
  const { source: registrySource, description: registryDescription } = resolveRegistrySource(options)
756
- const apiRoutes = extractApiRoutes(moduleId, registrySource, registryDescription, warnings)
757
- const diTokens = extractDiTokens(diFilePath)
1928
+ const apiRouteSourcePaths = extractApiRouteSourcePaths(moduleId, moduleRoot, sourceRoot)
1929
+ const apiRoutes = extractApiRoutes(moduleId, registrySource, registryDescription, apiRouteSourcePaths, warnings)
1930
+ const diRegistrations = extractDiRegistrationFacts(diFilePath, portableOf(diFilePath) ?? '')
1931
+ const diTokens = deriveLegacyDiTokens(diRegistrations.facts)
1932
+ for (const legacyToken of extractDiTokens(diFilePath)) {
1933
+ if (!diTokens.includes(legacyToken)) diTokens.push(legacyToken)
1934
+ }
758
1935
  const searchEntities = extractSearchEntities(searchFilePath, warnings)
759
1936
  const notifications = extractNotifications(notificationsFilePath, warnings)
760
1937
  const cli = extractCli(cliFilePath, warnings)
1938
+ const cliCommands = cliFilePath
1939
+ ? cli.map((command) => ({
1940
+ command,
1941
+ sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, cliFilePath),
1942
+ }))
1943
+ : []
1944
+ const backendPages = extractModulePages(moduleId, moduleRoot, sourceRoot, 'backend')
1945
+ const frontendPages = extractModulePages(moduleId, moduleRoot, sourceRoot, 'frontend')
1946
+ const aiTools = extractAiTools(moduleRoot, sourceRoot)
1947
+ const aiAgents = extractAiAgents(moduleRoot, sourceRoot)
761
1948
  const hostTokens: ModuleHostTokens = {
762
1949
  entityIds: extractHostEntityIds(entities),
763
- tableIds: extractTableIds(backendDir),
1950
+ tableIds: extractTableIds(moduleRoot),
764
1951
  }
1952
+ const extensionSurfaces = extractModuleExtensionFacts({
1953
+ moduleId,
1954
+ moduleRoot,
1955
+ sourceRoot,
1956
+ entities,
1957
+ events,
1958
+ apiRoutes,
1959
+ searchEntities,
1960
+ notifications,
1961
+ aiTools,
1962
+ aiAgents,
1963
+ })
765
1964
 
766
- return {
1965
+ const indexSourcePath = portableOf(indexFilePath)
1966
+ const diSourcePath = portableOf(diFilePath)
1967
+ const moduleMetadataFact = extractModuleMetadataContract(indexFilePath, moduleId, indexSourcePath)
1968
+ const commandCandidates = extractCommandContractCandidates(moduleId, moduleRoot, sourceRoot)
1969
+ const commandFacts = dedupeOwnedContractFacts(
1970
+ commandCandidates.map((candidate) => ({
1971
+ kind: 'command' as const,
1972
+ id: candidate.id,
1973
+ source: { sourcePath: candidate.sourcePath },
1974
+ })),
1975
+ )
1976
+ const workers = extractWorkerContracts(moduleId, moduleRoot, sourceRoot)
1977
+ const pageMiddleware = extractPageMiddlewareContracts(moduleRoot, sourceRoot)
1978
+ const setupFact = extractSetupContract(moduleId, setupFilePath, portableOf(setupFilePath))
1979
+ const encryptionFacts = extractEncryptionContracts(encryptionFilePath, portableOf(encryptionFilePath))
1980
+ const customEntityFacts = extractCustomEntityContracts(ceFilePath, portableOf(ceFilePath))
1981
+ const aiExtensionFacts = extractAiExtensionContracts(moduleRoot, sourceRoot)
1982
+ const generatorPlugins = extractGeneratorPluginContracts(moduleRoot, sourceRoot)
1983
+ const diContractFacts = dedupeOwnedContractFacts(
1984
+ diRegistrations.facts.map((fact) => ({
1985
+ kind: 'di-registration' as const,
1986
+ id: fact.token,
1987
+ source: fact.source,
1988
+ metadata: {
1989
+ registrationKind: fact.registrationKind,
1990
+ ...(fact.providerSymbol ? { providerSymbol: fact.providerSymbol } : {}),
1991
+ ...(fact.lifetime ? { lifetime: fact.lifetime } : {}),
1992
+ ...(fact.injectionMode ? { injectionMode: fact.injectionMode } : {}),
1993
+ },
1994
+ })),
1995
+ )
1996
+
1997
+ const ownedContracts = buildOwnedContracts([
1998
+ ['module-metadata', moduleMetadataFact ? [moduleMetadataFact] : []],
1999
+ ['command', commandFacts],
2000
+ ['worker', workers.facts],
2001
+ ['page-middleware', pageMiddleware.facts],
2002
+ ['setup', setupFact ? [setupFact] : []],
2003
+ ['encryption', encryptionFacts],
2004
+ ['di-registration', diContractFacts],
2005
+ ['custom-entity', customEntityFacts],
2006
+ ['ai-extension', aiExtensionFacts],
2007
+ ['generator-plugin', generatorPlugins.facts],
2008
+ ])
2009
+
2010
+ const provenanceCandidates: ProvenanceCandidate[] = []
2011
+ const pushSource = (kind: ModuleFactSourceKind, id: string, source: ModuleFactSourceRef): void => {
2012
+ provenanceCandidates.push({ kind, id, source })
2013
+ }
2014
+ const pushRef = (
2015
+ kind: ModuleFactSourceKind,
2016
+ id: string,
2017
+ source: ModuleFactSourceRef,
2018
+ factSection: string,
2019
+ factKey: string,
2020
+ ): void => {
2021
+ provenanceCandidates.push({ kind, id, source, factRef: { factSection, factKey } })
2022
+ }
2023
+ const pushOwnedRefs = (kind: ModuleOwnedContractKind, facts: readonly ModuleOwnedContractFact[]): void => {
2024
+ for (const fact of facts) pushRef(kind, fact.id, fact.source, `ownedContracts.${kind}`, fact.id)
2025
+ }
2026
+ if (moduleMetadataFact) {
2027
+ pushRef(
2028
+ 'module-metadata',
2029
+ moduleId,
2030
+ moduleMetadataFact.source,
2031
+ 'ownedContracts.module-metadata',
2032
+ moduleMetadataFact.id,
2033
+ )
2034
+ }
2035
+ if (entitiesFilePath) {
2036
+ const entitiesSourcePath = portableOf(entitiesFilePath) as string
2037
+ for (const entity of entities) {
2038
+ pushSource('entity', entity.id, { sourcePath: entitiesSourcePath, exportName: entity.class })
2039
+ }
2040
+ }
2041
+ if (eventsFilePath) {
2042
+ const eventsSourcePath = portableOf(eventsFilePath) as string
2043
+ for (const event of events) pushSource('event', event.id, { sourcePath: eventsSourcePath, exportName: 'events' })
2044
+ }
2045
+ if (aclFilePath) {
2046
+ const aclSourcePath = portableOf(aclFilePath) as string
2047
+ for (const feature of aclFeatures) pushSource('acl-feature', feature, { sourcePath: aclSourcePath, exportName: 'features' })
2048
+ }
2049
+ for (const route of apiRoutes) {
2050
+ if (route.sourcePath) {
2051
+ pushRef('api-route', route.path, { sourcePath: route.sourcePath }, 'apiRoutes', route.path)
2052
+ }
2053
+ }
2054
+ if (searchFilePath) {
2055
+ const searchSourcePath = portableOf(searchFilePath) as string
2056
+ for (const entityId of searchEntities) pushSource('search', entityId, { sourcePath: searchSourcePath, exportName: 'searchConfig' })
2057
+ }
2058
+ if (notificationsFilePath) {
2059
+ const notificationsSourcePath = portableOf(notificationsFilePath) as string
2060
+ for (const id of notifications) pushSource('notification', id, { sourcePath: notificationsSourcePath })
2061
+ }
2062
+ for (const command of cliCommands) {
2063
+ pushRef('cli-command', command.command, { sourcePath: command.sourcePath }, 'cliCommands', command.command)
2064
+ }
2065
+ for (const page of backendPages) {
2066
+ pushRef('backend-page', page.path, { sourcePath: page.sourcePath }, 'backendPages', page.path)
2067
+ }
2068
+ for (const page of frontendPages) {
2069
+ pushRef('frontend-page', page.path, { sourcePath: page.sourcePath }, 'frontendPages', page.path)
2070
+ }
2071
+ for (const tool of aiTools) pushRef('ai-tool', tool.name, { sourcePath: tool.sourcePath }, 'aiTools', tool.name)
2072
+ for (const agent of aiAgents) pushRef('ai-agent', agent.id, { sourcePath: agent.sourcePath }, 'aiAgents', agent.id)
2073
+ for (const candidate of commandCandidates) {
2074
+ pushRef('command', candidate.id, { sourcePath: candidate.sourcePath }, 'ownedContracts.command', candidate.id)
2075
+ }
2076
+ pushOwnedRefs('worker', workers.facts)
2077
+ pushOwnedRefs('page-middleware', pageMiddleware.facts)
2078
+ if (setupFact) pushOwnedRefs('setup', [setupFact])
2079
+ pushOwnedRefs('encryption', encryptionFacts)
2080
+ for (const fact of diRegistrations.facts) {
2081
+ pushRef('di-registration', fact.token, fact.source, 'ownedContracts.di-registration', fact.token)
2082
+ }
2083
+ pushOwnedRefs('custom-entity', customEntityFacts)
2084
+ pushOwnedRefs('ai-extension', aiExtensionFacts)
2085
+ pushOwnedRefs('generator-plugin', generatorPlugins.facts)
2086
+ const contributionRef = (contributionId: string): { factSection: string; factKey: string } => ({
2087
+ factSection: 'extensionSurfaces.contributions',
2088
+ factKey: contributionId,
2089
+ })
2090
+ for (const contribution of extensionSurfaces.contributions) {
2091
+ const contributionSource: ModuleFactSourceRef = {
2092
+ sourcePath: contribution.source.path,
2093
+ ...(contribution.source.symbol ? { exportName: contribution.source.symbol } : {}),
2094
+ }
2095
+ const ref = contributionRef(contribution.id)
2096
+ if (contribution.kind === 'subscriber') {
2097
+ pushRef('subscriber', contribution.id, contributionSource, ref.factSection, ref.factKey)
2098
+ continue
2099
+ }
2100
+ if (contribution.kind === 'specialized-registry') {
2101
+ const details = contribution.details as { registry?: string; registryId?: string }
2102
+ const sourceKind = details.registry ? REGISTRY_TO_SOURCE_KIND[details.registry] : undefined
2103
+ if (sourceKind) {
2104
+ pushRef(
2105
+ sourceKind,
2106
+ details.registryId ?? contribution.id,
2107
+ contributionSource,
2108
+ ref.factSection,
2109
+ ref.factKey,
2110
+ )
2111
+ }
2112
+ continue
2113
+ }
2114
+ const mapped = CONTRIBUTION_KIND_TO_SOURCE_KIND[contribution.kind]
2115
+ if (mapped) pushRef(mapped, contribution.id, contributionSource, ref.factSection, ref.factKey)
2116
+ }
2117
+ // extension-host / extension-contribution provenance is emitted as typed `factRef` pointers
2118
+ // rather than duplicated source refs: those facts already carry their own `source` inside
2119
+ // `extensionSurfaces.{hosts,contributions}`, and copying every declared host spot (hundreds
2120
+ // per large module) would push the shared JSON past the determinism byte guard asserted in
2121
+ // `module-facts.bc-guard.test.ts`.
2122
+ // Hosts are indexed by their registry `key`: `id` repeats across families (an entity id is
2123
+ // also an api-entity id), while `key` is unique inside a module.
2124
+ for (const host of extensionSurfaces.hosts) {
2125
+ provenanceCandidates.push({
2126
+ kind: 'extension-host',
2127
+ id: host.key,
2128
+ ...(host.source.kind === 'fact-ref' ? {} : { source: { sourcePath: host.source.path } }),
2129
+ factRef: { factSection: 'extensionSurfaces.hosts', factKey: host.key },
2130
+ })
2131
+ }
2132
+ for (const contribution of extensionSurfaces.contributions) {
2133
+ pushRef(
2134
+ 'extension-contribution',
2135
+ contribution.id,
2136
+ { sourcePath: contribution.source.path },
2137
+ 'extensionSurfaces.contributions',
2138
+ contribution.id,
2139
+ )
2140
+ }
2141
+
2142
+ const provenance = buildProvenanceIndex(provenanceCandidates)
2143
+ const factDiagnostics = [
2144
+ ...provenance.diagnostics,
2145
+ ...workers.unresolved,
2146
+ ...pageMiddleware.unresolved,
2147
+ ...generatorPlugins.unresolved,
2148
+ ...diRegistrations.unresolvedTokens.map((token): ModuleFactDiagnostic => ({
2149
+ code: 'unresolved-static-contract',
2150
+ kind: 'di-registration',
2151
+ id: token,
2152
+ ...(diSourcePath ? { source: { sourcePath: diSourcePath, exportName: token } } : {}),
2153
+ })),
2154
+ ].sort(
2155
+ (left, right) =>
2156
+ left.code.localeCompare(right.code) ||
2157
+ left.kind.localeCompare(right.kind) ||
2158
+ left.id.localeCompare(right.id),
2159
+ )
2160
+
2161
+ const facts: ModuleFacts = {
767
2162
  module: moduleId,
768
2163
  title,
769
2164
  description,
770
2165
  coreVersion,
2166
+ sourcePackage,
2167
+ sourceVersion,
2168
+ sourceRoot,
771
2169
  entities,
772
2170
  events,
773
2171
  aclFeatures,
@@ -777,20 +2175,43 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
777
2175
  hostTokens,
778
2176
  notifications,
779
2177
  cli,
2178
+ backendPages,
2179
+ frontendPages,
2180
+ cliCommands,
2181
+ aiTools,
2182
+ aiAgents,
2183
+ extensionSurfaces,
2184
+ ...(provenance.factSources.length > 0 ? { factSources: provenance.factSources } : {}),
2185
+ ...(ownedContracts ? { ownedContracts } : {}),
2186
+ ...(factDiagnostics.length > 0 ? { factDiagnostics } : {}),
780
2187
  warnings,
781
2188
  }
2189
+
2190
+ // Exact unified override targets (spec 2026-08-02-module-facts-exact-override-targets).
2191
+ // Adapters consume the fully-populated owned facts + contributions above.
2192
+ const overrides = collectModuleOverrideTargets(facts, buildFactSourceLookup(facts))
2193
+ if (overrides.overrideTargets.length > 0) facts.overrideTargets = overrides.overrideTargets
2194
+ if (overrides.overrideTargetDiagnostics.length > 0) {
2195
+ facts.overrideTargetDiagnostics = overrides.overrideTargetDiagnostics
2196
+ }
2197
+ return facts
782
2198
  }
783
2199
 
784
2200
  export interface ModuleFactsJsonEvent {
785
2201
  id: string
786
2202
  category: string | null
787
2203
  entity: string | null
2204
+ clientBroadcast?: boolean
2205
+ portalBroadcast?: boolean
788
2206
  }
789
2207
 
790
2208
  export interface ModuleFactsJsonEntry {
791
2209
  title: string | null
792
2210
  description: string | null
793
2211
  coreVersion: string | null
2212
+ sourcePackage: string | null
2213
+ sourceVersion: string | null
2214
+ sourceRoot: string
794
2215
  entities: ModuleEntityFact[]
795
2216
  events: ModuleFactsJsonEvent[]
796
2217
  aclFeatures: string[]
@@ -800,32 +2221,54 @@ export interface ModuleFactsJsonEntry {
800
2221
  hostTokens: ModuleHostTokens
801
2222
  notifications: string[]
802
2223
  cli: string[]
2224
+ backendPages: ModulePageFact[]
2225
+ frontendPages: ModulePageFact[]
2226
+ cliCommands: ModuleCliCommandFact[]
2227
+ aiTools: ModuleAiToolFact[]
2228
+ aiAgents: ModuleAiAgentFact[]
2229
+ extensionSurfaces?: ModuleExtensionSurfaceFacts
2230
+ factSources?: ModuleFactSourceEntry[]
2231
+ ownedContracts?: ModuleOwnedContracts
2232
+ factDiagnostics?: ModuleFactDiagnostic[]
2233
+ overrideTargets?: ModuleOverrideTarget[]
2234
+ overrideTargetDiagnostics?: ModuleOverrideTargetDiagnostic[]
803
2235
  }
804
2236
 
805
2237
  const EMPTY_SECTION_MARKER = '_none_'
806
2238
 
807
- function renderVersionStamp(coreVersion: string | null): string {
2239
+ function renderVersionStamp(
2240
+ coreVersion: string | null,
2241
+ sourcePackage: string | null,
2242
+ sourceVersion: string | null,
2243
+ ): string {
2244
+ if (sourcePackage) {
2245
+ return `<!-- generated from ${sourcePackage} ${sourceVersion || '<unknown>'}; core ${coreVersion || '<unknown>'} — R1 staleness stamp -->`
2246
+ }
808
2247
  const version = coreVersion && coreVersion.length > 0 ? coreVersion : '<unknown>'
809
2248
  return `<!-- generated from @open-mercato/core ${version} — R1 staleness stamp -->`
810
2249
  }
811
2250
 
812
- function renderEntitiesSection(entities: ModuleEntityFact[]): string {
2251
+ function renderEntitiesSection(entities: ModuleEntityFact[], lookup: FactSourceLookup): string {
813
2252
  if (entities.length === 0) return `## Entities\n\n${EMPTY_SECTION_MARKER}`
814
- const header = '| Entity ID | Class | Table | Editable | CustomFields |'
815
- const divider = '|---|---|---|---|---|'
2253
+ const header = '| Entity ID | Class | Table | Editable | CustomFields | Source |'
2254
+ const divider = '|---|---|---|---|---|---|'
816
2255
  const rows = entities.map(
817
2256
  (entity) =>
818
- `| ${entity.id} | ${entity.class} | ${entity.table} | ${entity.editable ? 'yes' : 'no'} | ${entity.customFields ? 'yes' : 'no'} |`,
2257
+ `| ${entity.id} | ${entity.class} | ${entity.table} | ${entity.editable ? 'yes' : 'no'} | ${entity.customFields ? 'yes' : 'no'} | ${renderLookedUpSource(lookup, 'entity', entity.id)} |`,
819
2258
  )
820
2259
  return ['## Entities', '', header, divider, ...rows].join('\n')
821
2260
  }
822
2261
 
823
- function renderEventsSection(events: ModuleEventFact[]): string {
2262
+ function renderEventsSection(events: ModuleEventFact[], lookup: FactSourceLookup): string {
824
2263
  const heading = `## Events (${events.length})`
825
2264
  if (events.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
826
- const header = '| ID | Category | Entity |'
827
- const divider = '|---|---|---|'
828
- const rows = events.map((event) => `| ${event.id} | ${event.category ?? '—'} | ${event.entity ?? '—'} |`)
2265
+ const header = '| ID | Category | Entity | Browser transport | Source |'
2266
+ const divider = '|---|---|---|---|---|'
2267
+ const rows = events.map((event) => {
2268
+ const transports = [event.clientBroadcast ? 'client' : null, event.portalBroadcast ? 'portal' : null]
2269
+ .filter((value): value is string => value !== null)
2270
+ return `| ${event.id} | ${event.category ?? '—'} | ${event.entity ?? '—'} | ${transports.join(', ') || '—'} | ${renderLookedUpSource(lookup, 'event', event.id)} |`
2271
+ })
829
2272
  return [heading, '', header, divider, ...rows].join('\n')
830
2273
  }
831
2274
 
@@ -834,6 +2277,73 @@ function renderInlineListSection(heading: string, values: string[]): string {
834
2277
  return `${heading}\n\n${values.join(' · ')}`
835
2278
  }
836
2279
 
2280
+ function renderSourceLinkedListSection(
2281
+ heading: string,
2282
+ values: readonly string[],
2283
+ lookup: FactSourceLookup,
2284
+ kind: ModuleFactSourceKind,
2285
+ idColumnLabel: string,
2286
+ ): string {
2287
+ if (values.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
2288
+ const rows = values.map((value) => `| ${value} | ${renderLookedUpSource(lookup, kind, value)} |`)
2289
+ return [heading, '', `| ${idColumnLabel} | Source |`, '|---|---|', ...rows].join('\n')
2290
+ }
2291
+
2292
+ function renderSourceLink(sourcePath: string): string {
2293
+ return `[${sourcePath}](../../../${sourcePath})`
2294
+ }
2295
+
2296
+ function renderSourceRefLink(source: ModuleFactSourceRef): string {
2297
+ const anchor = source.line ? `#L${source.line}` : ''
2298
+ const label = source.line ? `${source.sourcePath}:${source.line}` : source.sourcePath
2299
+ return `[${label}](../../../${source.sourcePath}${anchor})`
2300
+ }
2301
+
2302
+
2303
+ function renderLookedUpSource(lookup: FactSourceLookup, kind: ModuleFactSourceKind, id: string): string {
2304
+ const source = lookup(kind, id)
2305
+ return source ? renderSourceRefLink(source) : '—'
2306
+ }
2307
+
2308
+ function renderSafeScalar(value: ModuleFactSafeScalar | ModuleFactSafeScalar[] | undefined): string {
2309
+ if (value === undefined || value === null) return '—'
2310
+ if (Array.isArray(value)) return value.length > 0 ? value.map((entry) => String(entry)).join(', ') : '—'
2311
+ return String(value)
2312
+ }
2313
+
2314
+ function renderOwnedContractMetadata(metadata: ModuleOwnedContractFact['metadata']): string {
2315
+ if (!metadata) return '—'
2316
+ const keys = Object.keys(metadata).sort((left, right) => left.localeCompare(right))
2317
+ if (keys.length === 0) return '—'
2318
+ return keys.map((key) => `${key}=${renderSafeScalar(metadata[key])}`).join('; ')
2319
+ }
2320
+
2321
+ function renderOwnedContractSection(
2322
+ heading: string,
2323
+ facts: readonly ModuleOwnedContractFact[] | undefined,
2324
+ ): string {
2325
+ if (!facts || facts.length === 0) return ''
2326
+ const rows = facts.map(
2327
+ (fact) => `| ${fact.id} | ${renderOwnedContractMetadata(fact.metadata)} | ${renderSourceRefLink(fact.source)} |`,
2328
+ )
2329
+ return [heading, '', '| ID | Metadata | Source |', '|---|---|---|', ...rows].join('\n')
2330
+ }
2331
+
2332
+ function renderDiRegistrationsSection(facts: readonly ModuleOwnedContractFact[] | undefined): string {
2333
+ if (!facts || facts.length === 0) return ''
2334
+ const rows = facts.map((fact) => {
2335
+ const metadata = fact.metadata ?? {}
2336
+ return `| ${fact.id} | ${renderSafeScalar(metadata.registrationKind)} | ${renderSafeScalar(metadata.lifetime)} | ${renderSafeScalar(metadata.injectionMode)} | ${renderSafeScalar(metadata.providerSymbol)} | ${renderSourceRefLink(fact.source)} |`
2337
+ })
2338
+ return [
2339
+ '## DI registrations (rich)',
2340
+ '',
2341
+ '| Token | Kind | Lifetime | Injection | Provider | Source |',
2342
+ '|---|---|---|---|---|---|',
2343
+ ...rows,
2344
+ ].join('\n')
2345
+ }
2346
+
837
2347
  function describeAuthRule(rule: ApiRouteAuthRule | undefined): string {
838
2348
  if (!rule) return 'public'
839
2349
  if (rule.requireFeatures && rule.requireFeatures.length > 0) return rule.requireFeatures.join(', ')
@@ -856,44 +2366,253 @@ function renderApiRouteAuthCell(route: ModuleApiRouteFact): string {
856
2366
 
857
2367
  function renderApiRoutesSection(routes: ModuleApiRouteFact[]): string {
858
2368
  if (routes.length === 0) return `## API routes\n\n${EMPTY_SECTION_MARKER}`
859
- const header = '| Path | Methods | Auth (per-method requireFeatures) |'
860
- const divider = '|---|---|---|'
2369
+ const header = '| Path | Methods | Auth (per-method requireFeatures) | Source |'
2370
+ const divider = '|---|---|---|---|'
861
2371
  const rows = routes.map(
862
- (route) => `| ${route.path} | ${route.methods.join(' ')} | ${renderApiRouteAuthCell(route)} |`,
2372
+ (route) => `| ${route.path} | ${route.methods.join(' ')} | ${renderApiRouteAuthCell(route)} | ${route.sourcePath ? renderSourceLink(route.sourcePath) : '—'} |`,
863
2373
  )
864
2374
  return ['## API routes', '', header, divider, ...rows].join('\n')
865
2375
  }
866
2376
 
2377
+ function renderLinkedFactsSection(
2378
+ heading: string,
2379
+ facts: ReadonlyArray<{ label: string; sourcePath: string }>,
2380
+ ): string {
2381
+ if (facts.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
2382
+ const rows = facts.map((fact) => `| ${fact.label} | ${renderSourceLink(fact.sourcePath)} |`)
2383
+ return [heading, '', '| ID / path | Source |', '|---|---|', ...rows].join('\n')
2384
+ }
2385
+
867
2386
  function renderHostTokensSection(hostTokens: ModuleHostTokens): string {
868
2387
  const entityIdsLine = hostTokens.entityIds.length > 0 ? hostTokens.entityIds.join(' · ') : EMPTY_SECTION_MARKER
869
2388
  const tableIdsLine = hostTokens.tableIds.length > 0 ? hostTokens.tableIds.join(' · ') : EMPTY_SECTION_MARKER
870
2389
  return ['## Host extension points', '', `- Entity IDs: ${entityIdsLine}`, `- Table IDs: ${tableIdsLine}`].join('\n')
871
2390
  }
872
2391
 
2392
+ function renderExtensionHostContext(host: ModuleExtensionSurfaceFacts['hosts'][number]): string {
2393
+ return host.contextContract ?? host.runtimeContract ?? host.scopeContract ?? '—'
2394
+ }
2395
+
2396
+ function renderExtensionHostSource(host: ModuleExtensionHostFact): string {
2397
+ if (host.source.kind === 'fact-ref') return `${host.source.factSection}:${host.source.factKey}`
2398
+ return renderSourceRefLink({ sourcePath: host.source.path })
2399
+ }
2400
+
2401
+ function renderExtensionHostsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
2402
+ const boundHosts = extensionSurfaces.hosts.filter((host) => host.bound)
2403
+ if (boundHosts.length === 0) return `## UMES hosts\n\n${EMPTY_SECTION_MARKER}`
2404
+ const rows = boundHosts.map((host) =>
2405
+ `| ${host.id} | ${host.family} | ${host.capabilities.join(', ') || '—'} | ${renderExtensionHostContext(host)} | ${host.stability.toUpperCase()} | ${renderExtensionHostSource(host)} |`,
2406
+ )
2407
+ return [
2408
+ '## UMES hosts',
2409
+ '',
2410
+ '| ID / pattern | Family | Supports | Context | Stability | Source |',
2411
+ '|---|---|---|---|---|---|',
2412
+ ...rows,
2413
+ ].join('\n')
2414
+ }
2415
+
2416
+ function compactContributionDetails(contribution: ModuleExtensionContributionFact): string {
2417
+ const details = contribution.details as unknown as Record<string, unknown>
2418
+ return Object.keys(details).sort((left, right) => left.localeCompare(right)).flatMap((key) => {
2419
+ const value = details[key]
2420
+ if (value === undefined) return []
2421
+ if (Array.isArray(value)) return [`${key}=${value.join(',') || 'none'}`]
2422
+ if (value && typeof value === 'object') {
2423
+ const nested = Object.keys(value as Record<string, unknown>).sort((left, right) => left.localeCompare(right)).map((nestedKey) => {
2424
+ const nestedValue = (value as Record<string, unknown>)[nestedKey]
2425
+ return `${nestedKey}:${Array.isArray(nestedValue) ? nestedValue.join(',') : String(nestedValue)}`
2426
+ })
2427
+ return [`${key}={${nested.join(';')}}`]
2428
+ }
2429
+ return [`${key}=${String(value)}`]
2430
+ }).join('; ')
2431
+ }
2432
+
2433
+ function renderExtensionContributionsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
2434
+ if (extensionSurfaces.contributions.length === 0) return `## UMES contributions\n\n${EMPTY_SECTION_MARKER}`
2435
+ const rows = extensionSurfaces.contributions.map((contribution) => {
2436
+ const targets = contribution.targets.map((entry) => entry.id).join(', ')
2437
+ const resolution = contribution.targets.map((entry) => entry.resolution).join(', ')
2438
+ const phases = [...(contribution.phases ?? []), ...(contribution.operations ?? [])].join(', ') || '—'
2439
+ const source = renderSourceRefLink({ sourcePath: contribution.source.path })
2440
+ return `| ${contribution.id} | ${contribution.kind} | ${targets || '—'} | ${phases} | ${compactContributionDetails(contribution)} | ${resolution || '—'} | ${source} |`
2441
+ })
2442
+ return [
2443
+ '## UMES contributions',
2444
+ '',
2445
+ '| ID | Kind | Target | Phase / operations | Contract | Resolution | Source |',
2446
+ '|---|---|---|---|---|---|---|',
2447
+ ...rows,
2448
+ ].join('\n')
2449
+ }
2450
+
2451
+ function renderExtensionDiagnosticsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
2452
+ const unbound = extensionSurfaces.hosts.filter((host) => !host.bound)
2453
+ if (unbound.length === 0 && extensionSurfaces.unresolved.length === 0) return ''
2454
+ const diagnostics = [
2455
+ ...unbound.map((host) => `- unbound-helper: ${host.id}`),
2456
+ ...extensionSurfaces.unresolved.map((entry) => `- ${entry.reason}: ${entry.key} (${entry.source.path})`),
2457
+ ]
2458
+ return ['## UMES diagnostics', '', ...diagnostics].join('\n')
2459
+ }
2460
+
2461
+ function renderExtensionTargetRef(target: ModuleExtensionTargetRef): string {
2462
+ const method = target.method ? ` ${target.method}` : ''
2463
+ const owner = target.moduleId ? ` @${target.moduleId}` : ''
2464
+ return `${target.kind}:${target.id}${method}${owner}`
2465
+ }
2466
+
2467
+ function renderActivationBindingsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
2468
+ const activations = extensionSurfaces.activations ?? []
2469
+ if (activations.length === 0) return `## Active extension bindings\n\n${EMPTY_SECTION_MARKER}`
2470
+ const rows = activations.map((activation) => {
2471
+ const phases = activation.phases && activation.phases.length > 0 ? activation.phases.join(', ') : '—'
2472
+ const source = activation.source ? renderSourceRefLink(activation.source) : '—'
2473
+ const bridge = activation.bridge ? `${activation.bridge.factSection}:${activation.bridge.factKey}` : '—'
2474
+ return `| ${activation.id} | ${activation.kind} | ${renderExtensionTargetRef(activation.host)} | ${activation.contributionKinds.join(', ') || '—'} | ${phases} | ${bridge} | ${source} |`
2475
+ })
2476
+ return [
2477
+ '## Active extension bindings',
2478
+ '',
2479
+ '| Activation | Kind | Host | Contribution kinds | Phases | Bridge | Source |',
2480
+ '|---|---|---|---|---|---|---|',
2481
+ ...rows,
2482
+ ].join('\n')
2483
+ }
2484
+
2485
+ function renderIncomingContributionsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
2486
+ const incoming = extensionSurfaces.incoming ?? []
2487
+ if (incoming.length === 0) return `## Incoming installed contributions\n\n${EMPTY_SECTION_MARKER}`
2488
+ const rows = incoming.map((entry) => {
2489
+ const activation = entry.activationId ?? '—'
2490
+ const source = entry.source ? renderSourceRefLink(entry.source) : '—'
2491
+ return `| ${entry.contributorModuleId} | ${entry.contributionKind} | ${renderExtensionTargetRef(entry.target)} | ${entry.resolution} | ${activation} | ${entry.contributionId} · ${source} |`
2492
+ })
2493
+ return [
2494
+ '## Incoming installed contributions',
2495
+ '',
2496
+ '| Contributor | Kind | Target | Resolution | Activation | Contribution · Source |',
2497
+ '|---|---|---|---|---|---|',
2498
+ ...rows,
2499
+ ].join('\n')
2500
+ }
2501
+
2502
+ function renderContributionResolutionsSection(
2503
+ extensionSurfaces: ModuleExtensionSurfaceFacts,
2504
+ lookup: FactSourceLookup,
2505
+ ): string {
2506
+ const resolutions = extensionSurfaces.contributionResolutions ?? []
2507
+ if (resolutions.length === 0) return `## Contribution resolutions\n\n${EMPTY_SECTION_MARKER}`
2508
+ const rows = resolutions.map((resolution) => {
2509
+ const activations = resolution.activationIds.length > 0 ? resolution.activationIds.join(', ') : '—'
2510
+ const source = renderLookedUpSource(lookup, 'extension-contribution', resolution.contributionId)
2511
+ return `| ${resolution.contributionId} | ${renderExtensionTargetRef(resolution.target)} | ${resolution.resolution} | ${activations} | ${source} |`
2512
+ })
2513
+ return [
2514
+ '## Contribution resolutions',
2515
+ '',
2516
+ '| Contribution | Target | Resolution | Activations | Source |',
2517
+ '|---|---|---|---|---|',
2518
+ ...rows,
2519
+ ].join('\n')
2520
+ }
2521
+
2522
+ function renderOverrideTargetPath(target: ModuleOverrideTarget): string {
2523
+ let display = 'overrides'
2524
+ for (let index = 0; index < target.path.length; index += 1) {
2525
+ const segment = target.path[index]
2526
+ const isKeyedTerminal = target.key !== undefined && index === target.path.length - 1
2527
+ display += isKeyedTerminal ? `[${JSON.stringify(segment)}]` : `.${segment}`
2528
+ }
2529
+ return display
2530
+ }
2531
+
2532
+ function renderOverrideTargetsSection(targets: readonly ModuleOverrideTarget[] | undefined): string {
2533
+ if (!targets || targets.length === 0) return `## Exact override targets\n\n${EMPTY_SECTION_MARKER}`
2534
+ const header = '| Domain | Path / key | Supported modes | Referenced fact | Source |'
2535
+ const divider = '|---|---|---|---|---|'
2536
+ const rows = targets.map((target) => {
2537
+ const path = `\`${renderOverrideTargetPath(target)}\``
2538
+ const modes = target.modes.join(', ') || '—'
2539
+ const fact = `${target.factRef.factSection}:${target.factRef.factKey}`
2540
+ return `| ${target.domain} | ${path} | ${modes} | ${fact} | ${renderSourceRefLink(target.source)} |`
2541
+ })
2542
+ return ['## Exact override targets', '', header, divider, ...rows].join('\n')
2543
+ }
2544
+
873
2545
  export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
2546
+ const extensionSurfaces = facts.extensionSurfaces ?? { hosts: [], contributions: [], unresolved: [] }
2547
+ const lookup = buildFactSourceLookup(facts)
874
2548
  const sections = [
875
2549
  `# ${facts.module} — module facts (generated, do not edit)`,
876
- renderVersionStamp(facts.coreVersion),
2550
+ renderVersionStamp(facts.coreVersion, facts.sourcePackage, facts.sourceVersion),
2551
+ `Source root: ${renderSourceLink(facts.sourceRoot)}`,
877
2552
  '',
878
- renderEntitiesSection(facts.entities),
2553
+ renderEntitiesSection(facts.entities, lookup),
879
2554
  '',
880
- renderEventsSection(facts.events),
2555
+ renderEventsSection(facts.events, lookup),
881
2556
  '',
882
- renderInlineListSection(`## ACL features (${facts.aclFeatures.length})`, facts.aclFeatures),
2557
+ renderSourceLinkedListSection(
2558
+ `## ACL features (${facts.aclFeatures.length})`,
2559
+ facts.aclFeatures,
2560
+ lookup,
2561
+ 'acl-feature',
2562
+ 'Feature',
2563
+ ),
883
2564
  '',
884
2565
  renderApiRoutesSection(facts.apiRoutes),
885
2566
  '',
886
- renderInlineListSection('## DI service tokens', facts.diTokens),
2567
+ renderLinkedFactsSection('## Backend pages', facts.backendPages.map((page) => ({ label: page.path, sourcePath: page.sourcePath }))),
2568
+ '',
2569
+ renderLinkedFactsSection('## Frontend pages', facts.frontendPages.map((page) => ({ label: page.path, sourcePath: page.sourcePath }))),
887
2570
  '',
888
- renderInlineListSection('## Search entities', facts.searchEntities),
2571
+ renderSourceLinkedListSection('## DI service tokens', facts.diTokens, lookup, 'di-registration', 'Token'),
2572
+ '',
2573
+ renderSourceLinkedListSection('## Search entities', facts.searchEntities, lookup, 'search', 'Entity ID'),
889
2574
  '',
890
2575
  renderHostTokensSection(facts.hostTokens),
891
2576
  '',
892
- renderInlineListSection('## Notifications', facts.notifications),
2577
+ renderExtensionHostsSection(extensionSurfaces),
2578
+ '',
2579
+ renderExtensionContributionsSection(extensionSurfaces),
2580
+ '',
2581
+ renderActivationBindingsSection(extensionSurfaces),
2582
+ '',
2583
+ renderIncomingContributionsSection(extensionSurfaces),
2584
+ '',
2585
+ renderContributionResolutionsSection(extensionSurfaces, lookup),
893
2586
  '',
894
- renderInlineListSection('## CLI', facts.cli),
2587
+ renderExtensionDiagnosticsSection(extensionSurfaces),
2588
+ '',
2589
+ renderSourceLinkedListSection('## Notifications', facts.notifications, lookup, 'notification', 'Notification ID'),
2590
+ '',
2591
+ renderLinkedFactsSection('## CLI commands', facts.cliCommands.map((command) => ({ label: command.command, sourcePath: command.sourcePath }))),
2592
+ '',
2593
+ renderLinkedFactsSection('## AI tools / MCP capabilities', facts.aiTools.map((tool) => ({ label: tool.name, sourcePath: tool.sourcePath }))),
2594
+ '',
2595
+ renderLinkedFactsSection('## AI agents', facts.aiAgents.map((agent) => ({ label: agent.id, sourcePath: agent.sourcePath }))),
2596
+ '',
2597
+ renderOverrideTargetsSection(facts.overrideTargets),
895
2598
  '',
896
2599
  ]
2600
+ const owned = facts.ownedContracts ?? {}
2601
+ const ownedSections = [
2602
+ renderOwnedContractSection('## Owned contract — module metadata', owned['module-metadata']),
2603
+ renderOwnedContractSection('## Domain commands', owned.command),
2604
+ renderOwnedContractSection('## Workers', owned.worker),
2605
+ renderOwnedContractSection('## Page middleware', owned['page-middleware']),
2606
+ renderOwnedContractSection('## Setup', owned.setup),
2607
+ renderOwnedContractSection('## Encryption', owned.encryption),
2608
+ renderDiRegistrationsSection(owned['di-registration']),
2609
+ renderOwnedContractSection('## Custom entities', owned['custom-entity']),
2610
+ renderOwnedContractSection('## AI extensions', owned['ai-extension']),
2611
+ renderOwnedContractSection('## Generator plugins', owned['generator-plugin']),
2612
+ ].filter((section) => section.length > 0)
2613
+ for (const section of ownedSections) {
2614
+ sections.push(section, '')
2615
+ }
897
2616
  return sections.join('\n')
898
2617
  }
899
2618
 
@@ -902,8 +2621,17 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
902
2621
  title: facts.title,
903
2622
  description: facts.description,
904
2623
  coreVersion: facts.coreVersion,
2624
+ sourcePackage: facts.sourcePackage,
2625
+ sourceVersion: facts.sourceVersion,
2626
+ sourceRoot: facts.sourceRoot,
905
2627
  entities: facts.entities,
906
- events: facts.events.map((event) => ({ id: event.id, category: event.category, entity: event.entity })),
2628
+ events: facts.events.map((event) => ({
2629
+ id: event.id,
2630
+ category: event.category,
2631
+ entity: event.entity,
2632
+ ...(event.clientBroadcast !== undefined ? { clientBroadcast: event.clientBroadcast } : {}),
2633
+ ...(event.portalBroadcast !== undefined ? { portalBroadcast: event.portalBroadcast } : {}),
2634
+ })),
907
2635
  aclFeatures: facts.aclFeatures,
908
2636
  apiRoutes: facts.apiRoutes,
909
2637
  diTokens: facts.diTokens,
@@ -911,6 +2639,25 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
911
2639
  hostTokens: facts.hostTokens,
912
2640
  notifications: facts.notifications,
913
2641
  cli: facts.cli,
2642
+ backendPages: facts.backendPages,
2643
+ frontendPages: facts.frontendPages,
2644
+ cliCommands: facts.cliCommands,
2645
+ aiTools: facts.aiTools,
2646
+ aiAgents: facts.aiAgents,
2647
+ ...(facts.extensionSurfaces ? { extensionSurfaces: facts.extensionSurfaces } : {}),
2648
+ ...(facts.factSources && facts.factSources.length > 0 ? { factSources: facts.factSources } : {}),
2649
+ ...(facts.ownedContracts && Object.keys(facts.ownedContracts).length > 0
2650
+ ? { ownedContracts: facts.ownedContracts }
2651
+ : {}),
2652
+ ...(facts.factDiagnostics && facts.factDiagnostics.length > 0
2653
+ ? { factDiagnostics: facts.factDiagnostics }
2654
+ : {}),
2655
+ ...(facts.overrideTargets && facts.overrideTargets.length > 0
2656
+ ? { overrideTargets: facts.overrideTargets }
2657
+ : {}),
2658
+ ...(facts.overrideTargetDiagnostics && facts.overrideTargetDiagnostics.length > 0
2659
+ ? { overrideTargetDiagnostics: facts.overrideTargetDiagnostics }
2660
+ : {}),
914
2661
  }
915
2662
  }
916
2663
 
@@ -947,9 +2694,14 @@ export interface ExtractAllModuleFactsResult {
947
2694
  factsByModule: Record<string, ModuleFacts>
948
2695
  markdownByModule: Record<string, string>
949
2696
  warnings: string[]
2697
+ frameworkMarkdown: string
950
2698
  }
951
2699
 
952
2700
  export function extractAllModuleFacts(options: ExtractAllModuleFactsOptions): ExtractAllModuleFactsResult {
2701
+ return withModuleExtensionFactExtractionCache(() => extractAllModuleFactsWithCache(options))
2702
+ }
2703
+
2704
+ function extractAllModuleFactsWithCache(options: ExtractAllModuleFactsOptions): ExtractAllModuleFactsResult {
953
2705
  const sources: ModuleFactSource[] = options.sources
954
2706
  ? [...options.sources]
955
2707
  : (options.coreSrcRoot
@@ -967,12 +2719,65 @@ export function extractAllModuleFacts(options: ExtractAllModuleFactsOptions): Ex
967
2719
  moduleId: source.moduleId,
968
2720
  moduleRoot: source.moduleRoot,
969
2721
  coreVersion: options.coreVersion ?? null,
2722
+ sourcePackage: source.from ?? null,
2723
+ sourceVersion: source.packageVersion ?? null,
970
2724
  registryPath: options.registryPath ?? null,
971
2725
  registrySource: options.registrySource ?? null,
972
2726
  })
973
2727
  factsByModule[source.moduleId] = facts
974
- markdownByModule[source.moduleId] = renderModuleFactsMarkdown(facts)
975
2728
  warnings.push(...facts.warnings)
976
2729
  }
977
- return { factsByModule, markdownByModule, warnings }
2730
+ const surfacesByModule = Object.fromEntries(
2731
+ Object.entries(factsByModule).map(([moduleId, facts]) => [
2732
+ moduleId,
2733
+ facts.extensionSurfaces ?? { hosts: [], contributions: [], unresolved: [] },
2734
+ ]),
2735
+ )
2736
+ const correlated = correlateModuleExtensionFacts({
2737
+ surfacesByModule,
2738
+ entityIds: new Set(Object.values(factsByModule).flatMap((facts) => facts.entities.map((entity) => entity.id))),
2739
+ eventIds: new Set(Object.values(factsByModule).flatMap((facts) => facts.events.map((event) => event.id))),
2740
+ apiRoutes: new Set([
2741
+ ...Object.values(factsByModule).flatMap((facts) => facts.apiRoutes.map((route) => route.path)),
2742
+ ...sources.flatMap((source) => extractKnownApiRouteIds(source.moduleId, source.moduleRoot)),
2743
+ ]),
2744
+ commandIds: new Set(sources.flatMap((source) => extractKnownCommandIds(source.moduleId, source.moduleRoot))),
2745
+ })
2746
+ assertNoUnresolvedExtensionTargets(correlated)
2747
+ const apiRouteOwners = new Map<string, {
2748
+ moduleId: string
2749
+ source: ModuleFactSourceRef
2750
+ bridges: ApiRouteInterceptorBridge[]
2751
+ }>()
2752
+ const commandOwners = new Map<string, { moduleId: string; source: ModuleFactSourceRef }>()
2753
+ for (const source of [...sources].sort((left, right) => left.moduleId.localeCompare(right.moduleId))) {
2754
+ const facts = factsByModule[source.moduleId]
2755
+ const sourceRoot = facts ? facts.sourceRoot : source.moduleId
2756
+ const owners = extractActivationTargetOwners({
2757
+ moduleId: source.moduleId,
2758
+ moduleRoot: source.moduleRoot,
2759
+ sourceRoot,
2760
+ })
2761
+ for (const route of owners.apiRoutes) {
2762
+ if (!apiRouteOwners.has(route.id)) {
2763
+ apiRouteOwners.set(route.id, { moduleId: source.moduleId, source: route.source, bridges: route.bridges })
2764
+ }
2765
+ }
2766
+ for (const command of owners.commands) {
2767
+ if (!commandOwners.has(command.id)) commandOwners.set(command.id, { moduleId: source.moduleId, source: command.source })
2768
+ }
2769
+ }
2770
+ const withIncoming = correlateIncomingExtensions({
2771
+ surfacesByModule: correlated,
2772
+ apiRouteOwners,
2773
+ commandOwners,
2774
+ })
2775
+ for (const moduleId of Object.keys(factsByModule).sort((left, right) => left.localeCompare(right))) {
2776
+ factsByModule[moduleId].extensionSurfaces = withIncoming[moduleId]
2777
+ markdownByModule[moduleId] = renderModuleFactsMarkdown(factsByModule[moduleId])
2778
+ warnings.push(...correlated[moduleId].unresolved.map((entry) =>
2779
+ `[module-facts] ${moduleId} ${entry.reason}: ${entry.key} (${entry.source.path})`,
2780
+ ))
2781
+ }
2782
+ return { factsByModule, markdownByModule, warnings, frameworkMarkdown: renderFrameworkExtensionPointsMarkdown() }
978
2783
  }