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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/build.mjs +24 -7
  3. package/dist/agentic/claude-code/hooks/entity-migration-check.ts +32 -88
  4. package/dist/agentic/codex/enforcement-rules.md +2 -16
  5. package/dist/agentic/cursor/hooks/entity-migration-check.mjs +19 -50
  6. package/dist/agentic/cursor/rules/entity-guard.mdc +9 -21
  7. package/dist/agentic/cursor/rules/generated-guard.mdc +3 -5
  8. package/dist/agentic/cursor/rules/open-mercato.mdc +3 -29
  9. package/dist/agentic/guides/ai-workflows.md +76 -0
  10. package/dist/agentic/guides/architecture.md +107 -0
  11. package/dist/agentic/guides/backend-ui.md +80 -0
  12. package/dist/agentic/guides/contracts.md +92 -0
  13. package/dist/agentic/guides/extensions.md +121 -0
  14. package/dist/agentic/guides/framework-extension-points.md +36 -0
  15. package/dist/agentic/guides/integrations.md +81 -0
  16. package/dist/agentic/guides/module-facts.json +115537 -3951
  17. package/dist/agentic/guides/module-system.md +12 -127
  18. package/dist/agentic/guides/modules/ai_assistant.md +227 -28
  19. package/dist/agentic/guides/modules/api_docs.md +58 -2
  20. package/dist/agentic/guides/modules/api_keys.md +92 -7
  21. package/dist/agentic/guides/modules/attachments.md +119 -14
  22. package/dist/agentic/guides/modules/audit_logs.md +131 -10
  23. package/dist/agentic/guides/modules/auth.md +276 -33
  24. package/dist/agentic/guides/modules/business_rules.md +149 -14
  25. package/dist/agentic/guides/modules/catalog.md +475 -43
  26. package/dist/agentic/guides/modules/channel_gmail.md +82 -3
  27. package/dist/agentic/guides/modules/channel_imap.md +82 -3
  28. package/dist/agentic/guides/modules/checkout.md +299 -27
  29. package/dist/agentic/guides/modules/communication_channels.md +315 -33
  30. package/dist/agentic/guides/modules/configs.md +104 -8
  31. package/dist/agentic/guides/modules/content.md +57 -2
  32. package/dist/agentic/guides/modules/currencies.md +152 -17
  33. package/dist/agentic/guides/modules/customer_accounts.md +387 -45
  34. package/dist/agentic/guides/modules/customers.md +746 -85
  35. package/dist/agentic/guides/modules/dashboards.md +155 -10
  36. package/dist/agentic/guides/modules/data_sync.md +149 -16
  37. package/dist/agentic/guides/modules/design_system.md +102 -0
  38. package/dist/agentic/guides/modules/dictionaries.md +107 -12
  39. package/dist/agentic/guides/modules/directory.md +148 -15
  40. package/dist/agentic/guides/modules/entities.md +145 -13
  41. package/dist/agentic/guides/modules/events.md +59 -2
  42. package/dist/agentic/guides/modules/feature_toggles.md +119 -9
  43. package/dist/agentic/guides/modules/gateway_stripe.md +92 -4
  44. package/dist/agentic/guides/modules/generators.md +45 -2
  45. package/dist/agentic/guides/modules/inbox_ops.md +207 -27
  46. package/dist/agentic/guides/modules/integrations.md +146 -16
  47. package/dist/agentic/guides/modules/messages.md +223 -24
  48. package/dist/agentic/guides/modules/notifications.md +96 -8
  49. package/dist/agentic/guides/modules/onboarding.md +77 -6
  50. package/dist/agentic/guides/modules/payment_gateways.md +164 -21
  51. package/dist/agentic/guides/modules/perspectives.md +77 -7
  52. package/dist/agentic/guides/modules/planner.md +136 -18
  53. package/dist/agentic/guides/modules/portal.md +93 -2
  54. package/dist/agentic/guides/modules/progress.md +104 -14
  55. package/dist/agentic/guides/modules/query_index.md +154 -15
  56. package/dist/agentic/guides/modules/record_locks.md +206 -21
  57. package/dist/agentic/guides/modules/resources.md +197 -30
  58. package/dist/agentic/guides/modules/sales.md +678 -84
  59. package/dist/agentic/guides/modules/scheduler.md +133 -14
  60. package/dist/agentic/guides/modules/search.md +170 -6
  61. package/dist/agentic/guides/modules/security.md +294 -35
  62. package/dist/agentic/guides/modules/shipping_carriers.md +134 -18
  63. package/dist/agentic/guides/modules/sso.md +151 -26
  64. package/dist/agentic/guides/modules/staff.md +369 -57
  65. package/dist/agentic/guides/modules/storage_s3.md +95 -5
  66. package/dist/agentic/guides/modules/sync_akeneo.md +85 -3
  67. package/dist/agentic/guides/modules/sync_excel.md +75 -6
  68. package/dist/agentic/guides/modules/system_status_overlays.md +60 -2
  69. package/dist/agentic/guides/modules/translations.md +104 -10
  70. package/dist/agentic/guides/modules/webhooks.md +203 -24
  71. package/dist/agentic/guides/modules/wms.md +441 -46
  72. package/dist/agentic/guides/modules/workflows.md +302 -43
  73. package/dist/agentic/guides/spec-delivery.md +22 -0
  74. package/dist/agentic/guides/testing-debugging.md +80 -0
  75. package/dist/agentic/guides/upstream/AGENTS.md +299 -0
  76. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +316 -0
  77. package/dist/agentic/guides/upstream/manifest.json +8 -0
  78. package/dist/agentic/shared/AGENTS.md.template +124 -311
  79. package/dist/agentic/shared/ai/agentic.config.json +2 -0
  80. package/dist/agentic/shared/ai/harness/README.md +50 -0
  81. package/dist/agentic/shared/ai/harness/RELEASE.md +57 -0
  82. package/dist/agentic/shared/ai/harness/cases.json +13680 -0
  83. package/dist/agentic/shared/ai/harness/cases.schema.json +133 -0
  84. package/dist/agentic/shared/ai/harness/fixtures/index.json +49 -0
  85. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +162 -0
  86. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +21 -0
  87. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +96 -0
  88. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +172 -0
  89. package/dist/agentic/shared/ai/harness/release-matrix.json +158 -0
  90. package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
  91. package/dist/agentic/shared/ai/harness/result.schema.json +76 -0
  92. package/dist/agentic/shared/ai/harness/routing-response.schema.json +12 -0
  93. package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
  94. package/dist/agentic/shared/ai/harness/validators.json +78 -0
  95. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +2121 -0
  96. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
  97. package/dist/agentic/shared/ai/lessons/_template.md +16 -0
  98. package/dist/agentic/shared/ai/lessons.md +26 -3
  99. package/dist/agentic/shared/ai/review-checklist.md +36 -0
  100. package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +21 -0
  101. package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
  102. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +98 -0
  103. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +20 -241
  104. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +16 -0
  105. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
  106. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +13 -0
  107. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
  108. package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
  109. package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
  110. package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
  111. package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
  112. package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
  113. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
  114. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
  115. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
  116. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
  117. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +16 -601
  118. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +19 -0
  119. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +13 -0
  120. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +14 -0
  121. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
  122. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +14 -310
  123. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +17 -0
  124. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +29 -0
  125. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +100 -0
  126. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
  127. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
  128. package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
  129. package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
  130. package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
  131. package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
  132. package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +18 -0
  133. package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
  134. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +33 -152
  135. package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +33 -0
  136. package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +40 -0
  137. package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +27 -0
  138. package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +24 -0
  139. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +24 -708
  140. package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
  141. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +14 -0
  142. package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
  143. package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
  144. package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +34 -0
  145. package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +4 -0
  146. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +11 -0
  147. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +31 -0
  148. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +39 -0
  149. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +43 -0
  150. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +11 -0
  151. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
  152. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +19 -0
  153. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +172 -0
  154. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +12 -0
  155. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
  156. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +21 -0
  157. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
  158. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
  159. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
  160. package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +41 -0
  161. package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +19 -0
  162. package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +44 -0
  163. package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +39 -0
  164. package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +20 -0
  165. package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +57 -0
  166. package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +11 -0
  167. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +486 -0
  168. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +19 -843
  169. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +18 -0
  170. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +27 -0
  171. package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
  172. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +41 -0
  173. package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
  174. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
  175. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
  176. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +16 -467
  177. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +16 -0
  178. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
  179. package/dist/agentic/shared/ai/skills/tiers.json +143 -21
  180. package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
  181. package/dist/agentic/shared/ai/specs/README.md +4 -3
  182. package/dist/agentic/shared/ai/specs/SPEC-000-template.md +226 -10
  183. package/dist/agentic/shared/ai/trackers/github.md +108 -4
  184. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +187 -0
  185. package/dist/agentic/shared/scripts/check-lessons.mjs +178 -0
  186. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +2958 -0
  187. package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
  188. package/dist/agentic/shared/scripts/framework-context.mjs +888 -0
  189. package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
  190. package/dist/agentic/shared/scripts/install-skills.sh +4 -694
  191. package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
  192. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1774 -0
  193. package/dist/bin.js +13 -1
  194. package/dist/bin.js.map +2 -2
  195. package/dist/lib/__integration__/TC-INT-008.spec.js +88 -8
  196. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  197. package/dist/lib/agentic-init.js +13 -3
  198. package/dist/lib/agentic-init.js.map +2 -2
  199. package/dist/lib/agentic-setup.js +434 -186
  200. package/dist/lib/agentic-setup.js.map +2 -2
  201. package/dist/lib/generators/extensions/injection-widgets.js +20 -0
  202. package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
  203. package/dist/lib/generators/module-extension-facts.js +1840 -0
  204. package/dist/lib/generators/module-extension-facts.js.map +7 -0
  205. package/dist/lib/generators/module-fact-sources.js +70 -0
  206. package/dist/lib/generators/module-fact-sources.js.map +7 -0
  207. package/dist/lib/generators/module-facts-discovery.js +41 -7
  208. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  209. package/dist/lib/generators/module-facts.js +1387 -45
  210. package/dist/lib/generators/module-facts.js.map +3 -3
  211. package/dist/lib/generators/module-override-targets.js +688 -0
  212. package/dist/lib/generators/module-override-targets.js.map +7 -0
  213. package/dist/lib/generators/module-registry.js +11 -5
  214. package/dist/lib/generators/module-registry.js.map +2 -2
  215. package/dist/lib/load-env.js +24 -0
  216. package/dist/lib/load-env.js.map +7 -0
  217. package/dist/lib/resolver.js +2 -0
  218. package/dist/lib/resolver.js.map +2 -2
  219. package/dist/lib/telemetry-init.js +358 -0
  220. package/dist/lib/telemetry-init.js.map +7 -0
  221. package/dist/lib/testing/integration.js +5 -0
  222. package/dist/lib/testing/integration.js.map +2 -2
  223. package/dist/mercato.js +23 -1
  224. package/dist/mercato.js.map +2 -2
  225. package/package.json +13 -11
  226. package/src/bin.ts +33 -1
  227. package/src/lib/__integration__/TC-INT-008.spec.ts +107 -9
  228. package/src/lib/__tests__/agentic-init.test.ts +41 -5
  229. package/src/lib/__tests__/agentic-setup.ownership.test.ts +386 -0
  230. package/src/lib/__tests__/injection-widget-id-validation.test.ts +184 -0
  231. package/src/lib/__tests__/load-env.test.ts +65 -0
  232. package/src/lib/__tests__/telemetry-init.test.ts +298 -0
  233. package/src/lib/agentic-init.ts +15 -3
  234. package/src/lib/agentic-setup.ts +573 -222
  235. package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +92 -0
  236. package/src/lib/generators/__tests__/module-extension-activations.test.ts +762 -0
  237. package/src/lib/generators/__tests__/module-extension-facts.test.ts +404 -0
  238. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +88 -2
  239. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
  240. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
  241. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
  242. package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +561 -0
  243. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +119 -0
  244. package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +149 -0
  245. package/src/lib/generators/__tests__/module-override-targets.test.ts +304 -0
  246. package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
  247. package/src/lib/generators/extensions/injection-widgets.ts +51 -0
  248. package/src/lib/generators/module-extension-facts.ts +2365 -0
  249. package/src/lib/generators/module-fact-sources.ts +108 -0
  250. package/src/lib/generators/module-facts-discovery.ts +58 -7
  251. package/src/lib/generators/module-facts.ts +1851 -46
  252. package/src/lib/generators/module-override-targets.ts +863 -0
  253. package/src/lib/generators/module-registry.ts +16 -6
  254. package/src/lib/load-env.ts +30 -0
  255. package/src/lib/resolver.ts +3 -0
  256. package/src/lib/telemetry-init.ts +461 -0
  257. package/src/lib/testing/__tests__/integration.test.ts +4 -0
  258. package/src/lib/testing/integration.ts +5 -0
  259. package/src/mercato.ts +26 -1
  260. package/dist/agentic/guides/cache.md +0 -45
  261. package/dist/agentic/guides/core.md +0 -453
  262. package/dist/agentic/guides/events.md +0 -117
  263. package/dist/agentic/guides/queue.md +0 -53
  264. package/dist/agentic/guides/search.md +0 -115
  265. package/dist/agentic/guides/shared.md +0 -108
  266. package/dist/agentic/guides/ui.md +0 -347
  267. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
  268. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
  269. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
  270. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
  271. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
  272. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
  273. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
  274. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
  275. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
  276. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
  277. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
  278. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
@@ -0,0 +1,863 @@
1
+ import crypto from 'node:crypto'
2
+ import type { ModuleOverrideDomain } from '@open-mercato/shared/modules/overrides'
3
+ import type { ModuleExtensionContributionFact } from '@open-mercato/shared/modules/widgets/extension-points'
4
+ import type {
5
+ ModuleFactRef,
6
+ ModuleFactSourceKind,
7
+ ModuleFactSourceRef,
8
+ ModuleFactsJsonEntry,
9
+ ModuleOwnedContractFact,
10
+ } from './module-facts'
11
+ import { buildFactSourceLookup, type FactSourceLookup } from './module-fact-sources'
12
+ import { getFrameworkOverrideModes } from './module-extension-facts'
13
+
14
+ /**
15
+ * Exact module-specific unified override targets.
16
+ *
17
+ * Spec: `.ai/specs/2026-08-02-module-facts-exact-override-targets.md`.
18
+ *
19
+ * Projects every concrete key a downstream app can set under
20
+ * `modules.<moduleId>.overrides.<domain>` from the normalized owned facts
21
+ * (spec 1 provenance) and the extension-surface contributions (spec 2). Each
22
+ * target carries the structured runtime `path`, the applicable framework mode,
23
+ * a module-owned `factRef`, and a portable `source`. The framework catalog
24
+ * (`framework-extension-points`) stays the authority for domains/modes; this
25
+ * layer only emits the module's concrete keys. Framework-only settings such as
26
+ * `nav.groupOrder` are never module targets.
27
+ */
28
+
29
+ export type ModuleOverrideMode = 'disable-replace' | 'replace' | 'additive'
30
+
31
+ export type ModuleOverrideTargetNote =
32
+ | 'safe-metadata-only'
33
+ | 'page-middleware-not-mutation-guard'
34
+
35
+ export type ModuleOverrideTarget = {
36
+ id: string
37
+ domain: ModuleOverrideDomain
38
+ key?: string
39
+ path: string[]
40
+ modes: ModuleOverrideMode[]
41
+ factRef: ModuleFactRef
42
+ source: ModuleFactSourceRef
43
+ notes?: ModuleOverrideTargetNote[]
44
+ }
45
+
46
+ export type ModuleOverrideTargetDiagnosticCode =
47
+ | 'missing-owned-fact'
48
+ | 'missing-source'
49
+ | 'unsupported-dynamic-key'
50
+ | 'unknown-framework-domain'
51
+
52
+ export type ModuleOverrideTargetDiagnostic = {
53
+ code: ModuleOverrideTargetDiagnosticCode
54
+ moduleId: string
55
+ domain: ModuleOverrideDomain
56
+ candidatePath?: string[]
57
+ source?: ModuleFactSourceRef
58
+ }
59
+
60
+ /**
61
+ * Public, spec-shaped adapter view. `collect` mirrors the spec's illustrative
62
+ * signature (targets only); `diagnose` is the additive per-module diagnostic
63
+ * companion. Every adapter is bound to exactly one top-level override domain,
64
+ * or classified `frameworkOnly` (framework-catalog settings that never become
65
+ * module targets, e.g. `nav`).
66
+ */
67
+ export type OverrideTargetAdapter = {
68
+ domain: ModuleOverrideDomain
69
+ collect(moduleFacts: ModuleFactsJsonEntry): ModuleOverrideTarget[]
70
+ diagnose(moduleFacts: ModuleFactsJsonEntry): ModuleOverrideTargetDiagnostic[]
71
+ frameworkOnly?: boolean
72
+ }
73
+
74
+ type AdapterResult = {
75
+ targets: ModuleOverrideTarget[]
76
+ diagnostics: ModuleOverrideTargetDiagnostic[]
77
+ }
78
+
79
+ type InternalAdapter = {
80
+ domain: ModuleOverrideDomain
81
+ frameworkOnly?: boolean
82
+ run(moduleFacts: ModuleFactsJsonEntry, resolveFactSource: FactSourceLookup): AdapterResult
83
+ }
84
+
85
+ const OVERRIDE_MODES = getFrameworkOverrideModes()
86
+
87
+ /**
88
+ * Supported modes come from the framework catalog only. A host the catalog does not
89
+ * describe is never guessed: the caller emits an `unknown-framework-domain`
90
+ * diagnostic and no target, so a downstream app is not told a key is overridable in
91
+ * a mode the runtime may not support.
92
+ */
93
+ function modesFor(dottedHost: string): ModuleOverrideMode[] | null {
94
+ const mode = OVERRIDE_MODES[dottedHost]
95
+ return mode ? [mode] : null
96
+ }
97
+
98
+ /**
99
+ * The module id equals the last segment of the portable `sourceRoot`
100
+ * (`node_modules/<pkg>/src/modules/<moduleId>`), which the provenance builder
101
+ * derives from the module id itself, so this round-trips losslessly.
102
+ */
103
+ function moduleIdOf(facts: ModuleFactsJsonEntry): string {
104
+ const segments = facts.sourceRoot.split('/').filter(Boolean)
105
+ return segments[segments.length - 1] ?? ''
106
+ }
107
+
108
+ /**
109
+ * Deterministic id derived from the JSON-serialized `[moduleId, domain, path]`
110
+ * tuple so segment boundaries cannot collide. `path` remains the lossless
111
+ * authority; this is a compact stable handle for consumers that key by id.
112
+ */
113
+ export function computeOverrideTargetId(moduleId: string, domain: ModuleOverrideDomain, path: string[]): string {
114
+ return crypto.createHash('sha1').update(JSON.stringify([moduleId, domain, path])).digest('hex').slice(0, 16)
115
+ }
116
+
117
+ function compareSourceRefs(left: ModuleFactSourceRef, right: ModuleFactSourceRef): number {
118
+ return (
119
+ left.sourcePath.localeCompare(right.sourcePath) ||
120
+ (left.line ?? 0) - (right.line ?? 0) ||
121
+ (left.exportName ?? '').localeCompare(right.exportName ?? '')
122
+ )
123
+ }
124
+
125
+ function comparePaths(left: string[], right: string[]): number {
126
+ const length = Math.max(left.length, right.length)
127
+ for (let index = 0; index < length; index += 1) {
128
+ const leftSegment = left[index]
129
+ const rightSegment = right[index]
130
+ if (leftSegment === undefined) return -1
131
+ if (rightSegment === undefined) return 1
132
+ const compared = leftSegment.localeCompare(rightSegment)
133
+ if (compared !== 0) return compared
134
+ }
135
+ return 0
136
+ }
137
+
138
+ type MakeTargetInput = {
139
+ moduleId: string
140
+ domain: ModuleOverrideDomain
141
+ path: string[]
142
+ dottedHost: string
143
+ factRef: ModuleFactRef
144
+ source: ModuleFactSourceRef
145
+ key?: string
146
+ notes?: ModuleOverrideTargetNote[]
147
+ }
148
+
149
+ function makeTarget(input: MakeTargetInput): ModuleOverrideTarget | null {
150
+ const modes = modesFor(input.dottedHost)
151
+ if (!modes) return null
152
+ const target: ModuleOverrideTarget = {
153
+ id: computeOverrideTargetId(input.moduleId, input.domain, input.path),
154
+ domain: input.domain,
155
+ ...(input.key !== undefined ? { key: input.key } : {}),
156
+ path: input.path,
157
+ modes,
158
+ factRef: input.factRef,
159
+ source: input.source,
160
+ ...(input.notes && input.notes.length > 0 ? { notes: input.notes } : {}),
161
+ }
162
+ return target
163
+ }
164
+
165
+ /**
166
+ * Emits the target, or an `unknown-framework-domain` diagnostic when the framework
167
+ * catalog does not describe the target's dotted host.
168
+ */
169
+ function pushTarget(
170
+ targets: ModuleOverrideTarget[],
171
+ diagnostics: ModuleOverrideTargetDiagnostic[],
172
+ input: MakeTargetInput,
173
+ ): void {
174
+ const target = makeTarget(input)
175
+ if (target) {
176
+ targets.push(target)
177
+ return
178
+ }
179
+ diagnostics.push({
180
+ code: 'unknown-framework-domain',
181
+ moduleId: input.moduleId,
182
+ domain: input.domain,
183
+ candidatePath: input.path,
184
+ source: input.source,
185
+ })
186
+ }
187
+
188
+ /** Portable contribution source ref → override target source ref. */
189
+ function contributionSource(contribution: ModuleExtensionContributionFact): ModuleFactSourceRef {
190
+ return {
191
+ sourcePath: contribution.source.path,
192
+ ...(contribution.source.symbol ? { exportName: contribution.source.symbol } : {}),
193
+ }
194
+ }
195
+
196
+ /** Owned-contract source ref, dropping the (constant) exportName for compactness. */
197
+ function ownedSource(fact: ModuleOwnedContractFact): ModuleFactSourceRef {
198
+ return {
199
+ sourcePath: fact.source.sourcePath,
200
+ ...(fact.source.line ? { line: fact.source.line } : {}),
201
+ }
202
+ }
203
+
204
+ function contributionsOfKind<K extends ModuleExtensionContributionFact['kind']>(
205
+ facts: ModuleFactsJsonEntry,
206
+ kind: K,
207
+ ): Array<Extract<ModuleExtensionContributionFact, { kind: K }>> {
208
+ const contributions = facts.extensionSurfaces?.contributions ?? []
209
+ return contributions.filter((contribution): contribution is Extract<ModuleExtensionContributionFact, { kind: K }> =>
210
+ contribution.kind === kind,
211
+ )
212
+ }
213
+
214
+ function indexedFactSourcePath(
215
+ resolveFactSource: FactSourceLookup,
216
+ kind: ModuleFactSourceKind,
217
+ id: string,
218
+ ): string | null {
219
+ return resolveFactSource(kind, id)?.sourcePath ?? null
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Local canonical override-key normalizers.
224
+ //
225
+ // These MIRROR the private `normalizeApiRouteOverrideKey` /
226
+ // `normalizePageRouteOverrideKey` in
227
+ // `packages/shared/src/modules/overrides.ts` (not exported by that module).
228
+ // Round-trip tests feed the emitted keys back through the exported runtime
229
+ // appliers (`applyApiOverridesToManifests` / `applyPageOverridesToManifests`)
230
+ // to prove equivalence. See the implementation report: exporting the shared
231
+ // normalizers would let this file import rather than mirror them.
232
+ // ---------------------------------------------------------------------------
233
+
234
+ const VALID_HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
235
+
236
+ /**
237
+ * Canonical `'METHOD /api/path'` public override key for a route method. Route
238
+ * fact paths are stored without the `/api` prefix (e.g. `/customers/x`), so the
239
+ * public key downstream apps write is `METHOD /api<path>`.
240
+ */
241
+ function apiRouteOverrideKey(method: string, routePath: string): string | null {
242
+ const upper = method.trim().toUpperCase()
243
+ if (!VALID_HTTP_METHODS.has(upper)) return null
244
+ const withLead = routePath.startsWith('/') ? routePath : `/${routePath}`
245
+ const publicPath = withLead.startsWith('/api/') || withLead === '/api' ? withLead : `/api${withLead}`
246
+ const normalized = publicPath.replace(/\/+$/, '') || '/'
247
+ return `${upper} ${normalized}`
248
+ }
249
+
250
+ function pageRouteOverrideKey(routePath: string): string | null {
251
+ const trimmed = routePath.trim()
252
+ if (!trimmed) return null
253
+ if (trimmed.startsWith('backend:') || trimmed.startsWith('frontend:')) return trimmed
254
+ const withLead = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
255
+ const stripped = withLead.replace(/\/+$/, '') || '/'
256
+ if (stripped === '/frontend') return 'frontend:/'
257
+ if (stripped.startsWith('/frontend/')) return `frontend:/${stripped.slice('/frontend/'.length)}`
258
+ if (stripped === '/backend' || stripped.startsWith('/backend/')) return `backend:${stripped}`
259
+ return `frontend:${stripped}`
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Adapters (one per top-level override domain).
264
+ // ---------------------------------------------------------------------------
265
+
266
+ const aiAdapter: InternalAdapter = {
267
+ domain: 'ai',
268
+ run(facts) {
269
+ const moduleId = moduleIdOf(facts)
270
+ const targets: ModuleOverrideTarget[] = []
271
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
272
+
273
+ for (const agent of facts.aiAgents) {
274
+ if (!agent.sourcePath) {
275
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'ai', candidatePath: ['ai', 'agents', agent.id] })
276
+ continue
277
+ }
278
+ const path = ['ai', 'agents', agent.id]
279
+ pushTarget(targets, diagnostics, {
280
+ moduleId, domain: 'ai', path, key: agent.id, dottedHost: 'ai.agents',
281
+ factRef: { factSection: 'aiAgents', factKey: agent.id },
282
+ source: { sourcePath: agent.sourcePath },
283
+ })
284
+ }
285
+ for (const tool of facts.aiTools) {
286
+ if (!tool.sourcePath) {
287
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'ai', candidatePath: ['ai', 'tools', tool.name] })
288
+ continue
289
+ }
290
+ const path = ['ai', 'tools', tool.name]
291
+ pushTarget(targets, diagnostics, {
292
+ moduleId, domain: 'ai', path, key: tool.name, dottedHost: 'ai.tools',
293
+ factRef: { factSection: 'aiTools', factKey: tool.name },
294
+ source: { sourcePath: tool.sourcePath },
295
+ })
296
+ }
297
+ // AI file contracts split by mode: keyed `aiAgentOverrides` / `aiToolOverrides`
298
+ // entries are exact `ai.agents.<id>` / `ai.tools.<id>` targets, while additive
299
+ // `aiAgentExtensions` patches belong to the keyless `ai.extensions` array.
300
+ const aiFileContracts = facts.ownedContracts?.['ai-extension'] ?? []
301
+ const additiveExtensions: ModuleOwnedContractFact[] = []
302
+ for (const fact of aiFileContracts) {
303
+ const metadata = fact.metadata ?? {}
304
+ if (metadata.mode !== 'override') {
305
+ additiveExtensions.push(fact)
306
+ continue
307
+ }
308
+ const overrideKey = typeof metadata.overrideKey === 'string' ? metadata.overrideKey : null
309
+ const target = metadata.target === 'agent' ? 'agents' : metadata.target === 'tool' ? 'tools' : null
310
+ if (!overrideKey || !target) {
311
+ diagnostics.push({
312
+ code: 'unsupported-dynamic-key',
313
+ moduleId,
314
+ domain: 'ai',
315
+ candidatePath: ['ai', target ?? 'extensions'],
316
+ source: ownedSource(fact),
317
+ })
318
+ continue
319
+ }
320
+ const path = ['ai', target, overrideKey]
321
+ pushTarget(targets, diagnostics, {
322
+ moduleId, domain: 'ai', path, key: overrideKey, dottedHost: `ai.${target}`,
323
+ factRef: { factSection: 'ownedContracts.ai-extension', factKey: fact.id },
324
+ source: ownedSource(fact),
325
+ })
326
+ }
327
+ if (additiveExtensions.length > 0) {
328
+ const anchor = [...additiveExtensions].sort((left, right) => left.id.localeCompare(right.id))[0]
329
+ pushTarget(targets, diagnostics, {
330
+ moduleId, domain: 'ai', path: ['ai', 'extensions'], dottedHost: 'ai.extensions',
331
+ factRef: { factSection: 'ownedContracts.ai-extension', factKey: anchor.id },
332
+ source: ownedSource(anchor),
333
+ })
334
+ }
335
+ return { targets, diagnostics }
336
+ },
337
+ }
338
+
339
+ const routesAdapter: InternalAdapter = {
340
+ domain: 'routes',
341
+ run(facts) {
342
+ const moduleId = moduleIdOf(facts)
343
+ const targets: ModuleOverrideTarget[] = []
344
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
345
+
346
+ for (const route of facts.apiRoutes) {
347
+ for (const method of route.methods) {
348
+ const key = apiRouteOverrideKey(method, route.path)
349
+ if (!key) {
350
+ diagnostics.push({ code: 'unsupported-dynamic-key', moduleId, domain: 'routes', candidatePath: ['routes', 'api', `${method} ${route.path}`] })
351
+ continue
352
+ }
353
+ if (!route.sourcePath) {
354
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'routes', candidatePath: ['routes', 'api', key] })
355
+ continue
356
+ }
357
+ const path = ['routes', 'api', key]
358
+ pushTarget(targets, diagnostics, {
359
+ moduleId, domain: 'routes', path, key, dottedHost: 'routes.api',
360
+ factRef: { factSection: 'apiRoutes', factKey: route.path },
361
+ source: { sourcePath: route.sourcePath },
362
+ })
363
+ }
364
+ }
365
+
366
+ const emitPage = (page: { path: string; sourcePath: string }, section: 'backendPages' | 'frontendPages'): void => {
367
+ const key = pageRouteOverrideKey(page.path)
368
+ if (!key) {
369
+ diagnostics.push({ code: 'unsupported-dynamic-key', moduleId, domain: 'routes', candidatePath: ['routes', 'pages', page.path] })
370
+ return
371
+ }
372
+ if (!page.sourcePath) {
373
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'routes', candidatePath: ['routes', 'pages', key] })
374
+ return
375
+ }
376
+ const path = ['routes', 'pages', key]
377
+ pushTarget(targets, diagnostics, {
378
+ moduleId, domain: 'routes', path, key, dottedHost: 'routes.pages',
379
+ factRef: { factSection: section, factKey: page.path },
380
+ source: { sourcePath: page.sourcePath },
381
+ })
382
+ }
383
+ for (const page of facts.backendPages) emitPage(page, 'backendPages')
384
+ for (const page of facts.frontendPages) emitPage(page, 'frontendPages')
385
+
386
+ return { targets, diagnostics }
387
+ },
388
+ }
389
+
390
+ const eventsAdapter: InternalAdapter = {
391
+ domain: 'events',
392
+ run(facts) {
393
+ const moduleId = moduleIdOf(facts)
394
+ const targets: ModuleOverrideTarget[] = []
395
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
396
+ for (const contribution of contributionsOfKind(facts, 'subscriber')) {
397
+ const subscriberId = contribution.details.subscriberId || contribution.id
398
+ const path = ['events', 'subscribers', subscriberId]
399
+ if (!contribution.source.path) {
400
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'events', candidatePath: path })
401
+ continue
402
+ }
403
+ pushTarget(targets, diagnostics, {
404
+ moduleId, domain: 'events', path, key: subscriberId, dottedHost: 'events.subscribers',
405
+ factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
406
+ source: contributionSource(contribution),
407
+ })
408
+ }
409
+ return { targets, diagnostics }
410
+ },
411
+ }
412
+
413
+ const workersAdapter: InternalAdapter = {
414
+ domain: 'workers',
415
+ run(facts) {
416
+ const moduleId = moduleIdOf(facts)
417
+ const targets: ModuleOverrideTarget[] = []
418
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
419
+ for (const fact of facts.ownedContracts?.worker ?? []) {
420
+ const path = ['workers', fact.id]
421
+ if (!fact.source.sourcePath) {
422
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'workers', candidatePath: path })
423
+ continue
424
+ }
425
+ pushTarget(targets, diagnostics, {
426
+ moduleId, domain: 'workers', path, key: fact.id, dottedHost: 'workers',
427
+ factRef: { factSection: 'ownedContracts.worker', factKey: fact.id },
428
+ source: ownedSource(fact),
429
+ })
430
+ }
431
+ return { targets, diagnostics }
432
+ },
433
+ }
434
+
435
+ const widgetsAdapter: InternalAdapter = {
436
+ domain: 'widgets',
437
+ run(facts) {
438
+ const moduleId = moduleIdOf(facts)
439
+ const targets: ModuleOverrideTarget[] = []
440
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
441
+
442
+ const emitInjection = (registryKey: string, contribution: ModuleExtensionContributionFact): void => {
443
+ const path = ['widgets', 'injection', registryKey]
444
+ pushTarget(targets, diagnostics, {
445
+ moduleId, domain: 'widgets', path, key: registryKey, dottedHost: 'widgets.injection',
446
+ factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
447
+ source: contributionSource(contribution),
448
+ })
449
+ }
450
+
451
+ for (const contribution of contributionsOfKind(facts, 'widget')) {
452
+ const registryKey = contribution.details.registryKey
453
+ if (!registryKey) {
454
+ diagnostics.push({ code: 'unsupported-dynamic-key', moduleId, domain: 'widgets', candidatePath: ['widgets', 'injection', contribution.id] })
455
+ continue
456
+ }
457
+ if (contribution.details.payload === 'dashboard') {
458
+ const path = ['widgets', 'dashboard', registryKey]
459
+ pushTarget(targets, diagnostics, {
460
+ moduleId, domain: 'widgets', path, key: registryKey, dottedHost: 'widgets.dashboard',
461
+ factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
462
+ source: contributionSource(contribution),
463
+ })
464
+ } else {
465
+ emitInjection(registryKey, contribution)
466
+ }
467
+ }
468
+
469
+ // data-table / crud-form contributions are injection-widget entries whose
470
+ // registry key is the id segment before the `@<spot>` suffix.
471
+ for (const contribution of [
472
+ ...contributionsOfKind(facts, 'data-table'),
473
+ ...contributionsOfKind(facts, 'crud-form'),
474
+ ]) {
475
+ const registryKey = contribution.id.split('@')[0]
476
+ if (!registryKey || registryKey === contribution.id) {
477
+ diagnostics.push({ code: 'unsupported-dynamic-key', moduleId, domain: 'widgets', candidatePath: ['widgets', 'injection', contribution.id] })
478
+ continue
479
+ }
480
+ emitInjection(registryKey, contribution)
481
+ }
482
+
483
+ for (const contribution of contributionsOfKind(facts, 'component-override')) {
484
+ const componentId = contribution.details.handle
485
+ const path = ['widgets', 'components', componentId]
486
+ pushTarget(targets, diagnostics, {
487
+ moduleId, domain: 'widgets', path, key: componentId, dottedHost: 'widgets.components',
488
+ factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
489
+ source: contributionSource(contribution),
490
+ })
491
+ }
492
+
493
+ return { targets, diagnostics }
494
+ },
495
+ }
496
+
497
+ const notificationsAdapter: InternalAdapter = {
498
+ domain: 'notifications',
499
+ run(facts, resolveFactSource) {
500
+ const moduleId = moduleIdOf(facts)
501
+ const targets: ModuleOverrideTarget[] = []
502
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
503
+
504
+ const registrySources = new Map<string, ModuleFactSourceRef>()
505
+ for (const contribution of contributionsOfKind(facts, 'specialized-registry')) {
506
+ if (contribution.details.registry !== 'notification') continue
507
+ registrySources.set(contribution.details.registryId, contributionSource(contribution))
508
+ }
509
+ for (const type of facts.notifications) {
510
+ const path = ['notifications', 'types', type]
511
+ const indexedPath = indexedFactSourcePath(resolveFactSource, 'notification', type)
512
+ const source = registrySources.get(type) ?? (indexedPath ? { sourcePath: indexedPath } : null)
513
+ if (!source) {
514
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'notifications', candidatePath: path })
515
+ continue
516
+ }
517
+ pushTarget(targets, diagnostics, {
518
+ moduleId, domain: 'notifications', path, key: type, dottedHost: 'notifications.types',
519
+ factRef: { factSection: 'notifications', factKey: type },
520
+ source,
521
+ })
522
+ }
523
+
524
+ // Reactive handlers (`notifications.handlers.ts`) are keyed by their declared
525
+ // handler id; a handler that declares none cannot be addressed by runtime.
526
+ for (const contribution of contributionsOfKind(facts, 'browser-reaction')) {
527
+ if (!contribution.details.transports.includes('notification-effect')) continue
528
+ const overrideKey = contribution.details.overrideKey
529
+ if (!overrideKey) {
530
+ diagnostics.push({
531
+ code: 'unsupported-dynamic-key',
532
+ moduleId,
533
+ domain: 'notifications',
534
+ candidatePath: ['notifications', 'handlers'],
535
+ source: contributionSource(contribution),
536
+ })
537
+ continue
538
+ }
539
+ pushTarget(targets, diagnostics, {
540
+ moduleId,
541
+ domain: 'notifications',
542
+ path: ['notifications', 'handlers', overrideKey],
543
+ key: overrideKey,
544
+ dottedHost: 'notifications.handlers',
545
+ factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
546
+ source: contributionSource(contribution),
547
+ })
548
+ }
549
+ return { targets, diagnostics }
550
+ },
551
+ }
552
+
553
+ function contributionIdAdapter(
554
+ domain: ModuleOverrideDomain,
555
+ kind: ModuleExtensionContributionFact['kind'],
556
+ dottedHost: string,
557
+ ): InternalAdapter {
558
+ return {
559
+ domain,
560
+ run(facts) {
561
+ const moduleId = moduleIdOf(facts)
562
+ const targets: ModuleOverrideTarget[] = []
563
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
564
+ for (const contribution of contributionsOfKind(facts, kind)) {
565
+ const path = [domain, contribution.id]
566
+ if (!contribution.source.path) {
567
+ diagnostics.push({ code: 'missing-source', moduleId, domain, candidatePath: path })
568
+ continue
569
+ }
570
+ pushTarget(targets, diagnostics, {
571
+ moduleId, domain, path, key: contribution.id, dottedHost,
572
+ factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
573
+ source: contributionSource(contribution),
574
+ })
575
+ }
576
+ return { targets, diagnostics }
577
+ },
578
+ }
579
+ }
580
+
581
+ const interceptorsAdapter = contributionIdAdapter('interceptors', 'api-interceptor', 'interceptors')
582
+ const commandInterceptorsAdapter = contributionIdAdapter('commandInterceptors', 'command-interceptor', 'commandInterceptors')
583
+ const enrichersAdapter = contributionIdAdapter('enrichers', 'response-enricher', 'enrichers')
584
+
585
+ /**
586
+ * `guards` targets ONLY backend/frontend page route middleware
587
+ * (`ownedContracts['page-middleware']`). `data/guards.ts` mutation guards
588
+ * (contribution kind `mutation-guard`) are a different runtime contract and
589
+ * MUST NEVER surface here — every target carries the closed
590
+ * `page-middleware-not-mutation-guard` note.
591
+ */
592
+ const guardsAdapter: InternalAdapter = {
593
+ domain: 'guards',
594
+ run(facts) {
595
+ const moduleId = moduleIdOf(facts)
596
+ const targets: ModuleOverrideTarget[] = []
597
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
598
+ for (const fact of facts.ownedContracts?.['page-middleware'] ?? []) {
599
+ const path = ['guards', fact.id]
600
+ if (!fact.source.sourcePath) {
601
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'guards', candidatePath: path })
602
+ continue
603
+ }
604
+ pushTarget(targets, diagnostics, {
605
+ moduleId, domain: 'guards', path, key: fact.id, dottedHost: 'guards',
606
+ factRef: { factSection: 'ownedContracts.page-middleware', factKey: fact.id },
607
+ source: ownedSource(fact),
608
+ notes: ['page-middleware-not-mutation-guard'],
609
+ })
610
+ }
611
+ return { targets, diagnostics }
612
+ },
613
+ }
614
+
615
+ const cliAdapter: InternalAdapter = {
616
+ domain: 'cli',
617
+ run(facts) {
618
+ const moduleId = moduleIdOf(facts)
619
+ const targets: ModuleOverrideTarget[] = []
620
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
621
+ for (const command of facts.cliCommands) {
622
+ const path = ['cli', command.command]
623
+ if (!command.sourcePath) {
624
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'cli', candidatePath: path })
625
+ continue
626
+ }
627
+ pushTarget(targets, diagnostics, {
628
+ moduleId, domain: 'cli', path, key: command.command, dottedHost: 'cli',
629
+ factRef: { factSection: 'cliCommands', factKey: command.command },
630
+ source: { sourcePath: command.sourcePath },
631
+ })
632
+ }
633
+ return { targets, diagnostics }
634
+ },
635
+ }
636
+
637
+ // `SetupOverridesShape` keys the runtime override resolver accepts. Only these
638
+ // supported hook/profile properties become `setup` targets.
639
+ const SUPPORTED_SETUP_HOOKS = new Set(['seedDefaults', 'seedExamples', 'onTenantCreated'])
640
+
641
+ const setupAdapter: InternalAdapter = {
642
+ domain: 'setup',
643
+ run(facts) {
644
+ const moduleId = moduleIdOf(facts)
645
+ const targets: ModuleOverrideTarget[] = []
646
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
647
+ const setupFacts = facts.ownedContracts?.setup ?? []
648
+ for (const fact of setupFacts) {
649
+ if (!fact.source.sourcePath) {
650
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'setup', candidatePath: ['setup'] })
651
+ continue
652
+ }
653
+ const source = ownedSource(fact)
654
+ const properties: string[] = []
655
+ const hooks = fact.metadata?.hooks
656
+ if (Array.isArray(hooks)) {
657
+ for (const hook of hooks) {
658
+ if (typeof hook === 'string' && SUPPORTED_SETUP_HOOKS.has(hook)) properties.push(hook)
659
+ }
660
+ }
661
+ const roles = fact.metadata?.roles
662
+ if (Array.isArray(roles) && roles.length > 0) properties.push('defaultRoleFeatures')
663
+ const customerRoles = fact.metadata?.customerRoles
664
+ if (Array.isArray(customerRoles) && customerRoles.length > 0) properties.push('defaultCustomerRoleFeatures')
665
+ for (const property of properties) {
666
+ const path = ['setup', property]
667
+ pushTarget(targets, diagnostics, {
668
+ moduleId, domain: 'setup', path, dottedHost: 'setup',
669
+ factRef: { factSection: 'ownedContracts.setup', factKey: fact.id },
670
+ source,
671
+ })
672
+ }
673
+ }
674
+ return { targets, diagnostics }
675
+ },
676
+ }
677
+
678
+ const aclAdapter: InternalAdapter = {
679
+ domain: 'acl',
680
+ run(facts, resolveFactSource) {
681
+ const moduleId = moduleIdOf(facts)
682
+ const targets: ModuleOverrideTarget[] = []
683
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
684
+ for (const feature of facts.aclFeatures) {
685
+ const path = ['acl', 'features', feature]
686
+ const sourcePath = indexedFactSourcePath(resolveFactSource, 'acl-feature', feature)
687
+ if (!sourcePath) {
688
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'acl', candidatePath: path })
689
+ continue
690
+ }
691
+ pushTarget(targets, diagnostics, {
692
+ moduleId, domain: 'acl', path, key: feature, dottedHost: 'acl.features',
693
+ factRef: { factSection: 'aclFeatures', factKey: feature },
694
+ source: { sourcePath },
695
+ })
696
+ }
697
+ return { targets, diagnostics }
698
+ },
699
+ }
700
+
701
+ /**
702
+ * `di` targets expose the exact registration token and correlate to the rich DI
703
+ * owned fact (which carries the registration kind). They NEVER include the
704
+ * `asValue` payload, constructed service, or factory/class body — the target
705
+ * schema has no such field and carries the `safe-metadata-only` note.
706
+ */
707
+ const diAdapter: InternalAdapter = {
708
+ domain: 'di',
709
+ run(facts) {
710
+ const moduleId = moduleIdOf(facts)
711
+ const targets: ModuleOverrideTarget[] = []
712
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
713
+ for (const fact of facts.ownedContracts?.['di-registration'] ?? []) {
714
+ const path = ['di', fact.id]
715
+ if (!fact.source.sourcePath) {
716
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'di', candidatePath: path })
717
+ continue
718
+ }
719
+ pushTarget(targets, diagnostics, {
720
+ moduleId, domain: 'di', path, key: fact.id, dottedHost: 'di',
721
+ factRef: { factSection: 'ownedContracts.di-registration', factKey: fact.id },
722
+ source: ownedSource(fact),
723
+ notes: ['safe-metadata-only'],
724
+ })
725
+ }
726
+ return { targets, diagnostics }
727
+ },
728
+ }
729
+
730
+ const encryptionAdapter: InternalAdapter = {
731
+ domain: 'encryption',
732
+ run(facts) {
733
+ const moduleId = moduleIdOf(facts)
734
+ const targets: ModuleOverrideTarget[] = []
735
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
736
+ for (const fact of facts.ownedContracts?.encryption ?? []) {
737
+ const path = ['encryption', 'maps', fact.id]
738
+ if (!fact.source.sourcePath) {
739
+ diagnostics.push({ code: 'missing-source', moduleId, domain: 'encryption', candidatePath: path })
740
+ continue
741
+ }
742
+ pushTarget(targets, diagnostics, {
743
+ moduleId, domain: 'encryption', path, key: fact.id, dottedHost: 'encryption.maps',
744
+ factRef: { factSection: 'ownedContracts.encryption', factKey: fact.id },
745
+ source: ownedSource(fact),
746
+ })
747
+ }
748
+ return { targets, diagnostics }
749
+ },
750
+ }
751
+
752
+ /** `nav.groupOrder` is a framework-only setting; never a module target. */
753
+ const navAdapter: InternalAdapter = {
754
+ domain: 'nav',
755
+ frameworkOnly: true,
756
+ run() {
757
+ return { targets: [], diagnostics: [] }
758
+ },
759
+ }
760
+
761
+ const INTERNAL_ADAPTERS: InternalAdapter[] = [
762
+ aiAdapter,
763
+ routesAdapter,
764
+ eventsAdapter,
765
+ workersAdapter,
766
+ widgetsAdapter,
767
+ notificationsAdapter,
768
+ interceptorsAdapter,
769
+ commandInterceptorsAdapter,
770
+ enrichersAdapter,
771
+ guardsAdapter,
772
+ cliAdapter,
773
+ setupAdapter,
774
+ aclAdapter,
775
+ diAdapter,
776
+ encryptionAdapter,
777
+ navAdapter,
778
+ ]
779
+
780
+ /**
781
+ * Runtime enumeration of every unified override domain. Typed as a
782
+ * `Record<ModuleOverrideDomain, true>` so adding a member to the runtime
783
+ * `ModuleOverrideDomain` union fails the build here until it is listed — and
784
+ * the coverage test then fails until an adapter (or `frameworkOnly`
785
+ * classification) is registered.
786
+ */
787
+ const OVERRIDE_DOMAIN_PRESENCE: Record<ModuleOverrideDomain, true> = {
788
+ ai: true,
789
+ routes: true,
790
+ events: true,
791
+ workers: true,
792
+ widgets: true,
793
+ notifications: true,
794
+ interceptors: true,
795
+ commandInterceptors: true,
796
+ enrichers: true,
797
+ guards: true,
798
+ cli: true,
799
+ setup: true,
800
+ acl: true,
801
+ di: true,
802
+ encryption: true,
803
+ nav: true,
804
+ }
805
+
806
+ export const ALL_OVERRIDE_DOMAINS = Object.keys(OVERRIDE_DOMAIN_PRESENCE) as ModuleOverrideDomain[]
807
+
808
+ export const MODULE_OVERRIDE_TARGET_ADAPTERS: OverrideTargetAdapter[] = INTERNAL_ADAPTERS.map((adapter) => ({
809
+ domain: adapter.domain,
810
+ ...(adapter.frameworkOnly ? { frameworkOnly: true } : {}),
811
+ collect: (moduleFacts) => adapter.run(moduleFacts, buildFactSourceLookup(moduleFacts)).targets,
812
+ diagnose: (moduleFacts) => adapter.run(moduleFacts, buildFactSourceLookup(moduleFacts)).diagnostics,
813
+ }))
814
+
815
+ export type ModuleOverrideTargetsResult = {
816
+ overrideTargets: ModuleOverrideTarget[]
817
+ overrideTargetDiagnostics: ModuleOverrideTargetDiagnostic[]
818
+ }
819
+
820
+ /**
821
+ * Project every exact override target (and per-module diagnostic) for one
822
+ * module's facts. Output sorts by domain, then path segments, then source;
823
+ * diagnostics sort by moduleId, domain, code, then path/source. Targets are
824
+ * deduped by `(domain, path)` keeping the source-first entry.
825
+ */
826
+ export function collectModuleOverrideTargets(
827
+ facts: ModuleFactsJsonEntry,
828
+ resolveFactSource: FactSourceLookup = buildFactSourceLookup(facts),
829
+ ): ModuleOverrideTargetsResult {
830
+ const collected: ModuleOverrideTarget[] = []
831
+ const diagnostics: ModuleOverrideTargetDiagnostic[] = []
832
+ for (const adapter of INTERNAL_ADAPTERS) {
833
+ if (adapter.frameworkOnly) continue
834
+ const result = adapter.run(facts, resolveFactSource)
835
+ collected.push(...result.targets)
836
+ diagnostics.push(...result.diagnostics)
837
+ }
838
+
839
+ const byPath = new Map<string, ModuleOverrideTarget>()
840
+ for (const target of collected) {
841
+ const pathKey = `${target.domain} ${JSON.stringify(target.path)}`
842
+ const existing = byPath.get(pathKey)
843
+ if (!existing || compareSourceRefs(target.source, existing.source) < 0) byPath.set(pathKey, target)
844
+ }
845
+
846
+ const overrideTargets = [...byPath.values()].sort(
847
+ (left, right) =>
848
+ left.domain.localeCompare(right.domain) ||
849
+ comparePaths(left.path, right.path) ||
850
+ compareSourceRefs(left.source, right.source),
851
+ )
852
+
853
+ const overrideTargetDiagnostics = diagnostics.sort(
854
+ (left, right) =>
855
+ left.moduleId.localeCompare(right.moduleId) ||
856
+ left.domain.localeCompare(right.domain) ||
857
+ left.code.localeCompare(right.code) ||
858
+ comparePaths(left.candidatePath ?? [], right.candidatePath ?? []) ||
859
+ compareSourceRefs(left.source ?? { sourcePath: '' }, right.source ?? { sourcePath: '' }),
860
+ )
861
+
862
+ return { overrideTargets, overrideTargetDiagnostics }
863
+ }