@intentius/chant 0.13.0 → 0.15.0

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 (453) hide show
  1. package/dist/audit/catalog.d.ts +45 -5
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/fetch.d.ts.map +1 -1
  4. package/dist/audit/proof.d.ts +3 -0
  5. package/dist/audit/proof.d.ts.map +1 -1
  6. package/dist/audit/report-model.d.ts +4 -1
  7. package/dist/audit/report-model.d.ts.map +1 -1
  8. package/dist/audit/report.d.ts +3 -0
  9. package/dist/audit/report.d.ts.map +1 -1
  10. package/dist/audit/rules-doc.d.ts +8 -2
  11. package/dist/audit/rules-doc.d.ts.map +1 -1
  12. package/dist/cli/commands/audit.d.ts.map +1 -1
  13. package/dist/cli/commands/doctor.d.ts.map +1 -1
  14. package/dist/cli/commands/lexicon-rolling-upgrade.d.ts +37 -0
  15. package/dist/cli/commands/lexicon-rolling-upgrade.d.ts.map +1 -0
  16. package/dist/cli/commands/lexicon-surface-diff.d.ts +48 -0
  17. package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -0
  18. package/dist/cli/commands/lint.d.ts.map +1 -1
  19. package/dist/cli/commands/pinned-upgrade.d.ts +41 -0
  20. package/dist/cli/commands/pinned-upgrade.d.ts.map +1 -0
  21. package/dist/cli/handlers/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/components.d.ts +39 -0
  23. package/dist/cli/handlers/components.d.ts.map +1 -0
  24. package/dist/cli/handlers/dev.d.ts +3 -0
  25. package/dist/cli/handlers/dev.d.ts.map +1 -1
  26. package/dist/cli/handlers/graph.d.ts +4 -2
  27. package/dist/cli/handlers/graph.d.ts.map +1 -1
  28. package/dist/cli/handlers/misc.d.ts.map +1 -1
  29. package/dist/cli/handlers/run.d.ts +49 -0
  30. package/dist/cli/handlers/run.d.ts.map +1 -1
  31. package/dist/cli/main.d.ts.map +1 -1
  32. package/dist/cli/registry.d.ts +35 -0
  33. package/dist/cli/registry.d.ts.map +1 -1
  34. package/dist/codegen/fetch.d.ts +22 -0
  35. package/dist/codegen/fetch.d.ts.map +1 -1
  36. package/dist/codegen/lexicon-regen.d.ts +110 -0
  37. package/dist/codegen/lexicon-regen.d.ts.map +1 -0
  38. package/dist/codegen/pinned-upgrade.d.ts +153 -0
  39. package/dist/codegen/pinned-upgrade.d.ts.map +1 -0
  40. package/dist/codegen/rolling-upgrade.d.ts +138 -0
  41. package/dist/codegen/rolling-upgrade.d.ts.map +1 -0
  42. package/dist/codegen/surface-snapshot.d.ts +173 -0
  43. package/dist/codegen/surface-snapshot.d.ts.map +1 -0
  44. package/dist/codegen/version-bump.d.ts +21 -0
  45. package/dist/codegen/version-bump.d.ts.map +1 -0
  46. package/dist/components/auto-release.d.ts +93 -0
  47. package/dist/components/auto-release.d.ts.map +1 -0
  48. package/dist/components/builders.d.ts +37 -0
  49. package/dist/components/builders.d.ts.map +1 -0
  50. package/dist/components/capability-plugin-loader.d.ts +111 -0
  51. package/dist/components/capability-plugin-loader.d.ts.map +1 -0
  52. package/dist/components/capability-plugin-schema.d.ts +24 -0
  53. package/dist/components/capability-plugin-schema.d.ts.map +1 -0
  54. package/dist/components/capability-plugin.d.ts +85 -0
  55. package/dist/components/capability-plugin.d.ts.map +1 -0
  56. package/dist/components/capability.d.ts +89 -0
  57. package/dist/components/capability.d.ts.map +1 -0
  58. package/dist/components/cli-support.d.ts +204 -0
  59. package/dist/components/cli-support.d.ts.map +1 -0
  60. package/dist/components/component.d.ts +179 -0
  61. package/dist/components/component.d.ts.map +1 -0
  62. package/dist/components/config-defaults.d.ts +41 -0
  63. package/dist/components/config-defaults.d.ts.map +1 -0
  64. package/dist/components/discover.d.ts +60 -0
  65. package/dist/components/discover.d.ts.map +1 -0
  66. package/dist/components/driver-output.d.ts +20 -0
  67. package/dist/components/driver-output.d.ts.map +1 -0
  68. package/dist/components/driver.d.ts +209 -0
  69. package/dist/components/driver.d.ts.map +1 -0
  70. package/dist/components/index.d.ts +22 -0
  71. package/dist/components/index.d.ts.map +1 -0
  72. package/dist/components/manifest-persistence.d.ts +79 -0
  73. package/dist/components/manifest-persistence.d.ts.map +1 -0
  74. package/dist/components/pilots/alb-ecs.pilot.d.ts +27 -0
  75. package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -0
  76. package/dist/components/pilots/dynamodb.pilot.d.ts +22 -0
  77. package/dist/components/pilots/dynamodb.pilot.d.ts.map +1 -0
  78. package/dist/components/pilots/jar-emr.pilot.d.ts +37 -0
  79. package/dist/components/pilots/jar-emr.pilot.d.ts.map +1 -0
  80. package/dist/components/pilots/lambda.pilot.d.ts +35 -0
  81. package/dist/components/pilots/lambda.pilot.d.ts.map +1 -0
  82. package/dist/components/pilots/neo4j-fanout.pilot.d.ts +17 -0
  83. package/dist/components/pilots/neo4j-fanout.pilot.d.ts.map +1 -0
  84. package/dist/components/pilots/project.d.ts +13 -0
  85. package/dist/components/pilots/project.d.ts.map +1 -0
  86. package/dist/components/presets/ecs-fargate.d.ts +59 -0
  87. package/dist/components/presets/ecs-fargate.d.ts.map +1 -0
  88. package/dist/components/presets/index.d.ts +29 -0
  89. package/dist/components/presets/index.d.ts.map +1 -0
  90. package/dist/components/presets/lambda.d.ts +35 -0
  91. package/dist/components/presets/lambda.d.ts.map +1 -0
  92. package/dist/components/presets/single-host-compose.d.ts +43 -0
  93. package/dist/components/presets/single-host-compose.d.ts.map +1 -0
  94. package/dist/components/registry.d.ts +29 -0
  95. package/dist/components/registry.d.ts.map +1 -0
  96. package/dist/components/starter-plugin.d.ts +51 -0
  97. package/dist/components/starter-plugin.d.ts.map +1 -0
  98. package/dist/components/temporal-codegen-loader.d.ts +27 -0
  99. package/dist/components/temporal-codegen-loader.d.ts.map +1 -0
  100. package/dist/components/verbs/__tests__/mock-cloud-executor.d.ts +35 -0
  101. package/dist/components/verbs/__tests__/mock-cloud-executor.d.ts.map +1 -0
  102. package/dist/components/verbs/__tests__/mock-process-runner.d.ts +45 -0
  103. package/dist/components/verbs/__tests__/mock-process-runner.d.ts.map +1 -0
  104. package/dist/components/verbs/__tests__/mock-sbom-generator.d.ts +33 -0
  105. package/dist/components/verbs/__tests__/mock-sbom-generator.d.ts.map +1 -0
  106. package/dist/components/verbs/bom-writer.d.ts +155 -0
  107. package/dist/components/verbs/bom-writer.d.ts.map +1 -0
  108. package/dist/components/verbs/build-archive.d.ts +205 -0
  109. package/dist/components/verbs/build-archive.d.ts.map +1 -0
  110. package/dist/components/verbs/build.d.ts +150 -0
  111. package/dist/components/verbs/build.d.ts.map +1 -0
  112. package/dist/components/verbs/cloud-executor.d.ts +88 -0
  113. package/dist/components/verbs/cloud-executor.d.ts.map +1 -0
  114. package/dist/components/verbs/component-bom.d.ts +103 -0
  115. package/dist/components/verbs/component-bom.d.ts.map +1 -0
  116. package/dist/components/verbs/index.d.ts +49 -0
  117. package/dist/components/verbs/index.d.ts.map +1 -0
  118. package/dist/components/verbs/license-policy.d.ts +56 -0
  119. package/dist/components/verbs/license-policy.d.ts.map +1 -0
  120. package/dist/components/verbs/lockfile-sbom-generator.d.ts +81 -0
  121. package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -0
  122. package/dist/components/verbs/process-runner.d.ts +64 -0
  123. package/dist/components/verbs/process-runner.d.ts.map +1 -0
  124. package/dist/components/verbs/reproducibility.d.ts +80 -0
  125. package/dist/components/verbs/reproducibility.d.ts.map +1 -0
  126. package/dist/components/verbs/sbom-generator.d.ts +157 -0
  127. package/dist/components/verbs/sbom-generator.d.ts.map +1 -0
  128. package/dist/components/verbs/sbom.d.ts +79 -0
  129. package/dist/components/verbs/sbom.d.ts.map +1 -0
  130. package/dist/components/verbs/shell.d.ts +35 -0
  131. package/dist/components/verbs/shell.d.ts.map +1 -0
  132. package/dist/components/verbs/sign.d.ts +222 -0
  133. package/dist/components/verbs/sign.d.ts.map +1 -0
  134. package/dist/components/verbs/stub.d.ts +18 -0
  135. package/dist/components/verbs/stub.d.ts.map +1 -0
  136. package/dist/components/verbs/tool-sbom-generator.d.ts +51 -0
  137. package/dist/components/verbs/tool-sbom-generator.d.ts.map +1 -0
  138. package/dist/components/verbs/verify.d.ts +106 -0
  139. package/dist/components/verbs/verify.d.ts.map +1 -0
  140. package/dist/components/verbs/vex.d.ts +65 -0
  141. package/dist/components/verbs/vex.d.ts.map +1 -0
  142. package/dist/components/verbs/vuln-gate.d.ts +91 -0
  143. package/dist/components/verbs/vuln-gate.d.ts.map +1 -0
  144. package/dist/components/verbs/vuln-scan.d.ts +116 -0
  145. package/dist/components/verbs/vuln-scan.d.ts.map +1 -0
  146. package/dist/components/verbs/wait-verify.d.ts +85 -0
  147. package/dist/components/verbs/wait-verify.d.ts.map +1 -0
  148. package/dist/config.d.ts +205 -0
  149. package/dist/config.d.ts.map +1 -1
  150. package/dist/index.d.ts +4 -0
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/lexicon.d.ts +78 -0
  153. package/dist/lexicon.d.ts.map +1 -1
  154. package/dist/lifecycle/build-ledger-store.d.ts +121 -0
  155. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -0
  156. package/dist/lifecycle/build-ledger.d.ts +225 -0
  157. package/dist/lifecycle/build-ledger.d.ts.map +1 -0
  158. package/dist/lifecycle/git.d.ts +84 -0
  159. package/dist/lifecycle/git.d.ts.map +1 -1
  160. package/dist/lifecycle/index.d.ts +5 -0
  161. package/dist/lifecycle/index.d.ts.map +1 -1
  162. package/dist/lifecycle/oras-referrer-lookup.d.ts +41 -0
  163. package/dist/lifecycle/oras-referrer-lookup.d.ts.map +1 -0
  164. package/dist/lifecycle/release-ledger.d.ts +122 -0
  165. package/dist/lifecycle/release-ledger.d.ts.map +1 -0
  166. package/dist/lifecycle/status.d.ts +150 -0
  167. package/dist/lifecycle/status.d.ts.map +1 -0
  168. package/dist/lint/component-checks.d.ts +95 -0
  169. package/dist/lint/component-checks.d.ts.map +1 -0
  170. package/dist/lint/engine.d.ts +23 -0
  171. package/dist/lint/engine.d.ts.map +1 -1
  172. package/dist/lint/presets/strict.json +8 -1
  173. package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-a.component.d.ts +4 -0
  174. package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-a.component.d.ts.map +1 -0
  175. package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-b.component.d.ts +4 -0
  176. package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-b.component.d.ts.map +1 -0
  177. package/dist/lint/rules/__fixtures__/comp/comp001/fail/orphan-lib.component.d.ts +4 -0
  178. package/dist/lint/rules/__fixtures__/comp/comp001/fail/orphan-lib.component.d.ts.map +1 -0
  179. package/dist/lint/rules/__fixtures__/comp/comp001/pass/emr-job.component.d.ts +4 -0
  180. package/dist/lint/rules/__fixtures__/comp/comp001/pass/emr-job.component.d.ts.map +1 -0
  181. package/dist/lint/rules/__fixtures__/comp/comp001/pass/jar-lib.component.d.ts +4 -0
  182. package/dist/lint/rules/__fixtures__/comp/comp001/pass/jar-lib.component.d.ts.map +1 -0
  183. package/dist/lint/rules/__fixtures__/comp/comp002/fail/search-service.component.d.ts +10 -0
  184. package/dist/lint/rules/__fixtures__/comp/comp002/fail/search-service.component.d.ts.map +1 -0
  185. package/dist/lint/rules/__fixtures__/comp/comp002/pass/jar-lib.component.d.ts +3 -0
  186. package/dist/lint/rules/__fixtures__/comp/comp002/pass/jar-lib.component.d.ts.map +1 -0
  187. package/dist/lint/rules/__fixtures__/comp/comp002/pass/search-service.component.d.ts +4 -0
  188. package/dist/lint/rules/__fixtures__/comp/comp002/pass/search-service.component.d.ts.map +1 -0
  189. package/dist/lint/rules/__fixtures__/comp/comp003/fail/migrator.component.d.ts +4 -0
  190. package/dist/lint/rules/__fixtures__/comp/comp003/fail/migrator.component.d.ts.map +1 -0
  191. package/dist/lint/rules/__fixtures__/comp/comp003/pass/migrator.component.d.ts +4 -0
  192. package/dist/lint/rules/__fixtures__/comp/comp003/pass/migrator.component.d.ts.map +1 -0
  193. package/dist/lint/rules/__fixtures__/comp/comp003/pass/orders-table.component.d.ts +4 -0
  194. package/dist/lint/rules/__fixtures__/comp/comp003/pass/orders-table.component.d.ts.map +1 -0
  195. package/dist/lint/rules/__fixtures__/comp/comp004/fail/neo4j-cluster.component.d.ts +4 -0
  196. package/dist/lint/rules/__fixtures__/comp/comp004/fail/neo4j-cluster.component.d.ts.map +1 -0
  197. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +4 -0
  198. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -0
  199. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +4 -0
  200. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -0
  201. package/dist/lint/rules/__fixtures__/comp/comp005/fail/search-service.component.d.ts +4 -0
  202. package/dist/lint/rules/__fixtures__/comp/comp005/fail/search-service.component.d.ts.map +1 -0
  203. package/dist/lint/rules/__fixtures__/comp/comp005/pass/search-service.component.d.ts +4 -0
  204. package/dist/lint/rules/__fixtures__/comp/comp005/pass/search-service.component.d.ts.map +1 -0
  205. package/dist/lint/rules/__fixtures__/comp/comp006/fail/legacy-tool.component.d.ts +4 -0
  206. package/dist/lint/rules/__fixtures__/comp/comp006/fail/legacy-tool.component.d.ts.map +1 -0
  207. package/dist/lint/rules/__fixtures__/comp/comp006/pass/legacy-tool.component.d.ts +4 -0
  208. package/dist/lint/rules/__fixtures__/comp/comp006/pass/legacy-tool.component.d.ts.map +1 -0
  209. package/dist/lint/rules/__fixtures__/comp/comp007/fail/inventory-table.component.d.ts +4 -0
  210. package/dist/lint/rules/__fixtures__/comp/comp007/fail/inventory-table.component.d.ts.map +1 -0
  211. package/dist/lint/rules/__fixtures__/comp/comp007/fail/orders-table.component.d.ts +4 -0
  212. package/dist/lint/rules/__fixtures__/comp/comp007/fail/orders-table.component.d.ts.map +1 -0
  213. package/dist/lint/rules/__fixtures__/comp/comp007/pass/orders-table.component.d.ts +3 -0
  214. package/dist/lint/rules/__fixtures__/comp/comp007/pass/orders-table.component.d.ts.map +1 -0
  215. package/dist/lint/rules/__fixtures__/comp/comp007/pass/search-service.component.d.ts +4 -0
  216. package/dist/lint/rules/__fixtures__/comp/comp007/pass/search-service.component.d.ts.map +1 -0
  217. package/dist/lint/rules/comp/comp001-publishes-never-applies.d.ts +20 -0
  218. package/dist/lint/rules/comp/comp001-publishes-never-applies.d.ts.map +1 -0
  219. package/dist/lint/rules/comp/comp002-dangling-wiring-ref.d.ts +28 -0
  220. package/dist/lint/rules/comp/comp002-dangling-wiring-ref.d.ts.map +1 -0
  221. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +39 -0
  222. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -0
  223. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +37 -0
  224. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +1 -0
  225. package/dist/lint/rules/comp/comp005-capability-kind-is-noun.d.ts +43 -0
  226. package/dist/lint/rules/comp/comp005-capability-kind-is-noun.d.ts.map +1 -0
  227. package/dist/lint/rules/comp/comp006-shell-needs-reason.d.ts +22 -0
  228. package/dist/lint/rules/comp/comp006-shell-needs-reason.d.ts.map +1 -0
  229. package/dist/lint/rules/comp/comp007-composition-sprawl.d.ts +33 -0
  230. package/dist/lint/rules/comp/comp007-composition-sprawl.d.ts.map +1 -0
  231. package/dist/lint/rules/comp/index.d.ts +18 -0
  232. package/dist/lint/rules/comp/index.d.ts.map +1 -0
  233. package/dist/lint/rules/comp/support.d.ts +71 -0
  234. package/dist/lint/rules/comp/support.d.ts.map +1 -0
  235. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  236. package/dist/op/activity-registry.d.ts +11 -6
  237. package/dist/op/activity-registry.d.ts.map +1 -1
  238. package/dist/op/activity-runtime.d.ts +24 -0
  239. package/dist/op/activity-runtime.d.ts.map +1 -0
  240. package/dist/op/builders.d.ts +56 -0
  241. package/dist/op/builders.d.ts.map +1 -1
  242. package/dist/op/index.d.ts +2 -1
  243. package/dist/op/index.d.ts.map +1 -1
  244. package/dist/ownership.d.ts +20 -14
  245. package/dist/ownership.d.ts.map +1 -1
  246. package/package.json +14 -1
  247. package/src/audit/catalog.test.ts +74 -22
  248. package/src/audit/catalog.ts +74 -471
  249. package/src/audit/fetch.test.ts +112 -8
  250. package/src/audit/fetch.ts +53 -8
  251. package/src/audit/proof.ts +4 -2
  252. package/src/audit/report-html.test.ts +14 -7
  253. package/src/audit/report-model.ts +20 -9
  254. package/src/audit/report.test.ts +36 -13
  255. package/src/audit/report.ts +2 -0
  256. package/src/audit/rules-doc.test.ts +8 -6
  257. package/src/audit/rules-doc.ts +13 -5
  258. package/src/cli/commands/audit.ts +23 -18
  259. package/src/cli/commands/component-lint.test.ts +158 -0
  260. package/src/cli/commands/doctor.test.ts +116 -2
  261. package/src/cli/commands/doctor.ts +46 -0
  262. package/src/cli/commands/lexicon-rolling-upgrade.ts +198 -0
  263. package/src/cli/commands/lexicon-surface-diff.test.ts +104 -0
  264. package/src/cli/commands/lexicon-surface-diff.ts +185 -0
  265. package/src/cli/commands/lint.ts +142 -1
  266. package/src/cli/commands/pinned-upgrade.ts +204 -0
  267. package/src/cli/handlers/build.ts +58 -1
  268. package/src/cli/handlers/components.test.ts +504 -0
  269. package/src/cli/handlers/components.ts +404 -0
  270. package/src/cli/handlers/dev.ts +60 -1
  271. package/src/cli/handlers/graph.ts +47 -2
  272. package/src/cli/handlers/misc.ts +45 -1
  273. package/src/cli/handlers/run.test.ts +967 -3
  274. package/src/cli/handlers/run.ts +618 -11
  275. package/src/cli/main.test.ts +66 -0
  276. package/src/cli/main.ts +105 -2
  277. package/src/cli/registry.ts +35 -0
  278. package/src/codegen/fetch.test.ts +84 -3
  279. package/src/codegen/fetch.ts +76 -2
  280. package/src/codegen/lexicon-regen.test.ts +298 -0
  281. package/src/codegen/lexicon-regen.ts +545 -0
  282. package/src/codegen/pinned-upgrade.test.ts +399 -0
  283. package/src/codegen/pinned-upgrade.ts +423 -0
  284. package/src/codegen/rolling-upgrade.test.ts +333 -0
  285. package/src/codegen/rolling-upgrade.ts +320 -0
  286. package/src/codegen/surface-snapshot.test.ts +461 -0
  287. package/src/codegen/surface-snapshot.ts +546 -0
  288. package/src/codegen/version-bump.test.ts +47 -0
  289. package/src/codegen/version-bump.ts +39 -0
  290. package/src/components/README.md +130 -0
  291. package/src/components/SPRAWL-VALIDATION.md +181 -0
  292. package/src/components/__fixtures__/alb-ecs-service.json +51 -0
  293. package/src/components/__fixtures__/dynamodb-infra.json +32 -0
  294. package/src/components/__fixtures__/emr-job-consumer.json +26 -0
  295. package/src/components/__fixtures__/jar-lib-producer.json +16 -0
  296. package/src/components/__fixtures__/lambda-image-processor.json +28 -0
  297. package/src/components/__fixtures__/neo4j-fanout.json +34 -0
  298. package/src/components/__fixtures__/single-host-compose.json +29 -0
  299. package/src/components/auto-release.test.ts +217 -0
  300. package/src/components/auto-release.ts +216 -0
  301. package/src/components/builders.test.ts +45 -0
  302. package/src/components/builders.ts +71 -0
  303. package/src/components/capability-plugin-loader.ts +218 -0
  304. package/src/components/capability-plugin-schema.ts +56 -0
  305. package/src/components/capability-plugin.test.ts +235 -0
  306. package/src/components/capability-plugin.ts +108 -0
  307. package/src/components/capability.ts +117 -0
  308. package/src/components/cli-support.test.ts +579 -0
  309. package/src/components/cli-support.ts +481 -0
  310. package/src/components/component-schema.test.ts +265 -0
  311. package/src/components/component.schema.json +235 -0
  312. package/src/components/component.test.ts +251 -0
  313. package/src/components/component.ts +255 -0
  314. package/src/components/config-defaults.test.ts +263 -0
  315. package/src/components/config-defaults.ts +145 -0
  316. package/src/components/discover.test.ts +352 -0
  317. package/src/components/discover.ts +167 -0
  318. package/src/components/driver-output.ts +71 -0
  319. package/src/components/driver.test.ts +638 -0
  320. package/src/components/driver.ts +690 -0
  321. package/src/components/index.ts +113 -0
  322. package/src/components/manifest-persistence.test.ts +152 -0
  323. package/src/components/manifest-persistence.ts +164 -0
  324. package/src/components/pilots/README.md +33 -0
  325. package/src/components/pilots/alb-ecs.pilot.ts +63 -0
  326. package/src/components/pilots/dynamodb.pilot.ts +59 -0
  327. package/src/components/pilots/jar-emr.pilot.ts +69 -0
  328. package/src/components/pilots/lambda.pilot.ts +55 -0
  329. package/src/components/pilots/neo4j-fanout.pilot.ts +92 -0
  330. package/src/components/pilots/pilots-lint-clean.test.ts +78 -0
  331. package/src/components/pilots/pilots.test.ts +126 -0
  332. package/src/components/pilots/project.ts +13 -0
  333. package/src/components/presets/comp007-reduction.test.ts +122 -0
  334. package/src/components/presets/ecs-fargate.test.ts +84 -0
  335. package/src/components/presets/ecs-fargate.ts +103 -0
  336. package/src/components/presets/index.ts +32 -0
  337. package/src/components/presets/lambda.test.ts +35 -0
  338. package/src/components/presets/lambda.ts +53 -0
  339. package/src/components/presets/single-host-compose.test.ts +65 -0
  340. package/src/components/presets/single-host-compose.ts +64 -0
  341. package/src/components/registry.test.ts +112 -0
  342. package/src/components/registry.ts +37 -0
  343. package/src/components/starter-plugin.ts +105 -0
  344. package/src/components/temporal-codegen-loader.ts +41 -0
  345. package/src/components/verbs/__fixtures__/package-lock.json +45 -0
  346. package/src/components/verbs/__fixtures__/pom.xml +37 -0
  347. package/src/components/verbs/__fixtures__/schemas/cyclonedx-1.5.schema.json +3800 -0
  348. package/src/components/verbs/__fixtures__/schemas/cyclonedx-spdx-license.schema.json +621 -0
  349. package/src/components/verbs/__fixtures__/schemas/jsf-0.82.schema.json +240 -0
  350. package/src/components/verbs/__fixtures__/schemas/spdx-2.3.schema.json +740 -0
  351. package/src/components/verbs/__tests__/mock-cloud-executor.ts +102 -0
  352. package/src/components/verbs/__tests__/mock-process-runner.ts +79 -0
  353. package/src/components/verbs/__tests__/mock-sbom-generator.ts +85 -0
  354. package/src/components/verbs/bom-writer.test.ts +331 -0
  355. package/src/components/verbs/bom-writer.ts +386 -0
  356. package/src/components/verbs/build-archive.test.ts +251 -0
  357. package/src/components/verbs/build-archive.ts +296 -0
  358. package/src/components/verbs/build.test.ts +222 -0
  359. package/src/components/verbs/build.ts +252 -0
  360. package/src/components/verbs/cloud-executor.ts +181 -0
  361. package/src/components/verbs/component-bom.test.ts +219 -0
  362. package/src/components/verbs/component-bom.ts +189 -0
  363. package/src/components/verbs/index.ts +49 -0
  364. package/src/components/verbs/license-policy.ts +115 -0
  365. package/src/components/verbs/lockfile-sbom-generator.test.ts +195 -0
  366. package/src/components/verbs/lockfile-sbom-generator.ts +236 -0
  367. package/src/components/verbs/process-runner.test.ts +79 -0
  368. package/src/components/verbs/process-runner.ts +113 -0
  369. package/src/components/verbs/reproducibility.test.ts +24 -0
  370. package/src/components/verbs/reproducibility.ts +100 -0
  371. package/src/components/verbs/sbom-generator.test.ts +96 -0
  372. package/src/components/verbs/sbom-generator.ts +209 -0
  373. package/src/components/verbs/sbom.test.ts +184 -0
  374. package/src/components/verbs/sbom.ts +152 -0
  375. package/src/components/verbs/shell.test.ts +36 -0
  376. package/src/components/verbs/shell.ts +51 -0
  377. package/src/components/verbs/sign.test.ts +210 -0
  378. package/src/components/verbs/sign.ts +350 -0
  379. package/src/components/verbs/stub.ts +34 -0
  380. package/src/components/verbs/tool-sbom-generator.test.ts +194 -0
  381. package/src/components/verbs/tool-sbom-generator.ts +192 -0
  382. package/src/components/verbs/verify.test.ts +159 -0
  383. package/src/components/verbs/verify.ts +179 -0
  384. package/src/components/verbs/vex.ts +182 -0
  385. package/src/components/verbs/vuln-gate.test.ts +201 -0
  386. package/src/components/verbs/vuln-gate.ts +170 -0
  387. package/src/components/verbs/vuln-scan.test.ts +145 -0
  388. package/src/components/verbs/vuln-scan.ts +266 -0
  389. package/src/components/verbs/wait-verify.test.ts +111 -0
  390. package/src/components/verbs/wait-verify.ts +171 -0
  391. package/src/config.test.ts +74 -1
  392. package/src/config.ts +219 -0
  393. package/src/index.ts +32 -0
  394. package/src/lexicon.ts +88 -0
  395. package/src/lifecycle/build-ledger-store.test.ts +217 -0
  396. package/src/lifecycle/build-ledger-store.ts +165 -0
  397. package/src/lifecycle/build-ledger.test.ts +349 -0
  398. package/src/lifecycle/build-ledger.ts +305 -0
  399. package/src/lifecycle/git.test.ts +118 -0
  400. package/src/lifecycle/git.ts +149 -14
  401. package/src/lifecycle/index.ts +5 -0
  402. package/src/lifecycle/oras-referrer-lookup.test.ts +116 -0
  403. package/src/lifecycle/oras-referrer-lookup.ts +110 -0
  404. package/src/lifecycle/release-ledger.test.ts +209 -0
  405. package/src/lifecycle/release-ledger.ts +185 -0
  406. package/src/lifecycle/status.test.ts +330 -0
  407. package/src/lifecycle/status.ts +278 -0
  408. package/src/lint/component-checks.ts +148 -0
  409. package/src/lint/config.test.ts +21 -0
  410. package/src/lint/engine.ts +11 -2
  411. package/src/lint/presets/relaxed.json +8 -1
  412. package/src/lint/presets/strict.json +8 -1
  413. package/src/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-a.component.ts +9 -0
  414. package/src/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-b.component.ts +9 -0
  415. package/src/lint/rules/__fixtures__/comp/comp001/fail/orphan-lib.component.ts +11 -0
  416. package/src/lint/rules/__fixtures__/comp/comp001/pass/emr-job.component.ts +13 -0
  417. package/src/lint/rules/__fixtures__/comp/comp001/pass/jar-lib.component.ts +11 -0
  418. package/src/lint/rules/__fixtures__/comp/comp002/fail/search-service.component.ts +27 -0
  419. package/src/lint/rules/__fixtures__/comp/comp002/pass/jar-lib.component.ts +10 -0
  420. package/src/lint/rules/__fixtures__/comp/comp002/pass/search-service.component.ts +23 -0
  421. package/src/lint/rules/__fixtures__/comp/comp003/fail/migrator.component.ts +10 -0
  422. package/src/lint/rules/__fixtures__/comp/comp003/pass/migrator.component.ts +19 -0
  423. package/src/lint/rules/__fixtures__/comp/comp003/pass/orders-table.component.ts +10 -0
  424. package/src/lint/rules/__fixtures__/comp/comp004/fail/neo4j-cluster.component.ts +20 -0
  425. package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +23 -0
  426. package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +10 -0
  427. package/src/lint/rules/__fixtures__/comp/comp005/fail/search-service.component.ts +14 -0
  428. package/src/lint/rules/__fixtures__/comp/comp005/pass/search-service.component.ts +18 -0
  429. package/src/lint/rules/__fixtures__/comp/comp006/fail/legacy-tool.component.ts +10 -0
  430. package/src/lint/rules/__fixtures__/comp/comp006/pass/legacy-tool.component.ts +14 -0
  431. package/src/lint/rules/__fixtures__/comp/comp007/fail/inventory-table.component.ts +13 -0
  432. package/src/lint/rules/__fixtures__/comp/comp007/fail/orders-table.component.ts +13 -0
  433. package/src/lint/rules/__fixtures__/comp/comp007/pass/orders-table.component.ts +12 -0
  434. package/src/lint/rules/__fixtures__/comp/comp007/pass/search-service.component.ts +18 -0
  435. package/src/lint/rules/comp/comp.test.ts +378 -0
  436. package/src/lint/rules/comp/comp001-publishes-never-applies.ts +78 -0
  437. package/src/lint/rules/comp/comp002-dangling-wiring-ref.ts +94 -0
  438. package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +115 -0
  439. package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +66 -0
  440. package/src/lint/rules/comp/comp005-capability-kind-is-noun.ts +113 -0
  441. package/src/lint/rules/comp/comp006-shell-needs-reason.ts +55 -0
  442. package/src/lint/rules/comp/comp007-composition-sprawl.ts +71 -0
  443. package/src/lint/rules/comp/index.ts +37 -0
  444. package/src/lint/rules/comp/support.ts +173 -0
  445. package/src/lsp/lexicon-providers.test.ts +55 -0
  446. package/src/lsp/lexicon-providers.ts +13 -3
  447. package/src/op/activity-registry.test.ts +38 -0
  448. package/src/op/activity-registry.ts +31 -12
  449. package/src/op/activity-runtime.ts +73 -0
  450. package/src/op/builders.ts +92 -0
  451. package/src/op/index.ts +3 -1
  452. package/src/ownership.test.ts +24 -26
  453. package/src/ownership.ts +26 -38
@@ -8,13 +8,43 @@ const loadChantConfigMock = vi.fn();
8
8
  const loadTemporalClientMock = vi.fn();
9
9
  const resolveProfileMock = vi.fn();
10
10
  const existsSyncMock = vi.fn();
11
+ const writeFileSyncMock = vi.fn();
12
+ const mkdirSyncMock = vi.fn();
11
13
  const spawnChildMock = vi.fn();
12
14
  const generateReportMock = vi.fn();
13
15
  const writeReportMock = vi.fn();
14
16
  const waitForTemporalSpy = vi.fn();
17
+ const runComponentsMock = vi.fn();
18
+ const resolveComponentTargetsMock = vi.fn();
19
+ const findComponentGateMock = vi.fn();
20
+ const loadComponentTemporalCodegenMock = vi.fn();
21
+ const maybeRecordAutoReleaseMock = vi.fn();
22
+ const maybePersistBuildManifestMock = vi.fn();
23
+ const listComponentsMock = vi.fn();
15
24
 
16
25
  vi.mock("../../op/discover", () => ({ discoverOps: () => discoverOpsMock() }));
17
- vi.mock("../../config", () => ({ loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args) }));
26
+ vi.mock("../../config", async () => {
27
+ const actual = await vi.importActual<typeof import("../../config")>("../../config");
28
+ return { ...actual, loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args) };
29
+ });
30
+ vi.mock("../../components/auto-release", () => ({
31
+ maybeRecordAutoRelease: (...args: unknown[]) => maybeRecordAutoReleaseMock(...args),
32
+ extractRunDigestFromPhaseOutputs: (phaseOutputs: Record<string, Record<string, unknown>> | undefined) => {
33
+ for (const output of Object.values(phaseOutputs ?? {})) {
34
+ if (output && typeof output === "object" && "digest" in output) return (output as { digest: string }).digest;
35
+ }
36
+ return undefined;
37
+ },
38
+ }));
39
+ vi.mock("../../components/manifest-persistence", () => ({
40
+ maybePersistBuildManifest: (...args: unknown[]) => maybePersistBuildManifestMock(...args),
41
+ extractRunManifestFromPhaseOutputs: (phaseOutputs: Record<string, Record<string, unknown>> | undefined) => {
42
+ for (const output of Object.values(phaseOutputs ?? {})) {
43
+ if (output && typeof output === "object" && "manifest" in output) return (output as { manifest: unknown }).manifest;
44
+ }
45
+ return undefined;
46
+ },
47
+ }));
18
48
  vi.mock("./run-client", () => ({
19
49
  loadTemporalClient: () => loadTemporalClientMock(),
20
50
  connectionOptions: (profile: { address: string }) => ({ address: profile.address }),
@@ -23,7 +53,12 @@ vi.mock("./run-client", () => ({
23
53
  }));
24
54
  vi.mock("node:fs", async () => {
25
55
  const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
26
- return { ...actual, existsSync: (p: string) => existsSyncMock(p) };
56
+ return {
57
+ ...actual,
58
+ existsSync: (p: string) => existsSyncMock(p),
59
+ writeFileSync: (...args: unknown[]) => writeFileSyncMock(...args),
60
+ mkdirSync: (...args: unknown[]) => mkdirSyncMock(...args),
61
+ };
27
62
  });
28
63
  vi.mock("node:child_process", async () => {
29
64
  const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
@@ -33,11 +68,20 @@ vi.mock("./run-report", () => ({
33
68
  generateReport: (...args: unknown[]) => generateReportMock(...args),
34
69
  writeReport: (...args: unknown[]) => writeReportMock(...args),
35
70
  }));
71
+ vi.mock("../../components/cli-support", () => ({
72
+ runComponents: (...args: unknown[]) => runComponentsMock(...args),
73
+ resolveComponentTargets: (...args: unknown[]) => resolveComponentTargetsMock(...args),
74
+ findComponentGate: (...args: unknown[]) => findComponentGateMock(...args),
75
+ listComponents: (...args: unknown[]) => listComponentsMock(...args),
76
+ }));
77
+ vi.mock("../../components/temporal-codegen-loader", () => ({
78
+ loadComponentTemporalCodegen: () => loadComponentTemporalCodegenMock(),
79
+ }));
36
80
 
37
81
  // Speed up runOp polling — POLL_INTERVAL_MS is 3000 in production. We use
38
82
  // fake timers in the runOp suite below; vi.advanceTimersByTime drives the loop.
39
83
 
40
- const { runOpList, runOpStatus, runOpLog, runOpSignal, runOpCancel, runOp } = await import("./run");
84
+ const { runOpList, runOpStatus, runOpLog, runOpSignal, runOpCancel, runOp, runOpComponents } = await import("./run");
41
85
 
42
86
  function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
43
87
  return {
@@ -125,6 +169,82 @@ describe("runOpList", () => {
125
169
  });
126
170
  });
127
171
 
172
+ // ── chant run list --components (#599) ──────────────────────────────────────
173
+
174
+ describe("runOpList --components", () => {
175
+ beforeEach(() => {
176
+ listComponentsMock.mockReset();
177
+ loadTemporalClientMock.mockReset();
178
+ loadChantConfigMock.mockReset();
179
+ resolveProfileMock.mockReset();
180
+ });
181
+
182
+ test("without --temporal → exit 1, actionable message, no discovery", async () => {
183
+ const stderr = makeStderrSpy();
184
+ const exit = await runOpList({ args: makeArgs({ components: true, temporal: false }), plugins: [], serializers: [] });
185
+ expect(exit).toBe(1);
186
+ expect(stderr.join("\n")).toContain("not available in local mode");
187
+ expect(listComponentsMock).not.toHaveBeenCalled();
188
+ });
189
+
190
+ test("warns when no components discovered, returns 0", async () => {
191
+ listComponentsMock.mockResolvedValue({ success: true, components: [], errors: [] });
192
+ const stderr = makeStderrSpy();
193
+ const exit = await runOpList({ args: makeArgs({ components: true }), plugins: [], serializers: [] });
194
+ expect(exit).toBe(0);
195
+ expect(stderr.join("\n")).toContain("No component definitions found");
196
+ });
197
+
198
+ test("discovery error → exit 1 with the error message", async () => {
199
+ listComponentsMock.mockResolvedValue({ success: false, components: [], errors: ["bad component file"] });
200
+ const stderr = makeStderrSpy();
201
+ const exit = await runOpList({ args: makeArgs({ components: true }), plugins: [], serializers: [] });
202
+ expect(exit).toBe(1);
203
+ expect(stderr.join("\n")).toContain("bad component file");
204
+ });
205
+
206
+ test("prints table with one row per component when Temporal connection fails", async () => {
207
+ listComponentsMock.mockResolvedValue({
208
+ success: true,
209
+ components: [
210
+ { name: "search-service", archetype: "service", dependsOn: ["shared-alb"], hasBuild: true, phases: ["Build", "Apply"], filePath: "src/search.component.ts" },
211
+ ],
212
+ errors: [],
213
+ });
214
+ loadTemporalClientMock.mockRejectedValue(new Error("not installed"));
215
+ const stdout = makeStdoutSpy();
216
+ const exit = await runOpList({ args: makeArgs({ components: true }), plugins: [], serializers: [] });
217
+ expect(exit).toBe(0);
218
+ const out = stdout.join("\n");
219
+ expect(out).toContain("NAME");
220
+ expect(out).toContain("search-service");
221
+ expect(out).toContain("shared-alb");
222
+ });
223
+
224
+ test("annotates components with Temporal status when client is available", async () => {
225
+ listComponentsMock.mockResolvedValue({
226
+ success: true,
227
+ components: [
228
+ { name: "gated-svc", archetype: "service", dependsOn: [], hasBuild: true, phases: ["Apply"], filePath: "src/gated.component.ts" },
229
+ ],
230
+ errors: [],
231
+ });
232
+ setupTemporalClient(createMockTemporalClient({
233
+ describeByWorkflowId: {
234
+ "chant-component-gated-svc": {
235
+ workflowId: "chant-component-gated-svc", runId: "r1",
236
+ status: { name: "RUNNING" }, startTime: new Date(),
237
+ taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
238
+ },
239
+ },
240
+ }));
241
+ const stdout = makeStdoutSpy();
242
+ const exit = await runOpList({ args: makeArgs({ components: true }), plugins: [], serializers: [] });
243
+ expect(exit).toBe(0);
244
+ expect(stdout.join("\n")).toContain("RUNNING");
245
+ });
246
+ });
247
+
128
248
  describe("runOpStatus", () => {
129
249
  beforeEach(() => {
130
250
  discoverOpsMock.mockReset();
@@ -179,6 +299,68 @@ describe("runOpStatus", () => {
179
299
  });
180
300
  });
181
301
 
302
+ // ── chant run status <name> --components (#599) ─────────────────────────────
303
+
304
+ describe("runOpStatus --components", () => {
305
+ beforeEach(() => {
306
+ loadTemporalClientMock.mockReset();
307
+ loadChantConfigMock.mockReset();
308
+ resolveProfileMock.mockReset();
309
+ });
310
+
311
+ test("missing component name → exit 1 with component-flavored message", async () => {
312
+ const stderr = makeStderrSpy();
313
+ const exit = await runOpStatus({ args: makeArgs({ extraPositional: undefined, components: true }), plugins: [], serializers: [] });
314
+ expect(exit).toBe(1);
315
+ expect(stderr.join("\n")).toContain("Component name is required");
316
+ });
317
+
318
+ test("without --temporal → exit 1, actionable message", async () => {
319
+ const stderr = makeStderrSpy();
320
+ const exit = await runOpStatus({ args: makeArgs({ extraPositional: "gated-svc", components: true, temporal: false }), plugins: [], serializers: [] });
321
+ expect(exit).toBe(1);
322
+ expect(stderr.join("\n")).toContain("not available in local mode");
323
+ });
324
+
325
+ test("connection error → exit 1 with message", async () => {
326
+ loadTemporalClientMock.mockRejectedValue(new Error("UNAVAILABLE"));
327
+ loadChantConfigMock.mockResolvedValue({ config: {} });
328
+ resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
329
+ const stderr = makeStderrSpy();
330
+ const exit = await runOpStatus({ args: makeArgs({ extraPositional: "gated-svc", components: true }), plugins: [], serializers: [] });
331
+ expect(exit).toBe(1);
332
+ expect(stderr.join("\n")).toContain("UNAVAILABLE");
333
+ });
334
+
335
+ test("happy path: queries the component workflow id, prints status + activity counts", async () => {
336
+ setupTemporalClient(createMockTemporalClient({
337
+ describeByWorkflowId: {
338
+ "chant-component-gated-svc": {
339
+ workflowId: "chant-component-gated-svc", runId: "r1",
340
+ status: { name: "RUNNING" },
341
+ startTime: new Date("2026-05-01T00:00:00Z"),
342
+ taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
343
+ },
344
+ },
345
+ historyByWorkflowId: {
346
+ "chant-component-gated-svc": [
347
+ { eventType: "ActivityTaskScheduled" },
348
+ { eventType: "ActivityTaskScheduled" },
349
+ { eventType: "ActivityTaskCompleted" },
350
+ ],
351
+ },
352
+ }));
353
+ const stdout = makeStdoutSpy();
354
+ const exit = await runOpStatus({ args: makeArgs({ extraPositional: "gated-svc", components: true }), plugins: [], serializers: [] });
355
+ expect(exit).toBe(0);
356
+ const out = stdout.join("\n");
357
+ expect(out).toContain("Component: gated-svc");
358
+ expect(out).toContain("chant-component-gated-svc");
359
+ expect(out).toContain("RUNNING");
360
+ expect(out).toContain("1/2 completed");
361
+ });
362
+ });
363
+
182
364
  describe("runOpLog", () => {
183
365
  beforeEach(() => {
184
366
  loadTemporalClientMock.mockReset();
@@ -212,6 +394,111 @@ describe("runOpLog", () => {
212
394
  });
213
395
  });
214
396
 
397
+ // ── chant run log <name> --components (#599) ────────────────────────────────
398
+
399
+ describe("runOpLog --components", () => {
400
+ beforeEach(() => {
401
+ loadTemporalClientMock.mockReset();
402
+ loadChantConfigMock.mockReset();
403
+ resolveProfileMock.mockReset();
404
+ loadComponentTemporalCodegenMock.mockReset();
405
+ });
406
+
407
+ test("missing component name → exit 1 with component-flavored message", async () => {
408
+ const stderr = makeStderrSpy();
409
+ const exit = await runOpLog({ args: makeArgs({ extraPositional: undefined, components: true }), plugins: [], serializers: [] });
410
+ expect(exit).toBe(1);
411
+ expect(stderr.join("\n")).toContain("Component name is required");
412
+ });
413
+
414
+ test("without --temporal → exit 1, actionable message", async () => {
415
+ const stderr = makeStderrSpy();
416
+ const exit = await runOpLog({ args: makeArgs({ extraPositional: "gated-svc", components: true, temporal: false }), plugins: [], serializers: [] });
417
+ expect(exit).toBe(1);
418
+ expect(stderr.join("\n")).toContain("not available in local mode");
419
+ });
420
+
421
+ test("codegen unavailable → exit 1 with the loader's error message", async () => {
422
+ setupTemporalClient(createMockTemporalClient());
423
+ loadComponentTemporalCodegenMock.mockRejectedValue(new Error("no durable component codegen available"));
424
+ const stderr = makeStderrSpy();
425
+ const exit = await runOpLog({ args: makeArgs({ extraPositional: "gated-svc", components: true }), plugins: [], serializers: [] });
426
+ expect(exit).toBe(1);
427
+ expect(stderr.join("\n")).toContain("no durable component codegen available");
428
+ });
429
+
430
+ test("prints one row per matching component workflow execution, keyed by the component's workflow type", async () => {
431
+ setupTemporalClient(createMockTemporalClient({
432
+ list: [
433
+ { workflowId: "chant-component-gated-svc", runId: "r1", type: { name: "gatedSvcComponentWorkflow" }, status: { name: "COMPLETED" }, startTime: new Date("2026-05-01T00:00:00Z"), closeTime: new Date("2026-05-01T01:00:00Z") },
434
+ { workflowId: "chant-component-gated-svc", runId: "r2", type: { name: "gatedSvcComponentWorkflow" }, status: { name: "RUNNING" }, startTime: new Date("2026-05-02T00:00:00Z") },
435
+ ],
436
+ }));
437
+ loadComponentTemporalCodegenMock.mockResolvedValue({
438
+ serializeComponent: vi.fn(),
439
+ componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
440
+ });
441
+ const stdout = makeStdoutSpy();
442
+ const exit = await runOpLog({ args: makeArgs({ extraPositional: "gated-svc", components: true }), plugins: [], serializers: [] });
443
+ expect(exit).toBe(0);
444
+ const out = stdout.join("\n");
445
+ expect(out).toContain("RUN-ID");
446
+ expect(out).toContain("r1");
447
+ expect(out).toContain("r2");
448
+ expect(out).toContain("COMPLETED");
449
+ expect(out).toContain("RUNNING");
450
+ });
451
+ });
452
+
453
+ // ── chant run <name> --components --temporal: config defaults reach codegen ──
454
+ // The interpret/local path fills chant.config's sbom/signing/vulnPolicy
455
+ // defaults via runComponents; the durable path inlines the composition into
456
+ // generated workflow code, so it must apply the same pass before serializing
457
+ // — otherwise the Temporal path silently drops project defaults the local
458
+ // path honors.
459
+ describe("runComponentTemporal — applies chant.config defaults before codegen", () => {
460
+ beforeEach(() => {
461
+ loadTemporalClientMock.mockReset();
462
+ loadChantConfigMock.mockReset();
463
+ resolveProfileMock.mockReset();
464
+ resolveComponentTargetsMock.mockReset();
465
+ loadComponentTemporalCodegenMock.mockReset();
466
+ writeFileSyncMock.mockReset();
467
+ mkdirSyncMock.mockReset();
468
+ });
469
+
470
+ test("serializeComponent receives a component with config's sbom.format filled in", async () => {
471
+ resolveComponentTargetsMock.mockResolvedValue({
472
+ success: true,
473
+ targets: [{
474
+ name: "search-svc",
475
+ dependsOn: [],
476
+ deploy: [{ phase: "Build", steps: [{ kind: "generate-sbom", artifactType: "image", path: "img" }] }],
477
+ }],
478
+ });
479
+ // config sets a project-wide SBOM format the step itself omits.
480
+ loadChantConfigMock.mockResolvedValue({ config: { sbom: { format: "cyclonedx" } } });
481
+ resolveProfileMock.mockReturnValue({ autoStart: false, address: "localhost:7233", namespace: "default", taskQueue: "q" });
482
+
483
+ const serializeSpy = vi.fn().mockReturnValue({});
484
+ loadComponentTemporalCodegenMock.mockResolvedValue({
485
+ serializeComponent: serializeSpy,
486
+ componentWorkflowFnName: (n: string) => `${n}ComponentWorkflow`,
487
+ });
488
+ // Fail the client connect right after codegen so the handler returns
489
+ // without driving the whole workflow-start machinery — the spy has already
490
+ // captured the (resolved) component by then.
491
+ loadTemporalClientMock.mockRejectedValue(new Error("client-unavailable"));
492
+ makeStderrSpy();
493
+
494
+ await runOpComponents({ args: makeArgs({ path: "search-svc", components: true, temporal: true }), plugins: [], serializers: [] });
495
+
496
+ expect(serializeSpy).toHaveBeenCalledTimes(1);
497
+ const passedComponent = serializeSpy.mock.calls[0]![0] as { deploy: Array<{ steps: Array<{ format?: string }> }> };
498
+ expect(passedComponent.deploy[0]!.steps[0]!.format).toBe("cyclonedx");
499
+ });
500
+ });
501
+
215
502
  describe("runOpSignal", () => {
216
503
  beforeEach(() => {
217
504
  loadTemporalClientMock.mockReset();
@@ -241,6 +528,21 @@ describe("runOpSignal", () => {
241
528
  expect(stderr.join("\n")).toContain("Signal");
242
529
  expect(stderr.join("\n")).toContain("gate-dns");
243
530
  });
531
+
532
+ test("--components: signals the component workflow id, not the Op one (#589)", async () => {
533
+ const mockClient = createMockTemporalClient();
534
+ setupTemporalClient(mockClient);
535
+ const stderr = makeStderrSpy();
536
+ const exit = await runOpSignal({
537
+ args: makeArgs({ extraPositional: "gated-svc", extraPositional2: "release-approval", components: true }),
538
+ plugins: [], serializers: [],
539
+ });
540
+ expect(exit).toBe(0);
541
+ expect(mockClient.calls.signalCalls).toEqual([
542
+ { workflowId: "chant-component-gated-svc", signalName: "release-approval" },
543
+ ]);
544
+ expect(stderr.join("\n")).toContain("component");
545
+ });
244
546
  });
245
547
 
246
548
  describe("runOpCancel", () => {
@@ -279,6 +581,19 @@ describe("runOpCancel", () => {
279
581
  expect(mockClient.calls.cancelCalls).toEqual([{ workflowId: "chant-op-alb-deploy" }]);
280
582
  expect(stderr.join("\n")).toContain("Cancellation requested");
281
583
  });
584
+
585
+ test("--components: cancels the component workflow id, not the Op one (#589)", async () => {
586
+ const mockClient = createMockTemporalClient();
587
+ setupTemporalClient(mockClient);
588
+ const stderr = makeStderrSpy();
589
+ const exit = await runOpCancel({
590
+ args: makeArgs({ extraPositional: "gated-svc", force: true, components: true }),
591
+ plugins: [], serializers: [],
592
+ });
593
+ expect(exit).toBe(0);
594
+ expect(mockClient.calls.cancelCalls).toEqual([{ workflowId: "chant-component-gated-svc" }]);
595
+ expect(stderr.join("\n")).toContain("component");
596
+ });
282
597
  });
283
598
 
284
599
  // ── runOp (the main `chant run <name>` command) ─────────────────────────────
@@ -544,3 +859,652 @@ describe("Temporal-only subcommand guards", () => {
544
859
  expect(stderr.join("\n")).toContain("not available in local mode");
545
860
  });
546
861
  });
862
+
863
+ // ── chant run --components <name|all> (#585) ────────────────────────────────
864
+
865
+ describe("runOp dispatcher: --components routes to runOpComponents", () => {
866
+ beforeEach(() => {
867
+ runComponentsMock.mockReset();
868
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
869
+ maybeRecordAutoReleaseMock.mockReset().mockResolvedValue({ recorded: false, reason: "no-digest" });
870
+ maybePersistBuildManifestMock.mockReset().mockResolvedValue({ persisted: false, reason: "no-manifest" });
871
+ });
872
+
873
+ test("runOp with --components dispatches to runComponents, not Op discovery", async () => {
874
+ discoverOpsMock.mockReset();
875
+ runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true } });
876
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
877
+
878
+ const exit = await runOp({ args: makeArgs({ path: "svc", components: true, temporal: false }), plugins: [], serializers: [] });
879
+
880
+ expect(exit).toBe(0);
881
+ expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: undefined, componentOutputs: {} });
882
+ expect(discoverOpsMock).not.toHaveBeenCalled();
883
+ vi.restoreAllMocks();
884
+ });
885
+ });
886
+
887
+ describe("runOpComponents", () => {
888
+ beforeEach(() => {
889
+ runComponentsMock.mockReset();
890
+ loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
891
+ maybeRecordAutoReleaseMock.mockReset().mockResolvedValue({ recorded: false, reason: "no-digest" });
892
+ maybePersistBuildManifestMock.mockReset().mockResolvedValue({ persisted: false, reason: "no-manifest" });
893
+ });
894
+
895
+ test("no component name → exit 1 with hint", async () => {
896
+ const stderr = makeStderrSpy();
897
+ const exit = await runOpComponents({ args: makeArgs({ path: ".", temporal: false }), plugins: [], serializers: [] });
898
+ expect(exit).toBe(1);
899
+ expect(stderr.join("\n")).toContain("Component name is required");
900
+ expect(runComponentsMock).not.toHaveBeenCalled();
901
+ });
902
+
903
+ test("happy path: single component, human output, exit 0", async () => {
904
+ runComponentsMock.mockResolvedValue({
905
+ success: true,
906
+ selected: ["svc"],
907
+ run: {
908
+ order: ["svc"],
909
+ waves: [["svc"]],
910
+ results: [{ component: "svc", ok: true, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "ok", durationMs: 5 }] }],
911
+ ok: true,
912
+ },
913
+ });
914
+ const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
915
+
916
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
917
+
918
+ expect(exit).toBe(0);
919
+ expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: undefined, componentOutputs: {} });
920
+ const printed = stderrWrite.mock.calls.map((c) => String(c[0])).join("");
921
+ expect(printed).toContain("interpret run completed");
922
+ vi.restoreAllMocks();
923
+ });
924
+
925
+ test("threads --env through to runComponents", async () => {
926
+ runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true } });
927
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
928
+
929
+ await runOpComponents({ args: makeArgs({ path: "svc", env: "staging", temporal: false }), plugins: [], serializers: [] });
930
+
931
+ expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", { env: "staging", componentOutputs: {} });
932
+ vi.restoreAllMocks();
933
+ });
934
+
935
+ test("--json emits the DriverRunResult as JSON on stdout", async () => {
936
+ const run = { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true };
937
+ runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run });
938
+ const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
939
+
940
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", json: true, temporal: false }), plugins: [], serializers: [] });
941
+
942
+ expect(exit).toBe(0);
943
+ const printed = stdoutWrite.mock.calls.map((c) => String(c[0])).join("");
944
+ expect(JSON.parse(printed.trim())).toEqual(run);
945
+ vi.restoreAllMocks();
946
+ });
947
+
948
+ test("all: dispatches the 'all' selector and renders every component", async () => {
949
+ runComponentsMock.mockResolvedValue({
950
+ success: true,
951
+ selected: ["shared-alb", "search-service"],
952
+ run: {
953
+ order: ["shared-alb", "search-service"],
954
+ waves: [["shared-alb"], ["search-service"]],
955
+ results: [
956
+ { component: "shared-alb", ok: true, records: [] },
957
+ { component: "search-service", ok: true, records: [] },
958
+ ],
959
+ ok: true,
960
+ },
961
+ });
962
+ const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
963
+
964
+ const exit = await runOpComponents({ args: makeArgs({ path: "all", temporal: false }), plugins: [], serializers: [] });
965
+
966
+ expect(exit).toBe(0);
967
+ expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "all", { env: undefined, componentOutputs: {} });
968
+ const printed = stderrWrite.mock.calls.map((c) => String(c[0])).join("");
969
+ expect(printed).toContain("shared-alb");
970
+ expect(printed).toContain("search-service");
971
+ vi.restoreAllMocks();
972
+ });
973
+
974
+ test("a failed component run → exit 1", async () => {
975
+ runComponentsMock.mockResolvedValue({
976
+ success: false,
977
+ selected: ["svc"],
978
+ run: {
979
+ order: ["svc"],
980
+ waves: [["svc"]],
981
+ results: [{ component: "svc", ok: false, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
982
+ ok: false,
983
+ failedComponent: "svc",
984
+ },
985
+ });
986
+ const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
987
+
988
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
989
+
990
+ expect(exit).toBe(1);
991
+ const printed = stderrWrite.mock.calls.map((c) => String(c[0])).join("");
992
+ expect(printed).toContain("interpret run failed");
993
+ vi.restoreAllMocks();
994
+ });
995
+
996
+ test("unknown component → exit 1 with the runComponents error message", async () => {
997
+ runComponentsMock.mockResolvedValue({ success: false, selected: [], error: 'Component "missing" not found. Known components: svc' });
998
+ const stderr = makeStderrSpy();
999
+
1000
+ const exit = await runOpComponents({ args: makeArgs({ path: "missing", temporal: false }), plugins: [], serializers: [] });
1001
+
1002
+ expect(exit).toBe(1);
1003
+ expect(stderr.join("\n")).toContain('Component "missing" not found');
1004
+ });
1005
+
1006
+ test("a gate in the component (local mode) → exit 1 with an actionable, Temporal-pointing message", async () => {
1007
+ runComponentsMock.mockResolvedValue({
1008
+ success: false,
1009
+ selected: ["svc"],
1010
+ gateUnsupported: { component: "svc", signalName: "release-approval" },
1011
+ });
1012
+ const stderr = makeStderrSpy();
1013
+
1014
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1015
+
1016
+ expect(exit).toBe(1);
1017
+ const out = stderr.join("\n");
1018
+ expect(out).toContain('gate "release-approval"');
1019
+ expect(out).toContain("--temporal");
1020
+ });
1021
+
1022
+ // ── auto-release recording post-run (#597) ────────────────────────────────
1023
+
1024
+ describe("auto-release recording", () => {
1025
+ test("a successful run → maybeRecordAutoRelease is called once per successful component, with the run's records", async () => {
1026
+ runComponentsMock.mockResolvedValue({
1027
+ success: true,
1028
+ selected: ["svc"],
1029
+ run: {
1030
+ order: ["svc"],
1031
+ waves: [["svc"]],
1032
+ results: [{ component: "svc", ok: true, records: [{ component: "svc", phase: "Publish", kind: "publish-image", status: "ok", durationMs: 5, output: { digest: "sha256:abc" } }] }],
1033
+ ok: true,
1034
+ },
1035
+ });
1036
+ maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: true, commit: "a".repeat(40), record: { version: 1, component: "svc", env: "staging", digest: "sha256:abc", gitSha: "x", runId: "local-1", timestamp: "t", actor: "a" } });
1037
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1038
+
1039
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", env: "staging", temporal: false }), plugins: [], serializers: [] });
1040
+
1041
+ expect(exit).toBe(0);
1042
+ expect(maybeRecordAutoReleaseMock).toHaveBeenCalledTimes(1);
1043
+ const [runInfo, options] = maybeRecordAutoReleaseMock.mock.calls[0];
1044
+ expect(runInfo).toMatchObject({ component: "svc", env: "staging", success: true });
1045
+ expect(runInfo.records).toEqual([{ component: "svc", phase: "Publish", kind: "publish-image", status: "ok", durationMs: 5, output: { digest: "sha256:abc" } }]);
1046
+ expect(options).toMatchObject({ disabled: false });
1047
+ vi.restoreAllMocks();
1048
+ });
1049
+
1050
+ test("a failed run → maybeRecordAutoRelease is never called (failed components write nothing)", async () => {
1051
+ runComponentsMock.mockResolvedValue({
1052
+ success: false,
1053
+ selected: ["svc"],
1054
+ run: {
1055
+ order: ["svc"],
1056
+ waves: [["svc"]],
1057
+ results: [{ component: "svc", ok: false, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1058
+ ok: false,
1059
+ failedComponent: "svc",
1060
+ },
1061
+ });
1062
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1063
+
1064
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1065
+
1066
+ expect(exit).toBe(1);
1067
+ expect(maybeRecordAutoReleaseMock).not.toHaveBeenCalled();
1068
+ vi.restoreAllMocks();
1069
+ });
1070
+
1071
+ test("--no-release-record → maybeRecordAutoRelease is called with disabled: true", async () => {
1072
+ runComponentsMock.mockResolvedValue({
1073
+ success: true,
1074
+ selected: ["svc"],
1075
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1076
+ });
1077
+ maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
1078
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1079
+
1080
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false, noReleaseRecord: true }), plugins: [], serializers: [] });
1081
+
1082
+ expect(exit).toBe(0);
1083
+ expect(maybeRecordAutoReleaseMock).toHaveBeenCalledTimes(1);
1084
+ const [, options] = maybeRecordAutoReleaseMock.mock.calls[0];
1085
+ expect(options).toMatchObject({ disabled: true });
1086
+ vi.restoreAllMocks();
1087
+ });
1088
+
1089
+ test("chant.config.ts release.autoRecord: false → disabled without the CLI flag", async () => {
1090
+ runComponentsMock.mockResolvedValue({
1091
+ success: true,
1092
+ selected: ["svc"],
1093
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1094
+ });
1095
+ loadChantConfigMock.mockResolvedValue({ config: { release: { autoRecord: false } } });
1096
+ maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
1097
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1098
+
1099
+ await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1100
+
1101
+ const [, options] = maybeRecordAutoReleaseMock.mock.calls[0];
1102
+ expect(options).toMatchObject({ disabled: true });
1103
+ vi.restoreAllMocks();
1104
+ });
1105
+
1106
+ test("a release-write error is surfaced as a warning but does not change the exit code", async () => {
1107
+ runComponentsMock.mockResolvedValue({
1108
+ success: true,
1109
+ selected: ["svc"],
1110
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1111
+ });
1112
+ maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "error", error: "ledger push failed" });
1113
+ const stderr = makeStderrSpy();
1114
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1115
+
1116
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1117
+
1118
+ expect(exit).toBe(0);
1119
+ expect(stderr.join("\n")).toContain("ledger push failed");
1120
+ vi.restoreAllMocks();
1121
+ });
1122
+ });
1123
+
1124
+ // ── build-manifest persistence post-run (#609) ──────────────────────────────
1125
+
1126
+ describe("build-manifest persistence", () => {
1127
+ test("a successful run → maybePersistBuildManifest is called once per successful component, with the run's records", async () => {
1128
+ const buildOutput = { archivePath: "image.tar", digest: "sha256:abc", manifest: { version: 1, component: "svc", createdAt: "t", contents: [], manifestDigest: "sha256:manifestabc" } };
1129
+ runComponentsMock.mockResolvedValue({
1130
+ success: true,
1131
+ selected: ["svc"],
1132
+ run: {
1133
+ order: ["svc"],
1134
+ waves: [["svc"]],
1135
+ results: [{ component: "svc", ok: true, records: [{ component: "svc", phase: "Build", kind: "docker-build", status: "ok", durationMs: 5, output: buildOutput }] }],
1136
+ ok: true,
1137
+ },
1138
+ });
1139
+ maybePersistBuildManifestMock.mockResolvedValue({ persisted: true, commit: "a".repeat(40), manifestDigest: "sha256:manifestabc" });
1140
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1141
+
1142
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", env: "staging", temporal: false }), plugins: [], serializers: [] });
1143
+
1144
+ expect(exit).toBe(0);
1145
+ expect(maybePersistBuildManifestMock).toHaveBeenCalledTimes(1);
1146
+ const [runInfo, options] = maybePersistBuildManifestMock.mock.calls[0];
1147
+ expect(runInfo).toMatchObject({ success: true });
1148
+ expect(runInfo.records).toEqual([{ component: "svc", phase: "Build", kind: "docker-build", status: "ok", durationMs: 5, output: buildOutput }]);
1149
+ expect(options).toMatchObject({ disabled: false });
1150
+ vi.restoreAllMocks();
1151
+ });
1152
+
1153
+ test("a failed run → maybePersistBuildManifest is never called (failed/dry-run deploys persist nothing)", async () => {
1154
+ runComponentsMock.mockResolvedValue({
1155
+ success: false,
1156
+ selected: ["svc"],
1157
+ run: {
1158
+ order: ["svc"],
1159
+ waves: [["svc"]],
1160
+ results: [{ component: "svc", ok: false, records: [{ component: "svc", phase: "Apply", kind: "cfn-deploy", status: "fail", durationMs: 5, error: "boom" }] }],
1161
+ ok: false,
1162
+ failedComponent: "svc",
1163
+ },
1164
+ });
1165
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1166
+
1167
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1168
+
1169
+ expect(exit).toBe(1);
1170
+ expect(maybePersistBuildManifestMock).not.toHaveBeenCalled();
1171
+ vi.restoreAllMocks();
1172
+ });
1173
+
1174
+ test("--no-release-record also disables manifest persistence (shared opt-out)", async () => {
1175
+ runComponentsMock.mockResolvedValue({
1176
+ success: true,
1177
+ selected: ["svc"],
1178
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1179
+ });
1180
+ maybePersistBuildManifestMock.mockResolvedValue({ persisted: false, reason: "opted-out" });
1181
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1182
+
1183
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false, noReleaseRecord: true }), plugins: [], serializers: [] });
1184
+
1185
+ expect(exit).toBe(0);
1186
+ expect(maybePersistBuildManifestMock).toHaveBeenCalledTimes(1);
1187
+ const [, options] = maybePersistBuildManifestMock.mock.calls[0];
1188
+ expect(options).toMatchObject({ disabled: true });
1189
+ vi.restoreAllMocks();
1190
+ });
1191
+
1192
+ test("a manifest-write error is surfaced as a warning but does not change the exit code", async () => {
1193
+ runComponentsMock.mockResolvedValue({
1194
+ success: true,
1195
+ selected: ["svc"],
1196
+ run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
1197
+ });
1198
+ maybePersistBuildManifestMock.mockResolvedValue({ persisted: false, reason: "error", error: "manifest push failed" });
1199
+ const stderr = makeStderrSpy();
1200
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1201
+
1202
+ const exit = await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
1203
+
1204
+ expect(exit).toBe(0);
1205
+ expect(stderr.join("\n")).toContain("manifest push failed");
1206
+ vi.restoreAllMocks();
1207
+ });
1208
+ });
1209
+ });
1210
+
1211
+ // ── chant run --components <name> --temporal (#589) ─────────────────────────
1212
+
1213
+ describe("runOpComponents: --temporal routes to the durable path", () => {
1214
+ beforeEach(() => {
1215
+ resolveComponentTargetsMock.mockReset();
1216
+ findComponentGateMock.mockReset();
1217
+ loadComponentTemporalCodegenMock.mockReset();
1218
+ loadChantConfigMock.mockReset();
1219
+ resolveProfileMock.mockReset();
1220
+ loadTemporalClientMock.mockReset();
1221
+ spawnChildMock.mockReset();
1222
+ existsSyncMock.mockReset();
1223
+ findComponentGateMock.mockReturnValue(undefined);
1224
+ maybeRecordAutoReleaseMock.mockReset().mockResolvedValue({ recorded: false, reason: "no-digest" });
1225
+ maybePersistBuildManifestMock.mockReset().mockResolvedValue({ persisted: false, reason: "no-manifest" });
1226
+ });
1227
+
1228
+ test("all --temporal → exit 1, not supported", async () => {
1229
+ const stderr = makeStderrSpy();
1230
+ const exit = await runOpComponents({ args: makeArgs({ path: "all", temporal: true }), plugins: [], serializers: [] });
1231
+ expect(exit).toBe(1);
1232
+ expect(stderr.join("\n")).toContain("not supported");
1233
+ expect(resolveComponentTargetsMock).not.toHaveBeenCalled();
1234
+ });
1235
+
1236
+ test("unknown component → exit 1 with the resolver's error message", async () => {
1237
+ resolveComponentTargetsMock.mockResolvedValue({ success: false, targets: [], error: 'Component "missing" not found.' });
1238
+ const stderr = makeStderrSpy();
1239
+
1240
+ const exit = await runOpComponents({ args: makeArgs({ path: "missing", temporal: true }), plugins: [], serializers: [] });
1241
+
1242
+ expect(exit).toBe(1);
1243
+ expect(stderr.join("\n")).toContain('Component "missing" not found');
1244
+ });
1245
+
1246
+ test("compiles the component, spawns the worker, submits the workflow, polls to COMPLETED", async () => {
1247
+ vi.useFakeTimers();
1248
+ try {
1249
+ resolveComponentTargetsMock.mockResolvedValue({
1250
+ success: true,
1251
+ targets: [{ name: "gated-svc", dependsOn: [], deploy: [] }],
1252
+ });
1253
+ loadChantConfigMock.mockResolvedValue({ config: {} });
1254
+ resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
1255
+ const serializeComponentMock = vi.fn().mockReturnValue({
1256
+ "components/gated-svc/workflow.ts": "// wf",
1257
+ "components/gated-svc/activities.ts": "// act",
1258
+ "components/gated-svc/worker.ts": "// worker",
1259
+ });
1260
+ loadComponentTemporalCodegenMock.mockResolvedValue({
1261
+ serializeComponent: serializeComponentMock,
1262
+ componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
1263
+ });
1264
+ const mockClient = createMockTemporalClient({
1265
+ describeByWorkflowId: {
1266
+ "chant-component-gated-svc": {
1267
+ workflowId: "chant-component-gated-svc", runId: "r1",
1268
+ status: { name: "COMPLETED" }, startTime: new Date(),
1269
+ taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
1270
+ },
1271
+ },
1272
+ historyByWorkflowId: { "chant-component-gated-svc": [] },
1273
+ });
1274
+ setupTemporalClient(mockClient);
1275
+ const { proc } = makeFakeChildProcess();
1276
+ spawnChildMock.mockReturnValue(proc);
1277
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1278
+
1279
+ const promise = runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true, env: "staging" }), plugins: [], serializers: [] });
1280
+ await vi.advanceTimersByTimeAsync(5000);
1281
+ const exit = await promise;
1282
+
1283
+ expect(exit).toBe(0);
1284
+ expect(spawnChildMock).toHaveBeenCalledTimes(1);
1285
+ expect(spawnChildMock.mock.calls[0][0]).toBe("npx");
1286
+ expect(mockClient.calls.startCalls).toHaveLength(1);
1287
+ expect(mockClient.calls.startCalls[0].opts.workflowId).toBe("chant-component-gated-svc");
1288
+ expect(proc.kill).toHaveBeenCalled();
1289
+ // --env must reach the codegen call, not just the local-mode path (the bug this locks in: #589 review).
1290
+ expect(serializeComponentMock).toHaveBeenCalledWith(
1291
+ expect.objectContaining({ name: "gated-svc" }),
1292
+ expect.objectContaining({ env: "staging" }),
1293
+ );
1294
+ } finally {
1295
+ vi.useRealTimers();
1296
+ }
1297
+ });
1298
+
1299
+ test("a failed (non-COMPLETED) workflow → exit 1", async () => {
1300
+ vi.useFakeTimers();
1301
+ try {
1302
+ resolveComponentTargetsMock.mockResolvedValue({
1303
+ success: true,
1304
+ targets: [{ name: "gated-svc", dependsOn: [], deploy: [] }],
1305
+ });
1306
+ loadChantConfigMock.mockResolvedValue({ config: {} });
1307
+ resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
1308
+ loadComponentTemporalCodegenMock.mockResolvedValue({
1309
+ serializeComponent: () => ({ "components/gated-svc/worker.ts": "// worker" }),
1310
+ componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
1311
+ });
1312
+ const mockClient = createMockTemporalClient({
1313
+ describeByWorkflowId: {
1314
+ "chant-component-gated-svc": {
1315
+ workflowId: "chant-component-gated-svc", runId: "r1",
1316
+ status: { name: "FAILED" }, startTime: new Date(),
1317
+ taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
1318
+ },
1319
+ },
1320
+ historyByWorkflowId: { "chant-component-gated-svc": [] },
1321
+ });
1322
+ setupTemporalClient(mockClient);
1323
+ const { proc } = makeFakeChildProcess();
1324
+ spawnChildMock.mockReturnValue(proc);
1325
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1326
+
1327
+ const promise = runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true }), plugins: [], serializers: [] });
1328
+ await vi.advanceTimersByTimeAsync(5000);
1329
+ const exit = await promise;
1330
+
1331
+ expect(exit).toBe(1);
1332
+ expect(proc.kill).toHaveBeenCalled();
1333
+ // (#597) a failed workflow never reaches auto-release recording.
1334
+ expect(maybeRecordAutoReleaseMock).not.toHaveBeenCalled();
1335
+ } finally {
1336
+ vi.useRealTimers();
1337
+ }
1338
+ });
1339
+
1340
+ // ── auto-release recording post-run (#597) ────────────────────────────────
1341
+
1342
+ describe("auto-release recording", () => {
1343
+ async function runCompletedWorkflow(overrides: Partial<ParsedArgs> = {}, resultByWorkflowId?: unknown) {
1344
+ resolveComponentTargetsMock.mockResolvedValue({
1345
+ success: true,
1346
+ targets: [{ name: "gated-svc", dependsOn: [], deploy: [] }],
1347
+ });
1348
+ loadChantConfigMock.mockResolvedValue({ config: {} });
1349
+ resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
1350
+ loadComponentTemporalCodegenMock.mockResolvedValue({
1351
+ serializeComponent: () => ({ "components/gated-svc/worker.ts": "// worker" }),
1352
+ componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
1353
+ });
1354
+ const mockClient = createMockTemporalClient({
1355
+ describeByWorkflowId: {
1356
+ "chant-component-gated-svc": {
1357
+ workflowId: "chant-component-gated-svc", runId: "r1",
1358
+ status: { name: "COMPLETED" }, startTime: new Date(),
1359
+ taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
1360
+ },
1361
+ },
1362
+ historyByWorkflowId: { "chant-component-gated-svc": [] },
1363
+ resultByWorkflowId: resultByWorkflowId ? { "chant-component-gated-svc": resultByWorkflowId } : undefined,
1364
+ });
1365
+ setupTemporalClient(mockClient);
1366
+ const { proc } = makeFakeChildProcess();
1367
+ spawnChildMock.mockReturnValue(proc);
1368
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1369
+
1370
+ const promise = runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true, ...overrides }), plugins: [], serializers: [] });
1371
+ await vi.advanceTimersByTimeAsync(5000);
1372
+ const exit = await promise;
1373
+ return { exit };
1374
+ }
1375
+
1376
+ test("a COMPLETED workflow → reads the digest via handle.result() and calls maybeRecordAutoRelease once", async () => {
1377
+ vi.useFakeTimers();
1378
+ try {
1379
+ maybeRecordAutoReleaseMock.mockResolvedValue({
1380
+ recorded: true, commit: "a".repeat(40),
1381
+ record: { version: 1, component: "gated-svc", env: "staging", digest: "sha256:temporal-digest", gitSha: "x", runId: "r1", timestamp: "t", actor: "a" },
1382
+ });
1383
+
1384
+ const { exit } = await runCompletedWorkflow({ env: "staging" }, { phaseOutputs: { Publish: { digest: "sha256:temporal-digest" } }, componentOutputs: {} });
1385
+
1386
+ expect(exit).toBe(0);
1387
+ expect(maybeRecordAutoReleaseMock).toHaveBeenCalledTimes(1);
1388
+ const [runInfo, options] = maybeRecordAutoReleaseMock.mock.calls[0];
1389
+ expect(runInfo).toMatchObject({ component: "gated-svc", env: "staging", success: true, digest: "sha256:temporal-digest", runId: "r1" });
1390
+ expect(options).toMatchObject({ disabled: false });
1391
+ } finally {
1392
+ vi.useRealTimers();
1393
+ }
1394
+ });
1395
+
1396
+ test("--no-release-record → disabled: true is threaded through on the Temporal path too", async () => {
1397
+ vi.useFakeTimers();
1398
+ try {
1399
+ maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
1400
+
1401
+ await runCompletedWorkflow({ noReleaseRecord: true });
1402
+
1403
+ const [, options] = maybeRecordAutoReleaseMock.mock.calls[0];
1404
+ expect(options).toMatchObject({ disabled: true });
1405
+ } finally {
1406
+ vi.useRealTimers();
1407
+ }
1408
+ });
1409
+
1410
+ test("no digest in the workflow result → maybeRecordAutoRelease still called, with digest: undefined", async () => {
1411
+ vi.useFakeTimers();
1412
+ try {
1413
+ maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "no-digest" });
1414
+
1415
+ const { exit } = await runCompletedWorkflow({}, { phaseOutputs: {}, componentOutputs: {} });
1416
+
1417
+ expect(exit).toBe(0);
1418
+ const [runInfo] = maybeRecordAutoReleaseMock.mock.calls[0];
1419
+ expect(runInfo.digest).toBeUndefined();
1420
+ } finally {
1421
+ vi.useRealTimers();
1422
+ }
1423
+ });
1424
+ });
1425
+
1426
+ // ── build-manifest persistence post-run (#609) ──────────────────────────────
1427
+
1428
+ describe("build-manifest persistence", () => {
1429
+ async function runCompletedWorkflow(overrides: Partial<ParsedArgs> = {}, resultByWorkflowId?: unknown) {
1430
+ resolveComponentTargetsMock.mockResolvedValue({
1431
+ success: true,
1432
+ targets: [{ name: "gated-svc", dependsOn: [], deploy: [] }],
1433
+ });
1434
+ loadChantConfigMock.mockResolvedValue({ config: {} });
1435
+ resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
1436
+ loadComponentTemporalCodegenMock.mockResolvedValue({
1437
+ serializeComponent: () => ({ "components/gated-svc/worker.ts": "// worker" }),
1438
+ componentWorkflowFnName: (name: string) => `${name}ComponentWorkflow`,
1439
+ });
1440
+ const mockClient = createMockTemporalClient({
1441
+ describeByWorkflowId: {
1442
+ "chant-component-gated-svc": {
1443
+ workflowId: "chant-component-gated-svc", runId: "r1",
1444
+ status: { name: "COMPLETED" }, startTime: new Date(),
1445
+ taskQueue: "gated-svc", type: { name: "gatedSvcComponentWorkflow" },
1446
+ },
1447
+ },
1448
+ historyByWorkflowId: { "chant-component-gated-svc": [] },
1449
+ resultByWorkflowId: resultByWorkflowId ? { "chant-component-gated-svc": resultByWorkflowId } : undefined,
1450
+ });
1451
+ setupTemporalClient(mockClient);
1452
+ const { proc } = makeFakeChildProcess();
1453
+ spawnChildMock.mockReturnValue(proc);
1454
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
1455
+
1456
+ const promise = runOpComponents({ args: makeArgs({ path: "gated-svc", temporal: true, ...overrides }), plugins: [], serializers: [] });
1457
+ await vi.advanceTimersByTimeAsync(5000);
1458
+ const exit = await promise;
1459
+ return { exit };
1460
+ }
1461
+
1462
+ const sampleManifest = { version: 1, component: "gated-svc", createdAt: "t", contents: [], manifestDigest: "sha256:manifest-temporal" };
1463
+
1464
+ test("a COMPLETED workflow → reads the manifest via handle.result() and calls maybePersistBuildManifest once", async () => {
1465
+ vi.useFakeTimers();
1466
+ try {
1467
+ maybePersistBuildManifestMock.mockResolvedValue({ persisted: true, commit: "a".repeat(40), manifestDigest: "sha256:manifest-temporal" });
1468
+
1469
+ const { exit } = await runCompletedWorkflow({ env: "staging" }, { phaseOutputs: { Build: { manifest: sampleManifest } }, componentOutputs: {} });
1470
+
1471
+ expect(exit).toBe(0);
1472
+ expect(maybePersistBuildManifestMock).toHaveBeenCalledTimes(1);
1473
+ const [runInfo, options] = maybePersistBuildManifestMock.mock.calls[0];
1474
+ expect(runInfo).toMatchObject({ success: true, manifest: sampleManifest });
1475
+ expect(options).toMatchObject({ disabled: false });
1476
+ } finally {
1477
+ vi.useRealTimers();
1478
+ }
1479
+ });
1480
+
1481
+ test("--no-release-record also disables manifest persistence on the Temporal path", async () => {
1482
+ vi.useFakeTimers();
1483
+ try {
1484
+ maybePersistBuildManifestMock.mockResolvedValue({ persisted: false, reason: "opted-out" });
1485
+
1486
+ await runCompletedWorkflow({ noReleaseRecord: true });
1487
+
1488
+ const [, options] = maybePersistBuildManifestMock.mock.calls[0];
1489
+ expect(options).toMatchObject({ disabled: true });
1490
+ } finally {
1491
+ vi.useRealTimers();
1492
+ }
1493
+ });
1494
+
1495
+ test("no manifest in the workflow result → maybePersistBuildManifest still called, with manifest: undefined", async () => {
1496
+ vi.useFakeTimers();
1497
+ try {
1498
+ maybePersistBuildManifestMock.mockResolvedValue({ persisted: false, reason: "no-manifest" });
1499
+
1500
+ const { exit } = await runCompletedWorkflow({}, { phaseOutputs: {}, componentOutputs: {} });
1501
+
1502
+ expect(exit).toBe(0);
1503
+ const [runInfo] = maybePersistBuildManifestMock.mock.calls[0];
1504
+ expect(runInfo.manifest).toBeUndefined();
1505
+ } finally {
1506
+ vi.useRealTimers();
1507
+ }
1508
+ });
1509
+ });
1510
+ });