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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/.turbo/turbo-build.log +0 -1
  2. package/build.mjs +21 -5
  3. package/dist/agentic/claude-code/hooks/entity-migration-check.ts +32 -88
  4. package/dist/agentic/codex/enforcement-rules.md +2 -16
  5. package/dist/agentic/cursor/hooks/entity-migration-check.mjs +19 -50
  6. package/dist/agentic/cursor/rules/entity-guard.mdc +9 -21
  7. package/dist/agentic/cursor/rules/generated-guard.mdc +3 -5
  8. package/dist/agentic/cursor/rules/open-mercato.mdc +3 -29
  9. package/dist/agentic/guides/ai-workflows.md +76 -0
  10. package/dist/agentic/guides/architecture.md +107 -0
  11. package/dist/agentic/guides/backend-ui.md +80 -0
  12. package/dist/agentic/guides/contracts.md +92 -0
  13. package/dist/agentic/guides/extensions.md +104 -0
  14. package/dist/agentic/guides/integrations.md +81 -0
  15. package/dist/agentic/guides/module-facts.json +217 -81
  16. package/dist/agentic/guides/module-system.md +12 -127
  17. package/dist/agentic/guides/modules/ai_assistant.md +1 -1
  18. package/dist/agentic/guides/modules/api_docs.md +1 -1
  19. package/dist/agentic/guides/modules/api_keys.md +1 -1
  20. package/dist/agentic/guides/modules/attachments.md +2 -2
  21. package/dist/agentic/guides/modules/audit_logs.md +2 -2
  22. package/dist/agentic/guides/modules/auth.md +1 -1
  23. package/dist/agentic/guides/modules/business_rules.md +1 -1
  24. package/dist/agentic/guides/modules/catalog.md +5 -5
  25. package/dist/agentic/guides/modules/channel_gmail.md +1 -1
  26. package/dist/agentic/guides/modules/channel_imap.md +1 -1
  27. package/dist/agentic/guides/modules/checkout.md +1 -1
  28. package/dist/agentic/guides/modules/communication_channels.md +1 -1
  29. package/dist/agentic/guides/modules/configs.md +1 -1
  30. package/dist/agentic/guides/modules/content.md +1 -1
  31. package/dist/agentic/guides/modules/currencies.md +1 -1
  32. package/dist/agentic/guides/modules/customer_accounts.md +1 -1
  33. package/dist/agentic/guides/modules/customers.md +2 -2
  34. package/dist/agentic/guides/modules/dashboards.md +1 -1
  35. package/dist/agentic/guides/modules/data_sync.md +1 -1
  36. package/dist/agentic/guides/modules/dictionaries.md +1 -1
  37. package/dist/agentic/guides/modules/directory.md +1 -1
  38. package/dist/agentic/guides/modules/entities.md +2 -2
  39. package/dist/agentic/guides/modules/events.md +1 -1
  40. package/dist/agentic/guides/modules/feature_toggles.md +1 -1
  41. package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
  42. package/dist/agentic/guides/modules/generators.md +1 -1
  43. package/dist/agentic/guides/modules/inbox_ops.md +1 -1
  44. package/dist/agentic/guides/modules/integrations.md +1 -1
  45. package/dist/agentic/guides/modules/messages.md +1 -1
  46. package/dist/agentic/guides/modules/notifications.md +1 -1
  47. package/dist/agentic/guides/modules/onboarding.md +1 -1
  48. package/dist/agentic/guides/modules/payment_gateways.md +1 -1
  49. package/dist/agentic/guides/modules/perspectives.md +1 -1
  50. package/dist/agentic/guides/modules/planner.md +1 -1
  51. package/dist/agentic/guides/modules/portal.md +1 -1
  52. package/dist/agentic/guides/modules/progress.md +1 -1
  53. package/dist/agentic/guides/modules/query_index.md +2 -2
  54. package/dist/agentic/guides/modules/record_locks.md +1 -1
  55. package/dist/agentic/guides/modules/resources.md +1 -1
  56. package/dist/agentic/guides/modules/sales.md +17 -17
  57. package/dist/agentic/guides/modules/scheduler.md +1 -1
  58. package/dist/agentic/guides/modules/search.md +1 -1
  59. package/dist/agentic/guides/modules/security.md +1 -1
  60. package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
  61. package/dist/agentic/guides/modules/sso.md +1 -1
  62. package/dist/agentic/guides/modules/staff.md +2 -2
  63. package/dist/agentic/guides/modules/storage_s3.md +1 -1
  64. package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
  65. package/dist/agentic/guides/modules/sync_excel.md +1 -1
  66. package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
  67. package/dist/agentic/guides/modules/translations.md +1 -1
  68. package/dist/agentic/guides/modules/webhooks.md +2 -2
  69. package/dist/agentic/guides/modules/wms.md +2 -2
  70. package/dist/agentic/guides/modules/workflows.md +1 -1
  71. package/dist/agentic/guides/testing-debugging.md +74 -0
  72. package/dist/agentic/guides/upstream/AGENTS.md +297 -0
  73. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +303 -0
  74. package/dist/agentic/guides/upstream/manifest.json +8 -0
  75. package/dist/agentic/shared/AGENTS.md.template +123 -311
  76. package/dist/agentic/shared/ai/agentic.config.json +1 -0
  77. package/dist/agentic/shared/ai/harness/README.md +48 -0
  78. package/dist/agentic/shared/ai/harness/RELEASE.md +57 -0
  79. package/dist/agentic/shared/ai/harness/cases.json +13487 -0
  80. package/dist/agentic/shared/ai/harness/cases.schema.json +132 -0
  81. package/dist/agentic/shared/ai/harness/fixtures/index.json +49 -0
  82. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +162 -0
  83. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +20 -0
  84. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +43 -0
  85. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +110 -0
  86. package/dist/agentic/shared/ai/harness/release-matrix.json +133 -0
  87. package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
  88. package/dist/agentic/shared/ai/harness/result.schema.json +76 -0
  89. package/dist/agentic/shared/ai/harness/routing-response.schema.json +12 -0
  90. package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
  91. package/dist/agentic/shared/ai/harness/validators.json +78 -0
  92. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +1768 -0
  93. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
  94. package/dist/agentic/shared/ai/review-checklist.md +36 -0
  95. package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
  96. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +20 -241
  97. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +16 -0
  98. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
  99. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +13 -0
  100. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
  101. package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
  102. package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
  103. package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
  104. package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
  105. package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
  106. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
  107. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
  108. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
  109. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
  110. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +16 -601
  111. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +19 -0
  112. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +13 -0
  113. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +14 -0
  114. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
  115. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +14 -310
  116. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +17 -0
  117. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +28 -0
  118. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +89 -0
  119. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
  120. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
  121. package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
  122. package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
  123. package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
  124. package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
  125. package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +17 -0
  126. package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
  127. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +15 -156
  128. package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +12 -0
  129. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +24 -708
  130. package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
  131. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +14 -0
  132. package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
  133. package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
  134. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
  135. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +19 -0
  136. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +107 -0
  137. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +12 -0
  138. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
  139. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +21 -0
  140. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
  141. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
  142. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
  143. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +19 -843
  144. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +18 -0
  145. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +25 -0
  146. package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
  147. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +38 -0
  148. package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
  149. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
  150. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
  151. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +16 -467
  152. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +16 -0
  153. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
  154. package/dist/agentic/shared/ai/skills/tiers.json +137 -20
  155. package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
  156. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +174 -0
  157. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +2783 -0
  158. package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
  159. package/dist/agentic/shared/scripts/framework-context.mjs +894 -0
  160. package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
  161. package/dist/agentic/shared/scripts/install-skills.sh +4 -694
  162. package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
  163. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1761 -0
  164. package/dist/lib/__integration__/TC-INT-008.spec.js +87 -8
  165. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  166. package/dist/lib/agentic-init.js +13 -3
  167. package/dist/lib/agentic-init.js.map +2 -2
  168. package/dist/lib/agentic-setup.js +418 -186
  169. package/dist/lib/agentic-setup.js.map +2 -2
  170. package/dist/lib/generators/module-facts-discovery.js +41 -7
  171. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  172. package/dist/lib/generators/module-facts.js +65 -17
  173. package/dist/lib/generators/module-facts.js.map +2 -2
  174. package/dist/lib/generators/module-registry.js +5 -4
  175. package/dist/lib/generators/module-registry.js.map +2 -2
  176. package/dist/lib/resolver.js +2 -0
  177. package/dist/lib/resolver.js.map +2 -2
  178. package/package.json +5 -5
  179. package/src/lib/__integration__/TC-INT-008.spec.ts +103 -8
  180. package/src/lib/__tests__/agentic-init.test.ts +41 -5
  181. package/src/lib/__tests__/agentic-setup.ownership.test.ts +380 -0
  182. package/src/lib/agentic-init.ts +15 -3
  183. package/src/lib/agentic-setup.ts +526 -224
  184. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +6 -0
  185. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
  186. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
  187. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
  188. package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
  189. package/src/lib/generators/module-facts-discovery.ts +58 -7
  190. package/src/lib/generators/module-facts.ts +84 -20
  191. package/src/lib/generators/module-registry.ts +8 -4
  192. package/src/lib/resolver.ts +3 -0
  193. package/dist/agentic/guides/cache.md +0 -45
  194. package/dist/agentic/guides/core.md +0 -453
  195. package/dist/agentic/guides/events.md +0 -117
  196. package/dist/agentic/guides/queue.md +0 -53
  197. package/dist/agentic/guides/search.md +0 -115
  198. package/dist/agentic/guides/shared.md +0 -108
  199. package/dist/agentic/guides/ui.md +0 -347
  200. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
  201. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
  202. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
  203. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
  204. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
  205. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
  206. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
  207. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
  208. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
  209. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
  210. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
  211. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
@@ -0,0 +1,1768 @@
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
+
14
+ const WRITABLE_CASES = Object.freeze({
15
+ 'OMH-009': {
16
+ sources: ['src/modules/library'],
17
+ artifacts: [
18
+ 'src/modules/library/data/entities.ts',
19
+ 'src/modules/library/data/validators.ts',
20
+ 'src/modules/library/migrations/**',
21
+ ],
22
+ },
23
+ 'OMH-011': {
24
+ sources: ['src/modules/library/api/books/route.ts'],
25
+ artifacts: ['src/modules/library/api/books/route.ts'],
26
+ },
27
+ 'OMH-012': {
28
+ sources: ['src/modules/library/api/books/checkout/route.ts', 'src/modules/library/commands'],
29
+ artifacts: ['src/modules/library/api/books/checkout/route.ts', 'src/modules/library/commands/**'],
30
+ },
31
+ 'OMH-014': {
32
+ sources: ['src/modules/library/backend'],
33
+ artifacts: ['src/modules/library/backend/**'],
34
+ },
35
+ 'OMH-026': {
36
+ sources: ['src/modules/app_customizations'],
37
+ artifacts: [
38
+ 'src/modules/app_customizations/widgets/**',
39
+ 'src/modules/app_customizations/data/enrichers.ts',
40
+ 'src/modules/app_customizations/api/interceptors.ts',
41
+ ],
42
+ },
43
+ 'OMH-027': {
44
+ sources: ['src/modules/app_customizations/widgets'],
45
+ artifacts: ['src/modules/app_customizations/widgets/**'],
46
+ },
47
+ 'OMH-029': {
48
+ sources: ['src/modules/app_customizations'],
49
+ artifacts: ['src/modules/app_customizations/**'],
50
+ },
51
+ 'OMH-031': {
52
+ sources: ['src/modules/app_customizations/api/interceptors.ts'],
53
+ artifacts: ['src/modules/app_customizations/api/interceptors.ts'],
54
+ },
55
+ 'OMH-042': {
56
+ sources: ['src/modules/magento'],
57
+ artifacts: ['src/modules/magento/**'],
58
+ },
59
+ 'OMH-045': {
60
+ sources: ['src/modules/external_sync/lib/client.ts'],
61
+ artifacts: ['src/modules/external_sync/lib/client.ts'],
62
+ },
63
+ 'OMH-049': {
64
+ sources: ['src/modules/library/ai-agents.ts'],
65
+ artifacts: ['src/modules/library/ai-agents.ts'],
66
+ },
67
+ 'OMH-054': {
68
+ sources: ['src/modules/automation/workflows/call-api.ts'],
69
+ artifacts: ['src/modules/automation/workflows/call-api.ts'],
70
+ },
71
+ 'OMH-057': {
72
+ sources: ['src/modules/harness_fixture/api/scope/route.ts'],
73
+ artifacts: ['src/modules/harness_fixture/api/scope/route.ts'],
74
+ },
75
+ 'OMH-060': {
76
+ sources: ['src/modules/harness_fixture/commands/update-record.ts'],
77
+ artifacts: ['src/modules/harness_fixture/commands/update-record.ts'],
78
+ },
79
+ 'OMH-061': {
80
+ sources: ['src/modules/harness_fixture/backend/edit/page.tsx'],
81
+ artifacts: ['src/modules/harness_fixture/backend/edit/page.tsx'],
82
+ },
83
+ 'OMH-070': {
84
+ sources: ['src/modules/harness_fixture/workers/sync.ts'],
85
+ artifacts: ['src/modules/harness_fixture/workers/sync.ts'],
86
+ },
87
+ 'OMH-093': {
88
+ family: 'business-command',
89
+ seam: 'mergeContacts',
90
+ sources: ['src/modules/customer_merge/commands/merge-contacts.ts'],
91
+ artifacts: ['src/modules/customer_merge/commands/merge-contacts.ts'],
92
+ },
93
+ 'OMH-105': {
94
+ family: 'business-command',
95
+ seam: 'changeDealStage',
96
+ sources: ['src/modules/deal_stages/commands/change-stage.ts'],
97
+ artifacts: ['src/modules/deal_stages/commands/change-stage.ts'],
98
+ },
99
+ 'OMH-107': {
100
+ family: 'business-command',
101
+ seam: 'requestQuoteDiscount',
102
+ sources: ['src/modules/quote_approval/commands/request-discount.ts'],
103
+ artifacts: ['src/modules/quote_approval/commands/request-discount.ts'],
104
+ },
105
+ 'OMH-115': {
106
+ family: 'ui-business-surface',
107
+ seam: 'moveDealAccessibly',
108
+ handler: 'handleDealBoardAction',
109
+ render: 'DealBoardPage',
110
+ event: 'onClick',
111
+ components: ['Page', 'PageHeader', 'PageBody', 'Button', 'Alert'],
112
+ metadata: ['requireAuth', 'requireFeatures'],
113
+ sources: [
114
+ 'src/modules/deal_accessibility/backend/board/page.tsx',
115
+ 'src/modules/deal_accessibility/backend/board/page.meta.ts',
116
+ 'src/modules/deal_accessibility/lib/move-deal.ts',
117
+ ],
118
+ artifacts: [
119
+ 'src/modules/deal_accessibility/backend/board/page.tsx',
120
+ 'src/modules/deal_accessibility/backend/board/page.meta.ts',
121
+ 'src/modules/deal_accessibility/lib/move-deal.ts',
122
+ 'src/modules/deal_accessibility/i18n/en.json',
123
+ ],
124
+ },
125
+ 'OMH-122': {
126
+ family: 'business-command',
127
+ seam: 'reserveStock',
128
+ sources: ['src/modules/stock_reservations/commands/reserve-stock.ts'],
129
+ artifacts: ['src/modules/stock_reservations/commands/reserve-stock.ts'],
130
+ },
131
+ 'OMH-128': {
132
+ family: 'async-operation',
133
+ seam: 'updatePrices',
134
+ sources: ['src/modules/bulk_pricing/commands/update-prices.ts'],
135
+ artifacts: ['src/modules/bulk_pricing/commands/update-prices.ts'],
136
+ },
137
+ 'OMH-130': {
138
+ family: 'ui-business-surface',
139
+ seam: 'submitDemoRequest',
140
+ handler: 'handleDemoRequest',
141
+ render: 'RequestDemoPage',
142
+ event: 'onSubmit',
143
+ components: ['FormField', 'Input', 'CheckboxField', 'Button', 'Alert'],
144
+ metadata: ['navHidden'],
145
+ allowFormTag: true,
146
+ sources: [
147
+ 'src/modules/demo_requests/frontend/request-demo/page.tsx',
148
+ 'src/modules/demo_requests/frontend/request-demo/page.meta.ts',
149
+ 'src/modules/demo_requests/lib/submit-demo-request.ts',
150
+ ],
151
+ artifacts: [
152
+ 'src/modules/demo_requests/frontend/request-demo/page.tsx',
153
+ 'src/modules/demo_requests/frontend/request-demo/page.meta.ts',
154
+ 'src/modules/demo_requests/lib/submit-demo-request.ts',
155
+ 'src/modules/demo_requests/i18n/en.json',
156
+ ],
157
+ },
158
+ 'OMH-133': {
159
+ family: 'business-command',
160
+ seam: 'approvePortalQuote',
161
+ sources: ['src/modules/portal_quote_approval/commands/approve-quote.ts'],
162
+ artifacts: ['src/modules/portal_quote_approval/commands/approve-quote.ts'],
163
+ },
164
+ 'OMH-137': {
165
+ family: 'ui-business-surface',
166
+ seam: 'advanceSetupWizard',
167
+ handler: 'handleSetupWizardAction',
168
+ render: 'SetupWizardPage',
169
+ event: 'onClick',
170
+ components: ['Page', 'PageHeader', 'PageBody', 'StepIndicator', 'Button', 'Alert', 'LoadingMessage'],
171
+ metadata: ['requireAuth', 'requireFeatures'],
172
+ sources: [
173
+ 'src/modules/setup_wizard/backend/setup/page.tsx',
174
+ 'src/modules/setup_wizard/backend/setup/page.meta.ts',
175
+ 'src/modules/setup_wizard/lib/advance-setup.ts',
176
+ ],
177
+ artifacts: [
178
+ 'src/modules/setup_wizard/backend/setup/page.tsx',
179
+ 'src/modules/setup_wizard/backend/setup/page.meta.ts',
180
+ 'src/modules/setup_wizard/lib/advance-setup.ts',
181
+ 'src/modules/setup_wizard/i18n/en.json',
182
+ ],
183
+ },
184
+ 'OMH-140': {
185
+ family: 'async-operation',
186
+ seam: 'runInvoiceDunning',
187
+ sources: ['src/modules/invoice_dunning'],
188
+ artifacts: ['src/modules/invoice_dunning/workflows/**', 'src/modules/invoice_dunning/events.ts'],
189
+ },
190
+ 'OMH-144': {
191
+ family: 'ai-safe-agent',
192
+ seam: 'saveQuoteDraftWithApproval',
193
+ mode: 'mutation',
194
+ sources: ['src/modules/quote_assistant/ai-agents.ts', 'src/modules/quote_assistant/ai-tools.ts'],
195
+ artifacts: ['src/modules/quote_assistant/ai-agents.ts', 'src/modules/quote_assistant/ai-tools.ts'],
196
+ },
197
+ 'OMH-146': {
198
+ family: 'ai-safe-agent',
199
+ seam: 'coordinateSalesQuestion',
200
+ mode: 'delegate',
201
+ sources: ['src/modules/sales_orchestrator/ai-agents.ts'],
202
+ artifacts: ['src/modules/sales_orchestrator/ai-agents.ts'],
203
+ },
204
+ 'OMH-149': {
205
+ family: 'provider-adapter',
206
+ seam: 'sendTransactionalEmail',
207
+ providerKind: 'transactional-email',
208
+ moduleId: 'smtp_email',
209
+ healthService: 'smtpEmailHealthCheck',
210
+ sources: ['src/modules/smtp_email', 'src/modules.ts'],
211
+ artifacts: [
212
+ 'src/modules/smtp_email/index.ts',
213
+ 'src/modules/smtp_email/integration.ts',
214
+ 'src/modules/smtp_email/di.ts',
215
+ 'src/modules/smtp_email/lib/client.ts',
216
+ 'src/modules/smtp_email/lib/health.ts',
217
+ 'src/modules.ts',
218
+ ],
219
+ },
220
+ 'OMH-150': {
221
+ family: 'provider-adapter',
222
+ seam: 'createCardPayment',
223
+ providerKind: 'payment',
224
+ moduleId: 'card_payments',
225
+ healthService: 'cardPaymentsHealthCheck',
226
+ adapterVariable: 'cardPaymentAdapter',
227
+ sources: ['src/modules/card_payments', 'src/modules.ts'],
228
+ artifacts: [
229
+ 'src/modules/card_payments/index.ts',
230
+ 'src/modules/card_payments/integration.ts',
231
+ 'src/modules/card_payments/di.ts',
232
+ 'src/modules/card_payments/acl.ts',
233
+ 'src/modules/card_payments/setup.ts',
234
+ 'src/modules/card_payments/lib/adapter.ts',
235
+ 'src/modules/card_payments/lib/client.ts',
236
+ 'src/modules/card_payments/lib/health.ts',
237
+ 'src/modules.ts',
238
+ ],
239
+ },
240
+ 'OMH-151': {
241
+ family: 'provider-adapter',
242
+ seam: 'bookCarrierShipment',
243
+ providerKind: 'shipping',
244
+ moduleId: 'carrier_shipping',
245
+ healthService: 'carrierShippingHealthCheck',
246
+ adapterVariable: 'carrierShippingAdapter',
247
+ sources: ['src/modules/carrier_shipping', 'src/modules.ts'],
248
+ artifacts: [
249
+ 'src/modules/carrier_shipping/index.ts',
250
+ 'src/modules/carrier_shipping/integration.ts',
251
+ 'src/modules/carrier_shipping/di.ts',
252
+ 'src/modules/carrier_shipping/acl.ts',
253
+ 'src/modules/carrier_shipping/setup.ts',
254
+ 'src/modules/carrier_shipping/lib/adapter.ts',
255
+ 'src/modules/carrier_shipping/lib/client.ts',
256
+ 'src/modules/carrier_shipping/lib/health.ts',
257
+ 'src/modules.ts',
258
+ ],
259
+ },
260
+ 'OMH-153': {
261
+ family: 'data-flow',
262
+ seam: 'synchronizeErpPage',
263
+ sources: ['src/modules/erp_sync'],
264
+ artifacts: ['src/modules/erp_sync/data-sync.ts', 'src/modules/erp_sync/backend/**', 'src/modules/erp_sync/workers/**'],
265
+ },
266
+ 'OMH-156': {
267
+ family: 'data-flow',
268
+ seam: 'transferProductRows',
269
+ sources: ['src/modules/product_transfer/lib/flow.ts'],
270
+ artifacts: ['src/modules/product_transfer/lib/flow.ts'],
271
+ },
272
+ 'OMH-163': {
273
+ family: 'test-authoring-unit',
274
+ sources: ['src/modules/quote_approval/commands/__tests__/approve-quote.test.ts'],
275
+ artifacts: ['src/modules/quote_approval/commands/__tests__/approve-quote.test.ts'],
276
+ },
277
+ 'OMH-164': {
278
+ family: 'test-authoring-api',
279
+ sources: ['src/modules/customer_api/__integration__/TC-API-CUSTOMERS-001.spec.ts'],
280
+ artifacts: ['src/modules/customer_api/__integration__/TC-API-CUSTOMERS-001.spec.ts'],
281
+ },
282
+ 'OMH-165': {
283
+ family: 'test-authoring-browser',
284
+ sources: ['src/modules/portal_quote_approval/__integration__/TC-PORTAL-QUOTE-001.spec.ts'],
285
+ artifacts: ['src/modules/portal_quote_approval/__integration__/TC-PORTAL-QUOTE-001.spec.ts'],
286
+ },
287
+ 'OMH-171': {
288
+ family: 'regression',
289
+ seam: 'listRecords',
290
+ sources: ['src/modules/harness_fixture/api/scope/route.ts'],
291
+ artifacts: ['src/modules/harness_fixture/api/scope/route.ts'],
292
+ },
293
+ 'OMH-172': {
294
+ family: 'regression',
295
+ sources: ['src/modules/harness_fixture/backend/edit/page.tsx'],
296
+ artifacts: ['src/modules/harness_fixture/backend/edit/page.tsx'],
297
+ },
298
+ 'OMH-181': {
299
+ family: 'data-table-extension',
300
+ seam: 'reviewOrderRisk',
301
+ sources: [
302
+ 'src/modules/order_risk/widgets/injection/order-risk-filter/widget.ts',
303
+ 'src/modules/order_risk/widgets/injection/order-risk-review/widget.ts',
304
+ 'src/modules/order_risk/widgets/injection-table.ts',
305
+ ],
306
+ artifacts: [
307
+ 'src/modules/order_risk/widgets/injection/order-risk-filter/widget.ts',
308
+ 'src/modules/order_risk/widgets/injection/order-risk-review/widget.ts',
309
+ 'src/modules/order_risk/widgets/injection-table.ts',
310
+ 'src/modules/order_risk/i18n/en.json',
311
+ ],
312
+ },
313
+ 'OMH-188': {
314
+ family: 'booking-overlap',
315
+ sources: ['src/modules/room_bookings', 'src/modules.ts'],
316
+ artifacts: [
317
+ 'src/modules/room_bookings/index.ts',
318
+ 'src/modules/room_bookings/data/entities.ts',
319
+ 'src/modules/room_bookings/data/validators.ts',
320
+ 'src/modules/room_bookings/migrations/**',
321
+ 'src/modules/room_bookings/commands/**',
322
+ 'src/modules/room_bookings/api/bookings/route.ts',
323
+ 'src/modules.ts',
324
+ ],
325
+ },
326
+ 'OMH-189': {
327
+ family: 'provider-transport',
328
+ moduleId: 'room_calendar_sync',
329
+ healthService: 'roomCalendarSyncHealth',
330
+ sources: ['src/modules/room_calendar_sync', 'src/modules.ts'],
331
+ artifacts: [
332
+ 'src/modules/room_calendar_sync/index.ts',
333
+ 'src/modules/room_calendar_sync/integration.ts',
334
+ 'src/modules/room_calendar_sync/di.ts',
335
+ 'src/modules/room_calendar_sync/lib/**',
336
+ 'src/modules.ts',
337
+ ],
338
+ },
339
+ 'OMH-190': {
340
+ family: 'response-enricher',
341
+ sources: ['src/modules/room_bookings/data/enrichers.ts'],
342
+ artifacts: ['src/modules/room_bookings/data/enrichers.ts'],
343
+ },
344
+ 'OMH-191': {
345
+ family: 'durable-workflow',
346
+ sources: ['src/modules/room_bookings/workflows.ts'],
347
+ artifacts: ['src/modules/room_bookings/workflows.ts'],
348
+ },
349
+ 'OMH-192': {
350
+ family: 'crm-library-regression',
351
+ sources: ['src/modules/library/commands/crm-loans.ts', 'src/modules/library/api/schemas.ts', 'src/modules/library/commands/__tests__/crm-loans.test.ts'],
352
+ schemaSource: 'src/modules/library/api/schemas.ts',
353
+ testSource: 'src/modules/library/commands/__tests__/crm-loans.test.ts',
354
+ artifacts: ['src/modules/library/commands/crm-loans.ts', 'src/modules/library/api/schemas.ts', 'src/modules/library/commands/__tests__/crm-loans.test.ts'],
355
+ },
356
+ 'OMH-185': {
357
+ family: 'complete-module',
358
+ sources: ['src/modules/library', 'src/modules.ts'],
359
+ artifacts: [
360
+ 'src/modules/library/index.ts',
361
+ 'src/modules/library/acl.ts',
362
+ 'src/modules/library/setup.ts',
363
+ 'src/modules/library/encryption.ts',
364
+ 'src/modules/library/search.ts',
365
+ 'src/modules/library/data/entities.ts',
366
+ 'src/modules/library/data/validators.ts',
367
+ 'src/modules/library/migrations/**',
368
+ 'src/modules/library/commands/**',
369
+ 'src/modules/library/commands/__tests__/**',
370
+ 'src/modules/library/api/books/route.ts',
371
+ 'src/modules/library/backend/books/**',
372
+ 'src/modules/library/i18n/en.json',
373
+ 'src/modules.ts',
374
+ ],
375
+ },
376
+ })
377
+
378
+ export const WRITABLE_CASE_IDS = Object.freeze(Object.keys(WRITABLE_CASES).sort())
379
+
380
+ function parseArgs(argv) {
381
+ const options = { root: undefined, caseId: undefined, phase: undefined, json: false }
382
+ for (let index = 0; index < argv.length; index += 1) {
383
+ const arg = argv[index]
384
+ const value = () => {
385
+ const next = argv[index + 1]
386
+ if (!next || next.startsWith('--')) throw new Error(`${arg} requires a value`)
387
+ index += 1
388
+ return next
389
+ }
390
+ if (arg === '--root') options.root = value()
391
+ else if (arg === '--case') options.caseId = value()
392
+ else if (arg === '--phase') options.phase = value()
393
+ else if (arg === '--json') options.json = true
394
+ else throw new Error(`unknown argument: ${arg}`)
395
+ }
396
+ if (!options.root || !options.caseId || !options.phase || !options.json) {
397
+ throw new Error('--root, --case, --phase before|after, and --json are required')
398
+ }
399
+ if (!path.isAbsolute(options.root)) throw new Error('--root must be absolute')
400
+ if (!WRITABLE_CASES[options.caseId]) throw new Error(`unsupported writable case: ${options.caseId}`)
401
+ if (!['before', 'after'].includes(options.phase)) throw new Error('--phase must be before or after')
402
+ return options
403
+ }
404
+
405
+ function loadTargetTypeScript(root) {
406
+ const targetRequire = createRequire(path.join(root, 'package.json'))
407
+ try {
408
+ return targetRequire('typescript')
409
+ } catch (error) {
410
+ throw new Error(`target app cannot resolve TypeScript: ${error instanceof Error ? error.message : String(error)}`)
411
+ }
412
+ }
413
+
414
+ function isTypeScriptSource(file) {
415
+ return /\.(?:cts|mts|ts|tsx)$/.test(file) && !/\.d\.(?:cts|mts|ts)$/.test(file)
416
+ }
417
+
418
+ function safeTargetEntry(root, absolute) {
419
+ const relative = path.relative(root, absolute)
420
+ if (!relative || relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)) {
421
+ throw new Error(`oracle path escapes the target: ${relative || '.'}`)
422
+ }
423
+ let current = root
424
+ for (const segment of relative.split(path.sep)) {
425
+ current = path.join(current, segment)
426
+ if (!fs.existsSync(current)) return undefined
427
+ const stat = fs.lstatSync(current)
428
+ if (stat.isSymbolicLink()) throw new Error(`oracle path contains a symbolic link: ${path.relative(root, current).replaceAll(path.sep, '/')}`)
429
+ if (!stat.isDirectory() && !stat.isFile()) throw new Error(`oracle path contains a special file: ${path.relative(root, current).replaceAll(path.sep, '/')}`)
430
+ }
431
+ if (!path.relative(root, fs.realpathSync(current)).startsWith('..')) return fs.lstatSync(current)
432
+ throw new Error(`oracle path resolves outside the target: ${relative.replaceAll(path.sep, '/')}`)
433
+ }
434
+
435
+ function collectSourceFiles(root, relativeEntries) {
436
+ const found = new Set()
437
+ const visit = (absolute) => {
438
+ const stat = safeTargetEntry(root, absolute)
439
+ if (!stat) return
440
+ if (stat.isFile()) {
441
+ if (isTypeScriptSource(absolute)) found.add(absolute)
442
+ return
443
+ }
444
+ if (!stat.isDirectory()) return
445
+ for (const entry of fs.readdirSync(absolute, { withFileTypes: true })) {
446
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
447
+ visit(path.join(absolute, entry.name))
448
+ }
449
+ }
450
+ for (const relative of relativeEntries) visit(path.join(root, relative))
451
+ return [...found].sort()
452
+ }
453
+
454
+ function artifactExists(root, pattern) {
455
+ if (!pattern.endsWith('/**')) return Boolean(safeTargetEntry(root, path.join(root, pattern)))
456
+ const directory = path.join(root, pattern.slice(0, -3))
457
+ const directoryStat = safeTargetEntry(root, directory)
458
+ if (!directoryStat?.isDirectory()) return false
459
+ const pending = [directory]
460
+ while (pending.length) {
461
+ const current = pending.pop()
462
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
463
+ const absolute = path.join(current, entry.name)
464
+ const stat = safeTargetEntry(root, absolute)
465
+ if (stat?.isFile()) return true
466
+ if (stat?.isDirectory()) pending.push(absolute)
467
+ }
468
+ }
469
+ return false
470
+ }
471
+
472
+ function propertyName(ts, node) {
473
+ if (!node) return undefined
474
+ if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node) || ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) {
475
+ return node.text
476
+ }
477
+ if (ts.isComputedPropertyName(node) && ts.isStringLiteralLike(node.expression)) return node.expression.text
478
+ return undefined
479
+ }
480
+
481
+ function expressionName(ts, expression) {
482
+ if (ts.isIdentifier(expression)) return expression.text
483
+ if (ts.isPropertyAccessExpression(expression)) return expression.name.text
484
+ if (ts.isElementAccessExpression(expression) && expression.argumentExpression && ts.isStringLiteralLike(expression.argumentExpression)) {
485
+ return expression.argumentExpression.text
486
+ }
487
+ return undefined
488
+ }
489
+
490
+ function fullExpressionName(ts, expression) {
491
+ if (ts.isIdentifier(expression)) return expression.text
492
+ if (ts.isPropertyAccessExpression(expression)) {
493
+ const left = fullExpressionName(ts, expression.expression)
494
+ return left ? `${left}.${expression.name.text}` : expression.name.text
495
+ }
496
+ return expressionName(ts, expression)
497
+ }
498
+
499
+ function expressionPath(ts, expression) {
500
+ if (ts.isIdentifier(expression)) return [expression.text]
501
+ if (ts.isPropertyAccessExpression(expression)) {
502
+ return [...expressionPath(ts, expression.expression), expression.name.text]
503
+ }
504
+ if (ts.isElementAccessExpression(expression) && expression.argumentExpression && ts.isStringLiteralLike(expression.argumentExpression)) {
505
+ return [...expressionPath(ts, expression.expression), expression.argumentExpression.text]
506
+ }
507
+ if (ts.isCallExpression(expression)) return expressionPath(ts, expression.expression)
508
+ if (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isNonNullExpression(expression)) {
509
+ return expressionPath(ts, expression.expression)
510
+ }
511
+ return []
512
+ }
513
+
514
+ const FORBIDDEN_TEST_ALIASES = new Set(['fdescribe', 'fit', 'pending', 'xdescribe', 'xit', 'xtest'])
515
+ const TEST_RUNNER_ROOTS = new Set(['describe', 'it', 'test'])
516
+ const FORBIDDEN_TEST_MODIFIERS = new Set(['fail', 'failing', 'fixme', 'only', 'skip', 'todo'])
517
+ const TEST_INFO_MODIFIERS = new Set(['fail', 'fixme', 'skip'])
518
+
519
+ function forbiddenTestModifier(ts, expression) {
520
+ const parts = expressionPath(ts, expression)
521
+ if (FORBIDDEN_TEST_ALIASES.has(parts[0])) return parts.join('.')
522
+ if (parts[0] === 'testInfo' && parts.slice(1).some((part) => TEST_INFO_MODIFIERS.has(part))) return parts.join('.')
523
+ if (!TEST_RUNNER_ROOTS.has(parts[0])) return undefined
524
+ return parts.slice(1).some((part) => FORBIDDEN_TEST_MODIFIERS.has(part)) ? parts.join('.') : undefined
525
+ }
526
+
527
+ function exactString(ts, expression, expected) {
528
+ return Boolean(expression && ts.isStringLiteralLike(expression) && expression.text === expected)
529
+ }
530
+
531
+ function listenBindsLoopback(ts, call) {
532
+ const first = call.arguments[0]
533
+ if (first && ts.isObjectLiteralExpression(first)) {
534
+ if (first.properties.some((property) => ts.isSpreadAssignment(property))) return false
535
+ if (first.properties.some((property) => property.name && ts.isComputedPropertyName(property.name))) return false
536
+ const hosts = first.properties.filter((property) => propertyName(ts, property.name) === 'host')
537
+ return hosts.length === 1 && ts.isPropertyAssignment(hosts[0]) && exactString(ts, hosts[0].initializer, '127.0.0.1')
538
+ }
539
+ return exactString(ts, call.arguments[1], '127.0.0.1')
540
+ }
541
+
542
+ function jsxTagName(ts, tag) {
543
+ if (ts.isIdentifier(tag)) return tag.text
544
+ if (ts.isPropertyAccessExpression(tag)) return tag.name.text
545
+ return tag.getText()
546
+ }
547
+
548
+ function newFacts() {
549
+ return {
550
+ calls: new Map(),
551
+ callArgumentFacts: new Map(),
552
+ callArgumentExpressions: new Map(),
553
+ callOptions: new Map(),
554
+ classes: [],
555
+ declarations: new Set(),
556
+ decorators: new Set(),
557
+ exportedFunctions: new Map(),
558
+ exportedFunctionNodes: new Map(),
559
+ exportedObjectFacts: new Map(),
560
+ exportedVariables: new Set(),
561
+ functionFacts: new Map(),
562
+ functionNodes: new Map(),
563
+ functions: new Set(),
564
+ finallyBlocks: 0,
565
+ importedBindings: new Map(),
566
+ importSources: new Set(),
567
+ forbiddenTestModifiers: new Set(),
568
+ jsxLiteralAttributes: new Map(),
569
+ jsxAttributes: new Set(),
570
+ jsxTags: new Set(),
571
+ jsxText: [],
572
+ loops: 0,
573
+ listenBindings: [],
574
+ newCalls: new Set(),
575
+ nullNodes: 0,
576
+ objectProperties: new Set(),
577
+ referencedFunctions: new Set(),
578
+ objectFacts: new Map(),
579
+ propertyIdentifiers: new Map(),
580
+ propertyAccesses: new Set(),
581
+ strings: new Set(),
582
+ throwStatements: 0,
583
+ variables: new Map(),
584
+ variableExpressions: new Map(),
585
+ assignments: new Set(),
586
+ awaitedCalls: new Set(),
587
+ moduleEntries: [],
588
+ }
589
+ }
590
+
591
+ function isExportedFunction(ts, node) {
592
+ return Boolean(node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword))
593
+ }
594
+
595
+ function isExportedVariable(ts, node) {
596
+ const statement = node.parent?.parent
597
+ return Boolean(statement && ts.isVariableStatement(statement) && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword))
598
+ }
599
+
600
+ function addMappedValue(map, key, value) {
601
+ if (!map.has(key)) map.set(key, new Set())
602
+ map.get(key).add(value)
603
+ }
604
+
605
+ function jsxAttributeLiteral(ts, attribute) {
606
+ if (!attribute.initializer) return undefined
607
+ if (ts.isStringLiteral(attribute.initializer)) return attribute.initializer.text
608
+ if (ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression && ts.isStringLiteralLike(attribute.initializer.expression)) {
609
+ return attribute.initializer.expression.text
610
+ }
611
+ return undefined
612
+ }
613
+
614
+ function collectFunctionFact(ts, node, { skipNestedDeclarations = false, skipNestedFunctions = false } = {}) {
615
+ const fact = {
616
+ binaryOperators: new Set(),
617
+ calls: new Set(),
618
+ callOptions: new Map(),
619
+ conditionalExpressions: 0,
620
+ finallyBlocks: 0,
621
+ jsxAttributes: new Set(),
622
+ jsxLiteralAttributes: new Map(),
623
+ jsxTags: new Set(),
624
+ jsxText: [],
625
+ loops: 0,
626
+ nullNodes: 0,
627
+ newCalls: new Set(),
628
+ objectProperties: new Set(),
629
+ referencedFunctions: new Set(),
630
+ strings: new Set(),
631
+ throws: 0,
632
+ }
633
+ const root = node.body ?? node
634
+ const visit = (current) => {
635
+ if (skipNestedFunctions && current !== root && ts.isFunctionLike(current)) return
636
+ if (skipNestedDeclarations && current !== root && ts.isFunctionDeclaration(current)) return
637
+ if (ts.isImportDeclaration(current) || ts.isImportEqualsDeclaration(current) || ts.isExportDeclaration(current)) return
638
+ if (ts.isCallExpression(current)) {
639
+ const names = [expressionName(ts, current.expression), fullExpressionName(ts, current.expression)].filter(Boolean)
640
+ const optionNames = current.arguments.flatMap((argument) => ts.isObjectLiteralExpression(argument)
641
+ ? argument.properties.map((property) => propertyName(ts, property.name)).filter(Boolean)
642
+ : [])
643
+ for (const name of names) {
644
+ fact.calls.add(name)
645
+ if (!fact.callOptions.has(name)) fact.callOptions.set(name, [])
646
+ fact.callOptions.get(name).push(new Set(optionNames))
647
+ }
648
+ for (const argument of current.arguments) {
649
+ const resolved = unwrapExpression(ts, argument)
650
+ if (resolved && ts.isIdentifier(resolved)) fact.referencedFunctions.add(resolved.text)
651
+ }
652
+ }
653
+ if (ts.isNewExpression(current)) {
654
+ const name = expressionName(ts, current.expression)
655
+ if (name) fact.newCalls.add(name)
656
+ }
657
+ if (ts.isObjectLiteralElementLike(current)) {
658
+ const name = propertyName(ts, current.name)
659
+ if (name) fact.objectProperties.add(name)
660
+ }
661
+ if (ts.isPropertyAssignment(current) && ts.isIdentifier(current.initializer)) {
662
+ fact.referencedFunctions.add(current.initializer.text)
663
+ }
664
+ if (ts.isThrowStatement(current)) fact.throws += 1
665
+ if (ts.isBinaryExpression(current)) fact.binaryOperators.add(current.operatorToken.kind)
666
+ if (ts.isConditionalExpression(current)) fact.conditionalExpressions += 1
667
+ if (current.kind === ts.SyntaxKind.NullKeyword) fact.nullNodes += 1
668
+ if (ts.isStringLiteralLike(current) || ts.isNoSubstitutionTemplateLiteral(current)) fact.strings.add(current.text)
669
+ if (ts.isJsxText(current) && current.text.trim()) fact.jsxText.push(current.text.trim())
670
+ if (ts.isJsxOpeningElement(current) || ts.isJsxSelfClosingElement(current)) {
671
+ fact.jsxTags.add(jsxTagName(ts, current.tagName))
672
+ for (const attribute of current.attributes.properties) {
673
+ if (!ts.isJsxAttribute(attribute)) continue
674
+ const name = attribute.name.text
675
+ fact.jsxAttributes.add(name)
676
+ const literal = jsxAttributeLiteral(ts, attribute)
677
+ if (literal !== undefined) addMappedValue(fact.jsxLiteralAttributes, name, literal)
678
+ }
679
+ }
680
+ if (ts.isTryStatement(current) && current.finallyBlock) fact.finallyBlocks += 1
681
+ if (ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current) || ts.isWhileStatement(current) || ts.isDoStatement(current)) {
682
+ fact.loops += 1
683
+ }
684
+ ts.forEachChild(current, visit)
685
+ }
686
+ visit(root)
687
+ return fact
688
+ }
689
+
690
+ function addCall(facts, name, optionNames = []) {
691
+ facts.calls.set(name, (facts.calls.get(name) ?? 0) + 1)
692
+ if (!facts.callOptions.has(name)) facts.callOptions.set(name, [])
693
+ facts.callOptions.get(name).push(new Set(optionNames))
694
+ }
695
+
696
+ function unwrapExpression(ts, node) {
697
+ let current = node
698
+ while (current && (
699
+ ts.isParenthesizedExpression(current)
700
+ || ts.isAsExpression(current)
701
+ || ts.isTypeAssertionExpression(current)
702
+ || ts.isNonNullExpression(current)
703
+ || (typeof ts.isSatisfiesExpression === 'function' && ts.isSatisfiesExpression(current))
704
+ )) {
705
+ current = current.expression
706
+ }
707
+ return current
708
+ }
709
+
710
+ function classMemberNames(ts, member) {
711
+ const names = new Set()
712
+ const identifier = propertyName(ts, member.name)
713
+ if (identifier) names.add(identifier)
714
+ const decorators = typeof ts.getDecorators === 'function' && ts.canHaveDecorators(member)
715
+ ? ts.getDecorators(member) ?? []
716
+ : []
717
+ for (const decorator of decorators) {
718
+ if (!ts.isCallExpression(decorator.expression)) continue
719
+ if (expressionName(ts, decorator.expression.expression) !== 'Property') continue
720
+ for (const argument of decorator.expression.arguments) {
721
+ if (!ts.isObjectLiteralExpression(argument)) continue
722
+ for (const option of argument.properties) {
723
+ if (!ts.isPropertyAssignment(option)) continue
724
+ const optionName = propertyName(ts, option.name)
725
+ if (!['name', 'fieldName'].includes(optionName) || !ts.isStringLiteralLike(option.initializer)) continue
726
+ names.add(option.initializer.text)
727
+ }
728
+ }
729
+ }
730
+ return names
731
+ }
732
+
733
+ function collectFacts(ts, sourceFiles) {
734
+ const facts = newFacts()
735
+ for (const file of sourceFiles) {
736
+ const source = ts.createSourceFile(
737
+ file,
738
+ fs.readFileSync(file, 'utf8'),
739
+ ts.ScriptTarget.Latest,
740
+ true,
741
+ file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
742
+ )
743
+ const visit = (node) => {
744
+ if (ts.isImportDeclaration(node)) {
745
+ const source = node.moduleSpecifier.text
746
+ facts.importSources.add(source)
747
+ const clause = node.importClause
748
+ if (clause?.name) facts.importedBindings.set(clause.name.text, source)
749
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
750
+ for (const element of clause.namedBindings.elements) facts.importedBindings.set(element.name.text, source)
751
+ } else if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
752
+ facts.importedBindings.set(clause.namedBindings.name.text, source)
753
+ }
754
+ return
755
+ }
756
+ if (ts.isImportEqualsDeclaration(node) || ts.isExportDeclaration(node)) return
757
+
758
+ if (ts.isClassDeclaration(node)) {
759
+ const members = new Set(node.members.flatMap((member) => [...classMemberNames(ts, member)]))
760
+ const decorators = typeof ts.getDecorators === 'function' && ts.canHaveDecorators(node)
761
+ ? (ts.getDecorators(node) ?? []).map((decorator) => expressionName(ts, decorator.expression.expression ?? decorator.expression)).filter(Boolean)
762
+ : []
763
+ facts.classes.push({ name: node.name?.text, members, decorators: new Set(decorators) })
764
+ if (node.name) facts.declarations.add(node.name.text)
765
+ for (const decorator of decorators) facts.decorators.add(decorator)
766
+ }
767
+ if (ts.isFunctionDeclaration(node) && node.name) {
768
+ const functionFact = collectFunctionFact(ts, node)
769
+ facts.functions.add(node.name.text)
770
+ facts.declarations.add(node.name.text)
771
+ facts.functionFacts.set(node.name.text, functionFact)
772
+ facts.functionNodes.set(node.name.text, node)
773
+ if (isExportedFunction(ts, node)) {
774
+ facts.exportedFunctions.set(node.name.text, functionFact)
775
+ facts.exportedFunctionNodes.set(node.name.text, node)
776
+ }
777
+ }
778
+ if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) {
779
+ facts.declarations.add(node.name.text)
780
+ }
781
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
782
+ facts.declarations.add(node.name.text)
783
+ if (isExportedVariable(ts, node)) facts.exportedVariables.add(node.name.text)
784
+ const properties = new Set()
785
+ const initializer = node.initializer ? unwrapExpression(ts, node.initializer) : undefined
786
+ if (initializer) facts.variableExpressions.set(node.name.text, initializer)
787
+ if (initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
788
+ const functionFact = collectFunctionFact(ts, initializer)
789
+ facts.functions.add(node.name.text)
790
+ facts.functionFacts.set(node.name.text, functionFact)
791
+ facts.functionNodes.set(node.name.text, initializer)
792
+ if (isExportedVariable(ts, node)) {
793
+ facts.exportedFunctions.set(node.name.text, functionFact)
794
+ facts.exportedFunctionNodes.set(node.name.text, initializer)
795
+ }
796
+ }
797
+ if (initializer && ts.isObjectLiteralExpression(initializer)) {
798
+ const objectFact = collectFunctionFact(ts, initializer)
799
+ facts.objectFacts.set(node.name.text, objectFact)
800
+ if (isExportedVariable(ts, node)) facts.exportedObjectFacts.set(node.name.text, objectFact)
801
+ for (const property of initializer.properties) {
802
+ const name = propertyName(ts, property.name)
803
+ if (name) properties.add(name)
804
+ }
805
+ }
806
+ facts.variables.set(node.name.text, properties)
807
+ if (node.name.text === 'enabledModules' && node.initializer && ts.isArrayLiteralExpression(node.initializer)) {
808
+ for (const element of node.initializer.elements) {
809
+ if (!ts.isObjectLiteralExpression(element)) continue
810
+ const entry = {}
811
+ for (const property of element.properties) {
812
+ if (!ts.isPropertyAssignment(property)) continue
813
+ const name = propertyName(ts, property.name)
814
+ if (!name || !ts.isStringLiteralLike(property.initializer)) continue
815
+ entry[name] = property.initializer.text
816
+ }
817
+ facts.moduleEntries.push(entry)
818
+ }
819
+ }
820
+ }
821
+ if (ts.isCallExpression(node)) {
822
+ const name = expressionName(ts, node.expression)
823
+ const fullName = fullExpressionName(ts, node.expression)
824
+ if (fullName === 'enabledModules.push') {
825
+ for (const argument of node.arguments) {
826
+ if (!ts.isObjectLiteralExpression(argument)) continue
827
+ const entry = {}
828
+ for (const property of argument.properties) {
829
+ if (!ts.isPropertyAssignment(property)) continue
830
+ const propertyKey = propertyName(ts, property.name)
831
+ if (!propertyKey || !ts.isStringLiteralLike(property.initializer)) continue
832
+ entry[propertyKey] = property.initializer.text
833
+ }
834
+ facts.moduleEntries.push(entry)
835
+ }
836
+ }
837
+ const forbiddenModifier = forbiddenTestModifier(ts, node.expression)
838
+ if (forbiddenModifier) facts.forbiddenTestModifiers.add(forbiddenModifier)
839
+ const callPath = expressionPath(ts, node.expression)
840
+ if (callPath.includes('listen')) facts.listenBindings.push(callPath.at(-1) === 'listen' && listenBindsLoopback(ts, node))
841
+ const optionNames = node.arguments.flatMap((argument) => ts.isObjectLiteralExpression(argument)
842
+ ? argument.properties.map((property) => propertyName(ts, property.name)).filter(Boolean)
843
+ : [])
844
+ if (name) {
845
+ const expressions = facts.callArgumentExpressions.get(name) ?? []
846
+ expressions.push(...node.arguments)
847
+ facts.callArgumentExpressions.set(name, expressions)
848
+ const argumentFacts = node.arguments
849
+ .map((argument) => unwrapExpression(ts, argument))
850
+ .filter((argument) => ts.isObjectLiteralExpression(argument) || ts.isArrayLiteralExpression(argument))
851
+ .map((argument) => collectFunctionFact(ts, argument))
852
+ if (argumentFacts.length > 0) {
853
+ const existing = facts.callArgumentFacts.get(name) ?? []
854
+ existing.push(...argumentFacts)
855
+ facts.callArgumentFacts.set(name, existing)
856
+ }
857
+ }
858
+ if (name) addCall(facts, name, optionNames)
859
+ if (fullName && fullName !== name) addCall(facts, fullName, optionNames)
860
+ if (node.parent && ts.isAwaitExpression(node.parent) && name) facts.awaitedCalls.add(name)
861
+ }
862
+ if (ts.isNewExpression(node)) {
863
+ const name = expressionName(ts, node.expression)
864
+ if (name) facts.newCalls.add(name)
865
+ }
866
+ if (ts.isObjectLiteralElementLike(node)) {
867
+ const name = propertyName(ts, node.name)
868
+ if (name) facts.objectProperties.add(name)
869
+ }
870
+ if (ts.isPropertyAssignment(node)) {
871
+ const name = propertyName(ts, node.name)
872
+ if (name && ts.isIdentifier(node.initializer)) addMappedValue(facts.propertyIdentifiers, name, node.initializer.text)
873
+ }
874
+ if (ts.isPropertyAccessExpression(node)) {
875
+ facts.propertyAccesses.add(node.name.text)
876
+ const forbiddenModifier = forbiddenTestModifier(ts, node)
877
+ if (forbiddenModifier) facts.forbiddenTestModifiers.add(forbiddenModifier)
878
+ }
879
+ if (ts.isElementAccessExpression(node)) {
880
+ const forbiddenModifier = forbiddenTestModifier(ts, node)
881
+ if (forbiddenModifier) facts.forbiddenTestModifiers.add(forbiddenModifier)
882
+ }
883
+ if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) facts.strings.add(node.text)
884
+ if (node.kind === ts.SyntaxKind.NullKeyword) facts.nullNodes += 1
885
+ if (ts.isThrowStatement(node)) facts.throwStatements += 1
886
+ if (ts.isTryStatement(node) && node.finallyBlock) facts.finallyBlocks += 1
887
+ if (ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node)) {
888
+ facts.loops += 1
889
+ }
890
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
891
+ const name = expressionName(ts, node.left)
892
+ if (name) facts.assignments.add(name)
893
+ }
894
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
895
+ facts.jsxTags.add(jsxTagName(ts, node.tagName))
896
+ for (const attribute of node.attributes.properties) {
897
+ if (!ts.isJsxAttribute(attribute)) continue
898
+ const name = attribute.name.text
899
+ facts.jsxAttributes.add(name)
900
+ const literal = jsxAttributeLiteral(ts, attribute)
901
+ if (literal !== undefined) addMappedValue(facts.jsxLiteralAttributes, name, literal)
902
+ }
903
+ }
904
+ if (ts.isJsxText(node) && node.text.trim()) facts.jsxText.push(node.text.trim())
905
+ ts.forEachChild(node, visit)
906
+ }
907
+ visit(source)
908
+ }
909
+ return facts
910
+ }
911
+
912
+ function hasCall(facts, ...names) {
913
+ return names.some((name) => (facts.calls.get(name) ?? 0) > 0)
914
+ }
915
+
916
+ function hasOnlyEnabledTests(facts) {
917
+ return facts.forbiddenTestModifiers.size === 0
918
+ }
919
+
920
+ function allServersBindLoopback(facts) {
921
+ return facts.listenBindings.length > 0 && facts.listenBindings.every(Boolean)
922
+ }
923
+
924
+ function hasCallOptions(facts, callName, required) {
925
+ return (facts.callOptions.get(callName) ?? []).some((options) => required.every((name) => options.has(name)))
926
+ }
927
+
928
+ function hasObjectVariable(facts, variableName, required) {
929
+ const properties = facts.variables.get(variableName)
930
+ return Boolean(properties && required.every((name) => properties.has(name)))
931
+ }
932
+
933
+ export function hasExactString(facts, value) {
934
+ return facts.strings.has(value)
935
+ }
936
+
937
+ // SQL migrations arrive as one template-literal string, so containment is the
938
+ // only way to assert a clause without hard-coding the author's formatting.
939
+ function hasStringIncluding(facts, ...fragments) {
940
+ return [...facts.strings].some((entry) => fragments.every((fragment) => entry.includes(fragment)))
941
+ }
942
+
943
+ function hasStringPrefix(facts, value) {
944
+ return [...facts.strings].some((entry) => entry.startsWith(value))
945
+ }
946
+
947
+ function factReachesCalls(facts, start, requiredCalls) {
948
+ if (!start) return false
949
+ const reached = new Set(start.calls)
950
+ const pending = [...start.calls, ...start.referencedFunctions]
951
+ const visited = new Set()
952
+ while (pending.length) {
953
+ const name = pending.pop()
954
+ if (visited.has(name)) continue
955
+ visited.add(name)
956
+ const local = facts.functionFacts.get(name)
957
+ if (!local) continue
958
+ for (const call of local.calls) {
959
+ reached.add(call)
960
+ pending.push(call)
961
+ }
962
+ for (const reference of local.referencedFunctions) pending.push(reference)
963
+ }
964
+ return requiredCalls.every((name) => reached.has(name))
965
+ }
966
+
967
+ function reachableFunctionFacts(facts, start) {
968
+ if (!start) return []
969
+ const reached = [start]
970
+ const pending = [...start.calls, ...start.referencedFunctions]
971
+ const visited = new Set()
972
+ while (pending.length) {
973
+ const name = pending.pop()
974
+ if (visited.has(name)) continue
975
+ visited.add(name)
976
+ const local = facts.functionFacts.get(name)
977
+ if (!local) continue
978
+ reached.push(local)
979
+ for (const call of local.calls) pending.push(call)
980
+ for (const reference of local.referencedFunctions) pending.push(reference)
981
+ }
982
+ return reached
983
+ }
984
+
985
+ function factReachesContract(facts, start, predicate) {
986
+ return reachableFunctionFacts(facts, start).some(predicate)
987
+ }
988
+
989
+ function callHasArgumentContract(facts, callName, predicate) {
990
+ return (facts.callArgumentFacts.get(callName) ?? []).some(predicate)
991
+ }
992
+
993
+ function resolveExpression(ts, facts, node, seen = new Set()) {
994
+ const expression = unwrapExpression(ts, node)
995
+ if (!expression || !ts.isIdentifier(expression) || seen.has(expression.text)) return expression
996
+ const target = facts.variableExpressions.get(expression.text)
997
+ if (!target) return expression
998
+ const nextSeen = new Set(seen)
999
+ nextSeen.add(expression.text)
1000
+ return resolveExpression(ts, facts, target, nextSeen)
1001
+ }
1002
+
1003
+ function objectProperty(ts, object, name) {
1004
+ if (!object || !ts.isObjectLiteralExpression(object)) return undefined
1005
+ return object.properties.find((property) => propertyName(ts, property.name) === name)
1006
+ }
1007
+
1008
+ function propertyExpression(ts, facts, object, name) {
1009
+ const property = objectProperty(ts, object, name)
1010
+ if (!property) return undefined
1011
+ if (ts.isPropertyAssignment(property)) return resolveExpression(ts, facts, property.initializer)
1012
+ if (ts.isShorthandPropertyAssignment(property)) return resolveExpression(ts, facts, property.name)
1013
+ return undefined
1014
+ }
1015
+
1016
+ function propertyFunction(ts, facts, object, name) {
1017
+ const property = objectProperty(ts, object, name)
1018
+ if (!property) return undefined
1019
+ if (ts.isMethodDeclaration(property)) return property
1020
+ if (!ts.isPropertyAssignment(property)) return undefined
1021
+ const expression = resolveExpression(ts, facts, property.initializer)
1022
+ return expression && (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression))
1023
+ ? expression
1024
+ : undefined
1025
+ }
1026
+
1027
+ function stringValue(ts, facts, node) {
1028
+ const expression = resolveExpression(ts, facts, node)
1029
+ return expression && ts.isStringLiteralLike(expression) ? expression.text : undefined
1030
+ }
1031
+
1032
+ function booleanValue(ts, facts, node) {
1033
+ const expression = resolveExpression(ts, facts, node)
1034
+ if (!expression) return undefined
1035
+ if (expression.kind === ts.SyntaxKind.TrueKeyword) return true
1036
+ if (expression.kind === ts.SyntaxKind.FalseKeyword) return false
1037
+ return undefined
1038
+ }
1039
+
1040
+ function numberValue(ts, facts, node) {
1041
+ const expression = resolveExpression(ts, facts, node)
1042
+ return expression && ts.isNumericLiteral(expression) ? Number(expression.text) : undefined
1043
+ }
1044
+
1045
+ function arrayElements(ts, facts, node) {
1046
+ const expression = resolveExpression(ts, facts, node)
1047
+ return expression && ts.isArrayLiteralExpression(expression)
1048
+ ? expression.elements.map((element) => resolveExpression(ts, facts, element)).filter(Boolean)
1049
+ : []
1050
+ }
1051
+
1052
+ function objectPropertyString(ts, facts, object, name) {
1053
+ return stringValue(ts, facts, propertyExpression(ts, facts, object, name))
1054
+ }
1055
+
1056
+ function objectPropertyObject(ts, facts, object, name) {
1057
+ const expression = propertyExpression(ts, facts, object, name)
1058
+ return expression && ts.isObjectLiteralExpression(expression) ? expression : undefined
1059
+ }
1060
+
1061
+ function objectPropertyArray(ts, facts, object, name) {
1062
+ return arrayElements(ts, facts, propertyExpression(ts, facts, object, name))
1063
+ }
1064
+
1065
+ function resolvedCallArguments(ts, facts, callName) {
1066
+ return (facts.callArgumentExpressions.get(callName) ?? [])
1067
+ .map((argument) => resolveExpression(ts, facts, argument))
1068
+ .filter(Boolean)
1069
+ }
1070
+
1071
+ function functionContractFacts(ts, facts, node, options) {
1072
+ const direct = collectFunctionFact(ts, node, { skipNestedDeclarations: true, ...options })
1073
+ if (options?.skipNestedFunctions) {
1074
+ const reached = [direct]
1075
+ const pending = [...direct.calls, ...direct.referencedFunctions]
1076
+ const visited = new Set()
1077
+ while (pending.length) {
1078
+ const name = pending.pop()
1079
+ if (visited.has(name)) continue
1080
+ visited.add(name)
1081
+ const target = facts.functionNodes.get(name)
1082
+ if (!target) continue
1083
+ const local = collectFunctionFact(ts, target, { skipNestedDeclarations: true, ...options })
1084
+ reached.push(local)
1085
+ for (const call of local.calls) pending.push(call)
1086
+ for (const reference of local.referencedFunctions) pending.push(reference)
1087
+ }
1088
+ return reached
1089
+ }
1090
+ return reachableFunctionFacts(facts, direct)
1091
+ }
1092
+
1093
+ function returnedObjectFunctions(ts, facts, node) {
1094
+ const returned = []
1095
+ const root = node.body ?? node
1096
+ const collectObjectMethods = (expression) => {
1097
+ const object = resolveExpression(ts, facts, expression)
1098
+ if (!object || !ts.isObjectLiteralExpression(object)) return
1099
+ for (const property of object.properties) {
1100
+ const name = propertyName(ts, property.name)
1101
+ const method = name ? propertyFunction(ts, facts, object, name) : undefined
1102
+ if (method) returned.push(method)
1103
+ }
1104
+ }
1105
+ if (ts.isObjectLiteralExpression(root)) collectObjectMethods(root)
1106
+ const visit = (current) => {
1107
+ if (current !== root && ts.isFunctionLike(current)) return
1108
+ if (ts.isReturnStatement(current) && current.expression) {
1109
+ collectObjectMethods(current.expression)
1110
+ return
1111
+ }
1112
+ ts.forEachChild(current, visit)
1113
+ }
1114
+ visit(root)
1115
+ return returned
1116
+ }
1117
+
1118
+ function functionContractCalls(ts, facts, node, requiredCalls) {
1119
+ const reached = functionContractFacts(ts, facts, node)
1120
+ const calls = new Set(reached.flatMap((fact) => [...fact.calls]))
1121
+ return requiredCalls.every((name) => calls.has(name))
1122
+ }
1123
+
1124
+ function functionContractMatches(ts, facts, node, predicate) {
1125
+ return functionContractFacts(ts, facts, node).some(predicate)
1126
+ }
1127
+
1128
+ function functionHasLiteralTrueOption(ts, facts, node, callName, optionName) {
1129
+ const functionNodes = [node]
1130
+ const pending = [...collectFunctionFact(ts, node, { skipNestedDeclarations: true }).calls]
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 target = facts.functionNodes.get(name)
1137
+ if (!target) continue
1138
+ functionNodes.push(target)
1139
+ for (const call of collectFunctionFact(ts, target, { skipNestedDeclarations: true }).calls) pending.push(call)
1140
+ }
1141
+ return functionNodes.some((functionNode) => {
1142
+ let matched = false
1143
+ const root = functionNode.body ?? functionNode
1144
+ const visit = (current) => {
1145
+ if (matched) return
1146
+ if (current !== root && ts.isFunctionDeclaration(current)) return
1147
+ if (ts.isCallExpression(current) && expressionName(ts, current.expression) === callName) {
1148
+ for (const argument of current.arguments) {
1149
+ const object = resolveExpression(ts, facts, argument)
1150
+ const value = object && ts.isObjectLiteralExpression(object)
1151
+ ? propertyExpression(ts, facts, object, optionName)
1152
+ : undefined
1153
+ if (booleanValue(ts, facts, value) === true) {
1154
+ matched = true
1155
+ return
1156
+ }
1157
+ }
1158
+ }
1159
+ ts.forEachChild(current, visit)
1160
+ }
1161
+ visit(root)
1162
+ return matched
1163
+ })
1164
+ }
1165
+
1166
+ function exportedFunctionCalls(facts, functionName, requiredCalls) {
1167
+ return factReachesCalls(facts, facts.exportedFunctions.get(functionName), requiredCalls)
1168
+ }
1169
+
1170
+ function exportedObjectCalls(facts, variableName, requiredCalls) {
1171
+ return factReachesCalls(facts, facts.exportedObjectFacts.get(variableName), requiredCalls)
1172
+ }
1173
+
1174
+ function exportedCommandObjectCalls(facts, variableName, requiredCalls) {
1175
+ return [variableName, `${variableName}Command`].some((name) => exportedObjectCalls(facts, name, requiredCalls))
1176
+ }
1177
+
1178
+ function exportedFunctionHasCallOptions(facts, functionName, callName, requiredOptions) {
1179
+ const fact = facts.exportedFunctions.get(functionName)
1180
+ return Boolean(fact && (fact.callOptions.get(callName) ?? []).some((options) => requiredOptions.every((name) => options.has(name))))
1181
+ }
1182
+
1183
+ function importsSharedComponent(facts, name) {
1184
+ return facts.importedBindings.get(name)?.startsWith('@open-mercato/ui') === true
1185
+ }
1186
+
1187
+ function uiPolicyFailures(facts, { allowFormTag = false } = {}) {
1188
+ const rawTags = ['button', 'input', 'select', 'textarea', 'svg', ...(allowFormTag ? [] : ['form'])]
1189
+ const failures = []
1190
+ for (const tag of rawTags) if (facts.jsxTags.has(tag)) failures.push(`raw <${tag}>`)
1191
+ if (facts.jsxAttributes.has('style')) failures.push('inline style')
1192
+ const strings = [...facts.strings]
1193
+ 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))) {
1194
+ failures.push('hard-coded palette class')
1195
+ }
1196
+ if (strings.some((value) => /(?:^|\s)\S*\[[^\]]+\]/.test(value))) failures.push('arbitrary Tailwind value')
1197
+ if (strings.some((value) => /(?:^|\s)dark:/.test(value))) failures.push('manual dark-mode override')
1198
+ for (const name of ['label', 'title', 'placeholder', 'aria-label', 'alt']) {
1199
+ if ((facts.jsxLiteralAttributes.get(name)?.size ?? 0) > 0) failures.push(`hard-coded ${name}`)
1200
+ }
1201
+ if (facts.jsxText.length > 0) failures.push('hard-coded JSX copy')
1202
+ return failures
1203
+ }
1204
+
1205
+ function renderedUiChecks(definition, facts) {
1206
+ const render = facts.exportedFunctions.get(definition.render)
1207
+ const handler = facts.exportedFunctions.get(definition.handler)
1208
+ const policyFailures = uiPolicyFailures(facts, { allowFormTag: definition.allowFormTag })
1209
+ const strings = [...(render?.strings ?? [])]
1210
+ return [
1211
+ 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`),
1212
+ 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`),
1213
+ 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`),
1214
+ 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`),
1215
+ check('business.ui-responsive', strings.some((value) => /(?:^|\s)(?:sm|md|lg|xl|2xl):/.test(value)), `exported ${definition.render} includes a standard responsive breakpoint`),
1216
+ 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`),
1217
+ check('business.ui-metadata', hasObjectVariable(facts, 'metadata', definition.metadata), `the sibling page metadata declares ${definition.metadata.join(' and ')}`),
1218
+ 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(', ')})` : ''}`),
1219
+ ]
1220
+ }
1221
+
1222
+ function check(id, passed, requirement) {
1223
+ return { id, passed: Boolean(passed), requirement }
1224
+ }
1225
+
1226
+ function caseChecks(ts, caseId, facts, root) {
1227
+ const definition = WRITABLE_CASES[caseId]
1228
+ if (definition.family === 'complete-module') {
1229
+ const scopedEntity = facts.classes.some((entry) => entry.decorators.has('Entity') && ['tenant_id', 'organization_id', 'updated_at'].every((name) => entry.members.has(name)))
1230
+ const uiFailures = uiPolicyFailures(facts)
1231
+ return [
1232
+ 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'),
1233
+ check('module.entity', scopedEntity, 'a scoped editable @Entity includes tenant_id, organization_id, and updated_at'),
1234
+ check('module.validator', hasCall(facts, 'z.object', 'object'), 'the book input boundary uses a concrete validator object'),
1235
+ check('module.acl', facts.exportedVariables.has('features') && hasExactString(facts, 'library.books.view') && hasExactString(facts, 'library.books.manage'), 'acl.ts exports stable view/manage features'),
1236
+ check('module.setup', facts.exportedVariables.has('setup') && facts.objectProperties.has('defaultRoleFeatures'), 'setup.ts grants module features through defaultRoleFeatures'),
1237
+ 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'),
1238
+ 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'),
1239
+ 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'),
1240
+ check('module.openapi', facts.exportedVariables.has('openApi'), 'the CRUD API exports OpenAPI documentation separately'),
1241
+ check('module.command-atomic', exportedCommandObjectCalls(facts, 'createBook', ['withAtomicFlush'])
1242
+ && ['updateBook', 'deleteBook'].every((name) => exportedCommandObjectCalls(facts, name, ['withAtomicFlush', 'enforceCommandOptimisticLock'])),
1243
+ 'each declared create/update/delete command reaches its own transaction seam, and update/delete each enforce their own optimistic lock'),
1244
+ check('module.command-undo', exportedCommandObjectCalls(facts, 'createBook', ['extractUndoPayload', 'emitCrudUndoSideEffects'])
1245
+ && ['updateBook', 'deleteBook'].every((name) => exportedCommandObjectCalls(facts, name, ['extractUndoPayload', 'buildCustomFieldResetMap', 'emitCrudUndoSideEffects']))
1246
+ && hasCall(facts, 'emitCrudSideEffects'),
1247
+ 'each declared create/update/delete command owns concrete undo behavior; update/delete restore custom fields and all commands retain forward side effects'),
1248
+ check('module.encryption-map', facts.exportedVariables.has('defaultEncryptionMaps') && hasExactString(facts, 'library:book'), 'encryption.ts exports a library:book defaultEncryptionMaps entry'),
1249
+ 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'),
1250
+ check('module.search', facts.exportedVariables.has('searchConfig') && ['fieldPolicy', 'checksumSource', 'formatResult'].every((name) => facts.objectProperties.has(name)), 'search.ts defines policy, checksum, and presentation contracts'),
1251
+ 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'),
1252
+ 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'),
1253
+ 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'),
1254
+ check('module.custom-fields', hasCall(facts, 'collectCustomFieldValues') && hasCall(facts, 'buildCustomFieldResetMap'), 'UI submission and command undo preserve custom fields'),
1255
+ 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'),
1256
+ 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(', ')})` : ''}`),
1257
+ ]
1258
+ }
1259
+ if (definition.family === 'booking-overlap') {
1260
+ const scopedEntity = facts.classes.some((entry) => entry.decorators.has('Entity') && ['tenant_id', 'organization_id', 'updated_at'].every((name) => entry.members.has(name)))
1261
+ const route = resolvedCallArguments(ts, facts, 'makeCrudRoute')
1262
+ .find((argument) => ts.isObjectLiteralExpression(argument) && objectProperty(ts, argument, 'actions'))
1263
+ const actions = route && ts.isObjectLiteralExpression(route)
1264
+ ? objectPropertyObject(ts, facts, route, 'actions')
1265
+ : undefined
1266
+ const routeCommandIds = actions
1267
+ ? ['create', 'update', 'delete'].map((action) => {
1268
+ const actionConfig = objectPropertyObject(ts, facts, actions, action)
1269
+ return actionConfig ? objectPropertyString(ts, facts, actionConfig, 'commandId') : undefined
1270
+ })
1271
+ : []
1272
+ const registeredCommands = resolvedCallArguments(ts, facts, 'registerCommand')
1273
+ .filter((argument) => ts.isObjectLiteralExpression(argument))
1274
+ const connectedCommands = routeCommandIds.map((commandId) =>
1275
+ registeredCommands.find((command) => objectPropertyString(ts, facts, command, 'id') === commandId))
1276
+ const commandFunctions = connectedCommands.map((command) => ({
1277
+ execute: command ? propertyFunction(ts, facts, command, 'execute') : undefined,
1278
+ undo: command ? propertyFunction(ts, facts, command, 'undo') : undefined,
1279
+ }))
1280
+ const allCommandsConnected = routeCommandIds.length === 3
1281
+ && routeCommandIds.every((commandId) => typeof commandId === 'string')
1282
+ && new Set(routeCommandIds).size === 3
1283
+ && connectedCommands.every(Boolean)
1284
+ && commandFunctions.every(({ execute, undo }) => execute && undo)
1285
+ const commandsAtomic = allCommandsConnected && commandFunctions.every(({ execute }) =>
1286
+ functionContractCalls(ts, facts, execute, ['withAtomicFlush', 'emitCrudSideEffects'])
1287
+ && functionHasLiteralTrueOption(ts, facts, execute, 'withAtomicFlush', 'transaction'))
1288
+ const commandsUndoable = allCommandsConnected && commandFunctions.every(({ undo }) =>
1289
+ functionContractCalls(ts, facts, undo, ['extractUndoPayload', 'emitCrudUndoSideEffects']))
1290
+ const conflictMapped = allCommandsConnected && commandFunctions.slice(0, 2).every(({ execute }) =>
1291
+ functionContractMatches(ts, facts, execute, (reachable) =>
1292
+ reachable.strings.has('23P01') && reachable.newCalls.has('CrudHttpError')))
1293
+ return [
1294
+ 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'),
1295
+ 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'),
1296
+ 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'),
1297
+ check('overlap.conflict-mapping', conflictMapped, 'the route-connected create and update commands each map SQLSTATE 23P01 to a conflict error'),
1298
+ 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'),
1299
+ check('overlap.scoped-entity', scopedEntity, 'the booking entity carries tenant_id, organization_id, and updated_at'),
1300
+ check('overlap.command-atomic', commandsAtomic, 'the route-connected create/update/delete commands each reach transactional flush and post-commit side effects'),
1301
+ check('overlap.command-undo', commandsUndoable, 'the route-connected create/update/delete commands each reach undo evidence and undo side effects'),
1302
+ check('overlap.crud-host', Boolean(route)
1303
+ && ['metadata', 'orm', 'list', 'actions', 'indexer'].every((name) => objectProperty(ts, route, name)), 'the bookings CRUD route wires metadata, scoped ORM, list, command actions, and indexer'),
1304
+ ]
1305
+ }
1306
+ if (definition.family === 'provider-transport') {
1307
+ const guard = facts.exportedFunctions.get('assertSafeEndpoint')
1308
+ const client = facts.exportedFunctions.get('createRoomCalendarClient')
1309
+ const clientNode = facts.exportedFunctionNodes.get('createRoomCalendarClient')
1310
+ const clientTree = clientNode
1311
+ ? [clientNode, ...returnedObjectFunctions(ts, facts, clientNode)]
1312
+ .flatMap((node) => functionContractFacts(ts, facts, node, { skipNestedFunctions: true }))
1313
+ : []
1314
+ const clientStrings = new Set(clientTree.flatMap((fact) => [...fact.strings]))
1315
+ const clientCalls = new Set(clientTree.flatMap((fact) => [...fact.calls]))
1316
+ return [
1317
+ 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'),
1318
+ 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'),
1319
+ check('provider.health-di', hasExactString(facts, definition.healthService) && hasCall(facts, 'container.register'), `DI registers the exact ${definition.healthService} service declared by integration.ts`),
1320
+ check('provider.ssrf-guard', Boolean(guard)
1321
+ && clientCalls.has('assertSafeEndpoint')
1322
+ && (clientCalls.has('fetch') || clientCalls.has('fetchImpl') || clientCalls.has('request')), 'exported createRoomCalendarClient reaches both the behavior-probed guard and request seam'),
1323
+ check('provider.idempotency-key', clientStrings.has('idempotency-key'), 'the client request path carries a stable idempotency-key header'),
1324
+ check('provider.redirect-refusal', clientStrings.has('manual'), 'the client request path refuses to follow redirects'),
1325
+ check('provider.bounded-retry', clientTree.some((fact) => fact.loops > 0), 'exported createRoomCalendarClient retries through a bounded local call tree'),
1326
+ check('provider.unconfigured-degraded', hasExactString(facts, 'unconfigured'), 'a missing configuration reports a degraded state instead of failing'),
1327
+ ]
1328
+ }
1329
+ if (definition.family === 'response-enricher') {
1330
+ const enrichersExpression = facts.exportedVariables.has('enrichers')
1331
+ ? facts.variableExpressions.get('enrichers')
1332
+ : undefined
1333
+ const registeredEnrichers = arrayElements(ts, facts, enrichersExpression)
1334
+ .filter((entry) => ts.isObjectLiteralExpression(entry))
1335
+ const enricher = registeredEnrichers.find((entry) =>
1336
+ objectPropertyString(ts, facts, entry, 'targetEntity') === 'customers.person'
1337
+ && objectPropertyArray(ts, facts, entry, 'features')
1338
+ .some((feature) => stringValue(ts, facts, feature) === 'room_bookings.bookings.view'))
1339
+ const enrichOne = enricher ? propertyFunction(ts, facts, enricher, 'enrichOne') : undefined
1340
+ const enrichMany = enricher ? propertyFunction(ts, facts, enricher, 'enrichMany') : undefined
1341
+ const fallback = enricher ? objectPropertyObject(ts, facts, enricher, 'fallback') : undefined
1342
+ const enrichOneNamespaced = Boolean(enrichOne) && (
1343
+ functionContractMatches(ts, facts, enrichOne, (fact) => fact.objectProperties.has('_room_bookings'))
1344
+ || functionContractCalls(ts, facts, enrichOne, ['enrichMany'])
1345
+ )
1346
+ const enrichManyFacts = enrichMany ? functionContractFacts(ts, facts, enrichMany) : []
1347
+ const enrichManyNamespaced = enrichManyFacts.some((fact) => fact.objectProperties.has('_room_bookings'))
1348
+ const batchReadCalls = new Set(['find', 'findWithDecryption', 'findAndCountWithDecryption', 'query', 'selectFrom'])
1349
+ const enrichManyReadsBookings = enrichManyFacts.some((fact) =>
1350
+ [...fact.calls].some((call) => batchReadCalls.has(call)))
1351
+ const enrichManyBatched = enrichManyFacts.some((fact) =>
1352
+ fact.loops > 0 || fact.calls.has('map') || fact.calls.has('Promise.all'))
1353
+ && enrichManyReadsBookings
1354
+ const resilient = Boolean(enricher && fallback)
1355
+ && Boolean(objectProperty(ts, fallback, '_room_bookings'))
1356
+ && (numberValue(ts, facts, propertyExpression(ts, facts, enricher, 'timeout')) ?? 0) > 0
1357
+ && booleanValue(ts, facts, propertyExpression(ts, facts, enricher, 'cacheableOnListHit')) === false
1358
+ return [
1359
+ 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'),
1360
+ check('enricher.batched', Boolean(enrichMany) && enrichManyBatched, 'the registered enricher batch-reads persisted booking data before mapping list results'),
1361
+ check('enricher.namespaced', enrichOneNamespaced && enrichManyNamespaced, 'the registered enricher constructs _room_bookings in detail and list paths'),
1362
+ check('enricher.resilience', resilient, 'the registered enricher declares a namespaced fallback, positive timeout, and cacheableOnListHit false'),
1363
+ check('enricher.acl', Boolean(enricher), 'that same enricher gates on room_bookings.bookings.view'),
1364
+ 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'),
1365
+ ]
1366
+ }
1367
+ if (definition.family === 'durable-workflow') {
1368
+ const workflow = resolvedCallArguments(ts, facts, 'defineWorkflow')
1369
+ .find((argument) => ts.isObjectLiteralExpression(argument)
1370
+ && objectProperty(ts, argument, 'steps') && objectProperty(ts, argument, 'transitions'))
1371
+ const steps = workflow && ts.isObjectLiteralExpression(workflow)
1372
+ ? objectPropertyArray(ts, facts, workflow, 'steps').filter((step) => ts.isObjectLiteralExpression(step))
1373
+ : []
1374
+ const transitions = workflow && ts.isObjectLiteralExpression(workflow)
1375
+ ? objectPropertyArray(ts, facts, workflow, 'transitions').filter((transition) => ts.isObjectLiteralExpression(transition))
1376
+ : []
1377
+ const stepById = new Map(steps.map((step) => [objectPropertyString(ts, facts, step, 'stepId'), step]).filter(([id]) => id))
1378
+ const start = steps.find((step) => objectPropertyString(ts, facts, step, 'stepType') === 'START')
1379
+ const end = steps.find((step) => objectPropertyString(ts, facts, step, 'stepType') === 'END')
1380
+ const timerStep = steps.find((step) => {
1381
+ if (objectPropertyString(ts, facts, step, 'stepType') !== 'WAIT_FOR_TIMER') return false
1382
+ const config = objectPropertyObject(ts, facts, step, 'config')
1383
+ return config && objectPropertyString(ts, facts, config, 'duration') === 'PT15M'
1384
+ })
1385
+ const startId = start ? objectPropertyString(ts, facts, start, 'stepId') : undefined
1386
+ const endId = end ? objectPropertyString(ts, facts, end, 'stepId') : undefined
1387
+ const timerId = timerStep ? objectPropertyString(ts, facts, timerStep, 'stepId') : undefined
1388
+ const parsedTransitions = transitions.map((transition) => ({
1389
+ node: transition,
1390
+ from: objectPropertyString(ts, facts, transition, 'fromStepId'),
1391
+ to: objectPropertyString(ts, facts, transition, 'toStepId'),
1392
+ trigger: objectPropertyString(ts, facts, transition, 'trigger'),
1393
+ priority: numberValue(ts, facts, propertyExpression(ts, facts, transition, 'priority')) ?? 0,
1394
+ }))
1395
+ const endpointsValid = parsedTransitions.length > 0
1396
+ && parsedTransitions.every(({ from, to }) => stepById.has(from) && stepById.has(to))
1397
+ const reachable = new Set(startId ? [startId] : [])
1398
+ const pending = startId ? [startId] : []
1399
+ while (pending.length) {
1400
+ const current = pending.pop()
1401
+ for (const transition of parsedTransitions.filter(({ from }) => from === current)) {
1402
+ if (reachable.has(transition.to)) continue
1403
+ reachable.add(transition.to)
1404
+ pending.push(transition.to)
1405
+ }
1406
+ }
1407
+ const signalTransition = parsedTransitions.find(({ from, trigger }) => from === timerId && trigger === 'signal')
1408
+ const timerTransition = parsedTransitions.find(({ from, trigger }) => from === timerId && trigger === 'timer')
1409
+ const updateActivity = timerTransition
1410
+ ? objectPropertyArray(ts, facts, timerTransition.node, 'activities')
1411
+ .find((activity) => {
1412
+ if (!ts.isObjectLiteralExpression(activity)
1413
+ || objectPropertyString(ts, facts, activity, 'activityType') !== 'UPDATE_ENTITY') return false
1414
+ const config = objectPropertyObject(ts, facts, activity, 'config')
1415
+ return config && objectPropertyString(ts, facts, config, 'commandId') === 'room_bookings.bookings.update'
1416
+ })
1417
+ : undefined
1418
+ const safeCommand = resolvedCallArguments(ts, facts, 'registerWorkflowSafeCommands')
1419
+ .flatMap((argument) => arrayElements(ts, facts, argument))
1420
+ .find((entry) => ts.isObjectLiteralExpression(entry)
1421
+ && objectPropertyString(ts, facts, entry, 'commandId') === 'room_bookings.bookings.update'
1422
+ && objectPropertyArray(ts, facts, entry, 'requiredFeatures')
1423
+ .some((feature) => Boolean(stringValue(ts, facts, feature))))
1424
+ const workflowGraph = Boolean(startId && endId && timerId)
1425
+ && endpointsValid && reachable.has(endId)
1426
+ const confirmationWins = Boolean(signalTransition && timerTransition
1427
+ && signalTransition.priority > timerTransition.priority
1428
+ && reachable.has(signalTransition.to) && reachable.has(timerTransition.to))
1429
+ return [
1430
+ check('workflow.timer-config', Boolean(timerStep), 'the WAIT_FOR_TIMER step owns the exact PT15M duration'),
1431
+ check('workflow.safe-commands', Boolean(safeCommand), 'registerWorkflowSafeCommands pairs the dispatched command with its required features'),
1432
+ check('workflow.builder', hasCall(facts, 'defineWorkflow') && hasCall(facts, 'createWorkflowsModuleConfig') && facts.exportedVariables.has('workflowsConfig'), 'the definition uses the typed builder and exports the discovered workflowsConfig'),
1433
+ check('workflow.terminal-graph', workflowGraph, 'the same graph declares explicit START and END steps'),
1434
+ check('workflow.confirmation-beats-expiry', confirmationWins, 'the signal confirmation has higher priority than the competing expiry timer'),
1435
+ check('workflow.dispatch-update-entity', Boolean(updateActivity), 'the timer release transition owns an UPDATE_ENTITY activity for the booking update command'),
1436
+ ]
1437
+ }
1438
+ if (definition.family === 'crm-library-regression') {
1439
+ const schemaFacts = collectFacts(ts, collectSourceFiles(root, [definition.schemaSource]))
1440
+ const testFacts = collectFacts(ts, collectSourceFiles(root, [definition.testSource]))
1441
+ const scopedActions = ['createBook', 'undoCreateBook', 'deleteBook', 'undoDeleteBook', 'checkoutBook', 'returnLoan', 'renewLoan', 'markLoanLost']
1442
+ return [
1443
+ check('crm-library.exports', scopedActions.every((name) => facts.exportedFunctions.has(name)), 'the stable lifecycle, checkout, return, renew, and mark-lost seams remain exported'),
1444
+ 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'),
1445
+ check('crm-library.lifecycle', exportedFunctionCalls(facts, 'createBook', ['effects.createBook'])
1446
+ && exportedFunctionCalls(facts, 'undoCreateBook', ['effects.softDeleteBook'])
1447
+ && exportedFunctionCalls(facts, 'deleteBook', ['effects.softDeleteBook'])
1448
+ && exportedFunctionCalls(facts, 'undoDeleteBook', ['effects.restoreBook']),
1449
+ 'create undo soft-deletes, delete soft-deletes, and delete undo restores through explicit scoped effects'),
1450
+ check('crm-library.crm-checkout', exportedFunctionCalls(facts, 'checkoutBook', ['effects.resolveCustomer', 'effects.claimCheckout'])
1451
+ && !hasExactString(facts, 'memberId'), 'checkout resolves an existing CRM customer and reaches the atomic claim seam without a duplicate local member'),
1452
+ 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'),
1453
+ check('crm-library.public-schema', !schemaFacts.objectProperties.has('tenantId') && !schemaFacts.objectProperties.has('organizationId')
1454
+ && schemaFacts.exportedVariables.has('createBookRequestSchema') && schemaFacts.exportedVariables.has('checkoutBookRequestSchema'),
1455
+ 'public create/checkout request schemas omit tenantId and organizationId because runtime scope is server-derived'),
1456
+ 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'),
1457
+ check('crm-library.semantic-tests', ['createBook', 'undoCreateBook', 'deleteBook', 'undoDeleteBook', 'returnLoan', 'renewLoan', 'markLoanLost'].every((name) => hasCall(testFacts, name))
1458
+ && (testFacts.calls.get('checkoutBook') ?? 0) >= 3 && hasCall(testFacts, 'Promise.all'),
1459
+ 'generated tests exercise lifecycle undo, every scoped loan action, concurrent checkout, and deterministic retry'),
1460
+ ]
1461
+ }
1462
+ if (definition.family === 'business-command') {
1463
+ const fact = facts.exportedFunctions.get(definition.seam)
1464
+ return [
1465
+ 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`),
1466
+ check('business.command-guard', (fact?.throws ?? 0) > 0, `exported ${definition.seam} rejects an invalid business invariant`),
1467
+ ]
1468
+ }
1469
+ if (definition.family === 'ui-business-surface') {
1470
+ const fact = facts.exportedFunctions.get(definition.seam)
1471
+ return [
1472
+ check('business.ui-seam', exportedFunctionCalls(facts, definition.seam, ['effects.execute', 'effects.restoreFocus', 'effects.announce']), `exported ${definition.seam} executes, restores focus, and announces the result`),
1473
+ check('business.ui-guard', (fact?.throws ?? 0) > 0, `exported ${definition.seam} rejects an invalid UI business action`),
1474
+ check('business.ui-finally', (fact?.finallyBlocks ?? 0) > 0, `exported ${definition.seam} restores focus after both success and failure`),
1475
+ ...renderedUiChecks(definition, facts),
1476
+ ]
1477
+ }
1478
+ if (definition.family === 'data-table-extension') {
1479
+ const fact = facts.exportedFunctions.get(definition.seam)
1480
+ const onExecute = facts.propertyIdentifiers.get('onExecute')
1481
+ return [
1482
+ 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'),
1483
+ 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'),
1484
+ 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'),
1485
+ 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}`),
1486
+ 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`),
1487
+ 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'),
1488
+ ]
1489
+ }
1490
+ if (definition.family === 'async-operation') {
1491
+ const fact = facts.exportedFunctions.get(definition.seam)
1492
+ return [
1493
+ 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`),
1494
+ check('business.async-loop', (fact?.loops ?? 0) > 0, `exported ${definition.seam} processes work through a bounded loop`),
1495
+ ]
1496
+ }
1497
+ if (definition.family === 'ai-safe-agent') {
1498
+ const requiredCalls = definition.mode === 'mutation'
1499
+ ? ['effects.authorize', 'effects.prepareMutation', 'effects.execute']
1500
+ : ['effects.authorize', 'effects.delegate']
1501
+ const checks = [
1502
+ check('business.ai-seam', exportedFunctionCalls(facts, definition.seam, requiredCalls), `exported ${definition.seam} uses the required authorization and ${definition.mode} seams`),
1503
+ ]
1504
+ if (definition.mode === 'delegate') {
1505
+ 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`))
1506
+ }
1507
+ return checks
1508
+ }
1509
+ if (definition.family === 'provider-adapter') {
1510
+ const fact = facts.exportedFunctions.get(definition.seam)
1511
+ const checks = [
1512
+ 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`),
1513
+ check('business.provider-retry', (fact?.loops ?? 0) > 0 && (fact?.throws ?? 0) > 0, `exported ${definition.seam} bounds retries and redacts terminal failure`),
1514
+ 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'),
1515
+ check('business.provider-secret', hasExactString(facts, 'secret'), 'integration credentials include a secret-bearing field type'),
1516
+ check('business.provider-health', hasExactString(facts, definition.healthService) && hasCall(facts, 'container.register'), `DI registers the exact ${definition.healthService} health service declared by integration.ts`),
1517
+ check('business.provider-activation', facts.moduleEntries.some((entry) => entry.id === definition.moduleId && entry.from === '@app'), `src/modules.ts activates ${definition.moduleId} from @app`),
1518
+ ]
1519
+ if (definition.providerKind === 'transactional-email') {
1520
+ checks.push(
1521
+ 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'),
1522
+ check('business.provider-not-mailbox', !hasExactString(facts, 'communication_channels') && !facts.importedBindings.has('ChannelAdapter'), 'transactional SMTP does not claim the mailbox ChannelAdapter contract'),
1523
+ )
1524
+ }
1525
+ if (definition.providerKind === 'payment') {
1526
+ checks.push(
1527
+ 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'),
1528
+ check(
1529
+ 'business.provider-payment-registration',
1530
+ hasCall(facts, 'registerGatewayAdapter')
1531
+ && hasCall(facts, 'registerPaymentGatewayDescriptor')
1532
+ && facts.importedBindings.get('registerWebhookHandler') === '@open-mercato/shared/modules/payment_gateways/types'
1533
+ && hasCall(facts, 'registerWebhookHandler')
1534
+ && facts.propertyAccesses.has('verifyWebhook'),
1535
+ 'DI registers the gateway adapter, its verifyWebhook handler, and the payment descriptor',
1536
+ ),
1537
+ )
1538
+ }
1539
+ if (definition.providerKind === 'shipping') {
1540
+ checks.push(
1541
+ 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'),
1542
+ check('business.provider-shipping-registration', hasCall(facts, 'registerShippingAdapter'), 'DI registers the shipping adapter'),
1543
+ )
1544
+ }
1545
+ if (definition.providerKind === 'payment' || definition.providerKind === 'shipping') {
1546
+ checks.push(
1547
+ 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'),
1548
+ check('business.provider-setup', facts.exportedVariables.has('setup') && hasObjectVariable(facts, 'setup', ['defaultRoleFeatures']), 'provider setup.ts grants its features through defaultRoleFeatures'),
1549
+ )
1550
+ }
1551
+ return checks
1552
+ }
1553
+ if (definition.family === 'data-flow') {
1554
+ const fact = facts.exportedFunctions.get(definition.seam)
1555
+ return [
1556
+ 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`),
1557
+ check('business.data-loop', (fact?.loops ?? 0) > 0, `exported ${definition.seam} isolates rows in a loop`),
1558
+ ]
1559
+ }
1560
+ if (definition.family === 'test-authoring-unit') {
1561
+ return [
1562
+ 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'),
1563
+ check('business.test-unit-enabled', hasOnlyEnabledTests(facts), 'generated Jest coverage contains no disabled, focused-only, todo, or expected-failure tests or suites'),
1564
+ 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'),
1565
+ check('business.test-unit-failure', hasCall(facts, 'toThrow', 'toReject', 'rejects.toThrow') && hasCall(facts, 'toHaveBeenCalledTimes', 'toEqual', 'toBe'), 'failure and rollback outcomes are asserted'),
1566
+ ]
1567
+ }
1568
+ if (definition.family === 'test-authoring-api') {
1569
+ return [
1570
+ 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'),
1571
+ check('business.test-api-enabled', hasOnlyEnabledTests(facts), 'generated API coverage contains no disabled, focused-only, todo, or expected-failure tests or suites'),
1572
+ 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'),
1573
+ 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'),
1574
+ 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'),
1575
+ ]
1576
+ }
1577
+ if (definition.family === 'test-authoring-browser') {
1578
+ return [
1579
+ 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'),
1580
+ check('business.test-browser-enabled', hasOnlyEnabledTests(facts), 'generated browser coverage contains no disabled, focused-only, todo, or expected-failure tests or suites'),
1581
+ 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'),
1582
+ 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'),
1583
+ 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'),
1584
+ ]
1585
+ }
1586
+ switch (caseId) {
1587
+ case 'OMH-009': {
1588
+ const entity = facts.classes.some((entry) => entry.decorators.has('Entity') && ['tenant_id', 'organization_id', 'updated_at'].every((name) => entry.members.has(name)))
1589
+ return [
1590
+ check('entity.declaration', entity, 'an @Entity class declaring tenant_id, organization_id, and updated_at'),
1591
+ check('entity.validator', hasCall(facts, 'z.object') || hasCall(facts, 'object'), 'a concrete validator object call'),
1592
+ check('entity.migration', facts.classes.some((entry) => entry.members.has('up')), 'a migration class with an up method'),
1593
+ ]
1594
+ }
1595
+ case 'OMH-011':
1596
+ return [
1597
+ check('crud.factory-import', facts.importedBindings.get('makeCrudRoute') === '@open-mercato/shared/lib/crud/factory', 'makeCrudRoute imported from @open-mercato/shared/lib/crud/factory'),
1598
+ check('crud.route', hasCallOptions(facts, 'makeCrudRoute', ['metadata', 'orm', 'list', 'actions', 'indexer']), 'makeCrudRoute called with metadata, orm, list, actions, and indexer options'),
1599
+ check('crud.openapi', facts.exportedVariables.has('openApi'), 'openApi exported separately from the CRUD factory options'),
1600
+ ]
1601
+ case 'OMH-012':
1602
+ return [
1603
+ check('command.guards', hasCall(facts, 'runMutationGuards'), 'a runMutationGuards call'),
1604
+ check('command.atomic', hasCall(facts, 'withAtomicFlush'), 'a withAtomicFlush call'),
1605
+ check('command.effects', hasCall(facts, 'emitCrudSideEffects'), 'an emitCrudSideEffects call'),
1606
+ ]
1607
+ case 'OMH-014':
1608
+ return [
1609
+ check('ui.table', facts.jsxTags.has('DataTable') && (facts.jsxAttributes.has('extensionTableId') || facts.objectProperties.has('extensionTableId')), 'a DataTable JSX use with extensionTableId'),
1610
+ check('ui.form', facts.jsxTags.has('CrudForm') && facts.jsxAttributes.has('initialValues'), 'a CrudForm JSX use with initialValues'),
1611
+ check('ui.conflict', hasCall(facts, 'surfaceRecordConflict'), 'a surfaceRecordConflict call'),
1612
+ ]
1613
+ case 'OMH-026':
1614
+ return [
1615
+ check('umes.form-spot', hasStringPrefix(facts, 'crud-form:'), 'a concrete crud-form:* spot ID literal'),
1616
+ check('umes.enricher', hasCall(facts, 'enrichMany'), 'an enrichMany call'),
1617
+ check('umes.interceptor', hasObjectVariable(facts, 'interceptors', []) || facts.declarations.has('interceptors'), 'an interceptors declaration'),
1618
+ ]
1619
+ case 'OMH-027':
1620
+ return [
1621
+ check('umes.table-spot', hasStringPrefix(facts, 'data-table:'), 'a concrete data-table:* spot ID literal'),
1622
+ check('umes.table-id', facts.objectProperties.has('extensionTableId') || facts.jsxAttributes.has('extensionTableId'), 'an extensionTableId option or JSX attribute'),
1623
+ ]
1624
+ case 'OMH-029':
1625
+ return [check('umes.page-override', facts.declarations.has('overrides') && facts.objectProperties.has('page'), 'an overrides declaration containing a page option')]
1626
+ case 'OMH-031':
1627
+ return [
1628
+ check('umes.interceptors', facts.declarations.has('interceptors'), 'an interceptors declaration'),
1629
+ 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'),
1630
+ check('umes.interceptor-hook', facts.objectProperties.has('before') || facts.objectProperties.has('after'), 'a before or after interceptor hook'),
1631
+ ]
1632
+ case 'OMH-042':
1633
+ return [
1634
+ 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'),
1635
+ check('provider.effects', hasCall(facts, 'fetchPage') && hasCall(facts, 'applyItem') && hasCall(facts, 'commitCursor'), 'fetchPage, applyItem, and commitCursor call sites'),
1636
+ ]
1637
+ case 'OMH-045':
1638
+ return [
1639
+ check('rest.url', facts.newCalls.has('URL'), 'a concrete new URL(...) call'),
1640
+ check('rest.retry', facts.loops > 0 || hasCall(facts, 'retry'), 'a retry loop or retry(...) call'),
1641
+ 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'),
1642
+ ]
1643
+ case 'OMH-049':
1644
+ return [check('ai.agent', hasCallOptions(facts, 'defineAiAgent', ['provider', 'model', 'allowedTools', 'requiredFeatures']), 'defineAiAgent called with provider, model, allowedTools, and requiredFeatures options')]
1645
+ case 'OMH-054':
1646
+ return [
1647
+ check('workflow.activity', facts.functions.has('callApiActivity') && hasExactString(facts, 'CALL_API'), 'a callApiActivity declaration and CALL_API literal'),
1648
+ check('workflow.transaction', hasCall(facts, 'transaction', 'transactional'), 'a transaction/transactional call'),
1649
+ 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'),
1650
+ ]
1651
+ case 'OMH-057':
1652
+ case 'OMH-171':
1653
+ return [
1654
+ check('regression.fail-closed', facts.functions.has('listRecords') && facts.throwStatements > 0, 'listRecords with a fail-closed throw'),
1655
+ check('regression.scope', ['tenantId', 'organizationId'].every((name) => facts.propertyAccesses.has(name) || facts.objectProperties.has(name)), 'tenantId and organizationId concrete scope access'),
1656
+ ]
1657
+ case 'OMH-060':
1658
+ return [
1659
+ check('regression.atomic', hasCall(facts, 'withAtomicFlush', 'transaction', 'transactional'), 'a withAtomicFlush/transaction/transactional call'),
1660
+ check('regression.phases', (facts.calls.get('persist') ?? 0) >= 2, 'two concrete persist call sites inside the atomic operation'),
1661
+ ]
1662
+ case 'OMH-061':
1663
+ return [
1664
+ check('regression.nullable', facts.nullNodes > 0, 'a concrete nullable type or null expression'),
1665
+ check('regression.form', facts.jsxTags.has('CrudForm') && facts.jsxAttributes.has('initialValues'), 'CrudForm JSX with initialValues'),
1666
+ ]
1667
+ case 'OMH-172': {
1668
+ const initialValues = facts.exportedFunctions.get('toInitialValues')
1669
+ const updatePayload = facts.exportedFunctions.get('toUpdatePayload')
1670
+ return [
1671
+ check('regression.null-load-seam', Boolean(initialValues) && !initialValues.binaryOperators.has(ts.SyntaxKind.QuestionQuestionToken), 'exported toInitialValues preserves explicit null instead of replacing it'),
1672
+ 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'),
1673
+ check('regression.form', facts.jsxTags.has('CrudForm') && facts.jsxAttributes.has('initialValues'), 'CrudForm JSX with initialValues'),
1674
+ ]
1675
+ }
1676
+ case 'OMH-070':
1677
+ return [
1678
+ check('regression.cursor-fetch', facts.functions.has('syncPage') && facts.awaitedCalls.has('fetchPage') && facts.assignments.has('cursor'), 'syncPage with awaited fetchPage and cursor assignment'),
1679
+ check('regression.cursor-retry', facts.loops > 0 || hasCall(facts, 'retry'), 'a retry loop or retry(...) call'),
1680
+ ]
1681
+ default:
1682
+ return [check('case.supported', false, 'a fixed writable oracle')]
1683
+ }
1684
+ }
1685
+
1686
+ function runTargetTypecheck(root) {
1687
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-harness-oracle-'))
1688
+ const home = path.join(tempRoot, 'home')
1689
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 })
1690
+ const env = {
1691
+ PATH: process.env.PATH ?? '', HOME: home, TMPDIR: tempRoot, TEMP: tempRoot, TMP: tempRoot,
1692
+ XDG_CONFIG_HOME: path.join(home, '.config'), XDG_CACHE_HOME: path.join(home, '.cache'),
1693
+ CI: '1', NO_COLOR: '1', NEXT_TELEMETRY_DISABLED: '1', YARN_ENABLE_TELEMETRY: '0', TZ: 'UTC',
1694
+ }
1695
+ const configuredCorepackHome = process.env.COREPACK_HOME || path.join(os.homedir(), '.cache', 'node', 'corepack')
1696
+ const corepackHome = fs.existsSync(configuredCorepackHome) && fs.statSync(configuredCorepackHome).isDirectory()
1697
+ ? fs.realpathSync(configuredCorepackHome)
1698
+ : undefined
1699
+ if (corepackHome) env.COREPACK_HOME = corepackHome
1700
+ let result
1701
+ try {
1702
+ const dependencyPath = path.join(root, 'node_modules')
1703
+ const invocation = sandboxedInvocation({
1704
+ command: process.platform === 'win32' ? 'yarn.cmd' : 'yarn',
1705
+ args: ['typecheck', '--tsBuildInfoFile', path.join(tempRoot, 'tsconfig.tsbuildinfo')],
1706
+ cwd: root,
1707
+ writableRoots: [root, tempRoot],
1708
+ readOnlyRoots: [...(fs.existsSync(dependencyPath) ? [fs.realpathSync(dependencyPath)] : []), ...(corepackHome ? [corepackHome] : [])],
1709
+ networkAllowed: false,
1710
+ env,
1711
+ })
1712
+ result = spawnSync(invocation.command, invocation.args, {
1713
+ cwd: invocation.cwd,
1714
+ env: invocation.env,
1715
+ encoding: 'utf8',
1716
+ shell: false,
1717
+ timeout: TYPECHECK_TIMEOUT_MS,
1718
+ windowsHide: true,
1719
+ })
1720
+ } catch (error) {
1721
+ result = { status: null, signal: null, stdout: '', stderr: '', error }
1722
+ } finally {
1723
+ fs.rmSync(tempRoot, { recursive: true, force: true })
1724
+ }
1725
+ if (result.error?.code === 'ETIMEDOUT' || result.signal) {
1726
+ return check('target.typecheck', false, `yarn typecheck must complete within ${TYPECHECK_TIMEOUT_MS}ms`)
1727
+ }
1728
+ if (result.error) return check('target.typecheck', false, `yarn typecheck could not start: ${result.error.message}`)
1729
+ if (result.status !== 0) {
1730
+ const summary = `${result.stderr ?? ''}\n${result.stdout ?? ''}`.trim().replaceAll(root, '<target>').slice(0, 1000)
1731
+ return check('target.typecheck', false, `yarn typecheck failed${summary ? `: ${summary}` : ''}`)
1732
+ }
1733
+ return check('target.typecheck', true, 'yarn typecheck succeeds')
1734
+ }
1735
+
1736
+ export function evaluateWritableAstOracle({ root: requestedRoot, caseId, phase }) {
1737
+ if (!path.isAbsolute(requestedRoot)) throw new Error('root must be absolute')
1738
+ if (!WRITABLE_CASES[caseId]) throw new Error(`unsupported writable case: ${caseId}`)
1739
+ if (!['before', 'after'].includes(phase)) throw new Error('phase must be before or after')
1740
+ const root = fs.realpathSync(requestedRoot)
1741
+ const ts = loadTargetTypeScript(root)
1742
+ const definition = WRITABLE_CASES[caseId]
1743
+ const checks = definition.artifacts.map((artifact) => check(`artifact:${artifact}`, artifactExists(root, artifact), `artifact ${artifact} exists`))
1744
+ const sourceFiles = collectSourceFiles(root, definition.sources)
1745
+ checks.push(check('source.present', sourceFiles.length > 0, 'at least one case-owned TypeScript source file'))
1746
+ if (sourceFiles.length) checks.push(...caseChecks(ts, caseId, collectFacts(ts, sourceFiles), root))
1747
+ if (phase === 'after') checks.push(runTargetTypecheck(root))
1748
+ const failures = checks.filter((entry) => !entry.passed).map((entry) => `${entry.id}: ${entry.requirement}`)
1749
+ return { passed: failures.length === 0, failures, checks }
1750
+ }
1751
+
1752
+ function main() {
1753
+ let options
1754
+ try {
1755
+ options = parseArgs(process.argv.slice(2))
1756
+ const result = evaluateWritableAstOracle({ root: options.root, caseId: options.caseId, phase: options.phase })
1757
+ process.stdout.write(`${JSON.stringify(result)}\n`)
1758
+ return result.passed ? 0 : 1
1759
+ } catch (error) {
1760
+ const message = error instanceof Error ? error.message : String(error)
1761
+ process.stdout.write(`${JSON.stringify({ passed: false, failures: [message], checks: [] })}\n`)
1762
+ return 2
1763
+ }
1764
+ }
1765
+
1766
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
1767
+ process.exitCode = main()
1768
+ }