@open-mercato/cli 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7

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 +7 -24
  3. package/dist/agentic/claude-code/hooks/entity-migration-check.ts +88 -32
  4. package/dist/agentic/codex/enforcement-rules.md +16 -2
  5. package/dist/agentic/cursor/hooks/entity-migration-check.mjs +50 -19
  6. package/dist/agentic/cursor/rules/entity-guard.mdc +21 -9
  7. package/dist/agentic/cursor/rules/generated-guard.mdc +5 -3
  8. package/dist/agentic/cursor/rules/open-mercato.mdc +29 -3
  9. package/dist/agentic/guides/cache.md +45 -0
  10. package/dist/agentic/guides/core.md +453 -0
  11. package/dist/agentic/guides/events.md +117 -0
  12. package/dist/agentic/guides/module-facts.json +3833 -115419
  13. package/dist/agentic/guides/module-system.md +127 -12
  14. package/dist/agentic/guides/modules/ai_assistant.md +28 -227
  15. package/dist/agentic/guides/modules/api_docs.md +2 -58
  16. package/dist/agentic/guides/modules/api_keys.md +7 -92
  17. package/dist/agentic/guides/modules/attachments.md +14 -119
  18. package/dist/agentic/guides/modules/audit_logs.md +10 -131
  19. package/dist/agentic/guides/modules/auth.md +33 -276
  20. package/dist/agentic/guides/modules/business_rules.md +14 -149
  21. package/dist/agentic/guides/modules/catalog.md +43 -475
  22. package/dist/agentic/guides/modules/channel_gmail.md +3 -82
  23. package/dist/agentic/guides/modules/channel_imap.md +3 -82
  24. package/dist/agentic/guides/modules/checkout.md +27 -299
  25. package/dist/agentic/guides/modules/communication_channels.md +33 -315
  26. package/dist/agentic/guides/modules/configs.md +8 -104
  27. package/dist/agentic/guides/modules/content.md +2 -57
  28. package/dist/agentic/guides/modules/currencies.md +17 -152
  29. package/dist/agentic/guides/modules/customer_accounts.md +45 -387
  30. package/dist/agentic/guides/modules/customers.md +85 -746
  31. package/dist/agentic/guides/modules/dashboards.md +10 -155
  32. package/dist/agentic/guides/modules/data_sync.md +16 -149
  33. package/dist/agentic/guides/modules/dictionaries.md +12 -107
  34. package/dist/agentic/guides/modules/directory.md +15 -148
  35. package/dist/agentic/guides/modules/entities.md +13 -145
  36. package/dist/agentic/guides/modules/events.md +2 -59
  37. package/dist/agentic/guides/modules/feature_toggles.md +9 -119
  38. package/dist/agentic/guides/modules/gateway_stripe.md +4 -92
  39. package/dist/agentic/guides/modules/generators.md +2 -45
  40. package/dist/agentic/guides/modules/inbox_ops.md +27 -207
  41. package/dist/agentic/guides/modules/integrations.md +16 -146
  42. package/dist/agentic/guides/modules/messages.md +24 -223
  43. package/dist/agentic/guides/modules/notifications.md +8 -96
  44. package/dist/agentic/guides/modules/onboarding.md +6 -77
  45. package/dist/agentic/guides/modules/payment_gateways.md +21 -164
  46. package/dist/agentic/guides/modules/perspectives.md +7 -77
  47. package/dist/agentic/guides/modules/planner.md +18 -136
  48. package/dist/agentic/guides/modules/portal.md +2 -93
  49. package/dist/agentic/guides/modules/progress.md +14 -104
  50. package/dist/agentic/guides/modules/query_index.md +15 -154
  51. package/dist/agentic/guides/modules/record_locks.md +21 -206
  52. package/dist/agentic/guides/modules/resources.md +30 -197
  53. package/dist/agentic/guides/modules/sales.md +84 -678
  54. package/dist/agentic/guides/modules/scheduler.md +14 -133
  55. package/dist/agentic/guides/modules/search.md +6 -170
  56. package/dist/agentic/guides/modules/security.md +35 -294
  57. package/dist/agentic/guides/modules/shipping_carriers.md +18 -134
  58. package/dist/agentic/guides/modules/sso.md +26 -151
  59. package/dist/agentic/guides/modules/staff.md +57 -369
  60. package/dist/agentic/guides/modules/storage_s3.md +5 -95
  61. package/dist/agentic/guides/modules/sync_akeneo.md +3 -85
  62. package/dist/agentic/guides/modules/sync_excel.md +6 -75
  63. package/dist/agentic/guides/modules/system_status_overlays.md +2 -60
  64. package/dist/agentic/guides/modules/translations.md +10 -104
  65. package/dist/agentic/guides/modules/webhooks.md +24 -203
  66. package/dist/agentic/guides/modules/wms.md +46 -441
  67. package/dist/agentic/guides/modules/workflows.md +43 -302
  68. package/dist/agentic/guides/queue.md +53 -0
  69. package/dist/agentic/guides/search.md +115 -0
  70. package/dist/agentic/guides/shared.md +108 -0
  71. package/dist/agentic/guides/ui.md +347 -0
  72. package/dist/agentic/shared/AGENTS.md.template +311 -124
  73. package/dist/agentic/shared/ai/agentic.config.json +0 -2
  74. package/dist/agentic/shared/ai/lessons.md +3 -26
  75. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +27 -0
  76. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +27 -0
  77. package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +0 -4
  78. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +241 -20
  79. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +260 -0
  80. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +601 -16
  81. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +146 -0
  82. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +310 -14
  83. package/dist/agentic/shared/ai/skills/om-help/SKILL.md +97 -15
  84. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +84 -0
  85. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +193 -0
  86. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +152 -33
  87. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +708 -24
  88. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +34 -0
  89. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +762 -0
  90. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +820 -20
  91. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +50 -0
  92. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +97 -0
  93. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +843 -19
  94. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +271 -0
  95. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +63 -15
  96. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +467 -16
  97. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +101 -0
  98. package/dist/agentic/shared/ai/skills/tiers.json +21 -143
  99. package/dist/agentic/shared/ai/skills/tiers.schema.json +40 -52
  100. package/dist/agentic/shared/ai/specs/README.md +3 -4
  101. package/dist/agentic/shared/ai/specs/SPEC-000-template.md +10 -226
  102. package/dist/agentic/shared/ai/trackers/github.md +4 -108
  103. package/dist/agentic/shared/scripts/install-skills.sh +694 -4
  104. package/dist/bin.js +1 -13
  105. package/dist/bin.js.map +2 -2
  106. package/dist/lib/__integration__/TC-INT-008.spec.js +8 -88
  107. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  108. package/dist/lib/agentic-init.js +3 -13
  109. package/dist/lib/agentic-init.js.map +2 -2
  110. package/dist/lib/agentic-setup.js +186 -418
  111. package/dist/lib/agentic-setup.js.map +2 -2
  112. package/dist/lib/generators/extensions/injection-widgets.js +0 -20
  113. package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
  114. package/dist/lib/generators/module-facts-discovery.js +7 -41
  115. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  116. package/dist/lib/generators/module-facts.js +45 -1387
  117. package/dist/lib/generators/module-facts.js.map +3 -3
  118. package/dist/lib/generators/module-registry.js +5 -11
  119. package/dist/lib/generators/module-registry.js.map +2 -2
  120. package/dist/lib/resolver.js +0 -2
  121. package/dist/lib/resolver.js.map +2 -2
  122. package/dist/lib/testing/integration.js +0 -5
  123. package/dist/lib/testing/integration.js.map +2 -2
  124. package/dist/mercato.js +1 -23
  125. package/dist/mercato.js.map +2 -2
  126. package/package.json +11 -13
  127. package/src/bin.ts +1 -33
  128. package/src/lib/__integration__/TC-INT-008.spec.ts +9 -107
  129. package/src/lib/__tests__/agentic-init.test.ts +5 -41
  130. package/src/lib/agentic-init.ts +3 -15
  131. package/src/lib/agentic-setup.ts +224 -530
  132. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +2 -88
  133. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +0 -1
  134. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +10 -54
  135. package/src/lib/generators/__tests__/module-subset.test.ts +0 -18
  136. package/src/lib/generators/extensions/injection-widgets.ts +0 -51
  137. package/src/lib/generators/module-facts-discovery.ts +7 -58
  138. package/src/lib/generators/module-facts.ts +46 -1851
  139. package/src/lib/generators/module-registry.ts +6 -16
  140. package/src/lib/resolver.ts +0 -3
  141. package/src/lib/testing/__tests__/integration.test.ts +0 -4
  142. package/src/lib/testing/integration.ts +0 -5
  143. package/src/mercato.ts +1 -26
  144. package/dist/agentic/guides/ai-workflows.md +0 -76
  145. package/dist/agentic/guides/architecture.md +0 -107
  146. package/dist/agentic/guides/backend-ui.md +0 -80
  147. package/dist/agentic/guides/contracts.md +0 -92
  148. package/dist/agentic/guides/extensions.md +0 -121
  149. package/dist/agentic/guides/framework-extension-points.md +0 -36
  150. package/dist/agentic/guides/integrations.md +0 -81
  151. package/dist/agentic/guides/modules/design_system.md +0 -102
  152. package/dist/agentic/guides/spec-delivery.md +0 -22
  153. package/dist/agentic/guides/testing-debugging.md +0 -80
  154. package/dist/agentic/guides/upstream/AGENTS.md +0 -298
  155. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +0 -316
  156. package/dist/agentic/guides/upstream/manifest.json +0 -8
  157. package/dist/agentic/shared/ai/harness/README.md +0 -50
  158. package/dist/agentic/shared/ai/harness/RELEASE.md +0 -57
  159. package/dist/agentic/shared/ai/harness/cases.json +0 -13680
  160. package/dist/agentic/shared/ai/harness/cases.schema.json +0 -133
  161. package/dist/agentic/shared/ai/harness/fixtures/index.json +0 -49
  162. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +0 -162
  163. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +0 -21
  164. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +0 -96
  165. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +0 -172
  166. package/dist/agentic/shared/ai/harness/release-matrix.json +0 -158
  167. package/dist/agentic/shared/ai/harness/release-result.schema.json +0 -219
  168. package/dist/agentic/shared/ai/harness/result.schema.json +0 -76
  169. package/dist/agentic/shared/ai/harness/routing-response.schema.json +0 -12
  170. package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +0 -73
  171. package/dist/agentic/shared/ai/harness/validators.json +0 -78
  172. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +0 -2121
  173. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +0 -1096
  174. package/dist/agentic/shared/ai/lessons/_template.md +0 -16
  175. package/dist/agentic/shared/ai/review-checklist.md +0 -36
  176. package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +0 -21
  177. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +0 -98
  178. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +0 -16
  179. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +0 -53
  180. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +0 -13
  181. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +0 -11
  182. package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +0 -30
  183. package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +0 -12
  184. package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +0 -13
  185. package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +0 -14
  186. package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +0 -28
  187. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +0 -12
  188. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +0 -22
  189. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +0 -12
  190. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +0 -19
  191. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +0 -19
  192. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +0 -13
  193. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +0 -14
  194. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +0 -10
  195. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +0 -17
  196. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +0 -29
  197. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +0 -100
  198. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +0 -16
  199. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +0 -17
  200. package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +0 -25
  201. package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +0 -11
  202. package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +0 -13
  203. package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +0 -18
  204. package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +0 -20
  205. package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +0 -33
  206. package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +0 -40
  207. package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +0 -27
  208. package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +0 -24
  209. package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +0 -16
  210. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +0 -14
  211. package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +0 -15
  212. package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +0 -12
  213. package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +0 -34
  214. package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +0 -4
  215. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +0 -11
  216. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +0 -31
  217. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +0 -39
  218. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +0 -43
  219. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +0 -11
  220. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +0 -19
  221. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +0 -172
  222. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +0 -12
  223. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +0 -83
  224. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +0 -21
  225. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +0 -11
  226. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +0 -22
  227. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +0 -13
  228. package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +0 -41
  229. package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +0 -19
  230. package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +0 -44
  231. package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +0 -39
  232. package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +0 -20
  233. package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +0 -57
  234. package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +0 -11
  235. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +0 -486
  236. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +0 -18
  237. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +0 -27
  238. package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +0 -13
  239. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +0 -41
  240. package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +0 -11
  241. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +0 -15
  242. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +0 -16
  243. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +0 -14
  244. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +0 -187
  245. package/dist/agentic/shared/scripts/check-lessons.mjs +0 -178
  246. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +0 -2958
  247. package/dist/agentic/shared/scripts/execution-sandbox.mjs +0 -329
  248. package/dist/agentic/shared/scripts/framework-context.mjs +0 -888
  249. package/dist/agentic/shared/scripts/install-skills.mjs +0 -1029
  250. package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +0 -222
  251. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +0 -1774
  252. package/dist/lib/generators/module-extension-facts.js +0 -1840
  253. package/dist/lib/generators/module-extension-facts.js.map +0 -7
  254. package/dist/lib/generators/module-fact-sources.js +0 -70
  255. package/dist/lib/generators/module-fact-sources.js.map +0 -7
  256. package/dist/lib/generators/module-override-targets.js +0 -688
  257. package/dist/lib/generators/module-override-targets.js.map +0 -7
  258. package/dist/lib/load-env.js +0 -24
  259. package/dist/lib/load-env.js.map +0 -7
  260. package/dist/lib/telemetry-init.js +0 -358
  261. package/dist/lib/telemetry-init.js.map +0 -7
  262. package/src/lib/__tests__/agentic-setup.ownership.test.ts +0 -386
  263. package/src/lib/__tests__/injection-widget-id-validation.test.ts +0 -184
  264. package/src/lib/__tests__/load-env.test.ts +0 -65
  265. package/src/lib/__tests__/telemetry-init.test.ts +0 -298
  266. package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +0 -92
  267. package/src/lib/generators/__tests__/module-extension-activations.test.ts +0 -762
  268. package/src/lib/generators/__tests__/module-extension-facts.test.ts +0 -404
  269. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +0 -108
  270. package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +0 -561
  271. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +0 -119
  272. package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +0 -149
  273. package/src/lib/generators/__tests__/module-override-targets.test.ts +0 -304
  274. package/src/lib/generators/module-extension-facts.ts +0 -2365
  275. package/src/lib/generators/module-fact-sources.ts +0 -108
  276. package/src/lib/generators/module-override-targets.ts +0 -863
  277. package/src/lib/load-env.ts +0 -30
  278. package/src/lib/telemetry-init.ts +0 -461
@@ -1,2121 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'node:fs'
4
- import os from 'node:os'
5
- import path from 'node:path'
6
- import process from 'node:process'
7
- import { spawnSync } from 'node:child_process'
8
- import { createRequire } from 'node:module'
9
- import { fileURLToPath } from 'node:url'
10
- import { sandboxedInvocation } from '../../scripts/execution-sandbox.mjs'
11
-
12
- const TYPECHECK_TIMEOUT_MS = 120_000
13
- const MODULE_LOCALE_DIRECTORY = 'src/modules/library/i18n'
14
- const MODULE_LOCALE_NAMESPACE = 'library.'
15
- const MAX_MODULE_LOCALE_KEYS = 256
16
- const MAX_LOCALE_FILES = 16
17
- const MAX_LOCALE_FILE_BYTES = 256 * 1024
18
- const MAX_TOTAL_LOCALE_BYTES = 1024 * 1024
19
- const DANGEROUS_LOCALE_KEY_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor'])
20
- const LOCALIZED_METADATA_KEYS = new Set(['pageTitleKey', 'pageGroupKey', 'labelKey'])
21
-
22
- const COMPLETE_MODULE_CASE = Object.freeze({
23
- family: 'complete-module',
24
- sources: ['src/modules/library', 'src/modules.ts'],
25
- artifacts: [
26
- 'src/modules/library/index.ts',
27
- 'src/modules/library/acl.ts',
28
- 'src/modules/library/setup.ts',
29
- 'src/modules/library/encryption.ts',
30
- 'src/modules/library/search.ts',
31
- 'src/modules/library/data/entities.ts',
32
- 'src/modules/library/data/validators.ts',
33
- 'src/modules/library/migrations/**',
34
- 'src/modules/library/commands/**',
35
- 'src/modules/library/commands/__tests__/**',
36
- 'src/modules/library/api/books/route.ts',
37
- 'src/modules/library/backend/books/**',
38
- 'src/modules/library/i18n/en.json',
39
- 'src/modules.ts',
40
- ],
41
- })
42
-
43
- const WRITABLE_CASES = Object.freeze({
44
- 'OMH-009': {
45
- sources: ['src/modules/library'],
46
- artifacts: [
47
- 'src/modules/library/data/entities.ts',
48
- 'src/modules/library/data/validators.ts',
49
- 'src/modules/library/migrations/**',
50
- ],
51
- },
52
- 'OMH-011': {
53
- sources: ['src/modules/library/api/books/route.ts'],
54
- artifacts: ['src/modules/library/api/books/route.ts'],
55
- },
56
- 'OMH-012': {
57
- sources: ['src/modules/library/api/books/checkout/route.ts', 'src/modules/library/commands'],
58
- artifacts: ['src/modules/library/api/books/checkout/route.ts', 'src/modules/library/commands/**'],
59
- },
60
- 'OMH-014': {
61
- sources: ['src/modules/library/backend'],
62
- artifacts: ['src/modules/library/backend/**'],
63
- },
64
- 'OMH-026': {
65
- sources: ['src/modules/app_customizations'],
66
- artifacts: [
67
- 'src/modules/app_customizations/widgets/**',
68
- 'src/modules/app_customizations/data/enrichers.ts',
69
- 'src/modules/app_customizations/api/interceptors.ts',
70
- ],
71
- },
72
- 'OMH-027': {
73
- sources: ['src/modules/app_customizations/widgets'],
74
- artifacts: ['src/modules/app_customizations/widgets/**'],
75
- },
76
- 'OMH-029': {
77
- sources: ['src/modules/app_customizations'],
78
- artifacts: ['src/modules/app_customizations/**'],
79
- },
80
- 'OMH-031': {
81
- sources: ['src/modules/app_customizations/api/interceptors.ts'],
82
- artifacts: ['src/modules/app_customizations/api/interceptors.ts'],
83
- },
84
- 'OMH-042': {
85
- sources: ['src/modules/magento'],
86
- artifacts: ['src/modules/magento/**'],
87
- },
88
- 'OMH-045': {
89
- sources: ['src/modules/external_sync/lib/client.ts'],
90
- artifacts: ['src/modules/external_sync/lib/client.ts'],
91
- },
92
- 'OMH-049': {
93
- sources: ['src/modules/library/ai-agents.ts'],
94
- artifacts: ['src/modules/library/ai-agents.ts'],
95
- },
96
- 'OMH-054': {
97
- sources: ['src/modules/automation/workflows/call-api.ts'],
98
- artifacts: ['src/modules/automation/workflows/call-api.ts'],
99
- },
100
- 'OMH-057': {
101
- sources: ['src/modules/harness_fixture/api/scope/route.ts'],
102
- artifacts: ['src/modules/harness_fixture/api/scope/route.ts'],
103
- },
104
- 'OMH-060': {
105
- sources: ['src/modules/harness_fixture/commands/update-record.ts'],
106
- artifacts: ['src/modules/harness_fixture/commands/update-record.ts'],
107
- },
108
- 'OMH-061': {
109
- sources: ['src/modules/harness_fixture/backend/edit/page.tsx'],
110
- artifacts: ['src/modules/harness_fixture/backend/edit/page.tsx'],
111
- },
112
- 'OMH-070': {
113
- sources: ['src/modules/harness_fixture/workers/sync.ts'],
114
- artifacts: ['src/modules/harness_fixture/workers/sync.ts'],
115
- },
116
- 'OMH-093': {
117
- family: 'business-command',
118
- seam: 'mergeContacts',
119
- sources: ['src/modules/customer_merge/commands/merge-contacts.ts'],
120
- artifacts: ['src/modules/customer_merge/commands/merge-contacts.ts'],
121
- },
122
- 'OMH-105': {
123
- family: 'business-command',
124
- seam: 'changeDealStage',
125
- sources: ['src/modules/deal_stages/commands/change-stage.ts'],
126
- artifacts: ['src/modules/deal_stages/commands/change-stage.ts'],
127
- },
128
- 'OMH-107': {
129
- family: 'business-command',
130
- seam: 'requestQuoteDiscount',
131
- sources: ['src/modules/quote_approval/commands/request-discount.ts'],
132
- artifacts: ['src/modules/quote_approval/commands/request-discount.ts'],
133
- },
134
- 'OMH-115': {
135
- family: 'ui-business-surface',
136
- seam: 'moveDealAccessibly',
137
- handler: 'handleDealBoardAction',
138
- render: 'DealBoardPage',
139
- event: 'onClick',
140
- components: ['Page', 'PageHeader', 'PageBody', 'Button', 'Alert'],
141
- metadata: ['requireAuth', 'requireFeatures'],
142
- sources: [
143
- 'src/modules/deal_accessibility/backend/board/page.tsx',
144
- 'src/modules/deal_accessibility/backend/board/page.meta.ts',
145
- 'src/modules/deal_accessibility/lib/move-deal.ts',
146
- ],
147
- artifacts: [
148
- 'src/modules/deal_accessibility/backend/board/page.tsx',
149
- 'src/modules/deal_accessibility/backend/board/page.meta.ts',
150
- 'src/modules/deal_accessibility/lib/move-deal.ts',
151
- 'src/modules/deal_accessibility/i18n/en.json',
152
- ],
153
- },
154
- 'OMH-122': {
155
- family: 'business-command',
156
- seam: 'reserveStock',
157
- sources: ['src/modules/stock_reservations/commands/reserve-stock.ts'],
158
- artifacts: ['src/modules/stock_reservations/commands/reserve-stock.ts'],
159
- },
160
- 'OMH-128': {
161
- family: 'async-operation',
162
- seam: 'updatePrices',
163
- sources: ['src/modules/bulk_pricing/commands/update-prices.ts'],
164
- artifacts: ['src/modules/bulk_pricing/commands/update-prices.ts'],
165
- },
166
- 'OMH-130': {
167
- family: 'ui-business-surface',
168
- seam: 'submitDemoRequest',
169
- handler: 'handleDemoRequest',
170
- render: 'RequestDemoPage',
171
- event: 'onSubmit',
172
- components: ['FormField', 'Input', 'CheckboxField', 'Button', 'Alert'],
173
- metadata: ['navHidden'],
174
- allowFormTag: true,
175
- sources: [
176
- 'src/modules/demo_requests/frontend/request-demo/page.tsx',
177
- 'src/modules/demo_requests/frontend/request-demo/page.meta.ts',
178
- 'src/modules/demo_requests/lib/submit-demo-request.ts',
179
- ],
180
- artifacts: [
181
- 'src/modules/demo_requests/frontend/request-demo/page.tsx',
182
- 'src/modules/demo_requests/frontend/request-demo/page.meta.ts',
183
- 'src/modules/demo_requests/lib/submit-demo-request.ts',
184
- 'src/modules/demo_requests/i18n/en.json',
185
- ],
186
- },
187
- 'OMH-133': {
188
- family: 'business-command',
189
- seam: 'approvePortalQuote',
190
- sources: ['src/modules/portal_quote_approval/commands/approve-quote.ts'],
191
- artifacts: ['src/modules/portal_quote_approval/commands/approve-quote.ts'],
192
- },
193
- 'OMH-137': {
194
- family: 'ui-business-surface',
195
- seam: 'advanceSetupWizard',
196
- handler: 'handleSetupWizardAction',
197
- render: 'SetupWizardPage',
198
- event: 'onClick',
199
- components: ['Page', 'PageHeader', 'PageBody', 'StepIndicator', 'Button', 'Alert', 'LoadingMessage'],
200
- metadata: ['requireAuth', 'requireFeatures'],
201
- sources: [
202
- 'src/modules/setup_wizard/backend/setup/page.tsx',
203
- 'src/modules/setup_wizard/backend/setup/page.meta.ts',
204
- 'src/modules/setup_wizard/lib/advance-setup.ts',
205
- ],
206
- artifacts: [
207
- 'src/modules/setup_wizard/backend/setup/page.tsx',
208
- 'src/modules/setup_wizard/backend/setup/page.meta.ts',
209
- 'src/modules/setup_wizard/lib/advance-setup.ts',
210
- 'src/modules/setup_wizard/i18n/en.json',
211
- ],
212
- },
213
- 'OMH-140': {
214
- family: 'async-operation',
215
- seam: 'runInvoiceDunning',
216
- sources: ['src/modules/invoice_dunning'],
217
- artifacts: ['src/modules/invoice_dunning/workflows/**', 'src/modules/invoice_dunning/events.ts'],
218
- },
219
- 'OMH-144': {
220
- family: 'ai-safe-agent',
221
- seam: 'saveQuoteDraftWithApproval',
222
- mode: 'mutation',
223
- sources: ['src/modules/quote_assistant/ai-agents.ts', 'src/modules/quote_assistant/ai-tools.ts'],
224
- artifacts: ['src/modules/quote_assistant/ai-agents.ts', 'src/modules/quote_assistant/ai-tools.ts'],
225
- },
226
- 'OMH-146': {
227
- family: 'ai-safe-agent',
228
- seam: 'coordinateSalesQuestion',
229
- mode: 'delegate',
230
- sources: ['src/modules/sales_orchestrator/ai-agents.ts'],
231
- artifacts: ['src/modules/sales_orchestrator/ai-agents.ts'],
232
- },
233
- 'OMH-149': {
234
- family: 'provider-adapter',
235
- seam: 'sendTransactionalEmail',
236
- providerKind: 'transactional-email',
237
- moduleId: 'smtp_email',
238
- healthService: 'smtpEmailHealthCheck',
239
- sources: ['src/modules/smtp_email', 'src/modules.ts'],
240
- artifacts: [
241
- 'src/modules/smtp_email/index.ts',
242
- 'src/modules/smtp_email/integration.ts',
243
- 'src/modules/smtp_email/di.ts',
244
- 'src/modules/smtp_email/lib/client.ts',
245
- 'src/modules/smtp_email/lib/health.ts',
246
- 'src/modules.ts',
247
- ],
248
- },
249
- 'OMH-150': {
250
- family: 'provider-adapter',
251
- seam: 'createCardPayment',
252
- providerKind: 'payment',
253
- moduleId: 'card_payments',
254
- healthService: 'cardPaymentsHealthCheck',
255
- adapterVariable: 'cardPaymentAdapter',
256
- sources: ['src/modules/card_payments', 'src/modules.ts'],
257
- artifacts: [
258
- 'src/modules/card_payments/index.ts',
259
- 'src/modules/card_payments/integration.ts',
260
- 'src/modules/card_payments/di.ts',
261
- 'src/modules/card_payments/acl.ts',
262
- 'src/modules/card_payments/setup.ts',
263
- 'src/modules/card_payments/lib/adapter.ts',
264
- 'src/modules/card_payments/lib/client.ts',
265
- 'src/modules/card_payments/lib/health.ts',
266
- 'src/modules.ts',
267
- ],
268
- },
269
- 'OMH-151': {
270
- family: 'provider-adapter',
271
- seam: 'bookCarrierShipment',
272
- providerKind: 'shipping',
273
- moduleId: 'carrier_shipping',
274
- healthService: 'carrierShippingHealthCheck',
275
- adapterVariable: 'carrierShippingAdapter',
276
- sources: ['src/modules/carrier_shipping', 'src/modules.ts'],
277
- artifacts: [
278
- 'src/modules/carrier_shipping/index.ts',
279
- 'src/modules/carrier_shipping/integration.ts',
280
- 'src/modules/carrier_shipping/di.ts',
281
- 'src/modules/carrier_shipping/acl.ts',
282
- 'src/modules/carrier_shipping/setup.ts',
283
- 'src/modules/carrier_shipping/lib/adapter.ts',
284
- 'src/modules/carrier_shipping/lib/client.ts',
285
- 'src/modules/carrier_shipping/lib/health.ts',
286
- 'src/modules.ts',
287
- ],
288
- },
289
- 'OMH-153': {
290
- family: 'data-flow',
291
- seam: 'synchronizeErpPage',
292
- sources: ['src/modules/erp_sync'],
293
- artifacts: ['src/modules/erp_sync/data-sync.ts', 'src/modules/erp_sync/backend/**', 'src/modules/erp_sync/workers/**'],
294
- },
295
- 'OMH-156': {
296
- family: 'data-flow',
297
- seam: 'transferProductRows',
298
- sources: ['src/modules/product_transfer/lib/flow.ts'],
299
- artifacts: ['src/modules/product_transfer/lib/flow.ts'],
300
- },
301
- 'OMH-163': {
302
- family: 'test-authoring-unit',
303
- sources: ['src/modules/quote_approval/commands/__tests__/approve-quote.test.ts'],
304
- artifacts: ['src/modules/quote_approval/commands/__tests__/approve-quote.test.ts'],
305
- },
306
- 'OMH-164': {
307
- family: 'test-authoring-api',
308
- sources: ['src/modules/customer_api/__integration__/TC-API-CUSTOMERS-001.spec.ts'],
309
- artifacts: ['src/modules/customer_api/__integration__/TC-API-CUSTOMERS-001.spec.ts'],
310
- },
311
- 'OMH-165': {
312
- family: 'test-authoring-browser',
313
- sources: ['src/modules/portal_quote_approval/__integration__/TC-PORTAL-QUOTE-001.spec.ts'],
314
- artifacts: ['src/modules/portal_quote_approval/__integration__/TC-PORTAL-QUOTE-001.spec.ts'],
315
- },
316
- 'OMH-171': {
317
- family: 'regression',
318
- seam: 'listRecords',
319
- sources: ['src/modules/harness_fixture/api/scope/route.ts'],
320
- artifacts: ['src/modules/harness_fixture/api/scope/route.ts'],
321
- },
322
- 'OMH-172': {
323
- family: 'regression',
324
- sources: ['src/modules/harness_fixture/backend/edit/page.tsx'],
325
- artifacts: ['src/modules/harness_fixture/backend/edit/page.tsx'],
326
- },
327
- 'OMH-181': {
328
- family: 'data-table-extension',
329
- seam: 'reviewOrderRisk',
330
- sources: [
331
- 'src/modules/order_risk/widgets/injection/order-risk-filter/widget.ts',
332
- 'src/modules/order_risk/widgets/injection/order-risk-review/widget.ts',
333
- 'src/modules/order_risk/widgets/injection-table.ts',
334
- ],
335
- artifacts: [
336
- 'src/modules/order_risk/widgets/injection/order-risk-filter/widget.ts',
337
- 'src/modules/order_risk/widgets/injection/order-risk-review/widget.ts',
338
- 'src/modules/order_risk/widgets/injection-table.ts',
339
- 'src/modules/order_risk/i18n/en.json',
340
- ],
341
- },
342
- 'OMH-188': {
343
- family: 'booking-overlap',
344
- sources: ['src/modules/room_bookings', 'src/modules.ts'],
345
- artifacts: [
346
- 'src/modules/room_bookings/index.ts',
347
- 'src/modules/room_bookings/data/entities.ts',
348
- 'src/modules/room_bookings/data/validators.ts',
349
- 'src/modules/room_bookings/migrations/**',
350
- 'src/modules/room_bookings/commands/**',
351
- 'src/modules/room_bookings/api/bookings/route.ts',
352
- 'src/modules.ts',
353
- ],
354
- },
355
- 'OMH-189': {
356
- family: 'provider-transport',
357
- moduleId: 'room_calendar_sync',
358
- healthService: 'roomCalendarSyncHealth',
359
- sources: ['src/modules/room_calendar_sync', 'src/modules.ts'],
360
- artifacts: [
361
- 'src/modules/room_calendar_sync/index.ts',
362
- 'src/modules/room_calendar_sync/integration.ts',
363
- 'src/modules/room_calendar_sync/di.ts',
364
- 'src/modules/room_calendar_sync/lib/**',
365
- 'src/modules.ts',
366
- ],
367
- },
368
- 'OMH-190': {
369
- family: 'response-enricher',
370
- sources: ['src/modules/room_bookings/data/enrichers.ts'],
371
- artifacts: ['src/modules/room_bookings/data/enrichers.ts'],
372
- },
373
- 'OMH-191': {
374
- family: 'durable-workflow',
375
- sources: ['src/modules/room_bookings/workflows.ts'],
376
- artifacts: ['src/modules/room_bookings/workflows.ts'],
377
- },
378
- 'OMH-192': {
379
- family: 'crm-library-regression',
380
- sources: ['src/modules/library/commands/crm-loans.ts', 'src/modules/library/api/schemas.ts', 'src/modules/library/commands/__tests__/crm-loans.test.ts'],
381
- schemaSource: 'src/modules/library/api/schemas.ts',
382
- testSource: 'src/modules/library/commands/__tests__/crm-loans.test.ts',
383
- artifacts: ['src/modules/library/commands/crm-loans.ts', 'src/modules/library/api/schemas.ts', 'src/modules/library/commands/__tests__/crm-loans.test.ts'],
384
- },
385
- 'OMH-185': COMPLETE_MODULE_CASE,
386
- 'OMH-193': COMPLETE_MODULE_CASE,
387
- })
388
-
389
- export const WRITABLE_CASE_IDS = Object.freeze(Object.keys(WRITABLE_CASES).sort())
390
-
391
- function parseArgs(argv) {
392
- const options = { root: undefined, caseId: undefined, phase: undefined, json: false }
393
- for (let index = 0; index < argv.length; index += 1) {
394
- const arg = argv[index]
395
- const value = () => {
396
- const next = argv[index + 1]
397
- if (!next || next.startsWith('--')) throw new Error(`${arg} requires a value`)
398
- index += 1
399
- return next
400
- }
401
- if (arg === '--root') options.root = value()
402
- else if (arg === '--case') options.caseId = value()
403
- else if (arg === '--phase') options.phase = value()
404
- else if (arg === '--json') options.json = true
405
- else throw new Error(`unknown argument: ${arg}`)
406
- }
407
- if (!options.root || !options.caseId || !options.phase || !options.json) {
408
- throw new Error('--root, --case, --phase before|after, and --json are required')
409
- }
410
- if (!path.isAbsolute(options.root)) throw new Error('--root must be absolute')
411
- if (!WRITABLE_CASES[options.caseId]) throw new Error(`unsupported writable case: ${options.caseId}`)
412
- if (!['before', 'after'].includes(options.phase)) throw new Error('--phase must be before or after')
413
- return options
414
- }
415
-
416
- function loadTargetTypeScript(root) {
417
- const targetRequire = createRequire(path.join(root, 'package.json'))
418
- try {
419
- return targetRequire('typescript')
420
- } catch (error) {
421
- throw new Error(`target app cannot resolve TypeScript: ${error instanceof Error ? error.message : String(error)}`)
422
- }
423
- }
424
-
425
- function isTypeScriptSource(file) {
426
- return /\.(?:cts|mts|ts|tsx)$/.test(file) && !/\.d\.(?:cts|mts|ts)$/.test(file)
427
- }
428
-
429
- function safeTargetEntry(root, absolute) {
430
- const relative = path.relative(root, absolute)
431
- if (!relative || relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)) {
432
- throw new Error(`oracle path escapes the target: ${relative || '.'}`)
433
- }
434
- let current = root
435
- for (const segment of relative.split(path.sep)) {
436
- current = path.join(current, segment)
437
- if (!fs.existsSync(current)) return undefined
438
- const stat = fs.lstatSync(current)
439
- if (stat.isSymbolicLink()) throw new Error(`oracle path contains a symbolic link: ${path.relative(root, current).replaceAll(path.sep, '/')}`)
440
- if (!stat.isDirectory() && !stat.isFile()) throw new Error(`oracle path contains a special file: ${path.relative(root, current).replaceAll(path.sep, '/')}`)
441
- }
442
- if (!path.relative(root, fs.realpathSync(current)).startsWith('..')) return fs.lstatSync(current)
443
- throw new Error(`oracle path resolves outside the target: ${relative.replaceAll(path.sep, '/')}`)
444
- }
445
-
446
- function collectSourceFiles(root, relativeEntries) {
447
- const found = new Set()
448
- const visit = (absolute) => {
449
- const stat = safeTargetEntry(root, absolute)
450
- if (!stat) return
451
- if (stat.isFile()) {
452
- if (isTypeScriptSource(absolute)) found.add(absolute)
453
- return
454
- }
455
- if (!stat.isDirectory()) return
456
- for (const entry of fs.readdirSync(absolute, { withFileTypes: true })) {
457
- if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
458
- if (!entry.isDirectory() && !isTypeScriptSource(entry.name)) continue
459
- visit(path.join(absolute, entry.name))
460
- }
461
- }
462
- for (const relative of relativeEntries) visit(path.join(root, relative))
463
- return [...found].sort()
464
- }
465
-
466
- function literalString(ts, node) {
467
- const expression = node ? unwrapExpression(ts, node) : undefined
468
- return expression && (ts.isStringLiteralLike(expression) || ts.isNoSubstitutionTemplateLiteral(expression))
469
- ? expression.text
470
- : undefined
471
- }
472
-
473
- function isModuleLocaleReferenceSource(file) {
474
- const normalized = file.replaceAll(path.sep, '/')
475
- if (normalized.includes('/__tests__/') || /\.(?:test|spec)\.(?:cts|mts|ts|tsx)$/.test(normalized)) return false
476
- if (normalized.includes('/migrations/')) return false
477
- const isRoutedUi = ['/backend/', '/frontend/', '/widgets/'].some((segment) => normalized.includes(segment))
478
- return isRoutedUi && (normalized.endsWith('.tsx') || normalized.endsWith('/page.meta.ts'))
479
- }
480
-
481
- function collectModuleLocaleKeys(ts, sourceFiles) {
482
- const keys = new Set()
483
- let excessive = false
484
- const addKey = (value) => {
485
- if (!value?.startsWith(MODULE_LOCALE_NAMESPACE)) return
486
- if (keys.size >= MAX_MODULE_LOCALE_KEYS && !keys.has(value)) {
487
- excessive = true
488
- return
489
- }
490
- keys.add(value)
491
- }
492
- for (const file of sourceFiles.filter(isModuleLocaleReferenceSource)) {
493
- const source = ts.createSourceFile(
494
- file,
495
- fs.readFileSync(file, 'utf8'),
496
- ts.ScriptTarget.Latest,
497
- true,
498
- file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
499
- )
500
- const visit = (node) => {
501
- if (ts.isCallExpression(node) && expressionName(ts, node.expression) === 't') {
502
- addKey(literalString(ts, node.arguments[0]))
503
- }
504
- if (ts.isPropertyAssignment(node)) {
505
- const name = propertyName(ts, node.name)
506
- if (LOCALIZED_METADATA_KEYS.has(name)) addKey(literalString(ts, node.initializer))
507
- }
508
- ts.forEachChild(node, visit)
509
- }
510
- visit(source)
511
- }
512
- return { keys: [...keys].sort(), excessive }
513
- }
514
-
515
- function isPlainLocaleObject(value) {
516
- if (!value || typeof value !== 'object' || Array.isArray(value)) return false
517
- const prototype = Object.getPrototypeOf(value)
518
- return prototype === Object.prototype || prototype === null
519
- }
520
-
521
- function safeDiagnosticToken(value) {
522
- return String(value).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 200)
523
- }
524
-
525
- function resolveLocaleValue(catalog, key) {
526
- const segments = key.split('.')
527
- if (segments.some((segment) => DANGEROUS_LOCALE_KEY_SEGMENTS.has(segment))) {
528
- return { valid: false, reason: 'contains a dangerous path segment' }
529
- }
530
- let current = catalog
531
- for (const segment of segments) {
532
- if (!isPlainLocaleObject(current) || !Object.prototype.hasOwnProperty.call(current, segment)) {
533
- return { valid: false, reason: 'is missing' }
534
- }
535
- current = current[segment]
536
- }
537
- if (typeof current !== 'string') return { valid: false, reason: 'is not a string' }
538
- if (!current.trim()) return { valid: false, reason: 'is blank' }
539
- return { valid: true }
540
- }
541
-
542
- function moduleLocaleCatalogCheck(ts, root, sourceFiles) {
543
- const failures = []
544
- const addFailure = (message) => {
545
- if (failures.length < 32) failures.push(message)
546
- }
547
- try {
548
- const extracted = collectModuleLocaleKeys(ts, sourceFiles)
549
- if (extracted.excessive) addFailure(`more than ${MAX_MODULE_LOCALE_KEYS} literal ${MODULE_LOCALE_NAMESPACE} keys`)
550
- if (extracted.keys.length === 0) addFailure(`no literal ${MODULE_LOCALE_NAMESPACE} translation keys`)
551
- const localeDirectory = path.join(root, MODULE_LOCALE_DIRECTORY)
552
- const directoryStat = safeTargetEntry(root, localeDirectory)
553
- if (!directoryStat?.isDirectory()) {
554
- addFailure('i18n directory is missing')
555
- return check('module.locale-catalog', false, `literal module locale catalogs resolve to non-empty strings (${failures.join('; ')})`)
556
- }
557
- const localeEntries = fs.readdirSync(localeDirectory, { withFileTypes: true })
558
- .filter((entry) => entry.name.endsWith('.json'))
559
- .sort((left, right) => left.name.localeCompare(right.name))
560
- if (localeEntries.length > MAX_LOCALE_FILES) addFailure(`locale file count exceeds ${MAX_LOCALE_FILES}`)
561
- const boundedEntries = localeEntries.slice(0, MAX_LOCALE_FILES)
562
- if (!boundedEntries.some((entry) => entry.name === 'en.json')) addFailure('en.json is missing')
563
- let totalBytes = 0
564
- const catalogs = []
565
- for (const entry of boundedEntries) {
566
- const localeName = safeDiagnosticToken(entry.name)
567
- const absolute = path.join(localeDirectory, entry.name)
568
- const stat = safeTargetEntry(root, absolute)
569
- if (!stat?.isFile()) {
570
- addFailure(`${localeName} is not a regular file`)
571
- continue
572
- }
573
- if (stat.size > MAX_LOCALE_FILE_BYTES) {
574
- addFailure(`${localeName} exceeds ${MAX_LOCALE_FILE_BYTES} bytes`)
575
- continue
576
- }
577
- totalBytes += stat.size
578
- if (totalBytes > MAX_TOTAL_LOCALE_BYTES) {
579
- addFailure(`locale input exceeds ${MAX_TOTAL_LOCALE_BYTES} total bytes`)
580
- break
581
- }
582
- let catalog
583
- try {
584
- catalog = JSON.parse(fs.readFileSync(absolute, 'utf8'))
585
- } catch {
586
- addFailure(`${localeName} contains malformed JSON`)
587
- continue
588
- }
589
- if (!isPlainLocaleObject(catalog)) {
590
- addFailure(`${localeName} root is not a plain object`)
591
- continue
592
- }
593
- catalogs.push({ name: localeName, catalog })
594
- }
595
- for (const { name, catalog } of catalogs) {
596
- for (const key of extracted.keys) {
597
- const resolution = resolveLocaleValue(catalog, key)
598
- if (!resolution.valid) addFailure(`${name} ${safeDiagnosticToken(key)} ${resolution.reason}`)
599
- }
600
- }
601
- } catch (error) {
602
- const message = error instanceof Error ? error.message : String(error)
603
- addFailure(safeDiagnosticToken(message))
604
- }
605
- return check(
606
- 'module.locale-catalog',
607
- failures.length === 0,
608
- failures.length
609
- ? `literal module locale catalogs resolve to non-empty strings (${failures.join('; ')})`
610
- : `en.json and every emitted sibling locale resolve every literal ${MODULE_LOCALE_NAMESPACE} key to a non-empty string`,
611
- )
612
- }
613
-
614
- function artifactExists(root, pattern) {
615
- if (!pattern.endsWith('/**')) return Boolean(safeTargetEntry(root, path.join(root, pattern)))
616
- const directory = path.join(root, pattern.slice(0, -3))
617
- const directoryStat = safeTargetEntry(root, directory)
618
- if (!directoryStat?.isDirectory()) return false
619
- const pending = [directory]
620
- while (pending.length) {
621
- const current = pending.pop()
622
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
623
- const absolute = path.join(current, entry.name)
624
- const stat = safeTargetEntry(root, absolute)
625
- if (stat?.isFile()) return true
626
- if (stat?.isDirectory()) pending.push(absolute)
627
- }
628
- }
629
- return false
630
- }
631
-
632
- function propertyName(ts, node) {
633
- if (!node) return undefined
634
- if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node) || ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) {
635
- return node.text
636
- }
637
- if (ts.isComputedPropertyName(node) && ts.isStringLiteralLike(node.expression)) return node.expression.text
638
- return undefined
639
- }
640
-
641
- function expressionName(ts, expression) {
642
- if (ts.isIdentifier(expression)) return expression.text
643
- if (ts.isPropertyAccessExpression(expression)) return expression.name.text
644
- if (ts.isElementAccessExpression(expression) && expression.argumentExpression && ts.isStringLiteralLike(expression.argumentExpression)) {
645
- return expression.argumentExpression.text
646
- }
647
- return undefined
648
- }
649
-
650
- function fullExpressionName(ts, expression) {
651
- if (ts.isIdentifier(expression)) return expression.text
652
- if (ts.isPropertyAccessExpression(expression)) {
653
- const left = fullExpressionName(ts, expression.expression)
654
- return left ? `${left}.${expression.name.text}` : expression.name.text
655
- }
656
- return expressionName(ts, expression)
657
- }
658
-
659
- function expressionPath(ts, expression) {
660
- if (ts.isIdentifier(expression)) return [expression.text]
661
- if (ts.isPropertyAccessExpression(expression)) {
662
- return [...expressionPath(ts, expression.expression), expression.name.text]
663
- }
664
- if (ts.isElementAccessExpression(expression) && expression.argumentExpression && ts.isStringLiteralLike(expression.argumentExpression)) {
665
- return [...expressionPath(ts, expression.expression), expression.argumentExpression.text]
666
- }
667
- if (ts.isCallExpression(expression)) return expressionPath(ts, expression.expression)
668
- if (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isNonNullExpression(expression)) {
669
- return expressionPath(ts, expression.expression)
670
- }
671
- return []
672
- }
673
-
674
- const FORBIDDEN_TEST_ALIASES = new Set(['fdescribe', 'fit', 'pending', 'xdescribe', 'xit', 'xtest'])
675
- const TEST_RUNNER_ROOTS = new Set(['describe', 'it', 'test'])
676
- const FORBIDDEN_TEST_MODIFIERS = new Set(['fail', 'failing', 'fixme', 'only', 'skip', 'todo'])
677
- const TEST_INFO_MODIFIERS = new Set(['fail', 'fixme', 'skip'])
678
-
679
- function forbiddenTestModifier(ts, expression) {
680
- const parts = expressionPath(ts, expression)
681
- if (FORBIDDEN_TEST_ALIASES.has(parts[0])) return parts.join('.')
682
- if (parts[0] === 'testInfo' && parts.slice(1).some((part) => TEST_INFO_MODIFIERS.has(part))) return parts.join('.')
683
- if (!TEST_RUNNER_ROOTS.has(parts[0])) return undefined
684
- return parts.slice(1).some((part) => FORBIDDEN_TEST_MODIFIERS.has(part)) ? parts.join('.') : undefined
685
- }
686
-
687
- function exactString(ts, expression, expected) {
688
- return Boolean(expression && ts.isStringLiteralLike(expression) && expression.text === expected)
689
- }
690
-
691
- function listenBindsLoopback(ts, call) {
692
- const first = call.arguments[0]
693
- if (first && ts.isObjectLiteralExpression(first)) {
694
- if (first.properties.some((property) => ts.isSpreadAssignment(property))) return false
695
- if (first.properties.some((property) => property.name && ts.isComputedPropertyName(property.name))) return false
696
- const hosts = first.properties.filter((property) => propertyName(ts, property.name) === 'host')
697
- return hosts.length === 1 && ts.isPropertyAssignment(hosts[0]) && exactString(ts, hosts[0].initializer, '127.0.0.1')
698
- }
699
- return exactString(ts, call.arguments[1], '127.0.0.1')
700
- }
701
-
702
- function jsxTagName(ts, tag) {
703
- if (ts.isIdentifier(tag)) return tag.text
704
- if (ts.isPropertyAccessExpression(tag)) return tag.name.text
705
- return tag.getText()
706
- }
707
-
708
- function newFacts() {
709
- return {
710
- calls: new Map(),
711
- callArgumentFacts: new Map(),
712
- callArgumentExpressions: new Map(),
713
- callOptions: new Map(),
714
- classes: [],
715
- declarations: new Set(),
716
- decorators: new Set(),
717
- exportedFunctions: new Map(),
718
- exportedFunctionNodes: new Map(),
719
- exportedObjectFacts: new Map(),
720
- exportedVariables: new Set(),
721
- functionFacts: new Map(),
722
- functionNodes: new Map(),
723
- functions: new Set(),
724
- finallyBlocks: 0,
725
- importedBindings: new Map(),
726
- importSources: new Set(),
727
- forbiddenTestModifiers: new Set(),
728
- jsxLiteralAttributes: new Map(),
729
- jsxAttributes: new Set(),
730
- jsxTags: new Set(),
731
- jsxText: [],
732
- loops: 0,
733
- listenBindings: [],
734
- newCalls: new Set(),
735
- nullNodes: 0,
736
- objectProperties: new Set(),
737
- referencedFunctions: new Set(),
738
- objectFacts: new Map(),
739
- propertyIdentifiers: new Map(),
740
- propertyAccesses: new Set(),
741
- strings: new Set(),
742
- throwStatements: 0,
743
- variables: new Map(),
744
- variableExpressions: new Map(),
745
- assignments: new Set(),
746
- awaitedCalls: new Set(),
747
- moduleEntries: [],
748
- }
749
- }
750
-
751
- function isExportedFunction(ts, node) {
752
- return Boolean(node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword))
753
- }
754
-
755
- function isExportedVariable(ts, node) {
756
- const statement = node.parent?.parent
757
- return Boolean(statement && ts.isVariableStatement(statement) && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword))
758
- }
759
-
760
- function addMappedValue(map, key, value) {
761
- if (!map.has(key)) map.set(key, new Set())
762
- map.get(key).add(value)
763
- }
764
-
765
- function jsxAttributeLiteral(ts, attribute) {
766
- if (!attribute.initializer) return undefined
767
- if (ts.isStringLiteral(attribute.initializer)) return attribute.initializer.text
768
- if (ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression && ts.isStringLiteralLike(attribute.initializer.expression)) {
769
- return attribute.initializer.expression.text
770
- }
771
- return undefined
772
- }
773
-
774
- function collectFunctionFact(ts, node, { skipNestedDeclarations = false, skipNestedFunctions = false } = {}) {
775
- const fact = {
776
- binaryOperators: new Set(),
777
- calls: new Set(),
778
- callOptions: new Map(),
779
- conditionalExpressions: 0,
780
- finallyBlocks: 0,
781
- jsxAttributes: new Set(),
782
- jsxLiteralAttributes: new Map(),
783
- jsxTags: new Set(),
784
- jsxText: [],
785
- loops: 0,
786
- nullNodes: 0,
787
- newCalls: new Set(),
788
- objectProperties: new Set(),
789
- referencedFunctions: new Set(),
790
- strings: new Set(),
791
- throws: 0,
792
- }
793
- const root = node.body ?? node
794
- const visit = (current) => {
795
- if (skipNestedFunctions && current !== root && ts.isFunctionLike(current)) return
796
- if (skipNestedDeclarations && current !== root && ts.isFunctionDeclaration(current)) return
797
- if (ts.isImportDeclaration(current) || ts.isImportEqualsDeclaration(current) || ts.isExportDeclaration(current)) return
798
- if (ts.isCallExpression(current)) {
799
- const names = [expressionName(ts, current.expression), fullExpressionName(ts, current.expression)].filter(Boolean)
800
- const optionNames = current.arguments.flatMap((argument) => ts.isObjectLiteralExpression(argument)
801
- ? argument.properties.map((property) => propertyName(ts, property.name)).filter(Boolean)
802
- : [])
803
- for (const name of names) {
804
- fact.calls.add(name)
805
- if (!fact.callOptions.has(name)) fact.callOptions.set(name, [])
806
- fact.callOptions.get(name).push(new Set(optionNames))
807
- }
808
- for (const argument of current.arguments) {
809
- const resolved = unwrapExpression(ts, argument)
810
- if (resolved && ts.isIdentifier(resolved)) fact.referencedFunctions.add(resolved.text)
811
- }
812
- }
813
- if (ts.isNewExpression(current)) {
814
- const name = expressionName(ts, current.expression)
815
- if (name) fact.newCalls.add(name)
816
- }
817
- if (ts.isObjectLiteralElementLike(current)) {
818
- const name = propertyName(ts, current.name)
819
- if (name) fact.objectProperties.add(name)
820
- }
821
- if (ts.isPropertyAssignment(current) && ts.isIdentifier(current.initializer)) {
822
- fact.referencedFunctions.add(current.initializer.text)
823
- }
824
- if (ts.isThrowStatement(current)) fact.throws += 1
825
- if (ts.isBinaryExpression(current)) fact.binaryOperators.add(current.operatorToken.kind)
826
- if (ts.isConditionalExpression(current)) fact.conditionalExpressions += 1
827
- if (current.kind === ts.SyntaxKind.NullKeyword) fact.nullNodes += 1
828
- if (ts.isStringLiteralLike(current) || ts.isNoSubstitutionTemplateLiteral(current)) fact.strings.add(current.text)
829
- if (ts.isJsxText(current) && current.text.trim()) fact.jsxText.push(current.text.trim())
830
- if (ts.isJsxOpeningElement(current) || ts.isJsxSelfClosingElement(current)) {
831
- fact.jsxTags.add(jsxTagName(ts, current.tagName))
832
- for (const attribute of current.attributes.properties) {
833
- if (!ts.isJsxAttribute(attribute)) continue
834
- const name = attribute.name.text
835
- fact.jsxAttributes.add(name)
836
- const literal = jsxAttributeLiteral(ts, attribute)
837
- if (literal !== undefined) addMappedValue(fact.jsxLiteralAttributes, name, literal)
838
- }
839
- }
840
- if (ts.isTryStatement(current) && current.finallyBlock) fact.finallyBlocks += 1
841
- if (ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current) || ts.isWhileStatement(current) || ts.isDoStatement(current)) {
842
- fact.loops += 1
843
- }
844
- ts.forEachChild(current, visit)
845
- }
846
- visit(root)
847
- return fact
848
- }
849
-
850
- function addCall(facts, name, optionNames = []) {
851
- facts.calls.set(name, (facts.calls.get(name) ?? 0) + 1)
852
- if (!facts.callOptions.has(name)) facts.callOptions.set(name, [])
853
- facts.callOptions.get(name).push(new Set(optionNames))
854
- }
855
-
856
- function unwrapExpression(ts, node) {
857
- let current = node
858
- while (current && (
859
- ts.isParenthesizedExpression(current)
860
- || ts.isAsExpression(current)
861
- || ts.isTypeAssertionExpression(current)
862
- || ts.isNonNullExpression(current)
863
- || (typeof ts.isSatisfiesExpression === 'function' && ts.isSatisfiesExpression(current))
864
- )) {
865
- current = current.expression
866
- }
867
- return current
868
- }
869
-
870
- function classMemberNames(ts, member) {
871
- const names = new Set()
872
- const identifier = propertyName(ts, member.name)
873
- if (identifier) names.add(identifier)
874
- const decorators = typeof ts.getDecorators === 'function' && ts.canHaveDecorators(member)
875
- ? ts.getDecorators(member) ?? []
876
- : []
877
- for (const decorator of decorators) {
878
- if (!ts.isCallExpression(decorator.expression)) continue
879
- if (expressionName(ts, decorator.expression.expression) !== 'Property') continue
880
- for (const argument of decorator.expression.arguments) {
881
- if (!ts.isObjectLiteralExpression(argument)) continue
882
- for (const option of argument.properties) {
883
- if (!ts.isPropertyAssignment(option)) continue
884
- const optionName = propertyName(ts, option.name)
885
- if (!['name', 'fieldName'].includes(optionName) || !ts.isStringLiteralLike(option.initializer)) continue
886
- names.add(option.initializer.text)
887
- }
888
- }
889
- }
890
- return names
891
- }
892
-
893
- function collectFacts(ts, sourceFiles) {
894
- const facts = newFacts()
895
- for (const file of sourceFiles) {
896
- const source = ts.createSourceFile(
897
- file,
898
- fs.readFileSync(file, 'utf8'),
899
- ts.ScriptTarget.Latest,
900
- true,
901
- file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
902
- )
903
- const visit = (node) => {
904
- if (ts.isImportDeclaration(node)) {
905
- const source = node.moduleSpecifier.text
906
- facts.importSources.add(source)
907
- const clause = node.importClause
908
- if (clause?.name) facts.importedBindings.set(clause.name.text, source)
909
- if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
910
- for (const element of clause.namedBindings.elements) facts.importedBindings.set(element.name.text, source)
911
- } else if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
912
- facts.importedBindings.set(clause.namedBindings.name.text, source)
913
- }
914
- return
915
- }
916
- if (ts.isImportEqualsDeclaration(node) || ts.isExportDeclaration(node)) return
917
-
918
- if (ts.isClassDeclaration(node)) {
919
- const members = new Set(node.members.flatMap((member) => [...classMemberNames(ts, member)]))
920
- const decorators = typeof ts.getDecorators === 'function' && ts.canHaveDecorators(node)
921
- ? (ts.getDecorators(node) ?? []).map((decorator) => expressionName(ts, decorator.expression.expression ?? decorator.expression)).filter(Boolean)
922
- : []
923
- facts.classes.push({ name: node.name?.text, members, decorators: new Set(decorators) })
924
- if (node.name) facts.declarations.add(node.name.text)
925
- for (const decorator of decorators) facts.decorators.add(decorator)
926
- }
927
- if (ts.isFunctionDeclaration(node) && node.name) {
928
- const functionFact = collectFunctionFact(ts, node)
929
- facts.functions.add(node.name.text)
930
- facts.declarations.add(node.name.text)
931
- facts.functionFacts.set(node.name.text, functionFact)
932
- facts.functionNodes.set(node.name.text, node)
933
- if (isExportedFunction(ts, node)) {
934
- facts.exportedFunctions.set(node.name.text, functionFact)
935
- facts.exportedFunctionNodes.set(node.name.text, node)
936
- }
937
- }
938
- if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) {
939
- facts.declarations.add(node.name.text)
940
- }
941
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
942
- facts.declarations.add(node.name.text)
943
- if (isExportedVariable(ts, node)) facts.exportedVariables.add(node.name.text)
944
- const properties = new Set()
945
- const initializer = node.initializer ? unwrapExpression(ts, node.initializer) : undefined
946
- if (initializer) facts.variableExpressions.set(node.name.text, initializer)
947
- if (initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
948
- const functionFact = collectFunctionFact(ts, initializer)
949
- facts.functions.add(node.name.text)
950
- facts.functionFacts.set(node.name.text, functionFact)
951
- facts.functionNodes.set(node.name.text, initializer)
952
- if (isExportedVariable(ts, node)) {
953
- facts.exportedFunctions.set(node.name.text, functionFact)
954
- facts.exportedFunctionNodes.set(node.name.text, initializer)
955
- }
956
- }
957
- if (initializer && ts.isObjectLiteralExpression(initializer)) {
958
- const objectFact = collectFunctionFact(ts, initializer)
959
- facts.objectFacts.set(node.name.text, objectFact)
960
- if (isExportedVariable(ts, node)) facts.exportedObjectFacts.set(node.name.text, objectFact)
961
- for (const property of initializer.properties) {
962
- const name = propertyName(ts, property.name)
963
- if (name) properties.add(name)
964
- }
965
- }
966
- facts.variables.set(node.name.text, properties)
967
- if (node.name.text === 'enabledModules' && node.initializer && ts.isArrayLiteralExpression(node.initializer)) {
968
- for (const element of node.initializer.elements) {
969
- if (!ts.isObjectLiteralExpression(element)) continue
970
- const entry = {}
971
- for (const property of element.properties) {
972
- if (!ts.isPropertyAssignment(property)) continue
973
- const name = propertyName(ts, property.name)
974
- if (!name || !ts.isStringLiteralLike(property.initializer)) continue
975
- entry[name] = property.initializer.text
976
- }
977
- facts.moduleEntries.push(entry)
978
- }
979
- }
980
- }
981
- if (ts.isCallExpression(node)) {
982
- const name = expressionName(ts, node.expression)
983
- const fullName = fullExpressionName(ts, node.expression)
984
- if (fullName === 'enabledModules.push') {
985
- for (const argument of node.arguments) {
986
- if (!ts.isObjectLiteralExpression(argument)) continue
987
- const entry = {}
988
- for (const property of argument.properties) {
989
- if (!ts.isPropertyAssignment(property)) continue
990
- const propertyKey = propertyName(ts, property.name)
991
- if (!propertyKey || !ts.isStringLiteralLike(property.initializer)) continue
992
- entry[propertyKey] = property.initializer.text
993
- }
994
- facts.moduleEntries.push(entry)
995
- }
996
- }
997
- const forbiddenModifier = forbiddenTestModifier(ts, node.expression)
998
- if (forbiddenModifier) facts.forbiddenTestModifiers.add(forbiddenModifier)
999
- const callPath = expressionPath(ts, node.expression)
1000
- if (callPath.includes('listen')) facts.listenBindings.push(callPath.at(-1) === 'listen' && listenBindsLoopback(ts, node))
1001
- const optionNames = node.arguments.flatMap((argument) => ts.isObjectLiteralExpression(argument)
1002
- ? argument.properties.map((property) => propertyName(ts, property.name)).filter(Boolean)
1003
- : [])
1004
- if (name) {
1005
- const expressions = facts.callArgumentExpressions.get(name) ?? []
1006
- expressions.push(...node.arguments)
1007
- facts.callArgumentExpressions.set(name, expressions)
1008
- const argumentFacts = node.arguments
1009
- .map((argument) => unwrapExpression(ts, argument))
1010
- .filter((argument) => ts.isObjectLiteralExpression(argument) || ts.isArrayLiteralExpression(argument))
1011
- .map((argument) => collectFunctionFact(ts, argument))
1012
- if (argumentFacts.length > 0) {
1013
- const existing = facts.callArgumentFacts.get(name) ?? []
1014
- existing.push(...argumentFacts)
1015
- facts.callArgumentFacts.set(name, existing)
1016
- }
1017
- }
1018
- if (name) addCall(facts, name, optionNames)
1019
- if (fullName && fullName !== name) addCall(facts, fullName, optionNames)
1020
- if (node.parent && ts.isAwaitExpression(node.parent) && name) facts.awaitedCalls.add(name)
1021
- }
1022
- if (ts.isNewExpression(node)) {
1023
- const name = expressionName(ts, node.expression)
1024
- if (name) facts.newCalls.add(name)
1025
- }
1026
- if (ts.isObjectLiteralElementLike(node)) {
1027
- const name = propertyName(ts, node.name)
1028
- if (name) facts.objectProperties.add(name)
1029
- }
1030
- if (ts.isPropertyAssignment(node)) {
1031
- const name = propertyName(ts, node.name)
1032
- if (name && ts.isIdentifier(node.initializer)) addMappedValue(facts.propertyIdentifiers, name, node.initializer.text)
1033
- }
1034
- if (ts.isPropertyAccessExpression(node)) {
1035
- facts.propertyAccesses.add(node.name.text)
1036
- const forbiddenModifier = forbiddenTestModifier(ts, node)
1037
- if (forbiddenModifier) facts.forbiddenTestModifiers.add(forbiddenModifier)
1038
- }
1039
- if (ts.isElementAccessExpression(node)) {
1040
- const forbiddenModifier = forbiddenTestModifier(ts, node)
1041
- if (forbiddenModifier) facts.forbiddenTestModifiers.add(forbiddenModifier)
1042
- }
1043
- if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) facts.strings.add(node.text)
1044
- if (node.kind === ts.SyntaxKind.NullKeyword) facts.nullNodes += 1
1045
- if (ts.isThrowStatement(node)) facts.throwStatements += 1
1046
- if (ts.isTryStatement(node) && node.finallyBlock) facts.finallyBlocks += 1
1047
- if (ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node)) {
1048
- facts.loops += 1
1049
- }
1050
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
1051
- const name = expressionName(ts, node.left)
1052
- if (name) facts.assignments.add(name)
1053
- }
1054
- if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
1055
- facts.jsxTags.add(jsxTagName(ts, node.tagName))
1056
- for (const attribute of node.attributes.properties) {
1057
- if (!ts.isJsxAttribute(attribute)) continue
1058
- const name = attribute.name.text
1059
- facts.jsxAttributes.add(name)
1060
- const literal = jsxAttributeLiteral(ts, attribute)
1061
- if (literal !== undefined) addMappedValue(facts.jsxLiteralAttributes, name, literal)
1062
- }
1063
- }
1064
- if (ts.isJsxText(node) && node.text.trim()) facts.jsxText.push(node.text.trim())
1065
- ts.forEachChild(node, visit)
1066
- }
1067
- visit(source)
1068
- }
1069
- return facts
1070
- }
1071
-
1072
- function hasCall(facts, ...names) {
1073
- return names.some((name) => (facts.calls.get(name) ?? 0) > 0)
1074
- }
1075
-
1076
- function hasOnlyEnabledTests(facts) {
1077
- return facts.forbiddenTestModifiers.size === 0
1078
- }
1079
-
1080
- function allServersBindLoopback(facts) {
1081
- return facts.listenBindings.length > 0 && facts.listenBindings.every(Boolean)
1082
- }
1083
-
1084
- function hasCallOptions(facts, callName, required) {
1085
- return (facts.callOptions.get(callName) ?? []).some((options) => required.every((name) => options.has(name)))
1086
- }
1087
-
1088
- function hasObjectVariable(facts, variableName, required) {
1089
- const properties = facts.variables.get(variableName)
1090
- return Boolean(properties && required.every((name) => properties.has(name)))
1091
- }
1092
-
1093
- export function hasExactString(facts, value) {
1094
- return facts.strings.has(value)
1095
- }
1096
-
1097
- // SQL migrations arrive as one template-literal string, so containment is the
1098
- // only way to assert a clause without hard-coding the author's formatting.
1099
- function hasStringIncluding(facts, ...fragments) {
1100
- return [...facts.strings].some((entry) => fragments.every((fragment) => entry.includes(fragment)))
1101
- }
1102
-
1103
- function hasStringPrefix(facts, value) {
1104
- return [...facts.strings].some((entry) => entry.startsWith(value))
1105
- }
1106
-
1107
- function factReachesCalls(facts, start, requiredCalls) {
1108
- if (!start) return false
1109
- const reached = new Set(start.calls)
1110
- const pending = [...start.calls, ...start.referencedFunctions]
1111
- const visited = new Set()
1112
- while (pending.length) {
1113
- const name = pending.pop()
1114
- if (visited.has(name)) continue
1115
- visited.add(name)
1116
- const local = facts.functionFacts.get(name)
1117
- if (!local) continue
1118
- for (const call of local.calls) {
1119
- reached.add(call)
1120
- pending.push(call)
1121
- }
1122
- for (const reference of local.referencedFunctions) pending.push(reference)
1123
- }
1124
- return requiredCalls.every((name) => reached.has(name))
1125
- }
1126
-
1127
- function reachableFunctionFacts(facts, start) {
1128
- if (!start) return []
1129
- const reached = [start]
1130
- const pending = [...start.calls, ...start.referencedFunctions]
1131
- const visited = new Set()
1132
- while (pending.length) {
1133
- const name = pending.pop()
1134
- if (visited.has(name)) continue
1135
- visited.add(name)
1136
- const local = facts.functionFacts.get(name)
1137
- if (!local) continue
1138
- reached.push(local)
1139
- for (const call of local.calls) pending.push(call)
1140
- for (const reference of local.referencedFunctions) pending.push(reference)
1141
- }
1142
- return reached
1143
- }
1144
-
1145
- function factReachesContract(facts, start, predicate) {
1146
- return reachableFunctionFacts(facts, start).some(predicate)
1147
- }
1148
-
1149
- function callHasArgumentContract(facts, callName, predicate) {
1150
- return (facts.callArgumentFacts.get(callName) ?? []).some(predicate)
1151
- }
1152
-
1153
- function resolveExpression(ts, facts, node, seen = new Set()) {
1154
- const expression = unwrapExpression(ts, node)
1155
- if (!expression || !ts.isIdentifier(expression) || seen.has(expression.text)) return expression
1156
- const target = facts.variableExpressions.get(expression.text)
1157
- if (!target) return expression
1158
- const nextSeen = new Set(seen)
1159
- nextSeen.add(expression.text)
1160
- return resolveExpression(ts, facts, target, nextSeen)
1161
- }
1162
-
1163
- function objectProperty(ts, object, name) {
1164
- if (!object || !ts.isObjectLiteralExpression(object)) return undefined
1165
- return object.properties.find((property) => propertyName(ts, property.name) === name)
1166
- }
1167
-
1168
- function propertyExpression(ts, facts, object, name) {
1169
- const property = objectProperty(ts, object, name)
1170
- if (!property) return undefined
1171
- if (ts.isPropertyAssignment(property)) return resolveExpression(ts, facts, property.initializer)
1172
- if (ts.isShorthandPropertyAssignment(property)) return resolveExpression(ts, facts, property.name)
1173
- return undefined
1174
- }
1175
-
1176
- function propertyFunction(ts, facts, object, name) {
1177
- const property = objectProperty(ts, object, name)
1178
- if (!property) return undefined
1179
- if (ts.isMethodDeclaration(property)) return property
1180
- if (!ts.isPropertyAssignment(property)) return undefined
1181
- const expression = resolveExpression(ts, facts, property.initializer)
1182
- return expression && (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression))
1183
- ? expression
1184
- : undefined
1185
- }
1186
-
1187
- function stringValue(ts, facts, node) {
1188
- const expression = resolveExpression(ts, facts, node)
1189
- return expression && ts.isStringLiteralLike(expression) ? expression.text : undefined
1190
- }
1191
-
1192
- function booleanValue(ts, facts, node) {
1193
- const expression = resolveExpression(ts, facts, node)
1194
- if (!expression) return undefined
1195
- if (expression.kind === ts.SyntaxKind.TrueKeyword) return true
1196
- if (expression.kind === ts.SyntaxKind.FalseKeyword) return false
1197
- return undefined
1198
- }
1199
-
1200
- function numberValue(ts, facts, node) {
1201
- const expression = resolveExpression(ts, facts, node)
1202
- return expression && ts.isNumericLiteral(expression) ? Number(expression.text) : undefined
1203
- }
1204
-
1205
- function arrayElements(ts, facts, node) {
1206
- const expression = resolveExpression(ts, facts, node)
1207
- return expression && ts.isArrayLiteralExpression(expression)
1208
- ? expression.elements.map((element) => resolveExpression(ts, facts, element)).filter(Boolean)
1209
- : []
1210
- }
1211
-
1212
- function objectPropertyString(ts, facts, object, name) {
1213
- return stringValue(ts, facts, propertyExpression(ts, facts, object, name))
1214
- }
1215
-
1216
- function objectPropertyObject(ts, facts, object, name) {
1217
- const expression = propertyExpression(ts, facts, object, name)
1218
- return expression && ts.isObjectLiteralExpression(expression) ? expression : undefined
1219
- }
1220
-
1221
- function objectPropertyArray(ts, facts, object, name) {
1222
- return arrayElements(ts, facts, propertyExpression(ts, facts, object, name))
1223
- }
1224
-
1225
- function resolvedCallArguments(ts, facts, callName) {
1226
- return (facts.callArgumentExpressions.get(callName) ?? [])
1227
- .map((argument) => resolveExpression(ts, facts, argument))
1228
- .filter(Boolean)
1229
- }
1230
-
1231
- function functionContractFacts(ts, facts, node, options) {
1232
- const direct = collectFunctionFact(ts, node, { skipNestedDeclarations: true, ...options })
1233
- if (options?.skipNestedFunctions) {
1234
- const reached = [direct]
1235
- const pending = [...direct.calls, ...direct.referencedFunctions]
1236
- const visited = new Set()
1237
- while (pending.length) {
1238
- const name = pending.pop()
1239
- if (visited.has(name)) continue
1240
- visited.add(name)
1241
- const target = facts.functionNodes.get(name)
1242
- if (!target) continue
1243
- const local = collectFunctionFact(ts, target, { skipNestedDeclarations: true, ...options })
1244
- reached.push(local)
1245
- for (const call of local.calls) pending.push(call)
1246
- for (const reference of local.referencedFunctions) pending.push(reference)
1247
- }
1248
- return reached
1249
- }
1250
- return reachableFunctionFacts(facts, direct)
1251
- }
1252
-
1253
- function returnedObjectFunctions(ts, facts, node) {
1254
- const returned = []
1255
- const root = node.body ?? node
1256
- const collectObjectMethods = (expression) => {
1257
- const object = resolveExpression(ts, facts, expression)
1258
- if (!object || !ts.isObjectLiteralExpression(object)) return
1259
- for (const property of object.properties) {
1260
- const name = propertyName(ts, property.name)
1261
- const method = name ? propertyFunction(ts, facts, object, name) : undefined
1262
- if (method) returned.push(method)
1263
- }
1264
- }
1265
- if (ts.isObjectLiteralExpression(root)) collectObjectMethods(root)
1266
- const visit = (current) => {
1267
- if (current !== root && ts.isFunctionLike(current)) return
1268
- if (ts.isReturnStatement(current) && current.expression) {
1269
- collectObjectMethods(current.expression)
1270
- return
1271
- }
1272
- ts.forEachChild(current, visit)
1273
- }
1274
- visit(root)
1275
- return returned
1276
- }
1277
-
1278
- function functionContractCalls(ts, facts, node, requiredCalls) {
1279
- const reached = functionContractFacts(ts, facts, node)
1280
- const calls = new Set(reached.flatMap((fact) => [...fact.calls]))
1281
- return requiredCalls.every((name) => calls.has(name))
1282
- }
1283
-
1284
- function functionContractMatches(ts, facts, node, predicate) {
1285
- return functionContractFacts(ts, facts, node).some(predicate)
1286
- }
1287
-
1288
- function functionHasLiteralTrueOption(ts, facts, node, callName, optionName) {
1289
- const functionNodes = [node]
1290
- const pending = [...collectFunctionFact(ts, node, { skipNestedDeclarations: true }).calls]
1291
- const visited = new Set()
1292
- while (pending.length) {
1293
- const name = pending.pop()
1294
- if (visited.has(name)) continue
1295
- visited.add(name)
1296
- const target = facts.functionNodes.get(name)
1297
- if (!target) continue
1298
- functionNodes.push(target)
1299
- for (const call of collectFunctionFact(ts, target, { skipNestedDeclarations: true }).calls) pending.push(call)
1300
- }
1301
- return functionNodes.some((functionNode) => {
1302
- let matched = false
1303
- const root = functionNode.body ?? functionNode
1304
- const visit = (current) => {
1305
- if (matched) return
1306
- if (current !== root && ts.isFunctionDeclaration(current)) return
1307
- if (ts.isCallExpression(current) && expressionName(ts, current.expression) === callName) {
1308
- for (const argument of current.arguments) {
1309
- const object = resolveExpression(ts, facts, argument)
1310
- const value = object && ts.isObjectLiteralExpression(object)
1311
- ? propertyExpression(ts, facts, object, optionName)
1312
- : undefined
1313
- if (booleanValue(ts, facts, value) === true) {
1314
- matched = true
1315
- return
1316
- }
1317
- }
1318
- }
1319
- ts.forEachChild(current, visit)
1320
- }
1321
- visit(root)
1322
- return matched
1323
- })
1324
- }
1325
-
1326
- function exportedFunctionCalls(facts, functionName, requiredCalls) {
1327
- return factReachesCalls(facts, facts.exportedFunctions.get(functionName), requiredCalls)
1328
- }
1329
-
1330
- function exportedObjectCalls(facts, variableName, requiredCalls) {
1331
- return factReachesCalls(facts, facts.exportedObjectFacts.get(variableName), requiredCalls)
1332
- }
1333
-
1334
- function exportedCommandObjectCalls(facts, variableName, requiredCalls) {
1335
- return [variableName, `${variableName}Command`].some((name) => exportedObjectCalls(facts, name, requiredCalls))
1336
- }
1337
-
1338
- function exportedFunctionHasCallOptions(facts, functionName, callName, requiredOptions) {
1339
- const fact = facts.exportedFunctions.get(functionName)
1340
- return Boolean(fact && (fact.callOptions.get(callName) ?? []).some((options) => requiredOptions.every((name) => options.has(name))))
1341
- }
1342
-
1343
- function importsSharedComponent(facts, name) {
1344
- return facts.importedBindings.get(name)?.startsWith('@open-mercato/ui') === true
1345
- }
1346
-
1347
- function uiPolicyFailures(facts, { allowFormTag = false } = {}) {
1348
- const rawTags = ['button', 'input', 'select', 'textarea', 'svg', ...(allowFormTag ? [] : ['form'])]
1349
- const failures = []
1350
- for (const tag of rawTags) if (facts.jsxTags.has(tag)) failures.push(`raw <${tag}>`)
1351
- if (facts.jsxAttributes.has('style')) failures.push('inline style')
1352
- const strings = [...facts.strings]
1353
- if (strings.some((value) => /(?:^|\s)(?:[a-z-]+:)*(?:text|bg|border|ring)-(?:red|green|emerald|blue|amber|orange|yellow|rose|lime|cyan|teal|indigo|violet|purple|pink)-\d{2,3}(?:\/\d+)?\b/.test(value))) {
1354
- failures.push('hard-coded palette class')
1355
- }
1356
- if (strings.some((value) => /(?:^|\s)\S*\[[^\]]+\]/.test(value))) failures.push('arbitrary Tailwind value')
1357
- if (strings.some((value) => /(?:^|\s)dark:/.test(value))) failures.push('manual dark-mode override')
1358
- for (const name of ['label', 'title', 'placeholder', 'aria-label', 'alt']) {
1359
- if ((facts.jsxLiteralAttributes.get(name)?.size ?? 0) > 0) failures.push(`hard-coded ${name}`)
1360
- }
1361
- if (facts.jsxText.length > 0) failures.push('hard-coded JSX copy')
1362
- return failures
1363
- }
1364
-
1365
- function renderedUiChecks(definition, facts) {
1366
- const render = facts.exportedFunctions.get(definition.render)
1367
- const handler = facts.exportedFunctions.get(definition.handler)
1368
- const policyFailures = uiPolicyFailures(facts, { allowFormTag: definition.allowFormTag })
1369
- const strings = [...(render?.strings ?? [])]
1370
- return [
1371
- check('business.ui-rendered-components', Boolean(render) && definition.components.every((name) => render.jsxTags.has(name) && importsSharedComponent(facts, name)), `exported ${definition.render} renders the required shared Open Mercato components`),
1372
- check('business.ui-event-link', Boolean(render) && render.jsxAttributes.has(definition.event) && render.calls.has(definition.handler) && handler?.calls.has(definition.seam), `rendered ${definition.event} reaches ${definition.handler} and the tested ${definition.seam} seam`),
1373
- check('business.ui-localized', Boolean(render) && render.calls.has('useT') && render.calls.has('t') && render.jsxText.length === 0, `exported ${definition.render} obtains all visible copy through useT`),
1374
- check('business.ui-accessible-feedback', Boolean(render) && render.jsxAttributes.has('aria-live') && render.jsxTags.has('Alert') && render.jsxAttributes.has('status'), `exported ${definition.render} renders an accessible shared Alert status region`),
1375
- check('business.ui-responsive', strings.some((value) => /(?:^|\s)(?:sm|md|lg|xl|2xl):/.test(value)), `exported ${definition.render} includes a standard responsive breakpoint`),
1376
- check('business.ui-semantic-token', strings.some((value) => /(?:^|\s)(?:text-(?:foreground|muted-foreground)|bg-(?:background|muted)|border-border)\b/.test(value)), `exported ${definition.render} uses semantic design tokens`),
1377
- check('business.ui-metadata', hasObjectVariable(facts, 'metadata', definition.metadata), `the sibling page metadata declares ${definition.metadata.join(' and ')}`),
1378
- check('business.ui-policy', policyFailures.length === 0, `case-owned UI avoids raw controls/forms, inline SVG/style, hard-coded copy/colors, arbitrary Tailwind, and manual dark overrides${policyFailures.length ? ` (${policyFailures.join(', ')})` : ''}`),
1379
- ]
1380
- }
1381
-
1382
- function check(id, passed, requirement) {
1383
- return { id, passed: Boolean(passed), requirement }
1384
- }
1385
-
1386
- function routeMetadataPath(ts, sourceFile) {
1387
- const source = ts.createSourceFile(
1388
- sourceFile,
1389
- fs.readFileSync(sourceFile, 'utf8'),
1390
- ts.ScriptTarget.Latest,
1391
- true,
1392
- sourceFile.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
1393
- )
1394
- for (const statement of source.statements) {
1395
- if (!ts.isVariableStatement(statement)) continue
1396
- if (!statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
1397
- for (const declaration of statement.declarationList.declarations) {
1398
- if (!ts.isIdentifier(declaration.name) || declaration.name.text !== 'metadata') continue
1399
- const initializer = unwrapExpression(ts, declaration.initializer)
1400
- if (!initializer || !ts.isObjectLiteralExpression(initializer)) continue
1401
- for (const property of initializer.properties) {
1402
- if (!ts.isPropertyAssignment(property) || propertyName(ts, property.name) !== 'path') continue
1403
- const value = unwrapExpression(ts, property.initializer)
1404
- if (value && ts.isStringLiteralLike(value)) return value.text
1405
- }
1406
- }
1407
- }
1408
- return undefined
1409
- }
1410
-
1411
- function normalizeRoutePath(value) {
1412
- const normalizedSegments = value
1413
- .replaceAll('\\', '/')
1414
- .split('/')
1415
- .filter((segment) => segment && !(segment.startsWith('(') && segment.endsWith(')')) && !segment.startsWith('@'))
1416
- .map((segment) => {
1417
- if (/^\[\[\.\.\.[^\]]+\]\]$/.test(segment)) return '[[...]]'
1418
- if (/^\[\.\.\.[^\]]+\]$/.test(segment)) return '[...]'
1419
- if (/^\[[^\]]+\]$/.test(segment)) return '[]'
1420
- return segment
1421
- })
1422
- return `/${normalizedSegments.join('/')}`
1423
- }
1424
-
1425
- function sourceHttpMethods(ts, sourceFile) {
1426
- const source = ts.createSourceFile(sourceFile, fs.readFileSync(sourceFile, 'utf8'), ts.ScriptTarget.Latest, true)
1427
- const methods = new Set()
1428
- for (const statement of source.statements) {
1429
- if (ts.isFunctionDeclaration(statement) && statement.name && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
1430
- if (['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(statement.name.text)) methods.add(statement.name.text)
1431
- }
1432
- if (!ts.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
1433
- for (const declaration of statement.declarationList.declarations) {
1434
- if (ts.isIdentifier(declaration.name) && ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(declaration.name.text)) methods.add(declaration.name.text)
1435
- if (!ts.isObjectBindingPattern(declaration.name)) continue
1436
- for (const element of declaration.name.elements) {
1437
- const name = element.propertyName ?? element.name
1438
- if (ts.isIdentifier(name) && ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(name.text)) methods.add(name.text)
1439
- }
1440
- }
1441
- }
1442
- return methods
1443
- }
1444
-
1445
- function pageRoutePath(moduleId, surface, pageFile) {
1446
- const surfaceRoot = path.join('src', 'modules', moduleId, surface)
1447
- const relative = path.relative(surfaceRoot, pageFile).replaceAll(path.sep, '/')
1448
- const segments = relative.split('/')
1449
- const file = segments.pop()
1450
- const modern = file === 'page.ts' || file === 'page.tsx'
1451
- const routeSegments = modern ? segments : [...segments, file.replace(/\.(?:ts|tsx)$/, '')]
1452
- if (surface === 'frontend') return normalizeRoutePath(`/${routeSegments.join('/')}`)
1453
- const backendSegments = modern
1454
- ? routeSegments
1455
- : routeSegments[0] === moduleId ? routeSegments : [moduleId, ...routeSegments]
1456
- return normalizeRoutePath(`/backend/${backendSegments.length ? backendSegments.join('/') : moduleId}`)
1457
- }
1458
-
1459
- function installedFactRoutes(root) {
1460
- const factsFile = path.join(root, '.ai', 'guides', 'module-facts.json')
1461
- const factsStat = safeTargetEntry(root, factsFile)
1462
- if (!factsStat) return []
1463
- if (!factsStat.isFile()) throw new Error('installed module facts must be a regular file')
1464
- const facts = JSON.parse(fs.readFileSync(factsFile, 'utf8'))
1465
- if (!facts || typeof facts !== 'object' || Array.isArray(facts)) {
1466
- throw new Error('installed module facts must contain a module object')
1467
- }
1468
-
1469
- const routes = []
1470
- for (const [moduleId, moduleFacts] of Object.entries(facts)) {
1471
- if (!moduleFacts || typeof moduleFacts !== 'object' || Array.isArray(moduleFacts)) continue
1472
- const sourceRoot = typeof moduleFacts.sourceRoot === 'string'
1473
- ? moduleFacts.sourceRoot
1474
- : `.ai/guides/module-facts.json#${moduleId}`
1475
- for (const apiRoute of Array.isArray(moduleFacts.apiRoutes) ? moduleFacts.apiRoutes : []) {
1476
- if (!apiRoute || typeof apiRoute !== 'object' || typeof apiRoute.path !== 'string') continue
1477
- const methods = new Set(
1478
- (Array.isArray(apiRoute.methods) ? apiRoute.methods : [])
1479
- .filter((method) => typeof method === 'string')
1480
- .map((method) => method.toUpperCase()),
1481
- )
1482
- if (!methods.size) continue
1483
- routes.push({
1484
- surface: 'api',
1485
- routePath: normalizeRoutePath(apiRoute.path),
1486
- sourceFile: undefined,
1487
- displayPath: typeof apiRoute.sourcePath === 'string' ? apiRoute.sourcePath : `${sourceRoot}#api:${apiRoute.path}`,
1488
- methods,
1489
- origin: 'installed',
1490
- })
1491
- }
1492
- for (const [surface, factKey] of [['backend', 'backendPages'], ['frontend', 'frontendPages']]) {
1493
- for (const page of Array.isArray(moduleFacts[factKey]) ? moduleFacts[factKey] : []) {
1494
- if (!page || typeof page !== 'object' || typeof page.path !== 'string') continue
1495
- routes.push({
1496
- surface,
1497
- routePath: normalizeRoutePath(page.path),
1498
- sourceFile: undefined,
1499
- displayPath: typeof page.sourcePath === 'string' ? page.sourcePath : `${sourceRoot}#${surface}:${page.path}`,
1500
- methods: new Set(),
1501
- origin: 'installed',
1502
- })
1503
- }
1504
- }
1505
- }
1506
- return routes
1507
- }
1508
-
1509
- function duplicateRouteChecks(ts, root) {
1510
- const modulesRoot = path.join(root, 'src', 'modules')
1511
- const moduleStat = safeTargetEntry(root, modulesRoot)
1512
- if (!moduleStat?.isDirectory()) return []
1513
- const routes = []
1514
- for (const moduleEntry of fs.readdirSync(modulesRoot, { withFileTypes: true })) {
1515
- if (!moduleEntry.isDirectory() || moduleEntry.name.startsWith('.')) continue
1516
- for (const surface of ['api', 'backend', 'frontend']) {
1517
- const surfaceRoot = path.join(modulesRoot, moduleEntry.name, surface)
1518
- const files = collectSourceFiles(root, [path.relative(root, surfaceRoot)])
1519
- for (const sourceFile of files) {
1520
- const relative = path.relative(surfaceRoot, sourceFile).replaceAll(path.sep, '/')
1521
- if (/\.(?:test|spec)\.(?:ts|tsx)$/.test(relative) || /(?:^|\/)page\.meta\.(?:ts|tsx)$/.test(relative) || /(?:^|\/)[^/]+\.meta\.(?:ts|tsx)$/.test(relative)) continue
1522
- if (surface === 'api') {
1523
- const segments = relative.split('/')
1524
- const file = segments.pop()
1525
- const legacyMethod = ['get', 'post', 'put', 'patch', 'delete'].includes(segments[0]) ? segments.shift().toUpperCase() : undefined
1526
- const modern = file === 'route.ts' || file === 'route.tsx'
1527
- const routeSegments = modern ? segments : [...segments, file.replace(/\.(?:ts|tsx)$/, '')]
1528
- const metadataPath = routeMetadataPath(ts, sourceFile)
1529
- const routePath = normalizeRoutePath(metadataPath ?? `/${[moduleEntry.name, ...routeSegments].join('/')}`)
1530
- const methods = legacyMethod ? new Set([legacyMethod]) : sourceHttpMethods(ts, sourceFile)
1531
- if (methods.size) routes.push({
1532
- surface,
1533
- routePath,
1534
- sourceFile,
1535
- displayPath: path.relative(root, sourceFile).replaceAll(path.sep, '/'),
1536
- methods,
1537
- origin: 'app',
1538
- })
1539
- continue
1540
- }
1541
- if (!/(?:^|\/)page\.(?:ts|tsx)$/.test(relative) && !/^[^/]+\.(?:ts|tsx)$/.test(relative)) continue
1542
- routes.push({
1543
- surface,
1544
- routePath: pageRoutePath(moduleEntry.name, surface, sourceFile),
1545
- sourceFile,
1546
- displayPath: path.relative(root, sourceFile).replaceAll(path.sep, '/'),
1547
- methods: new Set(),
1548
- origin: 'app',
1549
- })
1550
- }
1551
- }
1552
- }
1553
- routes.push(...installedFactRoutes(root))
1554
-
1555
- const duplicateGroups = new Map()
1556
- for (const route of routes) {
1557
- const key = `${route.surface}:${route.routePath}`
1558
- const peers = duplicateGroups.get(key) ?? []
1559
- peers.push(route)
1560
- duplicateGroups.set(key, peers)
1561
- }
1562
- return [...duplicateGroups.entries()].flatMap(([key, entries]) => {
1563
- if (entries.length < 2) return []
1564
- if (!entries.some((entry) => entry.origin === 'app')) return []
1565
- const conflicts = entries.filter((entry, index) => entries.some((peer, peerIndex) => {
1566
- if (peerIndex === index) return false
1567
- if (entry.surface !== 'api') return true
1568
- return [...entry.methods].some((method) => peer.methods.has(method))
1569
- }))
1570
- if (conflicts.length < 2) return []
1571
- const sources = conflicts.map((entry) => entry.displayPath).join(', ')
1572
- return [`${key} (${sources})`]
1573
- })
1574
- }
1575
-
1576
- function caseChecks(ts, caseId, facts, root, sourceFiles) {
1577
- const definition = WRITABLE_CASES[caseId]
1578
- if (definition.family === 'complete-module') {
1579
- const scopedEntity = facts.classes.some((entry) => entry.decorators.has('Entity') && ['tenant_id', 'organization_id', 'updated_at'].every((name) => entry.members.has(name)))
1580
- const uiFailures = uiPolicyFailures(facts)
1581
- return [
1582
- check('module.activation', facts.moduleEntries.some((entry) => entry.id === 'library' && entry.from === '@app') && facts.moduleEntries.some((entry) => entry.id === 'directory' && entry.from === '@open-mercato/core') && facts.moduleEntries.some((entry) => entry.id === 'example' && entry.from === '@app'), 'src/modules.ts preserves statically discoverable baseline entries and activates library from @app'),
1583
- check('module.entity', scopedEntity, 'a scoped editable @Entity includes tenant_id, organization_id, and updated_at'),
1584
- check('module.validator', hasCall(facts, 'z.object', 'object'), 'the book input boundary uses a concrete validator object'),
1585
- check('module.acl', facts.exportedVariables.has('features') && hasExactString(facts, 'library.books.view') && hasExactString(facts, 'library.books.manage'), 'acl.ts exports stable view/manage features'),
1586
- check('module.setup', facts.exportedVariables.has('setup') && facts.objectProperties.has('defaultRoleFeatures'), 'setup.ts grants module features through defaultRoleFeatures'),
1587
- check('module.crud-host', hasCallOptions(facts, 'makeCrudRoute', ['metadata', 'orm', 'list', 'actions', 'indexer', 'enrichers']) && hasExactString(facts, 'library:book'), 'the scoped CRUD route publishes aligned indexer and enricher hosts for library:book'),
1588
- check('module.crud-actions', hasCall(facts, 'registerCommand') && ['library.books.create', 'library.books.update', 'library.books.delete'].every((id) => hasExactString(facts, id)) && ['create', 'update', 'delete'].every((name) => facts.objectProperties.has(name)), 'create, update, and delete actions are backed by aligned registered commands'),
1589
- check('module.api-metadata', ['GET', 'POST', 'PUT', 'DELETE', 'requireAuth', 'requireFeatures'].every((name) => facts.objectProperties.has(name)), 'CRUD API metadata declares per-method auth and features'),
1590
- check('module.openapi', facts.exportedVariables.has('openApi'), 'the CRUD API exports OpenAPI documentation separately'),
1591
- check('module.command-atomic', exportedCommandObjectCalls(facts, 'createBook', ['withAtomicFlush'])
1592
- && ['updateBook', 'deleteBook'].every((name) => exportedCommandObjectCalls(facts, name, ['withAtomicFlush', 'enforceCommandOptimisticLock'])),
1593
- 'each declared create/update/delete command reaches its own transaction seam, and update/delete each enforce their own optimistic lock'),
1594
- check('module.command-undo', exportedCommandObjectCalls(facts, 'createBook', ['extractUndoPayload', 'emitCrudUndoSideEffects'])
1595
- && ['updateBook', 'deleteBook'].every((name) => exportedCommandObjectCalls(facts, name, ['extractUndoPayload', 'buildCustomFieldResetMap', 'emitCrudUndoSideEffects']))
1596
- && hasCall(facts, 'emitCrudSideEffects'),
1597
- 'each declared create/update/delete command owns concrete undo behavior; update/delete restore custom fields and all commands retain forward side effects'),
1598
- check('module.encryption-map', facts.exportedVariables.has('defaultEncryptionMaps') && hasExactString(facts, 'library:book'), 'encryption.ts exports a library:book defaultEncryptionMaps entry'),
1599
- check('module.encrypted-read', ['findWithDecryption', 'findOneWithDecryption', 'findAndCountWithDecryption'].some((name) => hasCall(facts, name)) && [...facts.importSources].includes('@open-mercato/shared/lib/encryption/find'), 'read paths use a scoped framework decryption helper'),
1600
- check('module.search', facts.exportedVariables.has('searchConfig') && ['fieldPolicy', 'checksumSource', 'formatResult'].every((name) => facts.objectProperties.has(name)), 'search.ts defines policy, checksum, and presentation contracts'),
1601
- check('module.table', facts.jsxTags.has('DataTable') && facts.jsxTags.has('RowActions') && facts.jsxAttributes.has('extensionTableId') && hasExactString(facts, '/backend/library/books/create') && [...facts.strings].some((value) => value === 'edit' || value.endsWith('.edit')) && [...facts.strings].some((value) => value === 'delete' || value.endsWith('.delete')), 'the extensible DataTable list exposes add, linked edit, and guarded delete actions'),
1602
- check('module.list-query', facts.jsxAttributes.has('searchValue') && facts.jsxAttributes.has('onSearchChange') && ['search', 'buildFilters'].every((name) => facts.objectProperties.has(name)), 'the DataTable and scoped CRUD list connect server search and filters'),
1603
- check('module.form', facts.jsxTags.has('CrudForm') && facts.jsxAttributes.has('initialValues') && (facts.jsxAttributes.has('entityId') || facts.objectProperties.has('entityIds')) && ['createCrud', 'updateCrud', 'deleteCrud'].every((name) => hasCall(facts, name)), 'CrudForm create/edit/delete binds the stable custom-field entity identity and initial values'),
1604
- check('module.custom-fields', hasCall(facts, 'collectCustomFieldValues') && hasCall(facts, 'buildCustomFieldResetMap'), 'UI submission and command undo preserve custom fields'),
1605
- check('module.sidebar', ['pageTitleKey', 'pageGroupKey', 'pagePriority', 'pageOrder', 'icon', 'breadcrumb'].every((name) => facts.objectProperties.has(name)), 'the Books list page metadata publishes localized main-sidebar navigation'),
1606
- check('module.localized-ui', hasCall(facts, 'useT') && hasCall(facts, 't') && uiFailures.length === 0, `rendered UI uses i18n and shared design-system policy${uiFailures.length ? ` (${uiFailures.join(', ')})` : ''}`),
1607
- moduleLocaleCatalogCheck(ts, root, sourceFiles),
1608
- ]
1609
- }
1610
- if (definition.family === 'booking-overlap') {
1611
- const scopedEntity = facts.classes.some((entry) => entry.decorators.has('Entity') && ['tenant_id', 'organization_id', 'updated_at'].every((name) => entry.members.has(name)))
1612
- const route = resolvedCallArguments(ts, facts, 'makeCrudRoute')
1613
- .find((argument) => ts.isObjectLiteralExpression(argument) && objectProperty(ts, argument, 'actions'))
1614
- const actions = route && ts.isObjectLiteralExpression(route)
1615
- ? objectPropertyObject(ts, facts, route, 'actions')
1616
- : undefined
1617
- const routeCommandIds = actions
1618
- ? ['create', 'update', 'delete'].map((action) => {
1619
- const actionConfig = objectPropertyObject(ts, facts, actions, action)
1620
- return actionConfig ? objectPropertyString(ts, facts, actionConfig, 'commandId') : undefined
1621
- })
1622
- : []
1623
- const registeredCommands = resolvedCallArguments(ts, facts, 'registerCommand')
1624
- .filter((argument) => ts.isObjectLiteralExpression(argument))
1625
- const connectedCommands = routeCommandIds.map((commandId) =>
1626
- registeredCommands.find((command) => objectPropertyString(ts, facts, command, 'id') === commandId))
1627
- const commandFunctions = connectedCommands.map((command) => ({
1628
- execute: command ? propertyFunction(ts, facts, command, 'execute') : undefined,
1629
- undo: command ? propertyFunction(ts, facts, command, 'undo') : undefined,
1630
- }))
1631
- const allCommandsConnected = routeCommandIds.length === 3
1632
- && routeCommandIds.every((commandId) => typeof commandId === 'string')
1633
- && new Set(routeCommandIds).size === 3
1634
- && connectedCommands.every(Boolean)
1635
- && commandFunctions.every(({ execute, undo }) => execute && undo)
1636
- const commandsAtomic = allCommandsConnected && commandFunctions.every(({ execute }) =>
1637
- functionContractCalls(ts, facts, execute, ['withAtomicFlush', 'emitCrudSideEffects'])
1638
- && functionHasLiteralTrueOption(ts, facts, execute, 'withAtomicFlush', 'transaction'))
1639
- const commandsUndoable = allCommandsConnected && commandFunctions.every(({ undo }) =>
1640
- functionContractCalls(ts, facts, undo, ['extractUndoPayload', 'emitCrudUndoSideEffects']))
1641
- const conflictMapped = allCommandsConnected && commandFunctions.slice(0, 2).every(({ execute }) =>
1642
- functionContractMatches(ts, facts, execute, (reachable) =>
1643
- reachable.strings.has('23P01') && reachable.newCalls.has('CrudHttpError')))
1644
- return [
1645
- check('overlap.exclusion-constraint', hasStringIncluding(facts, 'btree_gist') && hasStringIncluding(facts, 'exclude using gist', 'tstzrange', "'[)'"), 'the migration guards overlaps with a btree_gist exclusion constraint over a half-open tstzrange'),
1646
- check('overlap.constraint-scope', hasStringIncluding(facts, 'exclude using gist', 'room_id', 'tenant_id', 'organization_id'), 'the exclusion constraint scopes by room and tenant/organization, not by period alone'),
1647
- check('overlap.constraint-liveness', hasStringIncluding(facts, 'exclude using gist', 'deleted_at', 'cancelled'), 'cancelled and soft-deleted rows are excluded so they do not block the slot'),
1648
- check('overlap.conflict-mapping', conflictMapped, 'the route-connected create and update commands each map SQLSTATE 23P01 to a conflict error'),
1649
- check('overlap.entity-id-source', [...facts.importSources].includes('@/.mercato/generated/entities.ids.generated'), 'entity IDs come from the generated E map through the app alias, never hand-written strings'),
1650
- check('overlap.scoped-entity', scopedEntity, 'the booking entity carries tenant_id, organization_id, and updated_at'),
1651
- check('overlap.command-atomic', commandsAtomic, 'the route-connected create/update/delete commands each reach transactional flush and post-commit side effects'),
1652
- check('overlap.command-undo', commandsUndoable, 'the route-connected create/update/delete commands each reach undo evidence and undo side effects'),
1653
- check('overlap.crud-host', Boolean(route)
1654
- && ['metadata', 'orm', 'list', 'actions', 'indexer'].every((name) => objectProperty(ts, route, name)), 'the bookings CRUD route wires metadata, scoped ORM, list, command actions, and indexer'),
1655
- ]
1656
- }
1657
- if (definition.family === 'provider-transport') {
1658
- const guard = facts.exportedFunctions.get('assertSafeEndpoint')
1659
- const client = facts.exportedFunctions.get('createRoomCalendarClient')
1660
- const clientNode = facts.exportedFunctionNodes.get('createRoomCalendarClient')
1661
- const clientTree = clientNode
1662
- ? [clientNode, ...returnedObjectFunctions(ts, facts, clientNode)]
1663
- .flatMap((node) => functionContractFacts(ts, facts, node, { skipNestedFunctions: true }))
1664
- : []
1665
- const clientStrings = new Set(clientTree.flatMap((fact) => [...fact.strings]))
1666
- const clientCalls = new Set(clientTree.flatMap((fact) => [...fact.calls]))
1667
- return [
1668
- check('provider.paired-exports', ['integration', 'integrations', 'bundle', 'bundles'].every((name) => facts.exportedVariables.has(name)), 'integration.ts declares all four paired exports the generated module registry reads'),
1669
- check('provider.credential-schema', hasObjectVariable(facts, 'integration', ['id', 'credentials', 'healthCheck']) && hasExactString(facts, 'secret') && hasExactString(facts, 'url'), 'the integration declares typed credential fields including a secret and the endpoint URL'),
1670
- check('provider.health-di', hasExactString(facts, definition.healthService) && hasCall(facts, 'container.register'), `DI registers the exact ${definition.healthService} service declared by integration.ts`),
1671
- check('provider.ssrf-guard', Boolean(guard)
1672
- && clientCalls.has('assertSafeEndpoint')
1673
- && (clientCalls.has('fetch') || clientCalls.has('fetchImpl') || clientCalls.has('request')), 'exported createRoomCalendarClient reaches both the behavior-probed guard and request seam'),
1674
- check('provider.idempotency-key', clientStrings.has('idempotency-key'), 'the client request path carries a stable idempotency-key header'),
1675
- check('provider.redirect-refusal', clientStrings.has('manual'), 'the client request path refuses to follow redirects'),
1676
- check('provider.bounded-retry', clientTree.some((fact) => fact.loops > 0), 'exported createRoomCalendarClient retries through a bounded local call tree'),
1677
- check('provider.unconfigured-degraded', hasExactString(facts, 'unconfigured'), 'a missing configuration reports a degraded state instead of failing'),
1678
- ]
1679
- }
1680
- if (definition.family === 'response-enricher') {
1681
- const enrichersExpression = facts.exportedVariables.has('enrichers')
1682
- ? facts.variableExpressions.get('enrichers')
1683
- : undefined
1684
- const registeredEnrichers = arrayElements(ts, facts, enrichersExpression)
1685
- .filter((entry) => ts.isObjectLiteralExpression(entry))
1686
- const enricher = registeredEnrichers.find((entry) =>
1687
- objectPropertyString(ts, facts, entry, 'targetEntity') === 'customers.person'
1688
- && objectPropertyArray(ts, facts, entry, 'features')
1689
- .some((feature) => stringValue(ts, facts, feature) === 'room_bookings.bookings.view'))
1690
- const enrichOne = enricher ? propertyFunction(ts, facts, enricher, 'enrichOne') : undefined
1691
- const enrichMany = enricher ? propertyFunction(ts, facts, enricher, 'enrichMany') : undefined
1692
- const fallback = enricher ? objectPropertyObject(ts, facts, enricher, 'fallback') : undefined
1693
- const enrichOneNamespaced = Boolean(enrichOne) && (
1694
- functionContractMatches(ts, facts, enrichOne, (fact) => fact.objectProperties.has('_room_bookings'))
1695
- || functionContractCalls(ts, facts, enrichOne, ['enrichMany'])
1696
- )
1697
- const enrichManyFacts = enrichMany ? functionContractFacts(ts, facts, enrichMany) : []
1698
- const enrichManyNamespaced = enrichManyFacts.some((fact) => fact.objectProperties.has('_room_bookings'))
1699
- const batchReadCalls = new Set(['find', 'findWithDecryption', 'findAndCountWithDecryption', 'query', 'selectFrom'])
1700
- const enrichManyReadsBookings = enrichManyFacts.some((fact) =>
1701
- [...fact.calls].some((call) => batchReadCalls.has(call)))
1702
- const enrichManyBatched = enrichManyFacts.some((fact) =>
1703
- fact.loops > 0 || fact.calls.has('map') || fact.calls.has('Promise.all'))
1704
- && enrichManyReadsBookings
1705
- const resilient = Boolean(enricher && fallback)
1706
- && Boolean(objectProperty(ts, fallback, '_room_bookings'))
1707
- && (numberValue(ts, facts, propertyExpression(ts, facts, enricher, 'timeout')) ?? 0) > 0
1708
- && booleanValue(ts, facts, propertyExpression(ts, facts, enricher, 'cacheableOnListHit')) === false
1709
- return [
1710
- check('enricher.dot-target', Boolean(enricher) && !hasExactString(facts, 'customers:person'), 'one connected enricher object uses the exact dot-form customers.person target; the colon-form ID never matches'),
1711
- check('enricher.batched', Boolean(enrichMany) && enrichManyBatched, 'the registered enricher batch-reads persisted booking data before mapping list results'),
1712
- check('enricher.namespaced', enrichOneNamespaced && enrichManyNamespaced, 'the registered enricher constructs _room_bookings in detail and list paths'),
1713
- check('enricher.resilience', resilient, 'the registered enricher declares a namespaced fallback, positive timeout, and cacheableOnListHit false'),
1714
- check('enricher.acl', Boolean(enricher), 'that same enricher gates on room_bookings.bookings.view'),
1715
- check('enricher.registration', facts.exportedVariables.has('enrichers') && facts.importedBindings.get('ResponseEnricher') === '@open-mercato/shared/lib/crud/response-enricher', 'the typed enricher list is exported for discovery'),
1716
- ]
1717
- }
1718
- if (definition.family === 'durable-workflow') {
1719
- const workflow = resolvedCallArguments(ts, facts, 'defineWorkflow')
1720
- .find((argument) => ts.isObjectLiteralExpression(argument)
1721
- && objectProperty(ts, argument, 'steps') && objectProperty(ts, argument, 'transitions'))
1722
- const steps = workflow && ts.isObjectLiteralExpression(workflow)
1723
- ? objectPropertyArray(ts, facts, workflow, 'steps').filter((step) => ts.isObjectLiteralExpression(step))
1724
- : []
1725
- const transitions = workflow && ts.isObjectLiteralExpression(workflow)
1726
- ? objectPropertyArray(ts, facts, workflow, 'transitions').filter((transition) => ts.isObjectLiteralExpression(transition))
1727
- : []
1728
- const stepById = new Map(steps.map((step) => [objectPropertyString(ts, facts, step, 'stepId'), step]).filter(([id]) => id))
1729
- const start = steps.find((step) => objectPropertyString(ts, facts, step, 'stepType') === 'START')
1730
- const end = steps.find((step) => objectPropertyString(ts, facts, step, 'stepType') === 'END')
1731
- const timerStep = steps.find((step) => {
1732
- if (objectPropertyString(ts, facts, step, 'stepType') !== 'WAIT_FOR_TIMER') return false
1733
- const config = objectPropertyObject(ts, facts, step, 'config')
1734
- return config && objectPropertyString(ts, facts, config, 'duration') === 'PT15M'
1735
- })
1736
- const startId = start ? objectPropertyString(ts, facts, start, 'stepId') : undefined
1737
- const endId = end ? objectPropertyString(ts, facts, end, 'stepId') : undefined
1738
- const timerId = timerStep ? objectPropertyString(ts, facts, timerStep, 'stepId') : undefined
1739
- const parsedTransitions = transitions.map((transition) => ({
1740
- node: transition,
1741
- from: objectPropertyString(ts, facts, transition, 'fromStepId'),
1742
- to: objectPropertyString(ts, facts, transition, 'toStepId'),
1743
- trigger: objectPropertyString(ts, facts, transition, 'trigger'),
1744
- priority: numberValue(ts, facts, propertyExpression(ts, facts, transition, 'priority')) ?? 0,
1745
- }))
1746
- const endpointsValid = parsedTransitions.length > 0
1747
- && parsedTransitions.every(({ from, to }) => stepById.has(from) && stepById.has(to))
1748
- const reachable = new Set(startId ? [startId] : [])
1749
- const pending = startId ? [startId] : []
1750
- while (pending.length) {
1751
- const current = pending.pop()
1752
- for (const transition of parsedTransitions.filter(({ from }) => from === current)) {
1753
- if (reachable.has(transition.to)) continue
1754
- reachable.add(transition.to)
1755
- pending.push(transition.to)
1756
- }
1757
- }
1758
- const signalTransition = parsedTransitions.find(({ from, trigger }) => from === timerId && trigger === 'signal')
1759
- const timerTransition = parsedTransitions.find(({ from, trigger }) => from === timerId && trigger === 'timer')
1760
- const updateActivity = timerTransition
1761
- ? objectPropertyArray(ts, facts, timerTransition.node, 'activities')
1762
- .find((activity) => {
1763
- if (!ts.isObjectLiteralExpression(activity)
1764
- || objectPropertyString(ts, facts, activity, 'activityType') !== 'UPDATE_ENTITY') return false
1765
- const config = objectPropertyObject(ts, facts, activity, 'config')
1766
- return config && objectPropertyString(ts, facts, config, 'commandId') === 'room_bookings.bookings.update'
1767
- })
1768
- : undefined
1769
- const safeCommand = resolvedCallArguments(ts, facts, 'registerWorkflowSafeCommands')
1770
- .flatMap((argument) => arrayElements(ts, facts, argument))
1771
- .find((entry) => ts.isObjectLiteralExpression(entry)
1772
- && objectPropertyString(ts, facts, entry, 'commandId') === 'room_bookings.bookings.update'
1773
- && objectPropertyArray(ts, facts, entry, 'requiredFeatures')
1774
- .some((feature) => Boolean(stringValue(ts, facts, feature))))
1775
- const workflowGraph = Boolean(startId && endId && timerId)
1776
- && endpointsValid && reachable.has(endId)
1777
- const confirmationWins = Boolean(signalTransition && timerTransition
1778
- && signalTransition.priority > timerTransition.priority
1779
- && reachable.has(signalTransition.to) && reachable.has(timerTransition.to))
1780
- return [
1781
- check('workflow.timer-config', Boolean(timerStep), 'the WAIT_FOR_TIMER step owns the exact PT15M duration'),
1782
- check('workflow.safe-commands', Boolean(safeCommand), 'registerWorkflowSafeCommands pairs the dispatched command with its required features'),
1783
- check('workflow.builder', hasCall(facts, 'defineWorkflow') && hasCall(facts, 'createWorkflowsModuleConfig') && facts.exportedVariables.has('workflowsConfig'), 'the definition uses the typed builder and exports the discovered workflowsConfig'),
1784
- check('workflow.terminal-graph', workflowGraph, 'the same graph declares explicit START and END steps'),
1785
- check('workflow.confirmation-beats-expiry', confirmationWins, 'the signal confirmation has higher priority than the competing expiry timer'),
1786
- check('workflow.dispatch-update-entity', Boolean(updateActivity), 'the timer release transition owns an UPDATE_ENTITY activity for the booking update command'),
1787
- ]
1788
- }
1789
- if (definition.family === 'crm-library-regression') {
1790
- const schemaFacts = collectFacts(ts, collectSourceFiles(root, [definition.schemaSource]))
1791
- const testFacts = collectFacts(ts, collectSourceFiles(root, [definition.testSource]))
1792
- const scopedActions = ['createBook', 'undoCreateBook', 'deleteBook', 'undoDeleteBook', 'checkoutBook', 'returnLoan', 'renewLoan', 'markLoanLost']
1793
- return [
1794
- check('crm-library.exports', scopedActions.every((name) => facts.exportedFunctions.has(name)), 'the stable lifecycle, checkout, return, renew, and mark-lost seams remain exported'),
1795
- check('crm-library.trusted-scope', scopedActions.every((name) => exportedFunctionCalls(facts, name, ['requireTrustedScope'])), 'every mutating seam fails closed through the same trusted tenant/organization scope guard'),
1796
- check('crm-library.lifecycle', exportedFunctionCalls(facts, 'createBook', ['effects.createBook'])
1797
- && exportedFunctionCalls(facts, 'undoCreateBook', ['effects.softDeleteBook'])
1798
- && exportedFunctionCalls(facts, 'deleteBook', ['effects.softDeleteBook'])
1799
- && exportedFunctionCalls(facts, 'undoDeleteBook', ['effects.restoreBook']),
1800
- 'create undo soft-deletes, delete soft-deletes, and delete undo restores through explicit scoped effects'),
1801
- check('crm-library.crm-checkout', exportedFunctionCalls(facts, 'checkoutBook', ['effects.resolveCustomer', 'effects.claimCheckout'])
1802
- && !hasExactString(facts, 'memberId'), 'checkout resolves an existing CRM customer and reaches the atomic claim seam without a duplicate local member'),
1803
- check('crm-library.loan-scope', ['returnLoan', 'renewLoan', 'markLoanLost'].every((name) => exportedFunctionCalls(facts, name, ['effects.findLoan', 'effects.updateLoan'])), 'every loan action performs both lookup and mutation through scoped effects'),
1804
- check('crm-library.public-schema', !schemaFacts.objectProperties.has('tenantId') && !schemaFacts.objectProperties.has('organizationId')
1805
- && schemaFacts.exportedVariables.has('createBookRequestSchema') && schemaFacts.exportedVariables.has('checkoutBookRequestSchema'),
1806
- 'public create/checkout request schemas omit tenantId and organizationId because runtime scope is server-derived'),
1807
- check('crm-library.jest-import', [...testFacts.importSources].includes('@jest/globals') && hasOnlyEnabledTests(testFacts), 'generated Jest coverage imports its globals explicitly and contains no disabled/focused/expected-failure modifiers'),
1808
- check('crm-library.semantic-tests', ['createBook', 'undoCreateBook', 'deleteBook', 'undoDeleteBook', 'returnLoan', 'renewLoan', 'markLoanLost'].every((name) => hasCall(testFacts, name))
1809
- && (testFacts.calls.get('checkoutBook') ?? 0) >= 3 && hasCall(testFacts, 'Promise.all'),
1810
- 'generated tests exercise lifecycle undo, every scoped loan action, concurrent checkout, and deterministic retry'),
1811
- ]
1812
- }
1813
- if (definition.family === 'business-command') {
1814
- const fact = facts.exportedFunctions.get(definition.seam)
1815
- return [
1816
- check('business.command-seam', exportedFunctionCalls(facts, definition.seam, ['effects.reserveIdempotency', 'effects.transaction', 'effects.apply', 'effects.record']), `exported ${definition.seam} uses the idempotency, transaction, mutation, and lineage seams`),
1817
- check('business.command-guard', (fact?.throws ?? 0) > 0, `exported ${definition.seam} rejects an invalid business invariant`),
1818
- ]
1819
- }
1820
- if (definition.family === 'ui-business-surface') {
1821
- const fact = facts.exportedFunctions.get(definition.seam)
1822
- return [
1823
- check('business.ui-seam', exportedFunctionCalls(facts, definition.seam, ['effects.execute', 'effects.restoreFocus', 'effects.announce']), `exported ${definition.seam} executes, restores focus, and announces the result`),
1824
- check('business.ui-guard', (fact?.throws ?? 0) > 0, `exported ${definition.seam} rejects an invalid UI business action`),
1825
- check('business.ui-finally', (fact?.finallyBlocks ?? 0) > 0, `exported ${definition.seam} restores focus after both success and failure`),
1826
- ...renderedUiChecks(definition, facts),
1827
- ]
1828
- }
1829
- if (definition.family === 'data-table-extension') {
1830
- const fact = facts.exportedFunctions.get(definition.seam)
1831
- const onExecute = facts.propertyIdentifiers.get('onExecute')
1832
- return [
1833
- check('business.table-filter-host', hasObjectVariable(facts, 'injectionTable', ['data-table:sales.orders:filters']) && hasExactString(facts, 'order_risk.injection.order-risk-filter'), 'the exact sales.orders filters host loads the order-risk filter widget'),
1834
- check('business.table-bulk-host', hasObjectVariable(facts, 'injectionTable', ['data-table:sales.orders:bulk-actions']) && hasExactString(facts, 'order_risk.injection.order-risk-review'), 'the exact sales.orders bulk-actions host loads the order-risk review widget'),
1835
- check('business.table-filter', facts.exportedVariables.has('orderRiskFilterWidget') && hasObjectVariable(facts, 'orderRiskFilterWidget', ['metadata', 'filters']) && ['order-risk', 'order_risk.filters.risk.label', 'server', 'risk'].every((value) => hasExactString(facts, value)), 'an exported localized server-backed order-risk filter is registered'),
1836
- check('business.table-bulk-action', facts.exportedVariables.has('orderRiskReviewWidget') && hasObjectVariable(facts, 'orderRiskReviewWidget', ['metadata', 'bulkActions']) && hasExactString(facts, 'review-order-risk') && onExecute?.has(definition.seam), `the exported bulk action links onExecute directly to ${definition.seam}`),
1837
- check('business.table-safe-seam', exportedFunctionCalls(facts, definition.seam, ['effects.authorize', 'effects.checkVersion', 'effects.surfaceConflict', 'effects.execute']) && (fact?.loops ?? 0) > 0 && (fact?.throws ?? 0) > 0, `exported ${definition.seam} authorizes and version-checks every selected order before execution`),
1838
- check('business.table-ui-policy', uiPolicyFailures(facts).length === 0, 'the injected table extension avoids raw UI, inline SVG/style, hard-coded copy/colors, arbitrary Tailwind, and manual dark overrides'),
1839
- ]
1840
- }
1841
- if (definition.family === 'async-operation') {
1842
- const fact = facts.exportedFunctions.get(definition.seam)
1843
- return [
1844
- check('business.async-seam', exportedFunctionCalls(facts, definition.seam, ['effects.isCancelled', 'effects.shouldSkip', 'effects.applyChunk', 'effects.reportProgress', 'effects.registerUndo']), `exported ${definition.seam} uses cancellation, skip, mutation, progress, and undo seams`),
1845
- check('business.async-loop', (fact?.loops ?? 0) > 0, `exported ${definition.seam} processes work through a bounded loop`),
1846
- ]
1847
- }
1848
- if (definition.family === 'ai-safe-agent') {
1849
- const requiredCalls = definition.mode === 'mutation'
1850
- ? ['effects.authorize', 'effects.prepareMutation', 'effects.execute']
1851
- : ['effects.authorize', 'effects.delegate']
1852
- const checks = [
1853
- check('business.ai-seam', exportedFunctionCalls(facts, definition.seam, requiredCalls), `exported ${definition.seam} uses the required authorization and ${definition.mode} seams`),
1854
- ]
1855
- if (definition.mode === 'delegate') {
1856
- checks.push(check('business.ai-authority', exportedFunctionHasCallOptions(facts, definition.seam, 'effects.delegate', ['authority', 'allowedTools']), `exported ${definition.seam} delegates with an explicit authority ceiling and allowlist`))
1857
- }
1858
- return checks
1859
- }
1860
- if (definition.family === 'provider-adapter') {
1861
- const fact = facts.exportedFunctions.get(definition.seam)
1862
- const checks = [
1863
- check('business.provider-seam', exportedFunctionCalls(facts, definition.seam, ['effects.findExisting', 'effects.request', 'effects.reconcile', 'effects.redact']), `exported ${definition.seam} uses idempotency, provider, reconciliation, and redaction seams`),
1864
- check('business.provider-retry', (fact?.loops ?? 0) > 0 && (fact?.throws ?? 0) > 0, `exported ${definition.seam} bounds retries and redacts terminal failure`),
1865
- check('business.provider-integration', facts.importedBindings.get('IntegrationDefinition') === '@open-mercato/shared/modules/integrations/types' && facts.exportedVariables.has('integration') && facts.exportedVariables.has('integrations') && hasObjectVariable(facts, 'integration', ['id', 'category', 'providerKey', 'credentials', 'healthCheck']), 'integration.ts exports typed IntegrationDefinition metadata, credentials, and health'),
1866
- check('business.provider-secret', hasExactString(facts, 'secret'), 'integration credentials include a secret-bearing field type'),
1867
- check('business.provider-health', hasExactString(facts, definition.healthService) && hasCall(facts, 'container.register'), `DI registers the exact ${definition.healthService} health service declared by integration.ts`),
1868
- check('business.provider-activation', facts.moduleEntries.some((entry) => entry.id === definition.moduleId && entry.from === '@app'), `src/modules.ts activates ${definition.moduleId} from @app`),
1869
- ]
1870
- if (definition.providerKind === 'transactional-email') {
1871
- checks.push(
1872
- check('business.provider-transactional-di', hasObjectVariable(facts, 'smtpEmailService', ['send']) && facts.propertyIdentifiers.get('send')?.has(definition.seam), 'DI-facing SMTP sender delegates to the tested transactional client seam'),
1873
- check('business.provider-not-mailbox', !hasExactString(facts, 'communication_channels') && !facts.importedBindings.has('ChannelAdapter'), 'transactional SMTP does not claim the mailbox ChannelAdapter contract'),
1874
- )
1875
- }
1876
- if (definition.providerKind === 'payment') {
1877
- checks.push(
1878
- check('business.provider-payment-contract', facts.importedBindings.get('GatewayAdapter') === '@open-mercato/shared/modules/payment_gateways/types' && hasObjectVariable(facts, definition.adapterVariable, ['providerKey', 'createSession', 'capture', 'refund', 'cancel', 'getStatus', 'verifyWebhook', 'mapStatus']) && hasCall(facts, definition.seam), 'provider implements the installed GatewayAdapter surface and reaches the tested client seam'),
1879
- check(
1880
- 'business.provider-payment-registration',
1881
- hasCall(facts, 'registerGatewayAdapter')
1882
- && hasCall(facts, 'registerPaymentGatewayDescriptor')
1883
- && facts.importedBindings.get('registerWebhookHandler') === '@open-mercato/shared/modules/payment_gateways/types'
1884
- && hasCall(facts, 'registerWebhookHandler')
1885
- && facts.propertyAccesses.has('verifyWebhook'),
1886
- 'DI registers the gateway adapter, its verifyWebhook handler, and the payment descriptor',
1887
- ),
1888
- )
1889
- }
1890
- if (definition.providerKind === 'shipping') {
1891
- checks.push(
1892
- check('business.provider-shipping-contract', facts.importedBindings.get('ShippingAdapter') === '@open-mercato/core/modules/shipping_carriers/lib/adapter' && hasObjectVariable(facts, definition.adapterVariable, ['providerKey', 'calculateRates', 'createShipment', 'getTracking', 'cancelShipment', 'verifyWebhook', 'mapStatus']) && hasCall(facts, definition.seam), 'provider implements the installed ShippingAdapter surface and reaches the tested client seam'),
1893
- check('business.provider-shipping-registration', hasCall(facts, 'registerShippingAdapter'), 'DI registers the shipping adapter'),
1894
- )
1895
- }
1896
- if (definition.providerKind === 'payment' || definition.providerKind === 'shipping') {
1897
- checks.push(
1898
- check('business.provider-acl', facts.exportedVariables.has('features') && hasExactString(facts, `${definition.moduleId}.view`) && hasExactString(facts, `${definition.moduleId}.configure`), 'provider acl.ts exports stable view/configure features'),
1899
- check('business.provider-setup', facts.exportedVariables.has('setup') && hasObjectVariable(facts, 'setup', ['defaultRoleFeatures']), 'provider setup.ts grants its features through defaultRoleFeatures'),
1900
- )
1901
- }
1902
- return checks
1903
- }
1904
- if (definition.family === 'data-flow') {
1905
- const fact = facts.exportedFunctions.get(definition.seam)
1906
- return [
1907
- check('business.data-seam', exportedFunctionCalls(facts, definition.seam, ['effects.fetchPage', 'effects.sanitize', 'effects.apply', 'effects.commitCursor']), `exported ${definition.seam} uses fetch, sanitization, row mutation, and cursor seams`),
1908
- check('business.data-loop', (fact?.loops ?? 0) > 0, `exported ${definition.seam} isolates rows in a loop`),
1909
- ]
1910
- }
1911
- if (definition.family === 'test-authoring-unit') {
1912
- return [
1913
- check('business.test-unit-runner', (facts.calls.get('test') ?? 0) >= 3 && (facts.calls.get('expect') ?? 0) >= 3, 'at least three executable Jest tests with assertions'),
1914
- check('business.test-unit-enabled', hasOnlyEnabledTests(facts), 'generated Jest coverage contains no disabled, focused-only, todo, or expected-failure tests or suites'),
1915
- check('business.test-unit-invariants', ['already approved', 'requester', 'injected failure'].every((value) => [...facts.strings].some((entry) => entry.toLowerCase().includes(value))), 'test titles or assertions cover duplicate approval, separation of duties, and injected failure'),
1916
- check('business.test-unit-failure', hasCall(facts, 'toThrow', 'toReject', 'rejects.toThrow') && hasCall(facts, 'toHaveBeenCalledTimes', 'toEqual', 'toBe'), 'failure and rollback outcomes are asserted'),
1917
- ]
1918
- }
1919
- if (definition.family === 'test-authoring-api') {
1920
- return [
1921
- check('business.test-api-runner', facts.importedBindings.get('test') === '@playwright/test' && facts.importedBindings.get('expect') === '@playwright/test', 'the module-local spec uses the Playwright test runner'),
1922
- check('business.test-api-enabled', hasOnlyEnabledTests(facts), 'generated API coverage contains no disabled, focused-only, todo, or expected-failure tests or suites'),
1923
- check('business.test-api-http', ['request.post', 'request.patch', 'request.delete'].every((name) => hasCall(facts, name)), 'real HTTP creation, stale update, and cleanup requests are executed'),
1924
- check('business.test-api-scope-conflict', hasExactString(facts, 'x-organization-id') && hasExactString(facts, 'if-match') && (facts.calls.get('toBe') ?? 0) >= 4, 'organization denial and optimistic conflict statuses are asserted'),
1925
- check('business.test-api-lifecycle', allServersBindLoopback(facts) && hasCall(facts, 'close') && facts.finallyBlocks > 0, 'every ephemeral server listen call binds the literal host 127.0.0.1 and is closed in finally'),
1926
- ]
1927
- }
1928
- if (definition.family === 'test-authoring-browser') {
1929
- return [
1930
- check('business.test-browser-runner', facts.importedBindings.get('test') === '@playwright/test' && facts.importedBindings.get('expect') === '@playwright/test', 'the module-local spec uses the Playwright test runner'),
1931
- check('business.test-browser-enabled', hasOnlyEnabledTests(facts), 'generated browser coverage contains no disabled, focused-only, todo, or expected-failure tests or suites'),
1932
- check('business.test-browser-real-page', hasCall(facts, 'page.goto') && hasCall(facts, 'page.getByRole') && hasCall(facts, 'click'), 'a real browser navigates loopback HTTP and interacts through semantic roles'),
1933
- check('business.test-browser-coverage', hasExactString(facts, '/portal/orders/forbidden') && hasExactString(facts, '/api/backend/quotes/quote-1') && hasCall(facts, 'toHaveText', 'toContainText', 'toBe'), 'direct-route denial, stale conflict, and backend state are asserted'),
1934
- check('business.test-browser-lifecycle', allServersBindLoopback(facts) && hasCall(facts, 'close') && facts.finallyBlocks > 0, 'every ephemeral server listen call binds the literal host 127.0.0.1 and is closed in finally'),
1935
- ]
1936
- }
1937
- switch (caseId) {
1938
- case 'OMH-009': {
1939
- const entity = facts.classes.some((entry) => entry.decorators.has('Entity') && ['tenant_id', 'organization_id', 'updated_at'].every((name) => entry.members.has(name)))
1940
- return [
1941
- check('entity.declaration', entity, 'an @Entity class declaring tenant_id, organization_id, and updated_at'),
1942
- check('entity.validator', hasCall(facts, 'z.object') || hasCall(facts, 'object'), 'a concrete validator object call'),
1943
- check('entity.migration', facts.classes.some((entry) => entry.members.has('up')), 'a migration class with an up method'),
1944
- ]
1945
- }
1946
- case 'OMH-011':
1947
- return [
1948
- check('crud.factory-import', facts.importedBindings.get('makeCrudRoute') === '@open-mercato/shared/lib/crud/factory', 'makeCrudRoute imported from @open-mercato/shared/lib/crud/factory'),
1949
- check('crud.route', hasCallOptions(facts, 'makeCrudRoute', ['metadata', 'orm', 'list', 'actions', 'indexer']), 'makeCrudRoute called with metadata, orm, list, actions, and indexer options'),
1950
- check('crud.openapi', facts.exportedVariables.has('openApi'), 'openApi exported separately from the CRUD factory options'),
1951
- ]
1952
- case 'OMH-012':
1953
- return [
1954
- check('command.guards', hasCall(facts, 'runMutationGuards'), 'a runMutationGuards call'),
1955
- check('command.atomic', hasCall(facts, 'withAtomicFlush'), 'a withAtomicFlush call'),
1956
- check('command.effects', hasCall(facts, 'emitCrudSideEffects'), 'an emitCrudSideEffects call'),
1957
- ]
1958
- case 'OMH-014':
1959
- return [
1960
- check('ui.table', facts.jsxTags.has('DataTable') && (facts.jsxAttributes.has('extensionTableId') || facts.objectProperties.has('extensionTableId')), 'a DataTable JSX use with extensionTableId'),
1961
- check('ui.form', facts.jsxTags.has('CrudForm') && facts.jsxAttributes.has('initialValues'), 'a CrudForm JSX use with initialValues'),
1962
- check('ui.conflict', hasCall(facts, 'surfaceRecordConflict'), 'a surfaceRecordConflict call'),
1963
- ]
1964
- case 'OMH-026':
1965
- return [
1966
- check('umes.form-spot', hasStringPrefix(facts, 'crud-form:'), 'a concrete crud-form:* spot ID literal'),
1967
- check('umes.enricher', hasCall(facts, 'enrichMany'), 'an enrichMany call'),
1968
- check('umes.interceptor', hasObjectVariable(facts, 'interceptors', []) || facts.declarations.has('interceptors'), 'an interceptors declaration'),
1969
- ]
1970
- case 'OMH-027':
1971
- return [
1972
- check('umes.table-spot', hasStringPrefix(facts, 'data-table:'), 'a concrete data-table:* spot ID literal'),
1973
- check('umes.table-id', facts.objectProperties.has('extensionTableId') || facts.jsxAttributes.has('extensionTableId'), 'an extensionTableId option or JSX attribute'),
1974
- ]
1975
- case 'OMH-029':
1976
- return [check('umes.page-override', facts.declarations.has('overrides') && facts.objectProperties.has('page'), 'an overrides declaration containing a page option')]
1977
- case 'OMH-031':
1978
- return [
1979
- check('umes.interceptors', facts.declarations.has('interceptors'), 'an interceptors declaration'),
1980
- check('umes.interceptor-scope', ['metadata', 'organizationId', 'tenantId'].every((name) => facts.propertyAccesses.has(name) || facts.objectProperties.has(name)), 'metadata, organizationId, and tenantId used in executable AST'),
1981
- check('umes.interceptor-hook', facts.objectProperties.has('before') || facts.objectProperties.has('after'), 'a before or after interceptor hook'),
1982
- ]
1983
- case 'OMH-042':
1984
- return [
1985
- check('provider.adapter', [...facts.variables.entries()].some(([, properties]) => properties.has('health') && (properties.has('pull') || properties.has('run'))), 'an adapter object declaration with health and pull/run methods'),
1986
- check('provider.effects', hasCall(facts, 'fetchPage') && hasCall(facts, 'applyItem') && hasCall(facts, 'commitCursor'), 'fetchPage, applyItem, and commitCursor call sites'),
1987
- ]
1988
- case 'OMH-045':
1989
- return [
1990
- check('rest.url', facts.newCalls.has('URL'), 'a concrete new URL(...) call'),
1991
- check('rest.retry', facts.loops > 0 || hasCall(facts, 'retry'), 'a retry loop or retry(...) call'),
1992
- check('rest.cursor', facts.assignments.has('cursor') && (facts.awaitedCalls.has('fetch') || facts.awaitedCalls.has('fetchImpl') || facts.awaitedCalls.has('fetchPage')), 'cursor assignment and an awaited fetch call'),
1993
- ]
1994
- case 'OMH-049':
1995
- return [check('ai.agent', hasCallOptions(facts, 'defineAiAgent', ['provider', 'model', 'allowedTools', 'requiredFeatures']), 'defineAiAgent called with provider, model, allowedTools, and requiredFeatures options')]
1996
- case 'OMH-054':
1997
- return [
1998
- check('workflow.activity', facts.functions.has('callApiActivity') && hasExactString(facts, 'CALL_API'), 'a callApiActivity declaration and CALL_API literal'),
1999
- check('workflow.transaction', hasCall(facts, 'transaction', 'transactional'), 'a transaction/transactional call'),
2000
- check('workflow.idempotency', (hasCall(facts, 'fetch', 'fetchImpl') && facts.objectProperties.has('headers') && hasExactString(facts, 'Idempotency-Key')), 'a fetch call with headers and an Idempotency-Key literal'),
2001
- ]
2002
- case 'OMH-057':
2003
- case 'OMH-171':
2004
- return [
2005
- check('regression.fail-closed', facts.functions.has('listRecords') && facts.throwStatements > 0, 'listRecords with a fail-closed throw'),
2006
- check('regression.scope', ['tenantId', 'organizationId'].every((name) => facts.propertyAccesses.has(name) || facts.objectProperties.has(name)), 'tenantId and organizationId concrete scope access'),
2007
- ]
2008
- case 'OMH-060':
2009
- return [
2010
- check('regression.atomic', hasCall(facts, 'withAtomicFlush', 'transaction', 'transactional'), 'a withAtomicFlush/transaction/transactional call'),
2011
- check('regression.phases', (facts.calls.get('persist') ?? 0) >= 2, 'two concrete persist call sites inside the atomic operation'),
2012
- ]
2013
- case 'OMH-061':
2014
- return [
2015
- check('regression.nullable', facts.nullNodes > 0, 'a concrete nullable type or null expression'),
2016
- check('regression.form', facts.jsxTags.has('CrudForm') && facts.jsxAttributes.has('initialValues'), 'CrudForm JSX with initialValues'),
2017
- ]
2018
- case 'OMH-172': {
2019
- const initialValues = facts.exportedFunctions.get('toInitialValues')
2020
- const updatePayload = facts.exportedFunctions.get('toUpdatePayload')
2021
- return [
2022
- check('regression.null-load-seam', Boolean(initialValues) && !initialValues.binaryOperators.has(ts.SyntaxKind.QuestionQuestionToken), 'exported toInitialValues preserves explicit null instead of replacing it'),
2023
- check('regression.null-clear-seam', Boolean(updatePayload) && updatePayload.nullNodes > 0 && updatePayload.conditionalExpressions > 0 && updatePayload.binaryOperators.has(ts.SyntaxKind.EqualsEqualsEqualsToken), 'exported toUpdatePayload maps an explicit clear to null'),
2024
- check('regression.form', facts.jsxTags.has('CrudForm') && facts.jsxAttributes.has('initialValues'), 'CrudForm JSX with initialValues'),
2025
- ]
2026
- }
2027
- case 'OMH-070':
2028
- return [
2029
- check('regression.cursor-fetch', facts.functions.has('syncPage') && facts.awaitedCalls.has('fetchPage') && facts.assignments.has('cursor'), 'syncPage with awaited fetchPage and cursor assignment'),
2030
- check('regression.cursor-retry', facts.loops > 0 || hasCall(facts, 'retry'), 'a retry loop or retry(...) call'),
2031
- ]
2032
- default:
2033
- return [check('case.supported', false, 'a fixed writable oracle')]
2034
- }
2035
- }
2036
-
2037
- function runTargetTypecheck(root) {
2038
- const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-harness-oracle-'))
2039
- const home = path.join(tempRoot, 'home')
2040
- fs.mkdirSync(home, { recursive: true, mode: 0o700 })
2041
- const env = {
2042
- PATH: process.env.PATH ?? '', HOME: home, TMPDIR: tempRoot, TEMP: tempRoot, TMP: tempRoot,
2043
- XDG_CONFIG_HOME: path.join(home, '.config'), XDG_CACHE_HOME: path.join(home, '.cache'),
2044
- CI: '1', NO_COLOR: '1', NEXT_TELEMETRY_DISABLED: '1', YARN_ENABLE_TELEMETRY: '0', TZ: 'UTC',
2045
- }
2046
- const configuredCorepackHome = process.env.COREPACK_HOME || path.join(os.homedir(), '.cache', 'node', 'corepack')
2047
- const corepackHome = fs.existsSync(configuredCorepackHome) && fs.statSync(configuredCorepackHome).isDirectory()
2048
- ? fs.realpathSync(configuredCorepackHome)
2049
- : undefined
2050
- if (corepackHome) env.COREPACK_HOME = corepackHome
2051
- let result
2052
- try {
2053
- const dependencyPath = path.join(root, 'node_modules')
2054
- const invocation = sandboxedInvocation({
2055
- command: process.platform === 'win32' ? 'yarn.cmd' : 'yarn',
2056
- args: ['typecheck', '--tsBuildInfoFile', path.join(tempRoot, 'tsconfig.tsbuildinfo')],
2057
- cwd: root,
2058
- writableRoots: [root, tempRoot],
2059
- readOnlyRoots: [...(fs.existsSync(dependencyPath) ? [fs.realpathSync(dependencyPath)] : []), ...(corepackHome ? [corepackHome] : [])],
2060
- networkAllowed: false,
2061
- env,
2062
- })
2063
- result = spawnSync(invocation.command, invocation.args, {
2064
- cwd: invocation.cwd,
2065
- env: invocation.env,
2066
- encoding: 'utf8',
2067
- shell: false,
2068
- timeout: TYPECHECK_TIMEOUT_MS,
2069
- windowsHide: true,
2070
- })
2071
- } catch (error) {
2072
- result = { status: null, signal: null, stdout: '', stderr: '', error }
2073
- } finally {
2074
- fs.rmSync(tempRoot, { recursive: true, force: true })
2075
- }
2076
- if (result.error?.code === 'ETIMEDOUT' || result.signal) {
2077
- return check('target.typecheck', false, `yarn typecheck must complete within ${TYPECHECK_TIMEOUT_MS}ms`)
2078
- }
2079
- if (result.error) return check('target.typecheck', false, `yarn typecheck could not start: ${result.error.message}`)
2080
- if (result.status !== 0) {
2081
- const summary = `${result.stderr ?? ''}\n${result.stdout ?? ''}`.trim().replaceAll(root, '<target>').slice(0, 1000)
2082
- return check('target.typecheck', false, `yarn typecheck failed${summary ? `: ${summary}` : ''}`)
2083
- }
2084
- return check('target.typecheck', true, 'yarn typecheck succeeds')
2085
- }
2086
-
2087
- export function evaluateWritableAstOracle({ root: requestedRoot, caseId, phase }) {
2088
- if (!path.isAbsolute(requestedRoot)) throw new Error('root must be absolute')
2089
- if (!WRITABLE_CASES[caseId]) throw new Error(`unsupported writable case: ${caseId}`)
2090
- if (!['before', 'after'].includes(phase)) throw new Error('phase must be before or after')
2091
- const root = fs.realpathSync(requestedRoot)
2092
- const ts = loadTargetTypeScript(root)
2093
- const definition = WRITABLE_CASES[caseId]
2094
- const checks = definition.artifacts.map((artifact) => check(`artifact:${artifact}`, artifactExists(root, artifact), `artifact ${artifact} exists`))
2095
- const duplicateRoutes = duplicateRouteChecks(ts, root)
2096
- checks.push(check('routes.unique', duplicateRoutes.length === 0, `generated API, backend, and frontend URL patterns are unique${duplicateRoutes.length ? ` (${duplicateRoutes.join('; ')})` : ''}`))
2097
- const sourceFiles = collectSourceFiles(root, definition.sources)
2098
- checks.push(check('source.present', sourceFiles.length > 0, 'at least one case-owned TypeScript source file'))
2099
- if (sourceFiles.length) checks.push(...caseChecks(ts, caseId, collectFacts(ts, sourceFiles), root, sourceFiles))
2100
- if (phase === 'after') checks.push(runTargetTypecheck(root))
2101
- const failures = checks.filter((entry) => !entry.passed).map((entry) => `${entry.id}: ${entry.requirement}`)
2102
- return { passed: failures.length === 0, failures, checks }
2103
- }
2104
-
2105
- function main() {
2106
- let options
2107
- try {
2108
- options = parseArgs(process.argv.slice(2))
2109
- const result = evaluateWritableAstOracle({ root: options.root, caseId: options.caseId, phase: options.phase })
2110
- process.stdout.write(`${JSON.stringify(result)}\n`)
2111
- return result.passed ? 0 : 1
2112
- } catch (error) {
2113
- const message = error instanceof Error ? error.message : String(error)
2114
- process.stdout.write(`${JSON.stringify({ passed: false, failures: [message], checks: [] })}\n`)
2115
- return 2
2116
- }
2117
- }
2118
-
2119
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
2120
- process.exitCode = main()
2121
- }