@open-mercato/cli 0.6.7 → 0.6.8-canary.319.1.5b4be4bc52

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 (287) hide show
  1. package/.turbo/turbo-build.log +77 -4
  2. package/build.mjs +68 -11
  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 +84 -0
  12. package/dist/agentic/guides/contracts.md +92 -0
  13. package/dist/agentic/guides/extensions.md +121 -0
  14. package/dist/agentic/guides/framework-extension-points.md +36 -0
  15. package/dist/agentic/guides/integrations.md +81 -0
  16. package/dist/agentic/guides/module-facts.json +116757 -3951
  17. package/dist/agentic/guides/module-system.md +12 -127
  18. package/dist/agentic/guides/modules/ai_assistant.md +227 -28
  19. package/dist/agentic/guides/modules/api_docs.md +58 -2
  20. package/dist/agentic/guides/modules/api_keys.md +92 -7
  21. package/dist/agentic/guides/modules/attachments.md +119 -14
  22. package/dist/agentic/guides/modules/audit_logs.md +131 -10
  23. package/dist/agentic/guides/modules/auth.md +276 -33
  24. package/dist/agentic/guides/modules/business_rules.md +149 -14
  25. package/dist/agentic/guides/modules/catalog.md +482 -43
  26. package/dist/agentic/guides/modules/channel_gmail.md +82 -3
  27. package/dist/agentic/guides/modules/channel_imap.md +82 -3
  28. package/dist/agentic/guides/modules/checkout.md +305 -27
  29. package/dist/agentic/guides/modules/communication_channels.md +315 -33
  30. package/dist/agentic/guides/modules/configs.md +104 -8
  31. package/dist/agentic/guides/modules/content.md +57 -2
  32. package/dist/agentic/guides/modules/currencies.md +152 -17
  33. package/dist/agentic/guides/modules/customer_accounts.md +387 -45
  34. package/dist/agentic/guides/modules/customers.md +746 -85
  35. package/dist/agentic/guides/modules/dashboards.md +155 -10
  36. package/dist/agentic/guides/modules/data_sync.md +149 -16
  37. package/dist/agentic/guides/modules/design_system.md +102 -0
  38. package/dist/agentic/guides/modules/dictionaries.md +107 -12
  39. package/dist/agentic/guides/modules/directory.md +148 -15
  40. package/dist/agentic/guides/modules/entities.md +145 -13
  41. package/dist/agentic/guides/modules/events.md +59 -2
  42. package/dist/agentic/guides/modules/feature_toggles.md +119 -9
  43. package/dist/agentic/guides/modules/gateway_stripe.md +92 -4
  44. package/dist/agentic/guides/modules/generators.md +45 -2
  45. package/dist/agentic/guides/modules/inbox_ops.md +207 -27
  46. package/dist/agentic/guides/modules/integrations.md +149 -16
  47. package/dist/agentic/guides/modules/messages.md +223 -24
  48. package/dist/agentic/guides/modules/notifications.md +96 -8
  49. package/dist/agentic/guides/modules/onboarding.md +77 -6
  50. package/dist/agentic/guides/modules/payment_gateways.md +168 -21
  51. package/dist/agentic/guides/modules/perspectives.md +77 -7
  52. package/dist/agentic/guides/modules/planner.md +136 -18
  53. package/dist/agentic/guides/modules/portal.md +93 -2
  54. package/dist/agentic/guides/modules/progress.md +104 -14
  55. package/dist/agentic/guides/modules/query_index.md +154 -15
  56. package/dist/agentic/guides/modules/record_locks.md +206 -21
  57. package/dist/agentic/guides/modules/resources.md +197 -30
  58. package/dist/agentic/guides/modules/sales.md +687 -84
  59. package/dist/agentic/guides/modules/scheduler.md +133 -14
  60. package/dist/agentic/guides/modules/search.md +170 -6
  61. package/dist/agentic/guides/modules/security.md +294 -35
  62. package/dist/agentic/guides/modules/shipping_carriers.md +134 -18
  63. package/dist/agentic/guides/modules/sso.md +151 -26
  64. package/dist/agentic/guides/modules/staff.md +372 -57
  65. package/dist/agentic/guides/modules/storage_s3.md +95 -5
  66. package/dist/agentic/guides/modules/sync_akeneo.md +85 -3
  67. package/dist/agentic/guides/modules/sync_excel.md +75 -6
  68. package/dist/agentic/guides/modules/system_status_overlays.md +60 -2
  69. package/dist/agentic/guides/modules/translations.md +104 -10
  70. package/dist/agentic/guides/modules/webhooks.md +203 -24
  71. package/dist/agentic/guides/modules/wms.md +469 -47
  72. package/dist/agentic/guides/modules/workflows.md +302 -43
  73. package/dist/agentic/guides/reference-module-facts.json +6370 -0
  74. package/dist/agentic/guides/reference-modules/example.md +451 -0
  75. package/dist/agentic/guides/spec-delivery.md +42 -0
  76. package/dist/agentic/guides/testing-debugging.md +80 -0
  77. package/dist/agentic/guides/upstream/AGENTS.md +299 -0
  78. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +316 -0
  79. package/dist/agentic/guides/upstream/manifest.json +8 -0
  80. package/dist/agentic/shared/AGENTS.md.template +120 -311
  81. package/dist/agentic/shared/ai/agentic.config.json +2 -0
  82. package/dist/agentic/shared/ai/harness/README.md +50 -0
  83. package/dist/agentic/shared/ai/harness/RELEASE.md +57 -0
  84. package/dist/agentic/shared/ai/harness/cases.json +15050 -0
  85. package/dist/agentic/shared/ai/harness/cases.schema.json +188 -0
  86. package/dist/agentic/shared/ai/harness/fixtures/index.json +51 -0
  87. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +166 -0
  88. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +21 -0
  89. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +96 -0
  90. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +172 -0
  91. package/dist/agentic/shared/ai/harness/knowledge-change.schema.json +228 -0
  92. package/dist/agentic/shared/ai/harness/release-matrix.json +163 -0
  93. package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
  94. package/dist/agentic/shared/ai/harness/result.schema.json +109 -0
  95. package/dist/agentic/shared/ai/harness/routing-response.schema.json +22 -0
  96. package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
  97. package/dist/agentic/shared/ai/harness/validators.json +82 -0
  98. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +2121 -0
  99. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
  100. package/dist/agentic/shared/ai/harness/writable-spec-oracles.mjs +412 -0
  101. package/dist/agentic/shared/ai/lessons/_template.md +16 -0
  102. package/dist/agentic/shared/ai/lessons.md +26 -3
  103. package/dist/agentic/shared/ai/review-checklist.md +36 -0
  104. package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +21 -0
  105. package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
  106. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +98 -0
  107. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +21 -241
  108. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +31 -0
  109. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
  110. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +15 -0
  111. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
  112. package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
  113. package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
  114. package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
  115. package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
  116. package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
  117. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
  118. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
  119. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
  120. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
  121. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +17 -601
  122. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +21 -0
  123. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +15 -0
  124. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +16 -0
  125. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
  126. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +15 -310
  127. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +19 -0
  128. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +31 -0
  129. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +100 -0
  130. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
  131. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/knowledge-change.md +115 -0
  132. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
  133. package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
  134. package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
  135. package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
  136. package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
  137. package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +18 -0
  138. package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
  139. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +33 -152
  140. package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +33 -0
  141. package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +40 -0
  142. package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +27 -0
  143. package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +24 -0
  144. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +25 -708
  145. package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
  146. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +25 -0
  147. package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
  148. package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
  149. package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +34 -0
  150. package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +4 -0
  151. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +11 -0
  152. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +31 -0
  153. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +39 -0
  154. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +43 -0
  155. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +11 -0
  156. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
  157. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +32 -0
  158. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +172 -0
  159. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +14 -0
  160. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
  161. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +40 -0
  162. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
  163. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
  164. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
  165. package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +41 -0
  166. package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +19 -0
  167. package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +44 -0
  168. package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +39 -0
  169. package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +20 -0
  170. package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +57 -0
  171. package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +11 -0
  172. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +486 -0
  173. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +20 -843
  174. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +42 -0
  175. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +27 -0
  176. package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
  177. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +43 -0
  178. package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
  179. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
  180. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
  181. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +17 -467
  182. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +29 -0
  183. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
  184. package/dist/agentic/shared/ai/skills/tiers.json +143 -21
  185. package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
  186. package/dist/agentic/shared/ai/specs/README.md +4 -3
  187. package/dist/agentic/shared/ai/specs/SPEC-000-template.md +226 -10
  188. package/dist/agentic/shared/ai/trackers/github.md +108 -4
  189. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +224 -0
  190. package/dist/agentic/shared/scripts/check-lessons.mjs +178 -0
  191. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +3585 -0
  192. package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
  193. package/dist/agentic/shared/scripts/framework-context.mjs +888 -0
  194. package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
  195. package/dist/agentic/shared/scripts/install-skills.sh +4 -694
  196. package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
  197. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1774 -0
  198. package/dist/agentic/shared/scripts/validate-knowledge-change.mjs +911 -0
  199. package/dist/bin.js +13 -1
  200. package/dist/bin.js.map +2 -2
  201. package/dist/lib/__integration__/TC-INT-008.spec.js +88 -8
  202. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  203. package/dist/lib/agentic-init.js +13 -3
  204. package/dist/lib/agentic-init.js.map +2 -2
  205. package/dist/lib/agentic-setup.js +434 -186
  206. package/dist/lib/agentic-setup.js.map +2 -2
  207. package/dist/lib/generators/extensions/injection-widgets.js +20 -0
  208. package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
  209. package/dist/lib/generators/module-extension-facts.js +1872 -0
  210. package/dist/lib/generators/module-extension-facts.js.map +7 -0
  211. package/dist/lib/generators/module-fact-sources.js +70 -0
  212. package/dist/lib/generators/module-fact-sources.js.map +7 -0
  213. package/dist/lib/generators/module-facts-discovery.js +64 -7
  214. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  215. package/dist/lib/generators/module-facts.js +1629 -52
  216. package/dist/lib/generators/module-facts.js.map +3 -3
  217. package/dist/lib/generators/module-override-targets.js +688 -0
  218. package/dist/lib/generators/module-override-targets.js.map +7 -0
  219. package/dist/lib/generators/module-registry.js +11 -5
  220. package/dist/lib/generators/module-registry.js.map +2 -2
  221. package/dist/lib/load-env.js +24 -0
  222. package/dist/lib/load-env.js.map +7 -0
  223. package/dist/lib/resolver.js +2 -0
  224. package/dist/lib/resolver.js.map +2 -2
  225. package/dist/lib/telemetry-init.js +358 -0
  226. package/dist/lib/telemetry-init.js.map +7 -0
  227. package/dist/lib/testing/integration.js +5 -2
  228. package/dist/lib/testing/integration.js.map +2 -2
  229. package/dist/mercato.js +23 -1
  230. package/dist/mercato.js.map +2 -2
  231. package/package.json +13 -11
  232. package/src/bin.ts +33 -1
  233. package/src/lib/__integration__/TC-INT-008.spec.ts +107 -9
  234. package/src/lib/__tests__/agentic-init.test.ts +41 -5
  235. package/src/lib/__tests__/agentic-setup.ownership.test.ts +386 -0
  236. package/src/lib/__tests__/injection-widget-id-validation.test.ts +184 -0
  237. package/src/lib/__tests__/load-env.test.ts +65 -0
  238. package/src/lib/__tests__/telemetry-init.test.ts +298 -0
  239. package/src/lib/agentic-init.ts +15 -3
  240. package/src/lib/agentic-setup.ts +573 -222
  241. package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +92 -0
  242. package/src/lib/generators/__tests__/module-extension-activations.test.ts +762 -0
  243. package/src/lib/generators/__tests__/module-extension-facts.test.ts +603 -0
  244. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +139 -2
  245. package/src/lib/generators/__tests__/module-facts.computed-di-token.test.ts +88 -0
  246. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
  247. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
  248. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
  249. package/src/lib/generators/__tests__/module-facts.local-reference.test.ts +391 -0
  250. package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +561 -0
  251. package/src/lib/generators/__tests__/module-facts.search-entity-id.test.ts +151 -0
  252. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +160 -0
  253. package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +149 -0
  254. package/src/lib/generators/__tests__/module-override-targets.test.ts +328 -0
  255. package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
  256. package/src/lib/generators/extensions/injection-widgets.ts +51 -0
  257. package/src/lib/generators/module-extension-facts.ts +2433 -0
  258. package/src/lib/generators/module-fact-sources.ts +108 -0
  259. package/src/lib/generators/module-facts-discovery.ts +104 -7
  260. package/src/lib/generators/module-facts.ts +2387 -138
  261. package/src/lib/generators/module-override-targets.ts +863 -0
  262. package/src/lib/generators/module-registry.ts +16 -6
  263. package/src/lib/load-env.ts +30 -0
  264. package/src/lib/resolver.ts +3 -0
  265. package/src/lib/telemetry-init.ts +461 -0
  266. package/src/lib/testing/__tests__/integration.test.ts +4 -0
  267. package/src/lib/testing/integration.ts +5 -2
  268. package/src/mercato.ts +26 -1
  269. package/dist/agentic/guides/cache.md +0 -45
  270. package/dist/agentic/guides/core.md +0 -453
  271. package/dist/agentic/guides/events.md +0 -117
  272. package/dist/agentic/guides/queue.md +0 -53
  273. package/dist/agentic/guides/search.md +0 -115
  274. package/dist/agentic/guides/shared.md +0 -108
  275. package/dist/agentic/guides/ui.md +0 -347
  276. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
  277. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
  278. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
  279. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
  280. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
  281. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
  282. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
  283. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
  284. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
  285. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
  286. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
  287. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
@@ -0,0 +1,911 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process'
4
+ import { createHash } from 'node:crypto'
5
+ import fs from 'node:fs'
6
+ import os from 'node:os'
7
+ import path from 'node:path'
8
+ import process from 'node:process'
9
+ import { fileURLToPath, pathToFileURL } from 'node:url'
10
+
11
+ const EXIT_PASS = 0
12
+ const EXIT_FAILURE = 1
13
+ const EXIT_INVALID = 2
14
+
15
+ export const CHANGE_CLASSES = Object.freeze(['knowledge-contract', 'asset-sync'])
16
+ export const CHANGED_CONTRACTS = Object.freeze([
17
+ 'routing', 'skill-link', 'source-link', 'example-source', 'installed-source',
18
+ 'discovery', 'context-read', 'evaluator', 'oracle',
19
+ ])
20
+ export const RELEASE_LANE_IDS = Object.freeze([
21
+ 'deterministic', 'routing', 'writable', 'generatedCodeReview', 'generativeJudge', 'generatedTests',
22
+ ])
23
+ export const CONTROLLER_OWNED_FIELDS = Object.freeze(['resolvedBaseSha', 'headSha', 'focusedExecutions'])
24
+ export const SCHEMA_BASENAME = 'knowledge-change.schema.json'
25
+
26
+ // Sibling of the baseline and topics registry, which live OUTSIDE the shipped
27
+ // `agentic/shared/**` tree on purpose: that tree is copied wholesale into every generated
28
+ // app, and these are monorepo-only assets (the baseline pins monorepo SHAs and validates
29
+ // monorepo files). A forward reference to the old location would have sent the inventory
30
+ // back into every scaffold when it lands.
31
+ export const SOURCE_LINK_INVENTORY_PATH =
32
+ 'packages/create-app/scripts/source-links/source-link-inventory.json'
33
+ export const CANON_C_REASON =
34
+ `${SOURCE_LINK_INVENTORY_PATH} not present — CANON-C (canonical-example source-link inventory) is a ` +
35
+ 'monorepo-only asset and is absent from this tree, so source-link, example-source, and ' +
36
+ 'installed-source contracts cannot be resolved and fail closed'
37
+
38
+ const EXAMPLE_AUTHORING_ROOT = 'apps/mercato/src/modules/example'
39
+ const EXAMPLE_TEMPLATE_ROOT = 'packages/create-app/template/src/modules/example'
40
+ const EXAMPLE_QA_ONLY_SEGMENTS = Object.freeze(['__tests__', '__integration__'])
41
+
42
+ const FOCUSED_TEST_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/
43
+
44
+ // One argv token may not carry a shell metacharacter. The controller never interpolates a shell,
45
+ // and the recorded evidence must stay reproducible as an exact argv vector.
46
+ const SHELL_METACHARACTER_PATTERN = /[`$;&|<>\n]/
47
+
48
+ export const DEFAULT_EXECUTION_TIMEOUT_MS = 600_000
49
+
50
+ // Ordered; the first matching runner owns the focused test. Runner argv is controller-derived so an
51
+ // author can never substitute a weaker command for the test they declared.
52
+ const FOCUSED_RUNNERS = Object.freeze([
53
+ { runner: 'node-test', pattern: /\.(?:test|spec)\.[cm]?jsx?$/, args: (relativePath) => ['--test', relativePath] },
54
+ { runner: 'node-test-tsx', pattern: /\.(?:test|spec)\.[cm]?tsx?$/, args: (relativePath) => ['--import', 'tsx', '--test', relativePath] },
55
+ ])
56
+
57
+ // Ordered; the first matching rule owns the path. `contract: null` marks a path that
58
+ // carries no knowledge contract of its own — a generated/materialized copy or a docs
59
+ // snapshot — which is the only shape an `asset-sync` run may contain.
60
+ const CLASSIFICATION_RULES = Object.freeze([
61
+ {
62
+ rule: 'canonical-example-tree',
63
+ contract: 'example-source',
64
+ changeClass: 'knowledge-contract',
65
+ pattern: /^(?:apps\/mercato\/src|packages\/create-app\/template\/src|src)\/modules\/example\/.+$/,
66
+ },
67
+ {
68
+ rule: 'source-link-inventory',
69
+ contract: 'source-link',
70
+ changeClass: 'knowledge-contract',
71
+ pattern: /(?:^|\/)(?:source-link-inventory\.json|source-link-baseline\.json|source-link-baseline\.schema\.json|source-link-parity-ledger\.json)$/,
72
+ },
73
+ {
74
+ rule: 'installed-package-manifest',
75
+ contract: 'installed-source',
76
+ changeClass: 'knowledge-contract',
77
+ pattern: /(?:^|\/)(?:package\.json|package\.json\.template|yarn\.lock|package-lock\.json|pnpm-lock\.yaml)$/,
78
+ },
79
+ {
80
+ rule: 'installed-skill-closure',
81
+ contract: 'installed-source',
82
+ changeClass: 'knowledge-contract',
83
+ pattern: /(?:^|\/)skills\/tiers\.json$/,
84
+ },
85
+ {
86
+ rule: 'evaluator-implementation',
87
+ contract: 'evaluator',
88
+ changeClass: 'knowledge-contract',
89
+ pattern: /(?:^|\/)scripts\/(?:evaluate-agent-harness|agent-harness-tool-server|run-agent-harness-release|prepare-agent-harness-fixture|execution-sandbox|framework-context|validate-knowledge-change)\.mjs$/,
90
+ },
91
+ {
92
+ rule: 'writable-oracle',
93
+ contract: 'oracle',
94
+ changeClass: 'knowledge-contract',
95
+ pattern: /(?:^|\/)harness\/(?:writable-ast-oracles\.mjs|writable-behavior-oracles\.mjs|validators\.json)$/,
96
+ },
97
+ {
98
+ rule: 'case-context-policy',
99
+ contract: 'context-read',
100
+ changeClass: 'knowledge-contract',
101
+ pattern: /(?:^|\/)harness\/(?:cases\.json|release-matrix\.json|fixtures\/.+|[^/]+\.schema\.json)$/,
102
+ },
103
+ {
104
+ rule: 'agent-instruction-owner',
105
+ contract: 'routing',
106
+ changeClass: 'knowledge-contract',
107
+ pattern: /(?:^|\/)(?:AGENTS\.md|AGENTS\.md\.template|CLAUDE\.md|CLAUDE\.md\.template)$/,
108
+ },
109
+ {
110
+ rule: 'generated-fact-copy',
111
+ contract: null,
112
+ changeClass: 'asset-sync',
113
+ pattern: /(?:^|\/)(?:\.ai|ai)\/guides\/(?:modules|upstream)\/.+$/,
114
+ },
115
+ {
116
+ rule: 'routing-guide-owner',
117
+ contract: 'routing',
118
+ changeClass: 'knowledge-contract',
119
+ pattern: /(?:^|\/)(?:\.ai|ai)\/guides\/[^/]+\.md$/,
120
+ },
121
+ {
122
+ rule: 'skill-authority',
123
+ contract: 'skill-link',
124
+ changeClass: 'knowledge-contract',
125
+ pattern: /(?:^|\/)skills\/(?:[^/]+\/(?:SKILL\.md|references\/.+|scripts\/.+)|tiers\.schema\.json)$/,
126
+ },
127
+ {
128
+ rule: 'discovery-generator-contract',
129
+ contract: 'discovery',
130
+ changeClass: 'knowledge-contract',
131
+ pattern: /^(?:packages\/cli\/src\/lib\/generators\/.+|packages\/create-app\/src\/setup\/.+|packages\/create-app\/build\.mjs)$/,
132
+ },
133
+ {
134
+ rule: 'materialized-copy',
135
+ contract: null,
136
+ changeClass: 'asset-sync',
137
+ pattern: /^(?:packages\/create-app\/dist\/.+|apps\/docs\/.+|\.ai\/specs\/.+\.md)$/,
138
+ },
139
+ {
140
+ rule: 'docs-count-snapshot',
141
+ contract: null,
142
+ changeClass: 'asset-sync',
143
+ pattern: /(?:^|\/)(?:README\.md|RELEASE\.md|CHANGELOG\.md)$/,
144
+ },
145
+ {
146
+ rule: 'focused-test',
147
+ contract: 'evaluator',
148
+ changeClass: 'knowledge-contract',
149
+ pattern: FOCUSED_TEST_PATTERN,
150
+ },
151
+ ])
152
+
153
+ export function normalizeRelativePath(value) {
154
+ return String(value ?? '').split('\\').join('/').replace(/^\.\//, '')
155
+ }
156
+
157
+ export function isFocusedTestPath(value) {
158
+ return FOCUSED_TEST_PATTERN.test(normalizeRelativePath(value))
159
+ }
160
+
161
+ export function classifyChangedPath(value) {
162
+ const relativePath = normalizeRelativePath(value)
163
+ for (const rule of CLASSIFICATION_RULES) {
164
+ if (!rule.pattern.test(relativePath)) continue
165
+ return {
166
+ path: relativePath,
167
+ rule: rule.rule,
168
+ contract: rule.contract,
169
+ changeClass: rule.changeClass,
170
+ focusedTest: isFocusedTestPath(relativePath),
171
+ }
172
+ }
173
+ return {
174
+ path: relativePath,
175
+ rule: 'unclassified-fails-closed',
176
+ contract: null,
177
+ changeClass: 'knowledge-contract',
178
+ focusedTest: false,
179
+ }
180
+ }
181
+
182
+ export function deriveChangeClassification(paths) {
183
+ const records = [...new Set((paths ?? []).map(normalizeRelativePath))].sort().map(classifyChangedPath)
184
+ const contracts = new Set()
185
+ let changeClass = 'asset-sync'
186
+ for (const record of records) {
187
+ if (record.contract) contracts.add(record.contract)
188
+ if (record.changeClass === 'knowledge-contract') changeClass = 'knowledge-contract'
189
+ }
190
+ return {
191
+ changeClass,
192
+ changedContracts: CHANGED_CONTRACTS.filter((contract) => contracts.has(contract)),
193
+ records,
194
+ unclassifiedPaths: records.filter((record) => record.rule === 'unclassified-fails-closed').map((record) => record.path),
195
+ focusedTestPaths: records.filter((record) => record.focusedTest).map((record) => record.path),
196
+ }
197
+ }
198
+
199
+ function resolveSchemaReference(rootSchema, reference) {
200
+ if (typeof reference !== 'string' || !reference.startsWith('#/')) return null
201
+ let node = rootSchema
202
+ for (const rawSegment of reference.slice(2).split('/')) {
203
+ const segment = rawSegment.replaceAll('~1', '/').replaceAll('~0', '~')
204
+ if (node === null || typeof node !== 'object' || !Object.hasOwn(node, segment)) return null
205
+ node = node[segment]
206
+ }
207
+ return node
208
+ }
209
+
210
+ function isPlainObject(value) {
211
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
212
+ }
213
+
214
+ function matchesSchemaType(value, type) {
215
+ if (type === 'object') return isPlainObject(value)
216
+ if (type === 'array') return Array.isArray(value)
217
+ if (type === 'string') return typeof value === 'string'
218
+ if (type === 'integer') return Number.isInteger(value)
219
+ if (type === 'number') return typeof value === 'number' && Number.isFinite(value)
220
+ if (type === 'boolean') return typeof value === 'boolean'
221
+ if (type === 'null') return value === null
222
+ return false
223
+ }
224
+
225
+ export function validateJsonSchema(value, schema, location = '$', rootSchema = schema) {
226
+ if (schema === true) return []
227
+ if (schema === false) return [`${location} is not allowed by its schema`]
228
+ const errors = []
229
+ if (schema.$ref) {
230
+ const resolved = resolveSchemaReference(rootSchema, schema.$ref)
231
+ if (!resolved) return [`${location} has an unresolved schema reference ${schema.$ref}`]
232
+ errors.push(...validateJsonSchema(value, resolved, location, rootSchema))
233
+ }
234
+ for (const branch of schema.allOf ?? []) errors.push(...validateJsonSchema(value, branch, location, rootSchema))
235
+ if (Array.isArray(schema.anyOf) && !schema.anyOf.some((branch) => validateJsonSchema(value, branch, location, rootSchema).length === 0)) {
236
+ errors.push(`${location} does not match any allowed schema branch`)
237
+ }
238
+ if (Array.isArray(schema.oneOf)) {
239
+ const matched = schema.oneOf.filter((branch) => validateJsonSchema(value, branch, location, rootSchema).length === 0)
240
+ if (matched.length !== 1) errors.push(`${location} must match exactly one schema branch`)
241
+ }
242
+ if (Object.hasOwn(schema, 'not') && validateJsonSchema(value, schema.not, location, rootSchema).length === 0) {
243
+ errors.push(`${location} matches a forbidden schema branch${schema.description ? `: ${schema.description}` : ''}`)
244
+ }
245
+ const expectedTypes = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : []
246
+ if (expectedTypes.length && !expectedTypes.some((type) => matchesSchemaType(value, type))) {
247
+ errors.push(`${location} must be ${expectedTypes.join(' or ')}`)
248
+ return errors
249
+ }
250
+ if (Object.hasOwn(schema, 'const') && value !== schema.const) errors.push(`${location} must equal its schema constant`)
251
+ if (schema.enum && !schema.enum.some((item) => JSON.stringify(item) === JSON.stringify(value))) {
252
+ errors.push(`${location} is not in its schema enum`)
253
+ }
254
+ if (typeof value === 'string') {
255
+ if (Number.isInteger(schema.minLength) && value.length < schema.minLength) errors.push(`${location} is below minLength ${schema.minLength}`)
256
+ if (Number.isInteger(schema.maxLength) && value.length > schema.maxLength) errors.push(`${location} exceeds maxLength ${schema.maxLength}`)
257
+ if (schema.pattern && !new RegExp(schema.pattern).test(value)) errors.push(`${location} does not match its schema pattern`)
258
+ }
259
+ if (typeof value === 'number') {
260
+ if (Number.isFinite(schema.minimum) && value < schema.minimum) errors.push(`${location} is below minimum ${schema.minimum}`)
261
+ if (Number.isFinite(schema.maximum) && value > schema.maximum) errors.push(`${location} exceeds maximum ${schema.maximum}`)
262
+ }
263
+ if (Array.isArray(value)) {
264
+ if (Number.isInteger(schema.minItems) && value.length < schema.minItems) errors.push(`${location} is below minItems ${schema.minItems}`)
265
+ if (Number.isInteger(schema.maxItems) && value.length > schema.maxItems) errors.push(`${location} exceeds maxItems ${schema.maxItems}`)
266
+ if (schema.uniqueItems && new Set(value.map((item) => JSON.stringify(item))).size !== value.length) errors.push(`${location} must contain unique items`)
267
+ if (schema.items) value.forEach((item, index) => errors.push(...validateJsonSchema(item, schema.items, `${location}[${index}]`, rootSchema)))
268
+ }
269
+ if (isPlainObject(value)) {
270
+ for (const required of schema.required ?? []) if (!Object.hasOwn(value, required)) errors.push(`${location}.${required} is required`)
271
+ if (schema.additionalProperties === false) {
272
+ for (const key of Object.keys(value)) if (!Object.hasOwn(schema.properties ?? {}, key)) errors.push(`${location}.${key} is not allowed`)
273
+ }
274
+ for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
275
+ if (Object.hasOwn(value, key)) errors.push(...validateJsonSchema(value[key], childSchema, `${location}.${key}`, rootSchema))
276
+ }
277
+ }
278
+ return errors
279
+ }
280
+
281
+ function hashFile(absolutePath) {
282
+ return createHash('sha256').update(fs.readFileSync(absolutePath)).digest('hex')
283
+ }
284
+
285
+ function isRegularFile(absolutePath) {
286
+ try { return fs.statSync(absolutePath).isFile() } catch { return false }
287
+ }
288
+
289
+ function readJsonIfPresent(absolutePath) {
290
+ if (!isRegularFile(absolutePath)) return null
291
+ try { return JSON.parse(fs.readFileSync(absolutePath, 'utf8')) } catch { return null }
292
+ }
293
+
294
+ export function expandCaseRange(rangeId) {
295
+ const match = /^OMH-([0-9]{3})\.\.OMH-([0-9]{3})$/.exec(String(rangeId ?? ''))
296
+ if (!match) return null
297
+ const low = Number(match[1])
298
+ const high = Number(match[2])
299
+ if (low > high) return null
300
+ const ids = []
301
+ for (let index = low; index <= high; index += 1) ids.push(`OMH-${String(index).padStart(3, '0')}`)
302
+ return ids
303
+ }
304
+
305
+ function git(root, args) {
306
+ const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' })
307
+ if (result.status !== 0) {
308
+ throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`)
309
+ }
310
+ return result.stdout
311
+ }
312
+
313
+ export function resolveGitSha(root, ref) {
314
+ return git(root, ['rev-parse', '--verify', `${ref}^{commit}`]).trim()
315
+ }
316
+
317
+ export function deriveChangedPaths(root, baseSha) {
318
+ const tracked = git(root, ['diff', '--name-only', '--no-renames', baseSha]).split('\n')
319
+ const untracked = git(root, ['ls-files', '--others', '--exclude-standard']).split('\n')
320
+ return [...new Set([...tracked, ...untracked].map((line) => line.trim()).filter(Boolean).map(normalizeRelativePath))].sort()
321
+ }
322
+
323
+ function exampleMirrorPath(relativePath) {
324
+ if (relativePath.startsWith(`${EXAMPLE_AUTHORING_ROOT}/`)) {
325
+ return `${EXAMPLE_TEMPLATE_ROOT}/${relativePath.slice(EXAMPLE_AUTHORING_ROOT.length + 1)}`
326
+ }
327
+ if (relativePath.startsWith(`${EXAMPLE_TEMPLATE_ROOT}/`)) {
328
+ return `${EXAMPLE_AUTHORING_ROOT}/${relativePath.slice(EXAMPLE_TEMPLATE_ROOT.length + 1)}`
329
+ }
330
+ return null
331
+ }
332
+
333
+ export function exampleReadStatus(relativePath) {
334
+ const segments = normalizeRelativePath(relativePath).split('/')
335
+ return segments.some((segment) => EXAMPLE_QA_ONLY_SEGMENTS.includes(segment)) ? 'qa-only' : 'readable'
336
+ }
337
+
338
+ function checkExampleSourceMirrors(root, records, errors, derived) {
339
+ const mirrors = []
340
+ for (const record of records) {
341
+ if (record.contract !== 'example-source') continue
342
+ const mirror = exampleMirrorPath(record.path)
343
+ if (!mirror) continue
344
+ const sourceAbsolute = path.join(root, record.path)
345
+ const mirrorAbsolute = path.join(root, mirror)
346
+ const sourceExists = isRegularFile(sourceAbsolute)
347
+ const mirrorExists = isRegularFile(mirrorAbsolute)
348
+ const entry = { path: record.path, mirrorPath: mirror, readStatus: exampleReadStatus(record.path) }
349
+ if (!sourceExists && !mirrorExists) {
350
+ entry.state = 'deleted-on-both-sides'
351
+ } else if (!sourceExists || !mirrorExists) {
352
+ entry.state = 'mirror-missing'
353
+ errors.push(
354
+ `example-source ${record.path} was moved or deleted without its byte-identical mirror ${mirror}; ` +
355
+ 'run `yarn template:sync:fix` and update the surface inventory',
356
+ )
357
+ } else if (hashFile(sourceAbsolute) !== hashFile(mirrorAbsolute)) {
358
+ entry.state = 'mirror-drift'
359
+ errors.push(`example-source ${record.path} is not byte-identical to its mirror ${mirror}; run \`yarn template:sync:fix\``)
360
+ } else {
361
+ entry.state = 'mirrored'
362
+ entry.sha256 = hashFile(sourceAbsolute)
363
+ }
364
+ mirrors.push(entry)
365
+ }
366
+ derived.exampleSourceMirrors = mirrors
367
+ }
368
+
369
+ function checkFileRecords(root, label, records, errors, options = {}) {
370
+ for (const record of records) {
371
+ const absolute = path.join(root, record.path)
372
+ if (!isRegularFile(absolute)) {
373
+ errors.push(`${label} ${record.path} is not an exact regular file in the working tree`)
374
+ continue
375
+ }
376
+ const actual = hashFile(absolute)
377
+ if (actual !== record.sha256) {
378
+ errors.push(`${label} ${record.path} sha256 is stale: manifest ${record.sha256}, working tree ${actual}`)
379
+ }
380
+ if (!options.sourcePathRequired) continue
381
+ const sourceAbsolute = path.join(root, record.sourcePath)
382
+ if (!isRegularFile(sourceAbsolute)) {
383
+ errors.push(`${label} ${record.path} names sourcePath ${record.sourcePath}, which is not an exact regular file`)
384
+ }
385
+ }
386
+ }
387
+
388
+ function baseFileHash(root, baseSha, relativePath) {
389
+ const result = spawnSync('git', ['-C', root, 'show', `${baseSha}:${relativePath}`], { encoding: 'buffer' })
390
+ if (result.status !== 0) return null
391
+ return createHash('sha256').update(result.stdout).digest('hex')
392
+ }
393
+
394
+ function baseFileBuffer(root, baseSha, relativePath) {
395
+ const result = spawnSync('git', ['-C', root, 'show', `${baseSha}:${relativePath}`], { encoding: 'buffer' })
396
+ return result.status === 0 ? result.stdout : null
397
+ }
398
+
399
+ const NODE_TEST_SCRIPT_PATTERN = /(?:^|\s)--test(?:[=\s]|$)/
400
+
401
+ /**
402
+ * The controller only drives an exact `node --test` argv. It reads the nearest owning package's
403
+ * declared test script so a project on another runner is refused with its real script name instead
404
+ * of being handed a command that would not run its suite.
405
+ */
406
+ export function resolveTestRunnerFamily(root, testFile) {
407
+ const segments = normalizeRelativePath(testFile).split('/').slice(0, -1)
408
+ const directories = ['']
409
+ let accumulated = ''
410
+ for (const segment of segments) {
411
+ accumulated = accumulated ? `${accumulated}/${segment}` : segment
412
+ directories.push(accumulated)
413
+ }
414
+ for (const directory of directories.reverse()) {
415
+ const manifest = readJsonIfPresent(path.join(root, directory, 'package.json'))
416
+ const declared = manifest?.scripts?.test
417
+ if (typeof declared !== 'string' || !declared.trim()) continue
418
+ return NODE_TEST_SCRIPT_PATTERN.test(declared)
419
+ ? { family: 'node-test', declaredScript: declared }
420
+ : { family: 'unsupported', declaredScript: declared }
421
+ }
422
+ return { family: 'node-test', declaredScript: null }
423
+ }
424
+
425
+ export function deriveFocusedCommand(testFile, runnerFamily = { family: 'node-test', declaredScript: null }) {
426
+ const relativePath = normalizeRelativePath(testFile)
427
+ if (runnerFamily.family !== 'node-test') {
428
+ return {
429
+ error:
430
+ `the owning package of ${relativePath} declares the test script "${runnerFamily.declaredScript}", which the ` +
431
+ 'controller cannot drive as an exact argv; it only drives a `node --test` runner today',
432
+ }
433
+ }
434
+ const runner = FOCUSED_RUNNERS.find((candidate) => candidate.pattern.test(relativePath))
435
+ if (!runner) return { error: `no controller runner is registered for ${relativePath}` }
436
+ const argv = [process.execPath, ...runner.args(relativePath)]
437
+ const offending = argv.find((token) => SHELL_METACHARACTER_PATTERN.test(token))
438
+ if (offending !== undefined) {
439
+ return { error: `the command derived for ${relativePath} carries the shell metacharacter token "${offending}"` }
440
+ }
441
+ return { runner: runner.runner, argv }
442
+ }
443
+
444
+ function isContainedRelativePath(relativePath) {
445
+ const normalized = normalizeRelativePath(relativePath)
446
+ if (!normalized || normalized.startsWith('/') || /^[A-Za-z]:/.test(normalized)) return false
447
+ return !normalized.split('/').includes('..')
448
+ }
449
+
450
+ function overlayWorkingTreePaths(root, worktree, relativePaths) {
451
+ for (const relativePath of relativePaths) {
452
+ if (!isContainedRelativePath(relativePath)) continue
453
+ const source = path.join(root, relativePath)
454
+ const target = path.join(worktree, relativePath)
455
+ if (isRegularFile(source)) {
456
+ fs.mkdirSync(path.dirname(target), { recursive: true })
457
+ fs.copyFileSync(source, target)
458
+ } else if (fs.existsSync(target)) {
459
+ fs.rmSync(target, { force: true, recursive: true })
460
+ }
461
+ }
462
+ }
463
+
464
+ function linkResolutionRoots(root, worktree, relativePath) {
465
+ const segments = normalizeRelativePath(relativePath).split('/').slice(0, -1)
466
+ const directories = ['']
467
+ let accumulated = ''
468
+ for (const segment of segments) {
469
+ accumulated = accumulated ? `${accumulated}/${segment}` : segment
470
+ directories.push(accumulated)
471
+ }
472
+ for (const directory of directories) {
473
+ const target = path.join(root, directory, 'node_modules')
474
+ const link = path.join(worktree, directory, 'node_modules')
475
+ if (!fs.existsSync(target) || fs.existsSync(link)) continue
476
+ fs.mkdirSync(path.dirname(link), { recursive: true })
477
+ try { fs.symlinkSync(target, link, 'junction') } catch { /* resolution stays worktree-local */ }
478
+ }
479
+ }
480
+
481
+ // A controller invoked from inside a test runner would otherwise leak its runner context into the
482
+ // child, which suppresses the child's own exit status and makes every focused run look like a pass.
483
+ export const STRIPPED_EXECUTION_ENV_KEYS = Object.freeze([
484
+ 'NODE_OPTIONS', 'NODE_TEST_CONTEXT', 'NODE_V8_COVERAGE', 'TEST_RUNNER_CONCURRENCY',
485
+ ])
486
+
487
+ export function sanitizeExecutionEnv(sourceEnv) {
488
+ const env = { ...sourceEnv }
489
+ for (const key of STRIPPED_EXECUTION_ENV_KEYS) delete env[key]
490
+ return env
491
+ }
492
+
493
+ function runControlledCommand(cwd, argv, timeoutMs) {
494
+ const env = sanitizeExecutionEnv(process.env)
495
+ const result = spawnSync(argv[0], argv.slice(1), { cwd, env, timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 })
496
+ if (result.error || result.signal || typeof result.status !== 'number') {
497
+ return { failed: true, reason: result.signal ? `terminated by signal ${result.signal}` : String(result.error ?? 'no exit status') }
498
+ }
499
+ return {
500
+ exitCode: result.status,
501
+ stdoutSha256: createHash('sha256').update(result.stdout ?? Buffer.alloc(0)).digest('hex'),
502
+ stderrSha256: createHash('sha256').update(result.stderr ?? Buffer.alloc(0)).digest('hex'),
503
+ }
504
+ }
505
+
506
+ function addWorktree(root, sha, directory) {
507
+ git(root, ['worktree', 'add', '--detach', '--quiet', directory, sha])
508
+ }
509
+
510
+ // Returns true when the clean removal failed and left an administrative entry behind. The caller
511
+ // prunes only then: an unconditional prune would also drop sibling worktrees of the same repository
512
+ // whose directories happen to be unavailable, which is not this controller's to decide.
513
+ function removeWorktree(root, directory) {
514
+ try {
515
+ git(root, ['worktree', 'remove', '--force', directory])
516
+ return false
517
+ } catch {
518
+ fs.rmSync(directory, { recursive: true, force: true })
519
+ return true
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Controller-owned focused execution. For each declared focused test the controller builds two
525
+ * isolated worktrees: the base commit with ONLY that test's own head content applied, and the head
526
+ * commit with the whole working-tree diff applied. It runs the derived argv exactly once per side —
527
+ * there is no retry — and records exit codes plus output hashes as evidence.
528
+ */
529
+ export function runFocusedExecutions(input) {
530
+ const root = path.resolve(input.root)
531
+ const baseSha = input.baseSha
532
+ const changedPaths = [...new Set((input.changedPaths ?? []).map(normalizeRelativePath))]
533
+ const timeoutMs = input.timeoutMs ?? DEFAULT_EXECUTION_TIMEOUT_MS
534
+ const executions = []
535
+ const errors = []
536
+ const testFiles = [...new Set((input.testFiles ?? []).map(normalizeRelativePath))]
537
+ if (!testFiles.length) return { executions, errors }
538
+
539
+ const scratch = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'om-knowledge-exec-'))
540
+ let removalFellBack = false
541
+ try {
542
+ for (const [index, testFile] of testFiles.entries()) {
543
+ const headAbsolute = path.join(root, testFile)
544
+ if (!isContainedRelativePath(testFile) || !isRegularFile(headAbsolute)) {
545
+ errors.push(`focusedExecutions ${testFile} is not an exact regular file inside the repository, so it cannot be executed`)
546
+ continue
547
+ }
548
+ const derivedCommand = deriveFocusedCommand(testFile, resolveTestRunnerFamily(root, testFile))
549
+ if (derivedCommand.error) { errors.push(`focusedExecutions ${derivedCommand.error}`); continue }
550
+
551
+ const headContents = fs.readFileSync(headAbsolute)
552
+ const baseContents = baseFileBuffer(root, baseSha, testFile)
553
+ if (baseContents !== null && baseContents.equals(headContents)) {
554
+ errors.push(
555
+ `focusedExecutions ${testFile} has no test-only diff against the base; a knowledge-contract change must add ` +
556
+ 'or repair the regression that proves the new behavior',
557
+ )
558
+ continue
559
+ }
560
+
561
+ const baseWorktree = path.join(scratch, `base-${index}`)
562
+ const headWorktree = path.join(scratch, `head-${index}`)
563
+ let baseOutcome
564
+ let headOutcome
565
+ try {
566
+ addWorktree(root, baseSha, baseWorktree)
567
+ overlayWorkingTreePaths(root, baseWorktree, [testFile])
568
+ linkResolutionRoots(root, baseWorktree, testFile)
569
+ baseOutcome = runControlledCommand(baseWorktree, derivedCommand.argv, timeoutMs)
570
+
571
+ addWorktree(root, input.headSha, headWorktree)
572
+ overlayWorkingTreePaths(root, headWorktree, changedPaths)
573
+ linkResolutionRoots(root, headWorktree, testFile)
574
+ headOutcome = runControlledCommand(headWorktree, derivedCommand.argv, timeoutMs)
575
+ } catch (error) {
576
+ errors.push(`focusedExecutions ${testFile} could not be executed in an isolated worktree: ${error.message}`)
577
+ continue
578
+ } finally {
579
+ removalFellBack = removeWorktree(root, baseWorktree) || removalFellBack
580
+ removalFellBack = removeWorktree(root, headWorktree) || removalFellBack
581
+ }
582
+
583
+ if (baseOutcome.failed || headOutcome.failed) {
584
+ const failing = baseOutcome.failed ? `base-plus-test-only (${baseOutcome.reason})` : `head (${headOutcome.reason})`
585
+ errors.push(`focusedExecutions ${testFile} produced no usable ${failing} exit status`)
586
+ continue
587
+ }
588
+ executions.push({
589
+ testFile,
590
+ command: derivedCommand.argv,
591
+ baseWithTestPatch: baseOutcome,
592
+ head: headOutcome,
593
+ })
594
+ }
595
+ } finally {
596
+ fs.rmSync(scratch, { recursive: true, force: true })
597
+ if (removalFellBack) { try { git(root, ['worktree', 'prune']) } catch { /* pruning is best effort */ } }
598
+ }
599
+ return { executions, errors }
600
+ }
601
+
602
+ export function assertFocusedExecutionEvidence(declaredTests, evidence) {
603
+ const errors = []
604
+ if (!evidence) {
605
+ return ['the controller produced no focused base/head execution evidence; a knowledge-contract change cannot be certified without it']
606
+ }
607
+ errors.push(...(evidence.errors ?? []))
608
+ const executions = evidence.executions ?? []
609
+ for (const testFile of declaredTests) {
610
+ const execution = executions.find((entry) => entry.testFile === normalizeRelativePath(testFile))
611
+ if (!execution) {
612
+ errors.push(`focusedExecutions has no base/head execution record for declared focused test ${testFile}`)
613
+ continue
614
+ }
615
+ if (execution.baseWithTestPatch.exitCode === 0) {
616
+ errors.push(
617
+ `focusedExecutions ${testFile} exited 0 on base-plus-test-only; the focused test must fail for the old behavior`,
618
+ )
619
+ }
620
+ if (execution.head.exitCode !== 0) {
621
+ errors.push(`focusedExecutions ${testFile} exited ${execution.head.exitCode} at head; the focused test must pass after the change`)
622
+ }
623
+ }
624
+ for (const execution of executions) {
625
+ if (!declaredTests.map(normalizeRelativePath).includes(execution.testFile)) {
626
+ errors.push(`focusedExecutions carries an execution for ${execution.testFile}, which the manifest does not declare`)
627
+ }
628
+ }
629
+ return errors
630
+ }
631
+
632
+ /**
633
+ * Validates one knowledge-change run manifest against the derived classification of
634
+ * its own diff. `changedPaths` is supplied by the caller so the derivation stays
635
+ * testable without a shell; the CLI derives it from git.
636
+ */
637
+ export function validateKnowledgeChange(input) {
638
+ const root = path.resolve(input.root)
639
+ const manifest = input.manifest
640
+ const schema = input.schema
641
+ const errors = []
642
+ const derived = {}
643
+
644
+ const schemaErrors = validateJsonSchema(manifest, schema.$defs.authoredManifest, '$', schema)
645
+ for (const field of CONTROLLER_OWNED_FIELDS) {
646
+ if (isPlainObject(manifest) && Object.hasOwn(manifest, field)) {
647
+ errors.push(`${field} is controller-owned output; author-supplied evidence fails validation`)
648
+ }
649
+ }
650
+ errors.push(...schemaErrors.map((error) => `manifest schema: ${error}`))
651
+ if (!isPlainObject(manifest)) return { ok: false, errors, derived }
652
+
653
+ const classification = deriveChangeClassification(input.changedPaths)
654
+ derived.changeClass = classification.changeClass
655
+ derived.changedContracts = classification.changedContracts
656
+ derived.classifiedPaths = classification.records
657
+ derived.unclassifiedPaths = classification.unclassifiedPaths
658
+
659
+ if (manifest.changeClass !== classification.changeClass) {
660
+ errors.push(
661
+ `declared changeClass "${manifest.changeClass}" differs from the class derived from the diff ` +
662
+ `"${classification.changeClass}"`,
663
+ )
664
+ }
665
+ const declaredContracts = Array.isArray(manifest.changedContracts) ? manifest.changedContracts : []
666
+ const missingContracts = classification.changedContracts.filter((contract) => !declaredContracts.includes(contract))
667
+ const extraContracts = declaredContracts.filter((contract) => !classification.changedContracts.includes(contract))
668
+ if (missingContracts.length) errors.push(`changedContracts omits derived contracts: ${missingContracts.join(', ')}`)
669
+ if (extraContracts.length) errors.push(`changedContracts declares contracts the diff does not touch: ${extraContracts.join(', ')}`)
670
+
671
+ checkFileRecords(root, 'authoritativeFiles', manifest.authoritativeFiles ?? [], errors)
672
+ checkFileRecords(root, 'generatedFiles', manifest.generatedFiles ?? [], errors, { sourcePathRequired: true })
673
+ for (const documentationPath of manifest.documentationFiles ?? []) {
674
+ if (!isRegularFile(path.join(root, documentationPath))) {
675
+ errors.push(`documentationFiles ${documentationPath} is not an exact regular file in the working tree`)
676
+ }
677
+ }
678
+
679
+ if (classification.changeClass === 'asset-sync' || manifest.changeClass === 'asset-sync') {
680
+ for (const record of classification.records) {
681
+ if (record.contract) errors.push(`asset-sync run touches ${record.path}, which carries the ${record.contract} contract`)
682
+ }
683
+ }
684
+ if (classification.changeClass === 'asset-sync') {
685
+ if (input.baseSha) {
686
+ for (const generated of manifest.generatedFiles ?? []) {
687
+ const baseHash = baseFileHash(root, input.baseSha, generated.sourcePath)
688
+ const headAbsolute = path.join(root, generated.sourcePath)
689
+ const headHash = isRegularFile(headAbsolute) ? hashFile(headAbsolute) : null
690
+ if (baseHash !== null && headHash !== null && baseHash !== headHash) {
691
+ errors.push(
692
+ `asset-sync run changed authoritative source ${generated.sourcePath}; ` +
693
+ 'a semantic source change is a knowledge-contract change',
694
+ )
695
+ }
696
+ }
697
+ }
698
+ }
699
+
700
+ if (classification.changeClass === 'knowledge-contract') {
701
+ const declaredTests = Array.isArray(manifest.focusedTestFiles) ? manifest.focusedTestFiles.map(normalizeRelativePath) : []
702
+ const changedTests = declaredTests.filter((testPath) => classification.focusedTestPaths.includes(testPath))
703
+ if (!declaredTests.length) {
704
+ errors.push('a knowledge-contract change requires at least one focused test that demonstrates the affected contract')
705
+ }
706
+ for (const testPath of declaredTests) {
707
+ if (!isFocusedTestPath(testPath)) errors.push(`focusedTestFiles ${testPath} is not a test file`)
708
+ if (!isRegularFile(path.join(root, testPath))) errors.push(`focusedTestFiles ${testPath} is not an exact regular file in the working tree`)
709
+ }
710
+ if (declaredTests.length && !changedTests.length) {
711
+ errors.push('no declared focused test appears in the diff; a knowledge-contract change must add or repair its own regression')
712
+ }
713
+
714
+ const cases = input.cases ?? readJsonIfPresent(path.join(root, input.harnessRelativeDir ?? '', 'cases.json'))
715
+ if (!Array.isArray(cases)) {
716
+ errors.push('the case catalog could not be read, so affected cases and the catalog count cannot be verified')
717
+ } else {
718
+ const knownCaseIds = new Set(cases.map((entry) => entry?.id))
719
+ for (const caseId of manifest.affectedCaseIds ?? []) {
720
+ if (!knownCaseIds.has(caseId)) errors.push(`affectedCaseIds ${caseId} does not exist in the case catalog`)
721
+ }
722
+ if (manifest.expectedCatalogCount !== cases.length) {
723
+ errors.push(`expectedCatalogCount ${manifest.expectedCatalogCount} does not match the catalog's ${cases.length} cases`)
724
+ }
725
+ for (const rangeId of manifest.affectedRanges ?? []) {
726
+ const expanded = expandCaseRange(rangeId)
727
+ if (!expanded) { errors.push(`affectedRanges ${rangeId} is not an ascending OMH range`); continue }
728
+ const missing = expanded.filter((caseId) => !knownCaseIds.has(caseId))
729
+ if (missing.length) errors.push(`affectedRanges ${rangeId} covers cases absent from the catalog: ${missing.join(', ')}`)
730
+ }
731
+ const rangeMembers = new Set((manifest.affectedRanges ?? []).flatMap((rangeId) => expandCaseRange(rangeId) ?? []))
732
+ for (const caseId of manifest.affectedCaseIds ?? []) {
733
+ if (rangeMembers.size && !rangeMembers.has(caseId)) {
734
+ errors.push(`affectedCaseIds ${caseId} falls outside every declared affected range`)
735
+ }
736
+ }
737
+ }
738
+
739
+ const releaseMatrix = input.releaseMatrix ?? readJsonIfPresent(path.join(root, input.harnessRelativeDir ?? '', 'release-matrix.json'))
740
+ if (!isPlainObject(releaseMatrix)) {
741
+ errors.push('the release matrix could not be read, so required lanes cannot be verified')
742
+ } else {
743
+ for (const lane of manifest.requiredReleaseLanes ?? []) {
744
+ if (!Object.hasOwn(releaseMatrix, lane)) errors.push(`requiredReleaseLanes ${lane} is not a lane in the release matrix`)
745
+ }
746
+ if (!(manifest.requiredReleaseLanes ?? []).length) {
747
+ errors.push('a knowledge-contract change must name at least one affected certified release lane')
748
+ }
749
+ }
750
+
751
+ const executionErrors = assertFocusedExecutionEvidence(declaredTests, input.executionEvidence)
752
+ derived.focusedExecutions = input.executionEvidence?.executions ?? []
753
+ errors.push(...executionErrors)
754
+ }
755
+
756
+ const linkedContracts = ['source-link', 'example-source', 'installed-source']
757
+ const needsSourceLinkInventory = classification.changedContracts.some((contract) => linkedContracts.includes(contract))
758
+ derived.sourceLinkInventoryRequired = needsSourceLinkInventory
759
+ if (needsSourceLinkInventory) {
760
+ const declaredInventory = manifest.sourceLinkInventory?.path
761
+ if (declaredInventory && declaredInventory !== SOURCE_LINK_INVENTORY_PATH) {
762
+ errors.push(`sourceLinkInventory.path must be ${SOURCE_LINK_INVENTORY_PATH}`)
763
+ }
764
+ const inventoryPath = declaredInventory ?? SOURCE_LINK_INVENTORY_PATH
765
+ if (!isRegularFile(path.join(root, inventoryPath))) {
766
+ derived.sourceLinkInventoryStatus = 'absent'
767
+ errors.push(CANON_C_REASON)
768
+ } else {
769
+ derived.sourceLinkInventoryStatus = 'present'
770
+ }
771
+ }
772
+
773
+ checkExampleSourceMirrors(root, classification.records, errors, derived)
774
+
775
+ return { ok: errors.length === 0, errors, derived }
776
+ }
777
+
778
+ function writeJsonAtomic(absolutePath, value) {
779
+ const directory = path.dirname(absolutePath)
780
+ fs.mkdirSync(directory, { recursive: true })
781
+ const temporaryPath = path.join(directory, `.${path.basename(absolutePath)}.${process.pid}.tmp`)
782
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`)
783
+ fs.renameSync(temporaryPath, absolutePath)
784
+ }
785
+
786
+ function usage() {
787
+ return [
788
+ 'Usage: validate-knowledge-change --manifest <path> --base <ref> [--root <dir>] [--harness-dir <dir>]',
789
+ ' [--out <path>] [--execution-timeout <ms>]',
790
+ '',
791
+ 'Derives the knowledge-contract/asset-sync class from the diff between <ref> and the working tree,',
792
+ 'validates the authored run manifest against knowledge-change.schema.json, and — for a knowledge-contract',
793
+ 'change — runs each declared focused test once in an isolated base-plus-test-only worktree and once at head,',
794
+ 'recording exit codes and output hashes as controller-owned evidence. The result is written atomically.',
795
+ ].join('\n')
796
+ }
797
+
798
+ function parseArgs(argv) {
799
+ const options = { root: process.cwd(), help: false }
800
+ for (let index = 0; index < argv.length; index += 1) {
801
+ const token = argv[index]
802
+ if (token === '--help' || token === '-h') { options.help = true; continue }
803
+ const value = argv[index + 1]
804
+ if (token === '--manifest') { options.manifest = value; index += 1; continue }
805
+ if (token === '--base') { options.base = value; index += 1; continue }
806
+ if (token === '--root') { options.root = value; index += 1; continue }
807
+ if (token === '--harness-dir') { options.harnessDir = value; index += 1; continue }
808
+ if (token === '--out') { options.out = value; index += 1; continue }
809
+ if (token === '--execution-timeout') {
810
+ const parsed = Number(value)
811
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error('--execution-timeout must be a positive integer of milliseconds')
812
+ options.executionTimeoutMs = parsed
813
+ index += 1
814
+ continue
815
+ }
816
+ throw new Error(`unknown argument: ${token}`)
817
+ }
818
+ return options
819
+ }
820
+
821
+ function defaultHarnessRelativeDir(root) {
822
+ const candidates = ['packages/create-app/agentic/shared/ai/harness', 'agentic/shared/ai/harness', '.ai/harness']
823
+ return candidates.find((candidate) => isRegularFile(path.join(root, candidate, 'cases.json'))) ?? '.ai/harness'
824
+ }
825
+
826
+ export function main(argv = process.argv.slice(2)) {
827
+ let options
828
+ try { options = parseArgs(argv) } catch (error) { console.error(error.message); console.error(usage()); return EXIT_INVALID }
829
+ if (options.help) { console.log(usage()); return EXIT_PASS }
830
+ if (!options.manifest || !options.base) { console.error('--manifest and --base are required'); console.error(usage()); return EXIT_INVALID }
831
+
832
+ const root = path.resolve(options.root)
833
+ const harnessRelativeDir = options.harnessDir ?? defaultHarnessRelativeDir(root)
834
+ const schemaCandidates = [
835
+ path.join(root, harnessRelativeDir, SCHEMA_BASENAME),
836
+ fileURLToPath(new URL(`../ai/harness/${SCHEMA_BASENAME}`, import.meta.url)),
837
+ fileURLToPath(new URL(`../.ai/harness/${SCHEMA_BASENAME}`, import.meta.url)),
838
+ ]
839
+ const schemaPath = schemaCandidates.find((candidate) => isRegularFile(candidate))
840
+ if (!schemaPath) { console.error(`knowledge-change schema not found: ${schemaCandidates.join(', ')}`); return EXIT_INVALID }
841
+ const schema = readJsonIfPresent(schemaPath)
842
+ if (!schema) { console.error(`knowledge-change schema is not valid JSON: ${schemaPath}`); return EXIT_INVALID }
843
+
844
+ const manifestPath = path.resolve(root, options.manifest)
845
+ const manifest = readJsonIfPresent(manifestPath)
846
+ if (manifest === null) { console.error(`run manifest not found or not valid JSON: ${manifestPath}`); return EXIT_INVALID }
847
+
848
+ let baseSha
849
+ let headSha
850
+ let changedPaths
851
+ try {
852
+ baseSha = resolveGitSha(root, options.base)
853
+ headSha = resolveGitSha(root, 'HEAD')
854
+ changedPaths = deriveChangedPaths(root, baseSha)
855
+ } catch (error) { console.error(error.message); return EXIT_INVALID }
856
+
857
+ let baseRefError = null
858
+ if (manifest.baseRef !== options.base && manifest.baseRef !== baseSha) {
859
+ let authoredSha = null
860
+ try { authoredSha = resolveGitSha(root, manifest.baseRef) } catch { authoredSha = null }
861
+ if (authoredSha !== baseSha) {
862
+ baseRefError = `authored baseRef "${manifest.baseRef}" does not resolve to the --base SHA ${baseSha}`
863
+ }
864
+ }
865
+
866
+ const classification = deriveChangeClassification(changedPaths)
867
+ let executionEvidence = null
868
+ if (classification.changeClass === 'knowledge-contract') {
869
+ executionEvidence = baseRefError
870
+ ? { executions: [], errors: ['focused base/head execution was not attempted because the authored baseRef does not resolve to --base'] }
871
+ : runFocusedExecutions({
872
+ root,
873
+ baseSha,
874
+ headSha,
875
+ changedPaths,
876
+ testFiles: Array.isArray(manifest.focusedTestFiles) ? manifest.focusedTestFiles : [],
877
+ timeoutMs: options.executionTimeoutMs,
878
+ })
879
+ }
880
+
881
+ const result = validateKnowledgeChange({
882
+ root, manifest, schema, changedPaths, baseSha, harnessRelativeDir, executionEvidence,
883
+ })
884
+ if (baseRefError) { result.errors.unshift(baseRefError); result.ok = false }
885
+
886
+ const completed = {
887
+ ...manifest,
888
+ resolvedBaseSha: baseSha,
889
+ headSha,
890
+ focusedExecutions: executionEvidence?.executions ?? [],
891
+ }
892
+ const completedErrors = validateJsonSchema(completed, schema.$defs.completedManifest, '$', schema)
893
+ if (completedErrors.length) {
894
+ result.errors.push(...completedErrors.map((error) => `completed manifest schema: ${error}`))
895
+ result.ok = false
896
+ }
897
+ const outputPath = path.resolve(root, options.out ?? `${options.manifest}.result.json`)
898
+ writeJsonAtomic(outputPath, {
899
+ status: result.ok ? 'pass' : 'fail',
900
+ manifest: completed,
901
+ derived: result.derived,
902
+ changedPaths,
903
+ errors: result.errors,
904
+ })
905
+
906
+ for (const error of result.errors) console.error(`knowledge-change: ${error}`)
907
+ console.log(`${result.ok ? 'PASS' : 'FAIL'} knowledge-change ${result.derived.changeClass} — ${outputPath}`)
908
+ return result.ok ? EXIT_PASS : EXIT_FAILURE
909
+ }
910
+
911
+ if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) process.exitCode = main()