@open-mercato/cli 0.6.7 → 0.6.8-canary.320.1.bf889f49e4

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 (335) hide show
  1. package/.turbo/turbo-build.log +77 -4
  2. package/AGENTS.md +6 -1
  3. package/build.mjs +68 -11
  4. package/dist/agentic/claude-code/hooks/entity-migration-check.ts +32 -88
  5. package/dist/agentic/codex/enforcement-rules.md +2 -16
  6. package/dist/agentic/cursor/hooks/entity-migration-check.mjs +19 -50
  7. package/dist/agentic/cursor/rules/entity-guard.mdc +9 -21
  8. package/dist/agentic/cursor/rules/generated-guard.mdc +3 -5
  9. package/dist/agentic/cursor/rules/open-mercato.mdc +3 -29
  10. package/dist/agentic/guides/ai-workflows.md +76 -0
  11. package/dist/agentic/guides/architecture.md +107 -0
  12. package/dist/agentic/guides/backend-ui.md +86 -0
  13. package/dist/agentic/guides/contracts.md +92 -0
  14. package/dist/agentic/guides/extensions.md +121 -0
  15. package/dist/agentic/guides/framework-extension-points.md +36 -0
  16. package/dist/agentic/guides/integrations.md +81 -0
  17. package/dist/agentic/guides/module-facts.json +116757 -3951
  18. package/dist/agentic/guides/module-system.md +12 -127
  19. package/dist/agentic/guides/modules/ai_assistant.md +227 -28
  20. package/dist/agentic/guides/modules/api_docs.md +58 -2
  21. package/dist/agentic/guides/modules/api_keys.md +92 -7
  22. package/dist/agentic/guides/modules/attachments.md +119 -14
  23. package/dist/agentic/guides/modules/audit_logs.md +131 -10
  24. package/dist/agentic/guides/modules/auth.md +276 -33
  25. package/dist/agentic/guides/modules/business_rules.md +149 -14
  26. package/dist/agentic/guides/modules/catalog.md +482 -43
  27. package/dist/agentic/guides/modules/channel_gmail.md +82 -3
  28. package/dist/agentic/guides/modules/channel_imap.md +82 -3
  29. package/dist/agentic/guides/modules/checkout.md +305 -27
  30. package/dist/agentic/guides/modules/communication_channels.md +315 -33
  31. package/dist/agentic/guides/modules/configs.md +104 -8
  32. package/dist/agentic/guides/modules/content.md +57 -2
  33. package/dist/agentic/guides/modules/currencies.md +152 -17
  34. package/dist/agentic/guides/modules/customer_accounts.md +387 -45
  35. package/dist/agentic/guides/modules/customers.md +746 -85
  36. package/dist/agentic/guides/modules/dashboards.md +155 -10
  37. package/dist/agentic/guides/modules/data_sync.md +149 -16
  38. package/dist/agentic/guides/modules/design_system.md +102 -0
  39. package/dist/agentic/guides/modules/dictionaries.md +107 -12
  40. package/dist/agentic/guides/modules/directory.md +148 -15
  41. package/dist/agentic/guides/modules/entities.md +145 -13
  42. package/dist/agentic/guides/modules/events.md +59 -2
  43. package/dist/agentic/guides/modules/feature_toggles.md +119 -9
  44. package/dist/agentic/guides/modules/gateway_stripe.md +92 -4
  45. package/dist/agentic/guides/modules/generators.md +45 -2
  46. package/dist/agentic/guides/modules/inbox_ops.md +207 -27
  47. package/dist/agentic/guides/modules/integrations.md +149 -16
  48. package/dist/agentic/guides/modules/messages.md +223 -24
  49. package/dist/agentic/guides/modules/notifications.md +96 -8
  50. package/dist/agentic/guides/modules/onboarding.md +77 -6
  51. package/dist/agentic/guides/modules/payment_gateways.md +168 -21
  52. package/dist/agentic/guides/modules/perspectives.md +77 -7
  53. package/dist/agentic/guides/modules/planner.md +136 -18
  54. package/dist/agentic/guides/modules/portal.md +93 -2
  55. package/dist/agentic/guides/modules/progress.md +104 -14
  56. package/dist/agentic/guides/modules/query_index.md +154 -15
  57. package/dist/agentic/guides/modules/record_locks.md +206 -21
  58. package/dist/agentic/guides/modules/resources.md +197 -30
  59. package/dist/agentic/guides/modules/sales.md +687 -84
  60. package/dist/agentic/guides/modules/scheduler.md +133 -14
  61. package/dist/agentic/guides/modules/search.md +170 -6
  62. package/dist/agentic/guides/modules/security.md +294 -35
  63. package/dist/agentic/guides/modules/shipping_carriers.md +134 -18
  64. package/dist/agentic/guides/modules/sso.md +151 -26
  65. package/dist/agentic/guides/modules/staff.md +372 -57
  66. package/dist/agentic/guides/modules/storage_s3.md +95 -5
  67. package/dist/agentic/guides/modules/sync_akeneo.md +85 -3
  68. package/dist/agentic/guides/modules/sync_excel.md +75 -6
  69. package/dist/agentic/guides/modules/system_status_overlays.md +60 -2
  70. package/dist/agentic/guides/modules/translations.md +104 -10
  71. package/dist/agentic/guides/modules/webhooks.md +203 -24
  72. package/dist/agentic/guides/modules/wms.md +469 -47
  73. package/dist/agentic/guides/modules/workflows.md +302 -43
  74. package/dist/agentic/guides/reference-module-facts.json +6942 -0
  75. package/dist/agentic/guides/reference-modules/example.md +462 -0
  76. package/dist/agentic/guides/spec-delivery.md +56 -0
  77. package/dist/agentic/guides/testing-debugging.md +80 -0
  78. package/dist/agentic/guides/upstream/AGENTS.md +300 -0
  79. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +334 -0
  80. package/dist/agentic/guides/upstream/manifest.json +8 -0
  81. package/dist/agentic/shared/AGENTS.md.template +120 -311
  82. package/dist/agentic/shared/ai/agentic.config.json +2 -0
  83. package/dist/agentic/shared/ai/harness/README.md +50 -0
  84. package/dist/agentic/shared/ai/harness/RELEASE.md +59 -0
  85. package/dist/agentic/shared/ai/harness/cases.json +20596 -0
  86. package/dist/agentic/shared/ai/harness/cases.schema.json +196 -0
  87. package/dist/agentic/shared/ai/harness/design-system-inventory.json +6326 -0
  88. package/dist/agentic/shared/ai/harness/fixtures/index.json +51 -0
  89. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +166 -0
  90. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +21 -0
  91. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +96 -0
  92. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +172 -0
  93. package/dist/agentic/shared/ai/harness/knowledge-change.schema.json +228 -0
  94. package/dist/agentic/shared/ai/harness/release-matrix.json +163 -0
  95. package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
  96. package/dist/agentic/shared/ai/harness/result.schema.json +136 -0
  97. package/dist/agentic/shared/ai/harness/routing-response.schema.json +22 -0
  98. package/dist/agentic/shared/ai/harness/source-link-inventory.json +1706 -0
  99. package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
  100. package/dist/agentic/shared/ai/harness/validators.json +82 -0
  101. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +2121 -0
  102. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
  103. package/dist/agentic/shared/ai/harness/writable-spec-oracles.mjs +602 -0
  104. package/dist/agentic/shared/ai/lessons/_template.md +16 -0
  105. package/dist/agentic/shared/ai/lessons.md +26 -3
  106. package/dist/agentic/shared/ai/review-checklist.md +36 -0
  107. package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +21 -0
  108. package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
  109. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +98 -0
  110. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +21 -241
  111. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +31 -0
  112. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
  113. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +15 -0
  114. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
  115. package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
  116. package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
  117. package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
  118. package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
  119. package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
  120. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
  121. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
  122. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
  123. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
  124. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +17 -601
  125. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +21 -0
  126. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +15 -0
  127. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +16 -0
  128. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
  129. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +15 -310
  130. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +19 -0
  131. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +31 -0
  132. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +100 -0
  133. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
  134. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/knowledge-change.md +133 -0
  135. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
  136. package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
  137. package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
  138. package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
  139. package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
  140. package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +18 -0
  141. package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
  142. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +33 -152
  143. package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +33 -0
  144. package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +40 -0
  145. package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +27 -0
  146. package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +24 -0
  147. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +25 -708
  148. package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
  149. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +25 -0
  150. package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
  151. package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
  152. package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +34 -0
  153. package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +4 -0
  154. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +11 -0
  155. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +31 -0
  156. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +39 -0
  157. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +43 -0
  158. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +11 -0
  159. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
  160. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +32 -0
  161. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +172 -0
  162. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +14 -0
  163. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
  164. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +40 -0
  165. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
  166. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
  167. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
  168. package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +41 -0
  169. package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +19 -0
  170. package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +44 -0
  171. package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +39 -0
  172. package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +20 -0
  173. package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +57 -0
  174. package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +11 -0
  175. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +486 -0
  176. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +20 -843
  177. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +42 -0
  178. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +27 -0
  179. package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
  180. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +43 -0
  181. package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
  182. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
  183. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
  184. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +17 -467
  185. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +29 -0
  186. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
  187. package/dist/agentic/shared/ai/skills/tiers.json +143 -21
  188. package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
  189. package/dist/agentic/shared/ai/specs/2026-08-06-reference-module-activation.md +248 -0
  190. package/dist/agentic/shared/ai/specs/README.md +4 -3
  191. package/dist/agentic/shared/ai/specs/SPEC-000-template.md +226 -10
  192. package/dist/agentic/shared/ai/trackers/github.md +108 -4
  193. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +224 -0
  194. package/dist/agentic/shared/scripts/check-lessons.mjs +178 -0
  195. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +3812 -0
  196. package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
  197. package/dist/agentic/shared/scripts/framework-context.mjs +888 -0
  198. package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
  199. package/dist/agentic/shared/scripts/install-skills.sh +4 -694
  200. package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
  201. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1774 -0
  202. package/dist/agentic/shared/scripts/validate-knowledge-change.mjs +1172 -0
  203. package/dist/bin.js +32 -34
  204. package/dist/bin.js.map +2 -2
  205. package/dist/lib/__integration__/TC-INT-008.spec.js +88 -8
  206. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  207. package/dist/lib/agentic-init.js +13 -3
  208. package/dist/lib/agentic-init.js.map +2 -2
  209. package/dist/lib/agentic-setup.js +434 -186
  210. package/dist/lib/agentic-setup.js.map +2 -2
  211. package/dist/lib/cli-bootstrap-mode.js +32 -0
  212. package/dist/lib/cli-bootstrap-mode.js.map +7 -0
  213. package/dist/lib/dev-supervisor-manifest.js +105 -0
  214. package/dist/lib/dev-supervisor-manifest.js.map +7 -0
  215. package/dist/lib/generate-watch-events.js +15 -0
  216. package/dist/lib/generate-watch-events.js.map +2 -2
  217. package/dist/lib/generators/entity-ids.js +11 -24
  218. package/dist/lib/generators/entity-ids.js.map +2 -2
  219. package/dist/lib/generators/extensions/injection-widgets.js +20 -0
  220. package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
  221. package/dist/lib/generators/module-di.js +10 -23
  222. package/dist/lib/generators/module-di.js.map +2 -2
  223. package/dist/lib/generators/module-entities.js +10 -23
  224. package/dist/lib/generators/module-entities.js.map +2 -2
  225. package/dist/lib/generators/module-extension-facts.js +1882 -0
  226. package/dist/lib/generators/module-extension-facts.js.map +7 -0
  227. package/dist/lib/generators/module-fact-sources.js +70 -0
  228. package/dist/lib/generators/module-fact-sources.js.map +7 -0
  229. package/dist/lib/generators/module-facts-discovery.js +64 -7
  230. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  231. package/dist/lib/generators/module-facts.js +1913 -52
  232. package/dist/lib/generators/module-facts.js.map +3 -3
  233. package/dist/lib/generators/module-override-targets.js +712 -0
  234. package/dist/lib/generators/module-override-targets.js.map +7 -0
  235. package/dist/lib/generators/module-registry.js +405 -36
  236. package/dist/lib/generators/module-registry.js.map +2 -2
  237. package/dist/lib/generators/route-manifest-shards.js +170 -0
  238. package/dist/lib/generators/route-manifest-shards.js.map +7 -0
  239. package/dist/lib/in-process-generate-watcher.js +12 -3
  240. package/dist/lib/in-process-generate-watcher.js.map +2 -2
  241. package/dist/lib/load-env.js +24 -0
  242. package/dist/lib/load-env.js.map +7 -0
  243. package/dist/lib/queue-worker-supervisor.js.map +2 -2
  244. package/dist/lib/resolver.js +28 -3
  245. package/dist/lib/resolver.js.map +2 -2
  246. package/dist/lib/telemetry-init.js +358 -0
  247. package/dist/lib/telemetry-init.js.map +7 -0
  248. package/dist/lib/testing/integration.js +5 -2
  249. package/dist/lib/testing/integration.js.map +2 -2
  250. package/dist/lib/utils.js +15 -8
  251. package/dist/lib/utils.js.map +2 -2
  252. package/dist/mercato.js +35 -9
  253. package/dist/mercato.js.map +2 -2
  254. package/package.json +13 -11
  255. package/src/__tests__/mercato.test.ts +144 -0
  256. package/src/bin.ts +60 -42
  257. package/src/lib/__integration__/TC-INT-008.spec.ts +107 -9
  258. package/src/lib/__tests__/agentic-init.test.ts +41 -5
  259. package/src/lib/__tests__/agentic-setup.ownership.test.ts +386 -0
  260. package/src/lib/__tests__/cli-bootstrap-mode.test.ts +22 -0
  261. package/src/lib/__tests__/content-stable-writes.test.ts +74 -0
  262. package/src/lib/__tests__/dev-supervisor-manifest.test.ts +131 -0
  263. package/src/lib/__tests__/generate-watch-events.test.ts +76 -0
  264. package/src/lib/__tests__/in-process-generate-watcher.test.ts +52 -1
  265. package/src/lib/__tests__/injection-widget-id-validation.test.ts +184 -0
  266. package/src/lib/__tests__/load-env.test.ts +65 -0
  267. package/src/lib/__tests__/queue-worker-supervisor.test.ts +33 -0
  268. package/src/lib/__tests__/resolver.enterprise.test.ts +50 -0
  269. package/src/lib/__tests__/telemetry-init.test.ts +298 -0
  270. package/src/lib/agentic-init.ts +15 -3
  271. package/src/lib/agentic-setup.ts +573 -222
  272. package/src/lib/cli-bootstrap-mode.ts +31 -0
  273. package/src/lib/dev-supervisor-manifest.ts +142 -0
  274. package/src/lib/generate-watch-events.ts +14 -0
  275. package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +92 -0
  276. package/src/lib/generators/__tests__/entity-ids.test.ts +32 -0
  277. package/src/lib/generators/__tests__/helpers/override-target-diagnostic-fixtures.ts +150 -0
  278. package/src/lib/generators/__tests__/module-extension-activations.test.ts +762 -0
  279. package/src/lib/generators/__tests__/module-extension-facts.test.ts +603 -0
  280. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +139 -2
  281. package/src/lib/generators/__tests__/module-facts.computed-di-token.test.ts +88 -0
  282. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
  283. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
  284. package/src/lib/generators/__tests__/module-facts.example-fact-coverage.test.ts +414 -0
  285. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
  286. package/src/lib/generators/__tests__/module-facts.local-reference.test.ts +393 -0
  287. package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +561 -0
  288. package/src/lib/generators/__tests__/module-facts.search-entity-id.test.ts +151 -0
  289. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +160 -0
  290. package/src/lib/generators/__tests__/module-override-targets.diagnostic-wiring.test.ts +92 -0
  291. package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +149 -0
  292. package/src/lib/generators/__tests__/module-override-targets.test.ts +328 -0
  293. package/src/lib/generators/__tests__/module-override-targets.unknown-mode.test.ts +117 -0
  294. package/src/lib/generators/__tests__/module-subset.test.ts +266 -0
  295. package/src/lib/generators/__tests__/output-snapshots.test.ts +3 -3
  296. package/src/lib/generators/__tests__/route-manifest-shards.test.ts +62 -0
  297. package/src/lib/generators/entity-ids.ts +10 -26
  298. package/src/lib/generators/extensions/injection-widgets.ts +51 -0
  299. package/src/lib/generators/module-di.ts +9 -25
  300. package/src/lib/generators/module-entities.ts +9 -25
  301. package/src/lib/generators/module-extension-facts.ts +2472 -0
  302. package/src/lib/generators/module-fact-sources.ts +108 -0
  303. package/src/lib/generators/module-facts-discovery.ts +104 -7
  304. package/src/lib/generators/module-facts.ts +2747 -138
  305. package/src/lib/generators/module-override-targets.ts +907 -0
  306. package/src/lib/generators/module-registry.ts +448 -35
  307. package/src/lib/generators/route-manifest-shards.ts +146 -0
  308. package/src/lib/in-process-generate-watcher.ts +13 -4
  309. package/src/lib/load-env.ts +30 -0
  310. package/src/lib/queue-worker-supervisor.ts +7 -2
  311. package/src/lib/resolver.ts +34 -3
  312. package/src/lib/telemetry-init.ts +461 -0
  313. package/src/lib/testing/__tests__/integration.test.ts +4 -0
  314. package/src/lib/testing/integration.ts +5 -2
  315. package/src/lib/utils.ts +24 -16
  316. package/src/mercato.ts +46 -9
  317. package/dist/agentic/guides/cache.md +0 -45
  318. package/dist/agentic/guides/core.md +0 -453
  319. package/dist/agentic/guides/events.md +0 -117
  320. package/dist/agentic/guides/queue.md +0 -53
  321. package/dist/agentic/guides/search.md +0 -115
  322. package/dist/agentic/guides/shared.md +0 -108
  323. package/dist/agentic/guides/ui.md +0 -347
  324. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
  325. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
  326. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
  327. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
  328. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
  329. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
  330. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
  331. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
  332. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
  333. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
  334. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
  335. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
@@ -0,0 +1,1172 @@
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
+ // The spec's finite `main` parity ledger ships as `source-link-baseline.json`. Its path is pinned
34
+ // here rather than read out of the inventory's `inputs.baseline`, because that string lives inside
35
+ // an asset the same author is editing: anchoring the manifest to it would let an author point both
36
+ // at a ledger they fabricated. The inventory may now only agree with this literal or fail.
37
+ // `validate-source-links.mjs` hardcodes the same path for the same reason.
38
+ export const SOURCE_LINK_BASELINE_PATH =
39
+ 'packages/create-app/scripts/source-links/source-link-baseline.json'
40
+ // The schema pinning that ledger's shape is the ledger's exact sibling, so `baselineSchemaPath` is
41
+ // derived from the pinned ledger path rather than spelled out a second time.
42
+ export const SOURCE_LINK_BASELINE_SCHEMA_BASENAME = 'source-link-baseline.schema.json'
43
+ export const CANON_C_REASON =
44
+ `${SOURCE_LINK_INVENTORY_PATH} not present — CANON-C (canonical-example source-link inventory) is a ` +
45
+ 'monorepo-only asset and is absent from this tree, so source-link, example-source, and ' +
46
+ 'installed-source contracts cannot be resolved and fail closed'
47
+
48
+ const EXAMPLE_AUTHORING_ROOT = 'apps/mercato/src/modules/example'
49
+ const EXAMPLE_TEMPLATE_ROOT = 'packages/create-app/template/src/modules/example'
50
+ const EXAMPLE_QA_ONLY_SEGMENTS = Object.freeze(['__tests__', '__integration__'])
51
+
52
+ const FOCUSED_TEST_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/
53
+
54
+ // One argv token may not carry a shell metacharacter. The controller never interpolates a shell,
55
+ // and the recorded evidence must stay reproducible as an exact argv vector.
56
+ const SHELL_METACHARACTER_PATTERN = /[`$;&|<>\n]/
57
+
58
+ export const DEFAULT_EXECUTION_TIMEOUT_MS = 600_000
59
+
60
+ // Ordered; the first matching runner owns the focused test. Runner argv is controller-derived so an
61
+ // author can never substitute a weaker command for the test they declared.
62
+ const FOCUSED_RUNNERS = Object.freeze([
63
+ { runner: 'node-test', pattern: /\.(?:test|spec)\.[cm]?jsx?$/, args: (relativePath) => ['--test', relativePath] },
64
+ { runner: 'node-test-tsx', pattern: /\.(?:test|spec)\.[cm]?tsx?$/, args: (relativePath) => ['--import', 'tsx', '--test', relativePath] },
65
+ ])
66
+
67
+ // Ordered; the first matching rule owns the path. `contract: null` marks a path that
68
+ // carries no knowledge contract of its own — a generated/materialized copy or a docs
69
+ // snapshot — which is the only shape an `asset-sync` run may contain.
70
+ const CLASSIFICATION_RULES = Object.freeze([
71
+ {
72
+ rule: 'canonical-example-tree',
73
+ contract: 'example-source',
74
+ changeClass: 'knowledge-contract',
75
+ pattern: /^(?:apps\/mercato\/src|packages\/create-app\/template\/src|src)\/modules\/example\/.+$/,
76
+ },
77
+ {
78
+ // The derived inventory, the topic registry it derives from, the finite `main` parity ledger
79
+ // (`source-link-baseline.json` — the spec names it a parity ledger, it ships under this name),
80
+ // and the schema pinning that ledger's shape.
81
+ rule: 'source-link-inventory',
82
+ contract: 'source-link',
83
+ changeClass: 'knowledge-contract',
84
+ pattern: /(?:^|\/)(?:source-link-inventory\.json|source-link-topics\.json|source-link-baseline\.json|source-link-baseline\.schema\.json)$/,
85
+ },
86
+ {
87
+ rule: 'installed-package-manifest',
88
+ contract: 'installed-source',
89
+ changeClass: 'knowledge-contract',
90
+ pattern: /(?:^|\/)(?:package\.json|package\.json\.template|yarn\.lock|package-lock\.json|pnpm-lock\.yaml)$/,
91
+ },
92
+ {
93
+ rule: 'installed-skill-closure',
94
+ contract: 'installed-source',
95
+ changeClass: 'knowledge-contract',
96
+ pattern: /(?:^|\/)skills\/tiers\.json$/,
97
+ },
98
+ {
99
+ rule: 'evaluator-implementation',
100
+ contract: 'evaluator',
101
+ changeClass: 'knowledge-contract',
102
+ 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$/,
103
+ },
104
+ {
105
+ rule: 'writable-oracle',
106
+ contract: 'oracle',
107
+ changeClass: 'knowledge-contract',
108
+ pattern: /(?:^|\/)harness\/(?:writable-ast-oracles\.mjs|writable-behavior-oracles\.mjs|validators\.json)$/,
109
+ },
110
+ {
111
+ rule: 'case-context-policy',
112
+ contract: 'context-read',
113
+ changeClass: 'knowledge-contract',
114
+ pattern: /(?:^|\/)harness\/(?:cases\.json|release-matrix\.json|fixtures\/.+|[^/]+\.schema\.json)$/,
115
+ },
116
+ {
117
+ rule: 'agent-instruction-owner',
118
+ contract: 'routing',
119
+ changeClass: 'knowledge-contract',
120
+ pattern: /(?:^|\/)(?:AGENTS\.md|AGENTS\.md\.template|CLAUDE\.md|CLAUDE\.md\.template)$/,
121
+ },
122
+ {
123
+ rule: 'generated-fact-copy',
124
+ contract: null,
125
+ changeClass: 'asset-sync',
126
+ pattern: /(?:^|\/)(?:\.ai|ai)\/guides\/(?:modules|upstream)\/.+$/,
127
+ },
128
+ {
129
+ rule: 'routing-guide-owner',
130
+ contract: 'routing',
131
+ changeClass: 'knowledge-contract',
132
+ pattern: /(?:^|\/)(?:\.ai|ai)\/guides\/[^/]+\.md$/,
133
+ },
134
+ {
135
+ rule: 'skill-authority',
136
+ contract: 'skill-link',
137
+ changeClass: 'knowledge-contract',
138
+ pattern: /(?:^|\/)skills\/(?:[^/]+\/(?:SKILL\.md|references\/.+|scripts\/.+)|tiers\.schema\.json)$/,
139
+ },
140
+ {
141
+ rule: 'discovery-generator-contract',
142
+ contract: 'discovery',
143
+ changeClass: 'knowledge-contract',
144
+ pattern: /^(?:packages\/cli\/src\/lib\/generators\/.+|packages\/create-app\/src\/setup\/.+|packages\/create-app\/build\.mjs)$/,
145
+ },
146
+ {
147
+ rule: 'materialized-copy',
148
+ contract: null,
149
+ changeClass: 'asset-sync',
150
+ pattern: /^(?:packages\/create-app\/dist\/.+|apps\/docs\/.+|\.ai\/specs\/.+\.md)$/,
151
+ },
152
+ {
153
+ rule: 'docs-count-snapshot',
154
+ contract: null,
155
+ changeClass: 'asset-sync',
156
+ pattern: /(?:^|\/)(?:README\.md|RELEASE\.md|CHANGELOG\.md)$/,
157
+ },
158
+ {
159
+ rule: 'focused-test',
160
+ contract: 'evaluator',
161
+ changeClass: 'knowledge-contract',
162
+ pattern: FOCUSED_TEST_PATTERN,
163
+ },
164
+ ])
165
+
166
+ export function normalizeRelativePath(value) {
167
+ return String(value ?? '').split('\\').join('/').replace(/^\.\//, '')
168
+ }
169
+
170
+ export function isFocusedTestPath(value) {
171
+ return FOCUSED_TEST_PATTERN.test(normalizeRelativePath(value))
172
+ }
173
+
174
+ export function classifyChangedPath(value) {
175
+ const relativePath = normalizeRelativePath(value)
176
+ for (const rule of CLASSIFICATION_RULES) {
177
+ if (!rule.pattern.test(relativePath)) continue
178
+ return {
179
+ path: relativePath,
180
+ rule: rule.rule,
181
+ contract: rule.contract,
182
+ changeClass: rule.changeClass,
183
+ focusedTest: isFocusedTestPath(relativePath),
184
+ }
185
+ }
186
+ return {
187
+ path: relativePath,
188
+ rule: 'unclassified-fails-closed',
189
+ contract: null,
190
+ changeClass: 'knowledge-contract',
191
+ focusedTest: false,
192
+ }
193
+ }
194
+
195
+ export function deriveChangeClassification(paths) {
196
+ const records = [...new Set((paths ?? []).map(normalizeRelativePath))].sort().map(classifyChangedPath)
197
+ const contracts = new Set()
198
+ let changeClass = 'asset-sync'
199
+ for (const record of records) {
200
+ if (record.contract) contracts.add(record.contract)
201
+ if (record.changeClass === 'knowledge-contract') changeClass = 'knowledge-contract'
202
+ }
203
+ return {
204
+ changeClass,
205
+ changedContracts: CHANGED_CONTRACTS.filter((contract) => contracts.has(contract)),
206
+ records,
207
+ unclassifiedPaths: records.filter((record) => record.rule === 'unclassified-fails-closed').map((record) => record.path),
208
+ focusedTestPaths: records.filter((record) => record.focusedTest).map((record) => record.path),
209
+ }
210
+ }
211
+
212
+ function resolveSchemaReference(rootSchema, reference) {
213
+ if (typeof reference !== 'string' || !reference.startsWith('#/')) return null
214
+ let node = rootSchema
215
+ for (const rawSegment of reference.slice(2).split('/')) {
216
+ const segment = rawSegment.replaceAll('~1', '/').replaceAll('~0', '~')
217
+ if (node === null || typeof node !== 'object' || !Object.hasOwn(node, segment)) return null
218
+ node = node[segment]
219
+ }
220
+ return node
221
+ }
222
+
223
+ function isPlainObject(value) {
224
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
225
+ }
226
+
227
+ function matchesSchemaType(value, type) {
228
+ if (type === 'object') return isPlainObject(value)
229
+ if (type === 'array') return Array.isArray(value)
230
+ if (type === 'string') return typeof value === 'string'
231
+ if (type === 'integer') return Number.isInteger(value)
232
+ if (type === 'number') return typeof value === 'number' && Number.isFinite(value)
233
+ if (type === 'boolean') return typeof value === 'boolean'
234
+ if (type === 'null') return value === null
235
+ return false
236
+ }
237
+
238
+ export function validateJsonSchema(value, schema, location = '$', rootSchema = schema) {
239
+ if (schema === true) return []
240
+ if (schema === false) return [`${location} is not allowed by its schema`]
241
+ const errors = []
242
+ if (schema.$ref) {
243
+ const resolved = resolveSchemaReference(rootSchema, schema.$ref)
244
+ if (!resolved) return [`${location} has an unresolved schema reference ${schema.$ref}`]
245
+ errors.push(...validateJsonSchema(value, resolved, location, rootSchema))
246
+ }
247
+ for (const branch of schema.allOf ?? []) errors.push(...validateJsonSchema(value, branch, location, rootSchema))
248
+ if (Array.isArray(schema.anyOf) && !schema.anyOf.some((branch) => validateJsonSchema(value, branch, location, rootSchema).length === 0)) {
249
+ errors.push(`${location} does not match any allowed schema branch`)
250
+ }
251
+ if (Array.isArray(schema.oneOf)) {
252
+ const matched = schema.oneOf.filter((branch) => validateJsonSchema(value, branch, location, rootSchema).length === 0)
253
+ if (matched.length !== 1) errors.push(`${location} must match exactly one schema branch`)
254
+ }
255
+ if (Object.hasOwn(schema, 'not') && validateJsonSchema(value, schema.not, location, rootSchema).length === 0) {
256
+ errors.push(`${location} matches a forbidden schema branch${schema.description ? `: ${schema.description}` : ''}`)
257
+ }
258
+ const expectedTypes = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : []
259
+ if (expectedTypes.length && !expectedTypes.some((type) => matchesSchemaType(value, type))) {
260
+ errors.push(`${location} must be ${expectedTypes.join(' or ')}`)
261
+ return errors
262
+ }
263
+ if (Object.hasOwn(schema, 'const') && value !== schema.const) errors.push(`${location} must equal its schema constant`)
264
+ if (schema.enum && !schema.enum.some((item) => JSON.stringify(item) === JSON.stringify(value))) {
265
+ errors.push(`${location} is not in its schema enum`)
266
+ }
267
+ if (typeof value === 'string') {
268
+ if (Number.isInteger(schema.minLength) && value.length < schema.minLength) errors.push(`${location} is below minLength ${schema.minLength}`)
269
+ if (Number.isInteger(schema.maxLength) && value.length > schema.maxLength) errors.push(`${location} exceeds maxLength ${schema.maxLength}`)
270
+ if (schema.pattern && !new RegExp(schema.pattern).test(value)) errors.push(`${location} does not match its schema pattern`)
271
+ }
272
+ if (typeof value === 'number') {
273
+ if (Number.isFinite(schema.minimum) && value < schema.minimum) errors.push(`${location} is below minimum ${schema.minimum}`)
274
+ if (Number.isFinite(schema.maximum) && value > schema.maximum) errors.push(`${location} exceeds maximum ${schema.maximum}`)
275
+ }
276
+ if (Array.isArray(value)) {
277
+ if (Number.isInteger(schema.minItems) && value.length < schema.minItems) errors.push(`${location} is below minItems ${schema.minItems}`)
278
+ if (Number.isInteger(schema.maxItems) && value.length > schema.maxItems) errors.push(`${location} exceeds maxItems ${schema.maxItems}`)
279
+ if (schema.uniqueItems && new Set(value.map((item) => JSON.stringify(item))).size !== value.length) errors.push(`${location} must contain unique items`)
280
+ if (schema.items) value.forEach((item, index) => errors.push(...validateJsonSchema(item, schema.items, `${location}[${index}]`, rootSchema)))
281
+ }
282
+ if (isPlainObject(value)) {
283
+ for (const required of schema.required ?? []) if (!Object.hasOwn(value, required)) errors.push(`${location}.${required} is required`)
284
+ if (schema.additionalProperties === false) {
285
+ for (const key of Object.keys(value)) if (!Object.hasOwn(schema.properties ?? {}, key)) errors.push(`${location}.${key} is not allowed`)
286
+ }
287
+ for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
288
+ if (Object.hasOwn(value, key)) errors.push(...validateJsonSchema(value[key], childSchema, `${location}.${key}`, rootSchema))
289
+ }
290
+ }
291
+ return errors
292
+ }
293
+
294
+ function hashFile(absolutePath) {
295
+ return createHash('sha256').update(fs.readFileSync(absolutePath)).digest('hex')
296
+ }
297
+
298
+ function isRegularFile(absolutePath) {
299
+ try { return fs.statSync(absolutePath).isFile() } catch { return false }
300
+ }
301
+
302
+ function readJsonIfPresent(absolutePath) {
303
+ if (!isRegularFile(absolutePath)) return null
304
+ try { return JSON.parse(fs.readFileSync(absolutePath, 'utf8')) } catch { return null }
305
+ }
306
+
307
+ export function expandCaseRange(rangeId) {
308
+ const match = /^OMH-([0-9]{3})\.\.OMH-([0-9]{3})$/.exec(String(rangeId ?? ''))
309
+ if (!match) return null
310
+ const low = Number(match[1])
311
+ const high = Number(match[2])
312
+ if (low > high) return null
313
+ const ids = []
314
+ for (let index = low; index <= high; index += 1) ids.push(`OMH-${String(index).padStart(3, '0')}`)
315
+ return ids
316
+ }
317
+
318
+ function git(root, args) {
319
+ const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' })
320
+ if (result.status !== 0) {
321
+ throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`)
322
+ }
323
+ return result.stdout
324
+ }
325
+
326
+ export function resolveGitSha(root, ref) {
327
+ return git(root, ['rev-parse', '--verify', `${ref}^{commit}`]).trim()
328
+ }
329
+
330
+ export function deriveChangedPaths(root, baseSha) {
331
+ const tracked = git(root, ['diff', '--name-only', '--no-renames', baseSha]).split('\n')
332
+ const untracked = git(root, ['ls-files', '--others', '--exclude-standard']).split('\n')
333
+ return [...new Set([...tracked, ...untracked].map((line) => line.trim()).filter(Boolean).map(normalizeRelativePath))].sort()
334
+ }
335
+
336
+ function exampleMirrorPath(relativePath) {
337
+ if (relativePath.startsWith(`${EXAMPLE_AUTHORING_ROOT}/`)) {
338
+ return `${EXAMPLE_TEMPLATE_ROOT}/${relativePath.slice(EXAMPLE_AUTHORING_ROOT.length + 1)}`
339
+ }
340
+ if (relativePath.startsWith(`${EXAMPLE_TEMPLATE_ROOT}/`)) {
341
+ return `${EXAMPLE_AUTHORING_ROOT}/${relativePath.slice(EXAMPLE_TEMPLATE_ROOT.length + 1)}`
342
+ }
343
+ return null
344
+ }
345
+
346
+ export function exampleReadStatus(relativePath) {
347
+ const segments = normalizeRelativePath(relativePath).split('/')
348
+ return segments.some((segment) => EXAMPLE_QA_ONLY_SEGMENTS.includes(segment)) ? 'qa-only' : 'readable'
349
+ }
350
+
351
+ function checkExampleSourceMirrors(root, records, errors, derived) {
352
+ const mirrors = []
353
+ for (const record of records) {
354
+ if (record.contract !== 'example-source') continue
355
+ const mirror = exampleMirrorPath(record.path)
356
+ if (!mirror) continue
357
+ const sourceAbsolute = path.join(root, record.path)
358
+ const mirrorAbsolute = path.join(root, mirror)
359
+ const sourceExists = isRegularFile(sourceAbsolute)
360
+ const mirrorExists = isRegularFile(mirrorAbsolute)
361
+ const entry = { path: record.path, mirrorPath: mirror, readStatus: exampleReadStatus(record.path) }
362
+ if (!sourceExists && !mirrorExists) {
363
+ entry.state = 'deleted-on-both-sides'
364
+ } else if (!sourceExists || !mirrorExists) {
365
+ entry.state = 'mirror-missing'
366
+ errors.push(
367
+ `example-source ${record.path} was moved or deleted without its byte-identical mirror ${mirror}; ` +
368
+ 'run `yarn template:sync:fix` and update the surface inventory',
369
+ )
370
+ } else if (hashFile(sourceAbsolute) !== hashFile(mirrorAbsolute)) {
371
+ entry.state = 'mirror-drift'
372
+ errors.push(`example-source ${record.path} is not byte-identical to its mirror ${mirror}; run \`yarn template:sync:fix\``)
373
+ } else {
374
+ entry.state = 'mirrored'
375
+ entry.sha256 = hashFile(sourceAbsolute)
376
+ }
377
+ mirrors.push(entry)
378
+ }
379
+ derived.exampleSourceMirrors = mirrors
380
+ }
381
+
382
+ function checkFileRecords(root, label, records, errors, options = {}) {
383
+ for (const record of records) {
384
+ const absolute = path.join(root, record.path)
385
+ if (!isRegularFile(absolute)) {
386
+ errors.push(`${label} ${record.path} is not an exact regular file in the working tree`)
387
+ continue
388
+ }
389
+ const actual = hashFile(absolute)
390
+ if (actual !== record.sha256) {
391
+ errors.push(`${label} ${record.path} sha256 is stale: manifest ${record.sha256}, working tree ${actual}`)
392
+ }
393
+ if (!options.sourcePathRequired) continue
394
+ const sourceAbsolute = path.join(root, record.sourcePath)
395
+ if (!isRegularFile(sourceAbsolute)) {
396
+ errors.push(`${label} ${record.path} names sourcePath ${record.sourcePath}, which is not an exact regular file`)
397
+ }
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Re-derive the source-link facts a run manifest declares, from the inventory's own records
403
+ * rather than from its summary block: an author who edits the summary must not be able to
404
+ * make a wrong declaration agree with it.
405
+ */
406
+ export function deriveSourceLinkInventoryFacts(inventory) {
407
+ const records = isPlainObject(inventory) && Array.isArray(inventory.records) ? inventory.records : null
408
+ if (records === null) return null
409
+ return {
410
+ ownerCount: new Set(records.map((record) => record?.originAsset)).size,
411
+ topicCount: new Set(records.map((record) => record?.topicId)).size,
412
+ resolvedLinkCount: records.reduce(
413
+ (total, record) => total + (Number.isInteger(record?.renderedLinkCount) ? record.renderedLinkCount : 0),
414
+ 0,
415
+ ),
416
+ declaredBaselinePath: typeof inventory.inputs?.baseline === 'string' ? inventory.inputs.baseline : null,
417
+ }
418
+ }
419
+
420
+ const EMPTY_BASELINE_FACTS = Object.freeze({
421
+ baselineAssetCount: null,
422
+ baselineDispositionCount: null,
423
+ baselineSha: null,
424
+ })
425
+
426
+ export function deriveSourceLinkBaselineFacts(baseline) {
427
+ if (!isPlainObject(baseline)) return null
428
+ return {
429
+ baselineAssetCount: Array.isArray(baseline.baselineAssets) ? baseline.baselineAssets.length : null,
430
+ baselineDispositionCount: Array.isArray(baseline.blocks) ? baseline.blocks.length : null,
431
+ baselineSha: typeof baseline.baselineSha === 'string' ? baseline.baselineSha : null,
432
+ }
433
+ }
434
+
435
+ function siblingPath(relativePath, basename) {
436
+ const segments = normalizeRelativePath(relativePath).split('/')
437
+ segments[segments.length - 1] = basename
438
+ return segments.join('/')
439
+ }
440
+
441
+ /**
442
+ * A minimal, schema-valid parity ledger. It is never written anywhere: it exists only so the
443
+ * declared ledger schema can be probed before the real ledger is trusted to it.
444
+ */
445
+ const BASELINE_SCHEMA_PROBE_LEDGER = Object.freeze({
446
+ baselineSha: '0'.repeat(40),
447
+ baselineAssets: [{ path: 'AGENTS.md', sha256: '1'.repeat(64), expectedFenceCount: 1 }],
448
+ blocks: [{
449
+ id: 'main:AGENTS.md#fence:1',
450
+ asset: 'AGENTS.md',
451
+ ordinal: 1,
452
+ heading: null,
453
+ openingLine: '```ts',
454
+ info: 'ts',
455
+ contentSha256: '2'.repeat(64),
456
+ topicIds: [],
457
+ disposition: 'linked',
458
+ }],
459
+ })
460
+
461
+ function probeLedger(mutate) {
462
+ const ledger = structuredClone(BASELINE_SCHEMA_PROBE_LEDGER)
463
+ mutate(ledger)
464
+ return ledger
465
+ }
466
+
467
+ /**
468
+ * The clauses the gate relies on, each as a ledger the schema must reject. They are a readable
469
+ * checklist of what the pinned schema is trusted to assert — and a second line of defence for the
470
+ * one moment the SHA pin below is open, namely a change that legitimately updates the pin: guttings
471
+ * that the new bytes would smuggle in still have to survive every probe.
472
+ */
473
+ export const BASELINE_SCHEMA_PROBES = Object.freeze([
474
+ ['a non-hex baselineSha', probeLedger((ledger) => { ledger.baselineSha = 'not-a-commit' })],
475
+ ['a ledger missing its blocks array', probeLedger((ledger) => { delete ledger.blocks })],
476
+ ['a ledger missing its baselineAssets array', probeLedger((ledger) => { delete ledger.baselineAssets })],
477
+ ['an undeclared top-level ledger field', probeLedger((ledger) => { ledger.supersededBy = 'anything' })],
478
+ ['an absolute baseline asset path', probeLedger((ledger) => { ledger.baselineAssets[0].path = '/etc/passwd' })],
479
+ ['a parent-traversal baseline asset path', probeLedger((ledger) => { ledger.baselineAssets[0].path = '../outside.md' })],
480
+ ['a baseline asset missing its sha256', probeLedger((ledger) => { delete ledger.baselineAssets[0].sha256 })],
481
+ ['a baseline asset missing its expectedFenceCount', probeLedger((ledger) => { delete ledger.baselineAssets[0].expectedFenceCount })],
482
+ ['a non-hex baseline asset sha256', probeLedger((ledger) => { ledger.baselineAssets[0].sha256 = 'not-a-hash' })],
483
+ ['an undeclared baseline asset field', probeLedger((ledger) => { ledger.baselineAssets[0].note = 'anything' })],
484
+ ['a parent-traversal block asset path', probeLedger((ledger) => { ledger.blocks[0].asset = '../outside.md' })],
485
+ ['a block id that is not main:<asset>#fence:<ordinal>', probeLedger((ledger) => { ledger.blocks[0].id = 'AGENTS.md:1' })],
486
+ ['a block missing its contentSha256', probeLedger((ledger) => { delete ledger.blocks[0].contentSha256 })],
487
+ ['a block missing its topicIds', probeLedger((ledger) => { delete ledger.blocks[0].topicIds })],
488
+ ['a block missing its disposition', probeLedger((ledger) => { delete ledger.blocks[0].disposition })],
489
+ ['a block missing its ordinal', probeLedger((ledger) => { delete ledger.blocks[0].ordinal })],
490
+ ['a non-integer block ordinal', probeLedger((ledger) => { ledger.blocks[0].ordinal = '1' })],
491
+ ['a non-hex block contentSha256', probeLedger((ledger) => { ledger.blocks[0].contentSha256 = 'not-a-hash' })],
492
+ ['an invented block disposition', probeLedger((ledger) => { ledger.blocks[0].disposition = 'invented-disposition' })],
493
+ ['an undeclared block field', probeLedger((ledger) => { ledger.blocks[0].supersededBy = 'anything' })],
494
+ ['repeated block topic IDs', probeLedger((ledger) => { ledger.blocks[0].topicIds = ['guides/a:one.ts', 'guides/a:one.ts'] })],
495
+ ])
496
+
497
+ export const SOURCE_LINK_BASELINE_SCHEMA_ID = 'https://open-mercato.dev/schemas/source-link-baseline.schema.json'
498
+
499
+ /**
500
+ * The exact bytes of `source-link-baseline.schema.json`. Behavioural probing alone is not a pin:
501
+ * this engine supports `anyOf`/`oneOf`/`not`, so a schema carrying the pinned `$id`, the real body in
502
+ * one branch and an escape branch no probe ever matches passes every probe while accepting a lawless
503
+ * ledger. Pinning the file itself is what makes "a gutted or swapped schema fails the run" true, and
504
+ * pinning a baseline by SHA is what the rest of this subsystem already does — `source-link-baseline.json`
505
+ * pins eight asset SHAs the same way. Updating this constant when the schema legitimately changes is
506
+ * the normal path: that edit lands in the `evaluator` contract, not in the `source-link` assets under
507
+ * audit, so it can never be made by the ledger change it would be waving through.
508
+ * Recompute with `sha256sum packages/create-app/scripts/source-links/source-link-baseline.schema.json`.
509
+ */
510
+ export const SOURCE_LINK_BASELINE_SCHEMA_SHA256 =
511
+ 'b1ce4c0845b078b317cd0f699921628a2c23edb7271c883d0597de73dd401cc8'
512
+
513
+ /**
514
+ * Prove the declared schema is the pinned one and still asserts what the gate relies on. Returns the
515
+ * reasons it is not; a non-empty result means the ledger must not be validated against it.
516
+ */
517
+ export function baselineSchemaPinErrors(baselineSchema, schemaSha256) {
518
+ const reasons = []
519
+ if (schemaSha256 !== SOURCE_LINK_BASELINE_SCHEMA_SHA256) {
520
+ reasons.push(
521
+ `its sha256 is ${JSON.stringify(schemaSha256 ?? null)} rather than the pinned ${SOURCE_LINK_BASELINE_SCHEMA_SHA256}`,
522
+ )
523
+ }
524
+ if (!isPlainObject(baselineSchema)) {
525
+ reasons.push('it is not a JSON schema object')
526
+ return reasons
527
+ }
528
+ if (baselineSchema.$id !== SOURCE_LINK_BASELINE_SCHEMA_ID) {
529
+ reasons.push(`its $id is ${JSON.stringify(baselineSchema.$id)} rather than ${SOURCE_LINK_BASELINE_SCHEMA_ID}`)
530
+ }
531
+ const acceptErrors = validateJsonSchema(BASELINE_SCHEMA_PROBE_LEDGER, baselineSchema, '$', baselineSchema)
532
+ if (acceptErrors.length) reasons.push(`it rejects a canonical minimal ledger: ${acceptErrors.join('; ')}`)
533
+ for (const [label, ledger] of BASELINE_SCHEMA_PROBES) {
534
+ if (!validateJsonSchema(ledger, baselineSchema, '$', baselineSchema).length) reasons.push(`it accepts ${label}`)
535
+ }
536
+ return reasons
537
+ }
538
+
539
+ /**
540
+ * `uniqueItems` compares whole serialized records, so two blocks sharing an `id` and differing in
541
+ * any other field satisfy it. Block IDs are the ledger's join key, so uniqueness is checked here
542
+ * rather than left to a schema vocabulary that cannot express it.
543
+ */
544
+ function duplicateBlockIds(baseline) {
545
+ const seen = new Set()
546
+ const duplicates = new Set()
547
+ for (const block of Array.isArray(baseline?.blocks) ? baseline.blocks : []) {
548
+ if (typeof block?.id !== 'string') continue
549
+ if (seen.has(block.id)) duplicates.add(block.id)
550
+ seen.add(block.id)
551
+ }
552
+ return [...duplicates].sort()
553
+ }
554
+
555
+ function checkDeclaredCount(errors, field, declared, actual, subject) {
556
+ if (!Number.isInteger(declared) || declared === actual) return
557
+ errors.push(`sourceLinkInventory.${field} declares ${declared}, but ${subject} resolves ${actual}`)
558
+ }
559
+
560
+ /**
561
+ * Enforce every field of the manifest's `sourceLinkInventory` block against the checked assets it
562
+ * names. Declaring a count is worthless unless the count is compared with the real one, so each
563
+ * field is re-derived here: owners, topics and rendered links from the inventory records, assets,
564
+ * dispositions and the pinned baseline SHA from the parity ledger, and the ledger's whole shape
565
+ * from the schema the manifest names.
566
+ */
567
+ function checkSourceLinkInventory(root, declared, errors, derived) {
568
+ const declaredInventoryPath = declared?.path
569
+ if (typeof declaredInventoryPath === 'string' && declaredInventoryPath !== SOURCE_LINK_INVENTORY_PATH) {
570
+ errors.push(`sourceLinkInventory.path must be ${SOURCE_LINK_INVENTORY_PATH}`)
571
+ }
572
+ const inventoryPath = typeof declaredInventoryPath === 'string' ? declaredInventoryPath : SOURCE_LINK_INVENTORY_PATH
573
+ if (!isRegularFile(path.join(root, inventoryPath))) {
574
+ derived.sourceLinkInventoryStatus = 'absent'
575
+ errors.push(CANON_C_REASON)
576
+ return
577
+ }
578
+ derived.sourceLinkInventoryStatus = 'present'
579
+
580
+ const inventoryFacts = deriveSourceLinkInventoryFacts(readJsonIfPresent(path.join(root, inventoryPath)))
581
+ if (inventoryFacts === null) {
582
+ errors.push(`sourceLinkInventory.path ${inventoryPath} is not a readable inventory: it carries no records array`)
583
+ return
584
+ }
585
+ derived.sourceLinkInventoryFacts = { ...inventoryFacts, ...EMPTY_BASELINE_FACTS }
586
+ checkDeclaredCount(errors, 'expectedOwnerCount', declared.expectedOwnerCount, inventoryFacts.ownerCount, `${inventoryPath} distinct originAsset owners`)
587
+ checkDeclaredCount(errors, 'expectedTopicCount', declared.expectedTopicCount, inventoryFacts.topicCount, `${inventoryPath} distinct topic IDs`)
588
+ checkDeclaredCount(errors, 'resolvedLinkCount', declared.resolvedLinkCount, inventoryFacts.resolvedLinkCount, `${inventoryPath} rendered links`)
589
+
590
+ if (inventoryFacts.declaredBaselinePath !== SOURCE_LINK_BASELINE_PATH) {
591
+ errors.push(
592
+ `sourceLinkInventory: ${inventoryPath} declares inputs.baseline `
593
+ + `${JSON.stringify(inventoryFacts.declaredBaselinePath)}, but the parity ledger is pinned to `
594
+ + `${SOURCE_LINK_BASELINE_PATH}, so no asset under audit may redirect which ledger is checked`,
595
+ )
596
+ return
597
+ }
598
+ const declaredBaselinePath = declared?.baselinePath
599
+ if (typeof declaredBaselinePath !== 'string') return
600
+ if (declaredBaselinePath !== SOURCE_LINK_BASELINE_PATH) {
601
+ errors.push(`sourceLinkInventory.baselinePath must be ${SOURCE_LINK_BASELINE_PATH}`)
602
+ return
603
+ }
604
+ if (!isRegularFile(path.join(root, declaredBaselinePath))) {
605
+ errors.push(`sourceLinkInventory.baselinePath ${declaredBaselinePath} is not an exact regular file in the working tree`)
606
+ return
607
+ }
608
+ const baseline = readJsonIfPresent(path.join(root, declaredBaselinePath))
609
+ const baselineFacts = deriveSourceLinkBaselineFacts(baseline)
610
+ if (baselineFacts === null) {
611
+ errors.push(`sourceLinkInventory.baselinePath ${declaredBaselinePath} is not a readable JSON object`)
612
+ return
613
+ }
614
+ derived.sourceLinkInventoryFacts = { ...inventoryFacts, ...baselineFacts }
615
+ const repeatedBlockIds = duplicateBlockIds(baseline)
616
+ if (repeatedBlockIds.length) {
617
+ errors.push(`sourceLinkInventory ${declaredBaselinePath} repeats block ids: ${repeatedBlockIds.join(', ')}`)
618
+ }
619
+ checkDeclaredCount(errors, 'baselineAssetCount', declared.baselineAssetCount, baselineFacts.baselineAssetCount, `${declaredBaselinePath} pinned assets`)
620
+ checkDeclaredCount(
621
+ errors,
622
+ 'baselineDispositionCount',
623
+ declared.baselineDispositionCount,
624
+ baselineFacts.baselineDispositionCount,
625
+ `${declaredBaselinePath} disposition records`,
626
+ )
627
+ if (typeof declared.baselineRef === 'string' && declared.baselineRef !== baselineFacts.baselineSha) {
628
+ errors.push(
629
+ `sourceLinkInventory.baselineRef declares ${declared.baselineRef}, but ${declaredBaselinePath} pins `
630
+ + `${String(baselineFacts.baselineSha)}`,
631
+ )
632
+ }
633
+
634
+ const expectedSchemaPath = siblingPath(SOURCE_LINK_BASELINE_PATH, SOURCE_LINK_BASELINE_SCHEMA_BASENAME)
635
+ const declaredSchemaPath = declared?.baselineSchemaPath
636
+ if (typeof declaredSchemaPath !== 'string') return
637
+ if (declaredSchemaPath !== expectedSchemaPath) {
638
+ errors.push(`sourceLinkInventory.baselineSchemaPath declares ${declaredSchemaPath}, but the ledger's schema is ${expectedSchemaPath}`)
639
+ return
640
+ }
641
+ const schemaAbsolutePath = path.join(root, declaredSchemaPath)
642
+ if (!isRegularFile(schemaAbsolutePath)) {
643
+ errors.push(`sourceLinkInventory.baselineSchemaPath ${declaredSchemaPath} is not an exact regular file in the working tree`)
644
+ return
645
+ }
646
+ const baselineSchema = readJsonIfPresent(schemaAbsolutePath)
647
+ const pinErrors = baselineSchemaPinErrors(baselineSchema, hashFile(schemaAbsolutePath))
648
+ if (pinErrors.length) {
649
+ for (const reason of pinErrors) {
650
+ errors.push(`sourceLinkInventory.baselineSchemaPath ${declaredSchemaPath} no longer pins the ledger contract: ${reason}`)
651
+ }
652
+ return
653
+ }
654
+ for (const error of validateJsonSchema(baseline, baselineSchema, '$', baselineSchema)) {
655
+ errors.push(`sourceLinkInventory ${declaredBaselinePath} violates ${declaredSchemaPath}: ${error}`)
656
+ }
657
+ }
658
+
659
+ function baseFileHash(root, baseSha, relativePath) {
660
+ const result = spawnSync('git', ['-C', root, 'show', `${baseSha}:${relativePath}`], { encoding: 'buffer' })
661
+ if (result.status !== 0) return null
662
+ return createHash('sha256').update(result.stdout).digest('hex')
663
+ }
664
+
665
+ function baseFileBuffer(root, baseSha, relativePath) {
666
+ const result = spawnSync('git', ['-C', root, 'show', `${baseSha}:${relativePath}`], { encoding: 'buffer' })
667
+ return result.status === 0 ? result.stdout : null
668
+ }
669
+
670
+ const NODE_TEST_SCRIPT_PATTERN = /(?:^|\s)--test(?:[=\s]|$)/
671
+
672
+ /**
673
+ * The controller only drives an exact `node --test` argv. It reads the nearest owning package's
674
+ * declared test script so a project on another runner is refused with its real script name instead
675
+ * of being handed a command that would not run its suite.
676
+ */
677
+ export function resolveTestRunnerFamily(root, testFile) {
678
+ const segments = normalizeRelativePath(testFile).split('/').slice(0, -1)
679
+ const directories = ['']
680
+ let accumulated = ''
681
+ for (const segment of segments) {
682
+ accumulated = accumulated ? `${accumulated}/${segment}` : segment
683
+ directories.push(accumulated)
684
+ }
685
+ for (const directory of directories.reverse()) {
686
+ const manifest = readJsonIfPresent(path.join(root, directory, 'package.json'))
687
+ const declared = manifest?.scripts?.test
688
+ if (typeof declared !== 'string' || !declared.trim()) continue
689
+ return NODE_TEST_SCRIPT_PATTERN.test(declared)
690
+ ? { family: 'node-test', declaredScript: declared }
691
+ : { family: 'unsupported', declaredScript: declared }
692
+ }
693
+ return { family: 'node-test', declaredScript: null }
694
+ }
695
+
696
+ export function deriveFocusedCommand(testFile, runnerFamily = { family: 'node-test', declaredScript: null }) {
697
+ const relativePath = normalizeRelativePath(testFile)
698
+ if (runnerFamily.family !== 'node-test') {
699
+ return {
700
+ error:
701
+ `the owning package of ${relativePath} declares the test script "${runnerFamily.declaredScript}", which the ` +
702
+ 'controller cannot drive as an exact argv; it only drives a `node --test` runner today',
703
+ }
704
+ }
705
+ const runner = FOCUSED_RUNNERS.find((candidate) => candidate.pattern.test(relativePath))
706
+ if (!runner) return { error: `no controller runner is registered for ${relativePath}` }
707
+ const argv = [process.execPath, ...runner.args(relativePath)]
708
+ const offending = argv.find((token) => SHELL_METACHARACTER_PATTERN.test(token))
709
+ if (offending !== undefined) {
710
+ return { error: `the command derived for ${relativePath} carries the shell metacharacter token "${offending}"` }
711
+ }
712
+ return { runner: runner.runner, argv }
713
+ }
714
+
715
+ function isContainedRelativePath(relativePath) {
716
+ const normalized = normalizeRelativePath(relativePath)
717
+ if (!normalized || normalized.startsWith('/') || /^[A-Za-z]:/.test(normalized)) return false
718
+ return !normalized.split('/').includes('..')
719
+ }
720
+
721
+ function overlayWorkingTreePaths(root, worktree, relativePaths) {
722
+ for (const relativePath of relativePaths) {
723
+ if (!isContainedRelativePath(relativePath)) continue
724
+ const source = path.join(root, relativePath)
725
+ const target = path.join(worktree, relativePath)
726
+ if (isRegularFile(source)) {
727
+ fs.mkdirSync(path.dirname(target), { recursive: true })
728
+ fs.copyFileSync(source, target)
729
+ } else if (fs.existsSync(target)) {
730
+ fs.rmSync(target, { force: true, recursive: true })
731
+ }
732
+ }
733
+ }
734
+
735
+ function linkResolutionRoots(root, worktree, relativePath) {
736
+ const segments = normalizeRelativePath(relativePath).split('/').slice(0, -1)
737
+ const directories = ['']
738
+ let accumulated = ''
739
+ for (const segment of segments) {
740
+ accumulated = accumulated ? `${accumulated}/${segment}` : segment
741
+ directories.push(accumulated)
742
+ }
743
+ for (const directory of directories) {
744
+ const target = path.join(root, directory, 'node_modules')
745
+ const link = path.join(worktree, directory, 'node_modules')
746
+ if (!fs.existsSync(target) || fs.existsSync(link)) continue
747
+ fs.mkdirSync(path.dirname(link), { recursive: true })
748
+ try { fs.symlinkSync(target, link, 'junction') } catch { /* resolution stays worktree-local */ }
749
+ }
750
+ }
751
+
752
+ // A controller invoked from inside a test runner would otherwise leak its runner context into the
753
+ // child, which suppresses the child's own exit status and makes every focused run look like a pass.
754
+ export const STRIPPED_EXECUTION_ENV_KEYS = Object.freeze([
755
+ 'NODE_OPTIONS', 'NODE_TEST_CONTEXT', 'NODE_V8_COVERAGE', 'TEST_RUNNER_CONCURRENCY',
756
+ ])
757
+
758
+ export function sanitizeExecutionEnv(sourceEnv) {
759
+ const env = { ...sourceEnv }
760
+ for (const key of STRIPPED_EXECUTION_ENV_KEYS) delete env[key]
761
+ return env
762
+ }
763
+
764
+ function runControlledCommand(cwd, argv, timeoutMs) {
765
+ const env = sanitizeExecutionEnv(process.env)
766
+ const result = spawnSync(argv[0], argv.slice(1), { cwd, env, timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 })
767
+ if (result.error || result.signal || typeof result.status !== 'number') {
768
+ return { failed: true, reason: result.signal ? `terminated by signal ${result.signal}` : String(result.error ?? 'no exit status') }
769
+ }
770
+ return {
771
+ exitCode: result.status,
772
+ stdoutSha256: createHash('sha256').update(result.stdout ?? Buffer.alloc(0)).digest('hex'),
773
+ stderrSha256: createHash('sha256').update(result.stderr ?? Buffer.alloc(0)).digest('hex'),
774
+ }
775
+ }
776
+
777
+ function addWorktree(root, sha, directory) {
778
+ git(root, ['worktree', 'add', '--detach', '--quiet', directory, sha])
779
+ }
780
+
781
+ // Returns true when the clean removal failed and left an administrative entry behind. The caller
782
+ // prunes only then: an unconditional prune would also drop sibling worktrees of the same repository
783
+ // whose directories happen to be unavailable, which is not this controller's to decide.
784
+ function removeWorktree(root, directory) {
785
+ try {
786
+ git(root, ['worktree', 'remove', '--force', directory])
787
+ return false
788
+ } catch {
789
+ fs.rmSync(directory, { recursive: true, force: true })
790
+ return true
791
+ }
792
+ }
793
+
794
+ /**
795
+ * Controller-owned focused execution. For each declared focused test the controller builds two
796
+ * isolated worktrees: the base commit with ONLY that test's own head content applied, and the head
797
+ * commit with the whole working-tree diff applied. It runs the derived argv exactly once per side —
798
+ * there is no retry — and records exit codes plus output hashes as evidence.
799
+ */
800
+ export function runFocusedExecutions(input) {
801
+ const root = path.resolve(input.root)
802
+ const baseSha = input.baseSha
803
+ const changedPaths = [...new Set((input.changedPaths ?? []).map(normalizeRelativePath))]
804
+ const timeoutMs = input.timeoutMs ?? DEFAULT_EXECUTION_TIMEOUT_MS
805
+ const executions = []
806
+ const errors = []
807
+ const testFiles = [...new Set((input.testFiles ?? []).map(normalizeRelativePath))]
808
+ if (!testFiles.length) return { executions, errors }
809
+
810
+ const scratch = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'om-knowledge-exec-'))
811
+ let removalFellBack = false
812
+ try {
813
+ for (const [index, testFile] of testFiles.entries()) {
814
+ const headAbsolute = path.join(root, testFile)
815
+ if (!isContainedRelativePath(testFile) || !isRegularFile(headAbsolute)) {
816
+ errors.push(`focusedExecutions ${testFile} is not an exact regular file inside the repository, so it cannot be executed`)
817
+ continue
818
+ }
819
+ const derivedCommand = deriveFocusedCommand(testFile, resolveTestRunnerFamily(root, testFile))
820
+ if (derivedCommand.error) { errors.push(`focusedExecutions ${derivedCommand.error}`); continue }
821
+
822
+ const headContents = fs.readFileSync(headAbsolute)
823
+ const baseContents = baseFileBuffer(root, baseSha, testFile)
824
+ if (baseContents !== null && baseContents.equals(headContents)) {
825
+ errors.push(
826
+ `focusedExecutions ${testFile} has no test-only diff against the base; a knowledge-contract change must add ` +
827
+ 'or repair the regression that proves the new behavior',
828
+ )
829
+ continue
830
+ }
831
+
832
+ const baseWorktree = path.join(scratch, `base-${index}`)
833
+ const headWorktree = path.join(scratch, `head-${index}`)
834
+ let baseOutcome
835
+ let headOutcome
836
+ try {
837
+ addWorktree(root, baseSha, baseWorktree)
838
+ overlayWorkingTreePaths(root, baseWorktree, [testFile])
839
+ linkResolutionRoots(root, baseWorktree, testFile)
840
+ baseOutcome = runControlledCommand(baseWorktree, derivedCommand.argv, timeoutMs)
841
+
842
+ addWorktree(root, input.headSha, headWorktree)
843
+ overlayWorkingTreePaths(root, headWorktree, changedPaths)
844
+ linkResolutionRoots(root, headWorktree, testFile)
845
+ headOutcome = runControlledCommand(headWorktree, derivedCommand.argv, timeoutMs)
846
+ } catch (error) {
847
+ errors.push(`focusedExecutions ${testFile} could not be executed in an isolated worktree: ${error.message}`)
848
+ continue
849
+ } finally {
850
+ removalFellBack = removeWorktree(root, baseWorktree) || removalFellBack
851
+ removalFellBack = removeWorktree(root, headWorktree) || removalFellBack
852
+ }
853
+
854
+ if (baseOutcome.failed || headOutcome.failed) {
855
+ const failing = baseOutcome.failed ? `base-plus-test-only (${baseOutcome.reason})` : `head (${headOutcome.reason})`
856
+ errors.push(`focusedExecutions ${testFile} produced no usable ${failing} exit status`)
857
+ continue
858
+ }
859
+ executions.push({
860
+ testFile,
861
+ command: derivedCommand.argv,
862
+ baseWithTestPatch: baseOutcome,
863
+ head: headOutcome,
864
+ })
865
+ }
866
+ } finally {
867
+ fs.rmSync(scratch, { recursive: true, force: true })
868
+ if (removalFellBack) { try { git(root, ['worktree', 'prune']) } catch { /* pruning is best effort */ } }
869
+ }
870
+ return { executions, errors }
871
+ }
872
+
873
+ export function assertFocusedExecutionEvidence(declaredTests, evidence) {
874
+ const errors = []
875
+ if (!evidence) {
876
+ return ['the controller produced no focused base/head execution evidence; a knowledge-contract change cannot be certified without it']
877
+ }
878
+ errors.push(...(evidence.errors ?? []))
879
+ const executions = evidence.executions ?? []
880
+ for (const testFile of declaredTests) {
881
+ const execution = executions.find((entry) => entry.testFile === normalizeRelativePath(testFile))
882
+ if (!execution) {
883
+ errors.push(`focusedExecutions has no base/head execution record for declared focused test ${testFile}`)
884
+ continue
885
+ }
886
+ if (execution.baseWithTestPatch.exitCode === 0) {
887
+ errors.push(
888
+ `focusedExecutions ${testFile} exited 0 on base-plus-test-only; the focused test must fail for the old behavior`,
889
+ )
890
+ }
891
+ if (execution.head.exitCode !== 0) {
892
+ errors.push(`focusedExecutions ${testFile} exited ${execution.head.exitCode} at head; the focused test must pass after the change`)
893
+ }
894
+ }
895
+ for (const execution of executions) {
896
+ if (!declaredTests.map(normalizeRelativePath).includes(execution.testFile)) {
897
+ errors.push(`focusedExecutions carries an execution for ${execution.testFile}, which the manifest does not declare`)
898
+ }
899
+ }
900
+ return errors
901
+ }
902
+
903
+ /**
904
+ * Validates one knowledge-change run manifest against the derived classification of
905
+ * its own diff. `changedPaths` is supplied by the caller so the derivation stays
906
+ * testable without a shell; the CLI derives it from git.
907
+ */
908
+ export function validateKnowledgeChange(input) {
909
+ const root = path.resolve(input.root)
910
+ const manifest = input.manifest
911
+ const schema = input.schema
912
+ const errors = []
913
+ const derived = {}
914
+
915
+ const schemaErrors = validateJsonSchema(manifest, schema.$defs.authoredManifest, '$', schema)
916
+ for (const field of CONTROLLER_OWNED_FIELDS) {
917
+ if (isPlainObject(manifest) && Object.hasOwn(manifest, field)) {
918
+ errors.push(`${field} is controller-owned output; author-supplied evidence fails validation`)
919
+ }
920
+ }
921
+ errors.push(...schemaErrors.map((error) => `manifest schema: ${error}`))
922
+ if (!isPlainObject(manifest)) return { ok: false, errors, derived }
923
+
924
+ const classification = deriveChangeClassification(input.changedPaths)
925
+ derived.changeClass = classification.changeClass
926
+ derived.changedContracts = classification.changedContracts
927
+ derived.classifiedPaths = classification.records
928
+ derived.unclassifiedPaths = classification.unclassifiedPaths
929
+
930
+ if (manifest.changeClass !== classification.changeClass) {
931
+ errors.push(
932
+ `declared changeClass "${manifest.changeClass}" differs from the class derived from the diff ` +
933
+ `"${classification.changeClass}"`,
934
+ )
935
+ }
936
+ const declaredContracts = Array.isArray(manifest.changedContracts) ? manifest.changedContracts : []
937
+ const missingContracts = classification.changedContracts.filter((contract) => !declaredContracts.includes(contract))
938
+ const extraContracts = declaredContracts.filter((contract) => !classification.changedContracts.includes(contract))
939
+ if (missingContracts.length) errors.push(`changedContracts omits derived contracts: ${missingContracts.join(', ')}`)
940
+ if (extraContracts.length) errors.push(`changedContracts declares contracts the diff does not touch: ${extraContracts.join(', ')}`)
941
+
942
+ checkFileRecords(root, 'authoritativeFiles', manifest.authoritativeFiles ?? [], errors)
943
+ checkFileRecords(root, 'generatedFiles', manifest.generatedFiles ?? [], errors, { sourcePathRequired: true })
944
+ for (const documentationPath of manifest.documentationFiles ?? []) {
945
+ if (!isRegularFile(path.join(root, documentationPath))) {
946
+ errors.push(`documentationFiles ${documentationPath} is not an exact regular file in the working tree`)
947
+ }
948
+ }
949
+
950
+ if (classification.changeClass === 'asset-sync' || manifest.changeClass === 'asset-sync') {
951
+ for (const record of classification.records) {
952
+ if (record.contract) errors.push(`asset-sync run touches ${record.path}, which carries the ${record.contract} contract`)
953
+ }
954
+ }
955
+ if (classification.changeClass === 'asset-sync') {
956
+ if (input.baseSha) {
957
+ for (const generated of manifest.generatedFiles ?? []) {
958
+ const baseHash = baseFileHash(root, input.baseSha, generated.sourcePath)
959
+ const headAbsolute = path.join(root, generated.sourcePath)
960
+ const headHash = isRegularFile(headAbsolute) ? hashFile(headAbsolute) : null
961
+ if (baseHash !== null && headHash !== null && baseHash !== headHash) {
962
+ errors.push(
963
+ `asset-sync run changed authoritative source ${generated.sourcePath}; ` +
964
+ 'a semantic source change is a knowledge-contract change',
965
+ )
966
+ }
967
+ }
968
+ }
969
+ }
970
+
971
+ if (classification.changeClass === 'knowledge-contract') {
972
+ const declaredTests = Array.isArray(manifest.focusedTestFiles) ? manifest.focusedTestFiles.map(normalizeRelativePath) : []
973
+ const changedTests = declaredTests.filter((testPath) => classification.focusedTestPaths.includes(testPath))
974
+ if (!declaredTests.length) {
975
+ errors.push('a knowledge-contract change requires at least one focused test that demonstrates the affected contract')
976
+ }
977
+ for (const testPath of declaredTests) {
978
+ if (!isFocusedTestPath(testPath)) errors.push(`focusedTestFiles ${testPath} is not a test file`)
979
+ if (!isRegularFile(path.join(root, testPath))) errors.push(`focusedTestFiles ${testPath} is not an exact regular file in the working tree`)
980
+ }
981
+ if (declaredTests.length && !changedTests.length) {
982
+ errors.push('no declared focused test appears in the diff; a knowledge-contract change must add or repair its own regression')
983
+ }
984
+
985
+ const cases = input.cases ?? readJsonIfPresent(path.join(root, input.harnessRelativeDir ?? '', 'cases.json'))
986
+ if (!Array.isArray(cases)) {
987
+ errors.push('the case catalog could not be read, so affected cases and the catalog count cannot be verified')
988
+ } else {
989
+ const knownCaseIds = new Set(cases.map((entry) => entry?.id))
990
+ for (const caseId of manifest.affectedCaseIds ?? []) {
991
+ if (!knownCaseIds.has(caseId)) errors.push(`affectedCaseIds ${caseId} does not exist in the case catalog`)
992
+ }
993
+ if (manifest.expectedCatalogCount !== cases.length) {
994
+ errors.push(`expectedCatalogCount ${manifest.expectedCatalogCount} does not match the catalog's ${cases.length} cases`)
995
+ }
996
+ for (const rangeId of manifest.affectedRanges ?? []) {
997
+ const expanded = expandCaseRange(rangeId)
998
+ if (!expanded) { errors.push(`affectedRanges ${rangeId} is not an ascending OMH range`); continue }
999
+ const missing = expanded.filter((caseId) => !knownCaseIds.has(caseId))
1000
+ if (missing.length) errors.push(`affectedRanges ${rangeId} covers cases absent from the catalog: ${missing.join(', ')}`)
1001
+ }
1002
+ const rangeMembers = new Set((manifest.affectedRanges ?? []).flatMap((rangeId) => expandCaseRange(rangeId) ?? []))
1003
+ for (const caseId of manifest.affectedCaseIds ?? []) {
1004
+ if (rangeMembers.size && !rangeMembers.has(caseId)) {
1005
+ errors.push(`affectedCaseIds ${caseId} falls outside every declared affected range`)
1006
+ }
1007
+ }
1008
+ }
1009
+
1010
+ const releaseMatrix = input.releaseMatrix ?? readJsonIfPresent(path.join(root, input.harnessRelativeDir ?? '', 'release-matrix.json'))
1011
+ if (!isPlainObject(releaseMatrix)) {
1012
+ errors.push('the release matrix could not be read, so required lanes cannot be verified')
1013
+ } else {
1014
+ for (const lane of manifest.requiredReleaseLanes ?? []) {
1015
+ if (!Object.hasOwn(releaseMatrix, lane)) errors.push(`requiredReleaseLanes ${lane} is not a lane in the release matrix`)
1016
+ }
1017
+ if (!(manifest.requiredReleaseLanes ?? []).length) {
1018
+ errors.push('a knowledge-contract change must name at least one affected certified release lane')
1019
+ }
1020
+ }
1021
+
1022
+ const executionErrors = assertFocusedExecutionEvidence(declaredTests, input.executionEvidence)
1023
+ derived.focusedExecutions = input.executionEvidence?.executions ?? []
1024
+ errors.push(...executionErrors)
1025
+ }
1026
+
1027
+ const linkedContracts = ['source-link', 'example-source', 'installed-source']
1028
+ const needsSourceLinkInventory = classification.changedContracts.some((contract) => linkedContracts.includes(contract))
1029
+ derived.sourceLinkInventoryRequired = needsSourceLinkInventory
1030
+ if (needsSourceLinkInventory) {
1031
+ checkSourceLinkInventory(root, isPlainObject(manifest.sourceLinkInventory) ? manifest.sourceLinkInventory : {}, errors, derived)
1032
+ }
1033
+
1034
+ checkExampleSourceMirrors(root, classification.records, errors, derived)
1035
+
1036
+ return { ok: errors.length === 0, errors, derived }
1037
+ }
1038
+
1039
+ function writeJsonAtomic(absolutePath, value) {
1040
+ const directory = path.dirname(absolutePath)
1041
+ fs.mkdirSync(directory, { recursive: true })
1042
+ const temporaryPath = path.join(directory, `.${path.basename(absolutePath)}.${process.pid}.tmp`)
1043
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`)
1044
+ fs.renameSync(temporaryPath, absolutePath)
1045
+ }
1046
+
1047
+ function usage() {
1048
+ return [
1049
+ 'Usage: validate-knowledge-change --manifest <path> --base <ref> [--root <dir>] [--harness-dir <dir>]',
1050
+ ' [--out <path>] [--execution-timeout <ms>]',
1051
+ '',
1052
+ 'Derives the knowledge-contract/asset-sync class from the diff between <ref> and the working tree,',
1053
+ 'validates the authored run manifest against knowledge-change.schema.json, and — for a knowledge-contract',
1054
+ 'change — runs each declared focused test once in an isolated base-plus-test-only worktree and once at head,',
1055
+ 'recording exit codes and output hashes as controller-owned evidence. The result is written atomically.',
1056
+ ].join('\n')
1057
+ }
1058
+
1059
+ function parseArgs(argv) {
1060
+ const options = { root: process.cwd(), help: false }
1061
+ for (let index = 0; index < argv.length; index += 1) {
1062
+ const token = argv[index]
1063
+ if (token === '--help' || token === '-h') { options.help = true; continue }
1064
+ const value = argv[index + 1]
1065
+ if (token === '--manifest') { options.manifest = value; index += 1; continue }
1066
+ if (token === '--base') { options.base = value; index += 1; continue }
1067
+ if (token === '--root') { options.root = value; index += 1; continue }
1068
+ if (token === '--harness-dir') { options.harnessDir = value; index += 1; continue }
1069
+ if (token === '--out') { options.out = value; index += 1; continue }
1070
+ if (token === '--execution-timeout') {
1071
+ const parsed = Number(value)
1072
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error('--execution-timeout must be a positive integer of milliseconds')
1073
+ options.executionTimeoutMs = parsed
1074
+ index += 1
1075
+ continue
1076
+ }
1077
+ throw new Error(`unknown argument: ${token}`)
1078
+ }
1079
+ return options
1080
+ }
1081
+
1082
+ function defaultHarnessRelativeDir(root) {
1083
+ const candidates = ['packages/create-app/agentic/shared/ai/harness', 'agentic/shared/ai/harness', '.ai/harness']
1084
+ return candidates.find((candidate) => isRegularFile(path.join(root, candidate, 'cases.json'))) ?? '.ai/harness'
1085
+ }
1086
+
1087
+ export function main(argv = process.argv.slice(2)) {
1088
+ let options
1089
+ try { options = parseArgs(argv) } catch (error) { console.error(error.message); console.error(usage()); return EXIT_INVALID }
1090
+ if (options.help) { console.log(usage()); return EXIT_PASS }
1091
+ if (!options.manifest || !options.base) { console.error('--manifest and --base are required'); console.error(usage()); return EXIT_INVALID }
1092
+
1093
+ const root = path.resolve(options.root)
1094
+ const harnessRelativeDir = options.harnessDir ?? defaultHarnessRelativeDir(root)
1095
+ const schemaCandidates = [
1096
+ path.join(root, harnessRelativeDir, SCHEMA_BASENAME),
1097
+ fileURLToPath(new URL(`../ai/harness/${SCHEMA_BASENAME}`, import.meta.url)),
1098
+ fileURLToPath(new URL(`../.ai/harness/${SCHEMA_BASENAME}`, import.meta.url)),
1099
+ ]
1100
+ const schemaPath = schemaCandidates.find((candidate) => isRegularFile(candidate))
1101
+ if (!schemaPath) { console.error(`knowledge-change schema not found: ${schemaCandidates.join(', ')}`); return EXIT_INVALID }
1102
+ const schema = readJsonIfPresent(schemaPath)
1103
+ if (!schema) { console.error(`knowledge-change schema is not valid JSON: ${schemaPath}`); return EXIT_INVALID }
1104
+
1105
+ const manifestPath = path.resolve(root, options.manifest)
1106
+ const manifest = readJsonIfPresent(manifestPath)
1107
+ if (manifest === null) { console.error(`run manifest not found or not valid JSON: ${manifestPath}`); return EXIT_INVALID }
1108
+
1109
+ let baseSha
1110
+ let headSha
1111
+ let changedPaths
1112
+ try {
1113
+ baseSha = resolveGitSha(root, options.base)
1114
+ headSha = resolveGitSha(root, 'HEAD')
1115
+ changedPaths = deriveChangedPaths(root, baseSha)
1116
+ } catch (error) { console.error(error.message); return EXIT_INVALID }
1117
+
1118
+ let baseRefError = null
1119
+ if (manifest.baseRef !== options.base && manifest.baseRef !== baseSha) {
1120
+ let authoredSha = null
1121
+ try { authoredSha = resolveGitSha(root, manifest.baseRef) } catch { authoredSha = null }
1122
+ if (authoredSha !== baseSha) {
1123
+ baseRefError = `authored baseRef "${manifest.baseRef}" does not resolve to the --base SHA ${baseSha}`
1124
+ }
1125
+ }
1126
+
1127
+ const classification = deriveChangeClassification(changedPaths)
1128
+ let executionEvidence = null
1129
+ if (classification.changeClass === 'knowledge-contract') {
1130
+ executionEvidence = baseRefError
1131
+ ? { executions: [], errors: ['focused base/head execution was not attempted because the authored baseRef does not resolve to --base'] }
1132
+ : runFocusedExecutions({
1133
+ root,
1134
+ baseSha,
1135
+ headSha,
1136
+ changedPaths,
1137
+ testFiles: Array.isArray(manifest.focusedTestFiles) ? manifest.focusedTestFiles : [],
1138
+ timeoutMs: options.executionTimeoutMs,
1139
+ })
1140
+ }
1141
+
1142
+ const result = validateKnowledgeChange({
1143
+ root, manifest, schema, changedPaths, baseSha, harnessRelativeDir, executionEvidence,
1144
+ })
1145
+ if (baseRefError) { result.errors.unshift(baseRefError); result.ok = false }
1146
+
1147
+ const completed = {
1148
+ ...manifest,
1149
+ resolvedBaseSha: baseSha,
1150
+ headSha,
1151
+ focusedExecutions: executionEvidence?.executions ?? [],
1152
+ }
1153
+ const completedErrors = validateJsonSchema(completed, schema.$defs.completedManifest, '$', schema)
1154
+ if (completedErrors.length) {
1155
+ result.errors.push(...completedErrors.map((error) => `completed manifest schema: ${error}`))
1156
+ result.ok = false
1157
+ }
1158
+ const outputPath = path.resolve(root, options.out ?? `${options.manifest}.result.json`)
1159
+ writeJsonAtomic(outputPath, {
1160
+ status: result.ok ? 'pass' : 'fail',
1161
+ manifest: completed,
1162
+ derived: result.derived,
1163
+ changedPaths,
1164
+ errors: result.errors,
1165
+ })
1166
+
1167
+ for (const error of result.errors) console.error(`knowledge-change: ${error}`)
1168
+ console.log(`${result.ok ? 'PASS' : 'FAIL'} knowledge-change ${result.derived.changeClass} — ${outputPath}`)
1169
+ return result.ok ? EXIT_PASS : EXIT_FAILURE
1170
+ }
1171
+
1172
+ if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) process.exitCode = main()