@intentius/chant 0.124.27 → 0.124.29

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 (681) hide show
  1. package/README.md +1 -1
  2. package/dist/__golden__/wave-loop/fan-out-gated.json +8 -0
  3. package/dist/__golden__/wave-loop/fan-out-no-members.json +4 -0
  4. package/dist/__golden__/wave-loop/fan-out-only.json +6 -0
  5. package/dist/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  6. package/dist/__golden__/wave-loop/op-cost-member.json +3 -0
  7. package/dist/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  8. package/dist/__golden__/wave-loop/op-held-on.json +2 -0
  9. package/dist/__golden__/wave-loop/op-hooks-held.json +2 -0
  10. package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  11. package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  12. package/dist/__golden__/wave-loop/op-hooks-share.json +3 -0
  13. package/dist/__golden__/wave-loop/op-per-run.json +7 -0
  14. package/dist/__golden__/wave-loop/op-pr-review.json +7 -0
  15. package/dist/__golden__/wave-loop/op-sealed.json +3 -0
  16. package/dist/__golden__/wave-loop/op-spend.json +3 -0
  17. package/dist/__golden__/wave-loop/op-wave-commands.json +7 -0
  18. package/dist/__golden__/wave-loop/op-wide-waiting.json +4 -0
  19. package/dist/build-params.d.ts +34 -1
  20. package/dist/build-params.d.ts.map +1 -1
  21. package/dist/build-params.js +31 -1
  22. package/dist/change-set-graph.d.ts +63 -0
  23. package/dist/change-set-graph.d.ts.map +1 -0
  24. package/dist/change-set-graph.js +130 -0
  25. package/dist/change-set.d.ts +62 -3
  26. package/dist/change-set.d.ts.map +1 -1
  27. package/dist/change-set.js +36 -4
  28. package/dist/chant-version.d.ts +9 -0
  29. package/dist/chant-version.d.ts.map +1 -0
  30. package/dist/chant-version.js +16 -0
  31. package/dist/cli/build-params-cli.d.ts +7 -1
  32. package/dist/cli/build-params-cli.d.ts.map +1 -1
  33. package/dist/cli/build-params-cli.js +17 -8
  34. package/dist/cli/commands/build.d.ts +8 -0
  35. package/dist/cli/commands/build.d.ts.map +1 -1
  36. package/dist/cli/commands/build.js +15 -11
  37. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  38. package/dist/cli/commands/check-lexicon-docs.js +30 -2
  39. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  40. package/dist/cli/commands/check-lexicon.js +13 -2
  41. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
  42. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  43. package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
  44. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  45. package/dist/cli/commands/init-lexicon.js +17 -8
  46. package/dist/cli/handlers/approve.d.ts.map +1 -1
  47. package/dist/cli/handlers/approve.js +64 -4
  48. package/dist/cli/handlers/build.d.ts.map +1 -1
  49. package/dist/cli/handlers/build.js +43 -4
  50. package/dist/cli/handlers/change-set.d.ts +10 -0
  51. package/dist/cli/handlers/change-set.d.ts.map +1 -1
  52. package/dist/cli/handlers/change-set.js +51 -1
  53. package/dist/cli/handlers/fan-out.d.ts +10 -1
  54. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  55. package/dist/cli/handlers/fan-out.js +54 -6
  56. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  57. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  58. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  59. package/dist/cli/handlers/pr.d.ts +16 -2
  60. package/dist/cli/handlers/pr.d.ts.map +1 -1
  61. package/dist/cli/handlers/pr.js +269 -37
  62. package/dist/cli/handlers/promote.d.ts +7 -0
  63. package/dist/cli/handlers/promote.d.ts.map +1 -1
  64. package/dist/cli/handlers/promote.js +440 -28
  65. package/dist/cli/handlers/release.d.ts +91 -0
  66. package/dist/cli/handlers/release.d.ts.map +1 -0
  67. package/dist/cli/handlers/release.js +589 -0
  68. package/dist/cli/handlers/run.d.ts +38 -2
  69. package/dist/cli/handlers/run.d.ts.map +1 -1
  70. package/dist/cli/handlers/run.js +234 -16
  71. package/dist/cli/main.d.ts.map +1 -1
  72. package/dist/cli/main.js +143 -17
  73. package/dist/cli/mcp/workspace-tools.js +1 -1
  74. package/dist/cli/parse-args.d.ts.map +1 -1
  75. package/dist/cli/parse-args.js +73 -5
  76. package/dist/cli/registry.d.ts +43 -5
  77. package/dist/cli/registry.d.ts.map +1 -1
  78. package/dist/cli/run-events.d.ts +30 -0
  79. package/dist/cli/run-events.d.ts.map +1 -0
  80. package/dist/cli/run-events.js +49 -0
  81. package/dist/cli/version.d.ts +3 -5
  82. package/dist/cli/version.d.ts.map +1 -1
  83. package/dist/cli/version.js +3 -13
  84. package/dist/codegen/docs-in-site.d.ts +71 -0
  85. package/dist/codegen/docs-in-site.d.ts.map +1 -0
  86. package/dist/codegen/docs-in-site.js +177 -0
  87. package/dist/codegen/docs-types.d.ts +14 -1
  88. package/dist/codegen/docs-types.d.ts.map +1 -1
  89. package/dist/codegen/docs.d.ts +18 -1
  90. package/dist/codegen/docs.d.ts.map +1 -1
  91. package/dist/codegen/docs.js +40 -7
  92. package/dist/codegen/rehype-base-url.mjs +15 -9
  93. package/dist/components/auto-release.d.ts +6 -1
  94. package/dist/components/auto-release.d.ts.map +1 -1
  95. package/dist/components/auto-release.js +3 -0
  96. package/dist/components/capability.d.ts +9 -0
  97. package/dist/components/capability.d.ts.map +1 -1
  98. package/dist/components/cli-support.d.ts +6 -0
  99. package/dist/components/cli-support.d.ts.map +1 -1
  100. package/dist/components/cli-support.js +10 -9
  101. package/dist/components/driver.d.ts +50 -9
  102. package/dist/components/driver.d.ts.map +1 -1
  103. package/dist/components/driver.js +116 -54
  104. package/dist/components/fan-out-inputs.d.ts +69 -0
  105. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  106. package/dist/components/fan-out-inputs.js +169 -0
  107. package/dist/components/fan-out-record.d.ts +13 -0
  108. package/dist/components/fan-out-record.d.ts.map +1 -1
  109. package/dist/components/fan-out-record.js +16 -0
  110. package/dist/components/fan-out-run.d.ts +7 -0
  111. package/dist/components/fan-out-run.d.ts.map +1 -1
  112. package/dist/components/fan-out-run.js +85 -26
  113. package/dist/components/fan-out.d.ts +4 -0
  114. package/dist/components/fan-out.d.ts.map +1 -1
  115. package/dist/components/pr-pipeline.d.ts +25 -0
  116. package/dist/components/pr-pipeline.d.ts.map +1 -1
  117. package/dist/components/pr-pipeline.js +22 -4
  118. package/dist/components/pr-run.d.ts +11 -0
  119. package/dist/components/pr-run.d.ts.map +1 -1
  120. package/dist/components/pr-run.js +20 -1
  121. package/dist/components/promote-order.d.ts +38 -0
  122. package/dist/components/promote-order.d.ts.map +1 -0
  123. package/dist/components/promote-order.js +62 -0
  124. package/dist/components/promote.d.ts +38 -2
  125. package/dist/components/promote.d.ts.map +1 -1
  126. package/dist/components/promote.js +77 -2
  127. package/dist/components/release-compile.d.ts +142 -0
  128. package/dist/components/release-compile.d.ts.map +1 -0
  129. package/dist/components/release-compile.js +382 -0
  130. package/dist/components/release-gate.d.ts +135 -0
  131. package/dist/components/release-gate.d.ts.map +1 -0
  132. package/dist/components/release-gate.js +288 -0
  133. package/dist/components/release-promote.d.ts +96 -0
  134. package/dist/components/release-promote.d.ts.map +1 -0
  135. package/dist/components/release-promote.js +204 -0
  136. package/dist/components/run-progress.d.ts +7 -0
  137. package/dist/components/run-progress.d.ts.map +1 -1
  138. package/dist/components/wave-plan.d.ts +8 -1
  139. package/dist/components/wave-plan.d.ts.map +1 -1
  140. package/dist/components/wave-plan.js +11 -3
  141. package/dist/config.d.ts +45 -0
  142. package/dist/config.d.ts.map +1 -1
  143. package/dist/config.js +3 -0
  144. package/dist/flag-params.d.ts +54 -0
  145. package/dist/flag-params.d.ts.map +1 -0
  146. package/dist/flag-params.js +130 -0
  147. package/dist/index.d.ts +1 -0
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +2 -0
  150. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  151. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  152. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  153. package/dist/lifecycle/change-set-store.d.ts +68 -0
  154. package/dist/lifecycle/change-set-store.d.ts.map +1 -0
  155. package/dist/lifecycle/change-set-store.js +97 -0
  156. package/dist/lifecycle/commit-sha.d.ts +13 -0
  157. package/dist/lifecycle/commit-sha.d.ts.map +1 -0
  158. package/dist/lifecycle/commit-sha.js +21 -0
  159. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  160. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  161. package/dist/lifecycle/delivery-metrics.js +145 -0
  162. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  163. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  164. package/dist/lifecycle/deploy-outcome.js +101 -0
  165. package/dist/lifecycle/gate-change-set.d.ts +78 -0
  166. package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
  167. package/dist/lifecycle/gate-change-set.js +138 -0
  168. package/dist/lifecycle/gate-ledger.d.ts +51 -4
  169. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  170. package/dist/lifecycle/gate-ledger.js +18 -0
  171. package/dist/lifecycle/git.d.ts +30 -1
  172. package/dist/lifecycle/git.d.ts.map +1 -1
  173. package/dist/lifecycle/git.js +31 -5
  174. package/dist/lifecycle/index.d.ts +4 -0
  175. package/dist/lifecycle/index.d.ts.map +1 -1
  176. package/dist/lifecycle/index.js +4 -0
  177. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  178. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  179. package/dist/lifecycle/mixed-environment.js +154 -0
  180. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  181. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  182. package/dist/lifecycle/publish-attachments.js +179 -0
  183. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  184. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  185. package/dist/lifecycle/publish-credentials.js +69 -0
  186. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  187. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  188. package/dist/lifecycle/publish-declaration.js +83 -0
  189. package/dist/lifecycle/publish-forge.d.ts +53 -0
  190. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  191. package/dist/lifecycle/publish-forge.js +298 -0
  192. package/dist/lifecycle/publish-npm.d.ts +61 -0
  193. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  194. package/dist/lifecycle/publish-npm.js +242 -0
  195. package/dist/lifecycle/publish-oci.d.ts +64 -0
  196. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  197. package/dist/lifecycle/publish-oci.js +383 -0
  198. package/dist/lifecycle/publish-record.d.ts +66 -0
  199. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  200. package/dist/lifecycle/publish-record.js +84 -0
  201. package/dist/lifecycle/publish-targets.d.ts +45 -0
  202. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  203. package/dist/lifecycle/publish-targets.js +85 -0
  204. package/dist/lifecycle/release-cut.d.ts +154 -0
  205. package/dist/lifecycle/release-cut.d.ts.map +1 -0
  206. package/dist/lifecycle/release-cut.js +366 -0
  207. package/dist/lifecycle/release-join.d.ts +95 -0
  208. package/dist/lifecycle/release-join.d.ts.map +1 -0
  209. package/dist/lifecycle/release-join.js +139 -0
  210. package/dist/lifecycle/release-ledger.d.ts +54 -0
  211. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  212. package/dist/lifecycle/release-ledger.js +53 -20
  213. package/dist/lifecycle/release-publish.d.ts +312 -0
  214. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  215. package/dist/lifecycle/release-publish.js +335 -0
  216. package/dist/lifecycle/release-revocation.d.ts +80 -0
  217. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  218. package/dist/lifecycle/release-revocation.js +123 -0
  219. package/dist/lifecycle/release-store.d.ts +200 -0
  220. package/dist/lifecycle/release-store.d.ts.map +1 -0
  221. package/dist/lifecycle/release-store.js +320 -0
  222. package/dist/lifecycle/release-verify.d.ts +61 -0
  223. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  224. package/dist/lifecycle/release-verify.js +92 -0
  225. package/dist/lifecycle/release-workstation.d.ts +91 -0
  226. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  227. package/dist/lifecycle/release-workstation.js +180 -0
  228. package/dist/lifecycle/retention-policy.d.ts +45 -0
  229. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  230. package/dist/lifecycle/retention-policy.js +43 -0
  231. package/dist/lifecycle/retention.d.ts +185 -0
  232. package/dist/lifecycle/retention.d.ts.map +1 -0
  233. package/dist/lifecycle/retention.js +400 -0
  234. package/dist/lifecycle/run-ledger.d.ts +4 -0
  235. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  236. package/dist/lifecycle/run-ledger.js +2 -0
  237. package/dist/op/activities/change-coverage.d.ts.map +1 -1
  238. package/dist/op/activities/change-coverage.js +4 -2
  239. package/dist/op/activities/source-release.d.ts.map +1 -1
  240. package/dist/op/activities/source-release.js +7 -3
  241. package/dist/op/activities/source-rollback.d.ts.map +1 -1
  242. package/dist/op/activities/source-rollback.js +7 -3
  243. package/dist/op/gate-name.d.ts +11 -2
  244. package/dist/op/gate-name.d.ts.map +1 -1
  245. package/dist/op/gate-name.js +12 -3
  246. package/dist/op/gate-run-locator.d.ts +22 -0
  247. package/dist/op/gate-run-locator.d.ts.map +1 -1
  248. package/dist/op/gate-run-locator.js +31 -0
  249. package/dist/op/gate.d.ts +27 -3
  250. package/dist/op/gate.d.ts.map +1 -1
  251. package/dist/op/gate.js +57 -3
  252. package/dist/op/local-executor.d.ts +36 -1
  253. package/dist/op/local-executor.d.ts.map +1 -1
  254. package/dist/op/local-executor.js +158 -52
  255. package/dist/op/op-waves-run.d.ts.map +1 -1
  256. package/dist/op/op-waves-run.js +4 -0
  257. package/dist/op/run-live.d.ts +2 -0
  258. package/dist/op/run-live.d.ts.map +1 -1
  259. package/dist/op/run-live.js +1 -1
  260. package/dist/op/runtime.d.ts +46 -0
  261. package/dist/op/runtime.d.ts.map +1 -1
  262. package/dist/op/runtimes/local.d.ts.map +1 -1
  263. package/dist/op/runtimes/local.js +8 -0
  264. package/dist/plan-summary.d.ts.map +1 -1
  265. package/dist/plan-summary.js +8 -2
  266. package/dist/point-decision.d.ts +2 -0
  267. package/dist/point-decision.d.ts.map +1 -1
  268. package/dist/point-decision.js +9 -2
  269. package/dist/pr-loop.d.ts +7 -0
  270. package/dist/pr-loop.d.ts.map +1 -1
  271. package/dist/pr-loop.js +23 -4
  272. package/dist/pr-report-url.d.ts +82 -0
  273. package/dist/pr-report-url.d.ts.map +1 -0
  274. package/dist/pr-report-url.js +174 -0
  275. package/dist/provenance.d.ts +2 -2
  276. package/dist/provenance.d.ts.map +1 -1
  277. package/dist/run-event-store.d.ts +104 -0
  278. package/dist/run-event-store.d.ts.map +1 -0
  279. package/dist/run-event-store.js +251 -0
  280. package/dist/run-event-stream.d.ts +133 -0
  281. package/dist/run-event-stream.d.ts.map +1 -0
  282. package/dist/run-event-stream.js +242 -0
  283. package/dist/run-event.d.ts +285 -0
  284. package/dist/run-event.d.ts.map +1 -0
  285. package/dist/run-event.js +209 -0
  286. package/dist/wave-runtime.mjs +262 -226
  287. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  288. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  289. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  290. package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
  291. package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
  292. package/dist/workspace/__fixtures__/run-event-check.js +59 -0
  293. package/dist/workspace/change-set-show.d.ts +56 -0
  294. package/dist/workspace/change-set-show.d.ts.map +1 -0
  295. package/dist/workspace/change-set-show.js +54 -0
  296. package/dist/workspace/change-set-show.schema.json +102 -0
  297. package/dist/workspace/change-set.schema.json +32 -1
  298. package/dist/workspace/changes-cli.d.ts.map +1 -1
  299. package/dist/workspace/changes-cli.js +10 -1
  300. package/dist/workspace/changes.d.ts +37 -0
  301. package/dist/workspace/changes.d.ts.map +1 -1
  302. package/dist/workspace/changes.js +49 -2
  303. package/dist/workspace/changes.schema.json +135 -0
  304. package/dist/workspace/check.schema.json +4 -2
  305. package/dist/workspace/checks/records.d.ts +9 -1
  306. package/dist/workspace/checks/records.d.ts.map +1 -1
  307. package/dist/workspace/checks/records.js +35 -1
  308. package/dist/workspace/checks.d.ts +1 -0
  309. package/dist/workspace/checks.d.ts.map +1 -1
  310. package/dist/workspace/checks.js +2 -1
  311. package/dist/workspace/ci-green-cli.d.ts +19 -4
  312. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  313. package/dist/workspace/ci-green-cli.js +75 -12
  314. package/dist/workspace/ci-green-forge.d.ts +20 -3
  315. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  316. package/dist/workspace/ci-green-forge.js +6 -4
  317. package/dist/workspace/ci-green-workflow.d.ts +11 -0
  318. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  319. package/dist/workspace/ci-green-workflow.js +6 -0
  320. package/dist/workspace/ci-green.d.ts +281 -13
  321. package/dist/workspace/ci-green.d.ts.map +1 -1
  322. package/dist/workspace/ci-green.js +456 -38
  323. package/dist/workspace/ci-last-green.schema.json +32 -2
  324. package/dist/workspace/ci-list.schema.json +525 -0
  325. package/dist/workspace/commit-links.d.ts +85 -0
  326. package/dist/workspace/commit-links.d.ts.map +1 -0
  327. package/dist/workspace/commit-links.js +163 -0
  328. package/dist/workspace/declaration.d.ts +35 -0
  329. package/dist/workspace/declaration.d.ts.map +1 -1
  330. package/dist/workspace/declaration.js +27 -1
  331. package/dist/workspace/declaration.schema.json +76 -0
  332. package/dist/workspace/declared-kinds.d.ts +16 -0
  333. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  334. package/dist/workspace/declared-kinds.js +25 -0
  335. package/dist/workspace/delivery-metrics.d.ts +91 -0
  336. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  337. package/dist/workspace/delivery-metrics.js +217 -0
  338. package/dist/workspace/delivery-metrics.schema.json +519 -0
  339. package/dist/workspace/flags-read.d.ts +37 -0
  340. package/dist/workspace/flags-read.d.ts.map +1 -0
  341. package/dist/workspace/flags-read.js +103 -0
  342. package/dist/workspace/flags.d.ts +100 -0
  343. package/dist/workspace/flags.d.ts.map +1 -0
  344. package/dist/workspace/flags.js +98 -0
  345. package/dist/workspace/graph-cli.js +1 -1
  346. package/dist/workspace/identity-gate-rule.d.ts +37 -0
  347. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  348. package/dist/workspace/identity-gate-rule.js +72 -7
  349. package/dist/workspace/identity.d.ts +12 -0
  350. package/dist/workspace/identity.d.ts.map +1 -1
  351. package/dist/workspace/identity.js +13 -1
  352. package/dist/workspace/intent-cli.d.ts +11 -2
  353. package/dist/workspace/intent-cli.d.ts.map +1 -1
  354. package/dist/workspace/intent-cli.js +54 -5
  355. package/dist/workspace/intent-trace.d.ts +219 -0
  356. package/dist/workspace/intent-trace.d.ts.map +1 -0
  357. package/dist/workspace/intent-trace.js +314 -0
  358. package/dist/workspace/intent.d.ts +36 -7
  359. package/dist/workspace/intent.d.ts.map +1 -1
  360. package/dist/workspace/intent.js +192 -22
  361. package/dist/workspace/intent.schema.json +702 -6
  362. package/dist/workspace/ls.d.ts +4 -0
  363. package/dist/workspace/ls.d.ts.map +1 -1
  364. package/dist/workspace/ls.js +2 -0
  365. package/dist/workspace/ls.schema.json +3 -1
  366. package/dist/workspace/member-pipeline.d.ts +6 -0
  367. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  368. package/dist/workspace/member-pipeline.js +6 -0
  369. package/dist/workspace/overlap.d.ts +146 -0
  370. package/dist/workspace/overlap.d.ts.map +1 -0
  371. package/dist/workspace/overlap.js +276 -0
  372. package/dist/workspace/promotion-order.d.ts +41 -0
  373. package/dist/workspace/promotion-order.d.ts.map +1 -0
  374. package/dist/workspace/promotion-order.js +95 -0
  375. package/dist/workspace/reason-codes.d.ts +4 -0
  376. package/dist/workspace/reason-codes.d.ts.map +1 -1
  377. package/dist/workspace/reason-codes.js +7 -1
  378. package/dist/workspace/records.d.ts +15 -2
  379. package/dist/workspace/records.d.ts.map +1 -1
  380. package/dist/workspace/records.js +32 -0
  381. package/dist/workspace/records.schema.json +21 -2
  382. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  383. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  384. package/dist/workspace/releases.d.ts +81 -0
  385. package/dist/workspace/releases.d.ts.map +1 -0
  386. package/dist/workspace/releases.js +103 -0
  387. package/dist/workspace/releases.schema.json +513 -0
  388. package/dist/workspace/run-event.schema.json +295 -0
  389. package/dist/workspace/status-gates.d.ts +34 -1
  390. package/dist/workspace/status-gates.d.ts.map +1 -1
  391. package/dist/workspace/status-gates.js +31 -2
  392. package/dist/workspace/status-stewards.d.ts +4 -0
  393. package/dist/workspace/status-stewards.d.ts.map +1 -1
  394. package/dist/workspace/status-stewards.js +2 -0
  395. package/dist/workspace/status.d.ts +117 -0
  396. package/dist/workspace/status.d.ts.map +1 -1
  397. package/dist/workspace/status.js +218 -4
  398. package/dist/workspace/status.schema.json +268 -2
  399. package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
  400. package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
  401. package/dist/workspace/trust/ci-tag-statement.js +123 -0
  402. package/dist/workspace/trust/op-run-statement.d.ts +91 -0
  403. package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
  404. package/dist/workspace/trust/op-run-statement.js +143 -0
  405. package/dist/workspace/trust/rotation.d.ts +18 -2
  406. package/dist/workspace/trust/rotation.d.ts.map +1 -1
  407. package/dist/workspace/trust/rotation.js +65 -16
  408. package/dist/workspace/trust/seal.d.ts +11 -0
  409. package/dist/workspace/trust/seal.d.ts.map +1 -1
  410. package/dist/workspace/trust/seal.js +9 -0
  411. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  412. package/dist/workspace/trust/ssh-commit.js +10 -10
  413. package/dist/workspace/wip.d.ts.map +1 -1
  414. package/dist/workspace/wip.js +11 -3
  415. package/package.json +1 -1
  416. package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
  417. package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
  418. package/src/__golden__/wave-loop/fan-out-only.json +6 -0
  419. package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  420. package/src/__golden__/wave-loop/op-cost-member.json +3 -0
  421. package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  422. package/src/__golden__/wave-loop/op-held-on.json +2 -0
  423. package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
  424. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  425. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  426. package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
  427. package/src/__golden__/wave-loop/op-per-run.json +7 -0
  428. package/src/__golden__/wave-loop/op-pr-review.json +7 -0
  429. package/src/__golden__/wave-loop/op-sealed.json +3 -0
  430. package/src/__golden__/wave-loop/op-spend.json +3 -0
  431. package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
  432. package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
  433. package/src/build-params.test.ts +46 -0
  434. package/src/build-params.ts +55 -1
  435. package/src/change-set-graph.test.ts +72 -0
  436. package/src/change-set-graph.ts +141 -0
  437. package/src/change-set.ts +87 -6
  438. package/src/chant-version.ts +16 -0
  439. package/src/cli/build-params-cli.ts +22 -9
  440. package/src/cli/commands/build.ts +25 -11
  441. package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
  442. package/src/cli/commands/check-lexicon-docs.ts +39 -2
  443. package/src/cli/commands/check-lexicon.ts +14 -2
  444. package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
  445. package/src/cli/commands/init-lexicon.test.ts +25 -0
  446. package/src/cli/commands/init-lexicon.ts +18 -8
  447. package/src/cli/handlers/approve-release.test.ts +84 -0
  448. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  449. package/src/cli/handlers/approve.ts +65 -2
  450. package/src/cli/handlers/build.test.ts +67 -0
  451. package/src/cli/handlers/build.ts +46 -4
  452. package/src/cli/handlers/change-set.ts +49 -1
  453. package/src/cli/handlers/fan-out.test.ts +228 -1
  454. package/src/cli/handlers/fan-out.ts +53 -6
  455. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  456. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  457. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  458. package/src/cli/handlers/operator.test.ts +37 -0
  459. package/src/cli/handlers/pr.test.ts +341 -0
  460. package/src/cli/handlers/pr.ts +292 -47
  461. package/src/cli/handlers/promote.test.ts +669 -7
  462. package/src/cli/handlers/promote.ts +464 -29
  463. package/src/cli/handlers/release-implicit.e2e.test.ts +161 -0
  464. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  465. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  466. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  467. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  468. package/src/cli/handlers/release.e2e.test.ts +199 -0
  469. package/src/cli/handlers/release.ts +593 -0
  470. package/src/cli/handlers/run-auto-release.test.ts +80 -0
  471. package/src/cli/handlers/run-events-cmd.test.ts +92 -0
  472. package/src/cli/handlers/run-release.test.ts +55 -0
  473. package/src/cli/handlers/run.test.ts +30 -0
  474. package/src/cli/handlers/run.ts +243 -18
  475. package/src/cli/main.test.ts +8 -0
  476. package/src/cli/main.ts +143 -17
  477. package/src/cli/mcp/never-over-mcp.test.ts +2 -0
  478. package/src/cli/mcp/workspace-tools.ts +1 -1
  479. package/src/cli/parse-args.ts +56 -5
  480. package/src/cli/registry.ts +43 -5
  481. package/src/cli/run-events.ts +57 -0
  482. package/src/cli/version.ts +3 -13
  483. package/src/codegen/docs-in-site.test.ts +166 -0
  484. package/src/codegen/docs-in-site.ts +182 -0
  485. package/src/codegen/docs-site/AgentPrompt.astro +26 -1
  486. package/src/codegen/docs-types.ts +14 -1
  487. package/src/codegen/docs.ts +60 -7
  488. package/src/codegen/rehype-base-url.d.mts +3 -1
  489. package/src/codegen/rehype-base-url.mjs +15 -9
  490. package/src/codegen/rehype-base-url.test.ts +34 -0
  491. package/src/components/auto-release.test.ts +30 -0
  492. package/src/components/auto-release.ts +9 -1
  493. package/src/components/capability.ts +9 -0
  494. package/src/components/cli-support.test.ts +82 -0
  495. package/src/components/cli-support.ts +16 -9
  496. package/src/components/driver.test.ts +24 -0
  497. package/src/components/driver.ts +165 -56
  498. package/src/components/fan-out-inputs.test.ts +85 -0
  499. package/src/components/fan-out-inputs.ts +192 -0
  500. package/src/components/fan-out-record.test.ts +10 -0
  501. package/src/components/fan-out-record.ts +19 -0
  502. package/src/components/fan-out-run.ts +104 -35
  503. package/src/components/fan-out.ts +4 -0
  504. package/src/components/pr-pipeline.ts +35 -4
  505. package/src/components/pr-run.ts +30 -1
  506. package/src/components/promote-order.test.ts +90 -0
  507. package/src/components/promote-order.ts +75 -0
  508. package/src/components/promote.test.ts +10 -1
  509. package/src/components/promote.ts +94 -3
  510. package/src/components/release-compile.test.ts +147 -0
  511. package/src/components/release-compile.ts +459 -0
  512. package/src/components/release-gate.test.ts +215 -0
  513. package/src/components/release-gate.ts +355 -0
  514. package/src/components/release-promote.test.ts +159 -0
  515. package/src/components/release-promote.ts +232 -0
  516. package/src/components/run-progress.ts +7 -0
  517. package/src/components/wave-plan.ts +16 -3
  518. package/src/config.ts +15 -1
  519. package/src/flag-params.test.ts +144 -0
  520. package/src/flag-params.ts +134 -0
  521. package/src/index.ts +2 -0
  522. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  523. package/src/lifecycle/change-set-store.test.ts +217 -0
  524. package/src/lifecycle/change-set-store.ts +113 -0
  525. package/src/lifecycle/commit-sha.ts +23 -0
  526. package/src/lifecycle/delivery-metrics.ts +263 -0
  527. package/src/lifecycle/deploy-outcome.ts +147 -0
  528. package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
  529. package/src/lifecycle/gate-change-set.ts +164 -0
  530. package/src/lifecycle/gate-ledger.test.ts +33 -0
  531. package/src/lifecycle/gate-ledger.ts +68 -3
  532. package/src/lifecycle/git.ts +61 -8
  533. package/src/lifecycle/index.ts +4 -0
  534. package/src/lifecycle/mixed-environment.test.ts +97 -0
  535. package/src/lifecycle/mixed-environment.ts +204 -0
  536. package/src/lifecycle/publish-attachments.ts +196 -0
  537. package/src/lifecycle/publish-credentials.ts +84 -0
  538. package/src/lifecycle/publish-declaration.ts +95 -0
  539. package/src/lifecycle/publish-forge.ts +325 -0
  540. package/src/lifecycle/publish-npm.ts +259 -0
  541. package/src/lifecycle/publish-oci.ts +396 -0
  542. package/src/lifecycle/publish-record.ts +113 -0
  543. package/src/lifecycle/publish-resume.test.ts +168 -0
  544. package/src/lifecycle/publish-targets.test.ts +494 -0
  545. package/src/lifecycle/publish-targets.ts +100 -0
  546. package/src/lifecycle/release-cut-signed.test.ts +180 -0
  547. package/src/lifecycle/release-cut.test.ts +483 -0
  548. package/src/lifecycle/release-cut.ts +503 -0
  549. package/src/lifecycle/release-flag.test.ts +112 -0
  550. package/src/lifecycle/release-join.test.ts +68 -0
  551. package/src/lifecycle/release-join.ts +182 -0
  552. package/src/lifecycle/release-ledger.test.ts +14 -0
  553. package/src/lifecycle/release-ledger.ts +85 -17
  554. package/src/lifecycle/release-publish.test.ts +225 -0
  555. package/src/lifecycle/release-publish.ts +568 -0
  556. package/src/lifecycle/release-revocation.ts +147 -0
  557. package/src/lifecycle/release-store.ts +434 -0
  558. package/src/lifecycle/release-verify.test.ts +110 -0
  559. package/src/lifecycle/release-verify.ts +120 -0
  560. package/src/lifecycle/release-workstation.test.ts +204 -0
  561. package/src/lifecycle/release-workstation.ts +216 -0
  562. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  563. package/src/lifecycle/retention-policy.ts +63 -0
  564. package/src/lifecycle/retention.test.ts +363 -0
  565. package/src/lifecycle/retention.ts +508 -0
  566. package/src/lifecycle/run-ledger.test.ts +10 -0
  567. package/src/lifecycle/run-ledger.ts +6 -0
  568. package/src/op/activities/change-coverage.ts +4 -1
  569. package/src/op/activities/source-release.ts +8 -3
  570. package/src/op/activities/source-rollback.ts +8 -4
  571. package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
  572. package/src/op/gate-name.ts +13 -3
  573. package/src/op/gate-release.test.ts +126 -0
  574. package/src/op/gate-run-locator.test.ts +110 -0
  575. package/src/op/gate-run-locator.ts +35 -0
  576. package/src/op/gate.ts +88 -6
  577. package/src/op/local-executor.ts +207 -52
  578. package/src/op/op-waves-run.ts +4 -0
  579. package/src/op/run-events.test.ts +166 -0
  580. package/src/op/run-live.ts +1 -1
  581. package/src/op/runtime.ts +39 -0
  582. package/src/op/runtimes/local.test.ts +16 -0
  583. package/src/op/runtimes/local.ts +9 -0
  584. package/src/plan-summary.test.ts +20 -0
  585. package/src/plan-summary.ts +8 -1
  586. package/src/point-decision.ts +12 -2
  587. package/src/pr-loop.ts +25 -4
  588. package/src/pr-report-url.test.ts +131 -0
  589. package/src/pr-report-url.ts +209 -0
  590. package/src/provenance.ts +2 -2
  591. package/src/run-event-store.test.ts +144 -0
  592. package/src/run-event-store.ts +273 -0
  593. package/src/run-event-stream.test.ts +175 -0
  594. package/src/run-event-stream.ts +309 -0
  595. package/src/run-event.test.ts +277 -0
  596. package/src/run-event.ts +471 -0
  597. package/src/wave-loop-golden.test.ts +9 -0
  598. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  599. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  600. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  601. package/src/workspace/__fixtures__/run-event-check.ts +58 -0
  602. package/src/workspace/ancestry-index.test.ts +93 -0
  603. package/src/workspace/change-set-show.schema.json +102 -0
  604. package/src/workspace/change-set-show.ts +72 -0
  605. package/src/workspace/change-set.schema.json +32 -1
  606. package/src/workspace/changes-cli.ts +10 -1
  607. package/src/workspace/changes.schema.json +135 -0
  608. package/src/workspace/changes.test.ts +1 -1
  609. package/src/workspace/changes.ts +84 -2
  610. package/src/workspace/check.schema.json +4 -2
  611. package/src/workspace/checks/records.ts +35 -1
  612. package/src/workspace/checks.ts +2 -1
  613. package/src/workspace/ci-green-cli.ts +75 -13
  614. package/src/workspace/ci-green-forge.ts +24 -4
  615. package/src/workspace/ci-green-runs.test.ts +242 -0
  616. package/src/workspace/ci-green-signed.test.ts +302 -0
  617. package/src/workspace/ci-green-workflow.test.ts +16 -0
  618. package/src/workspace/ci-green-workflow.ts +12 -0
  619. package/src/workspace/ci-green.test.ts +131 -2
  620. package/src/workspace/ci-green.ts +635 -42
  621. package/src/workspace/ci-last-green.schema.json +32 -2
  622. package/src/workspace/ci-list.schema.json +525 -0
  623. package/src/workspace/commit-links.test.ts +279 -0
  624. package/src/workspace/commit-links.ts +183 -0
  625. package/src/workspace/declaration.schema.json +76 -0
  626. package/src/workspace/declaration.test.ts +28 -0
  627. package/src/workspace/declaration.ts +60 -2
  628. package/src/workspace/declared-kinds.ts +40 -1
  629. package/src/workspace/delivery-metrics.schema.json +519 -0
  630. package/src/workspace/delivery-metrics.test.ts +236 -0
  631. package/src/workspace/delivery-metrics.ts +286 -0
  632. package/src/workspace/flags-read.ts +121 -0
  633. package/src/workspace/flags.test.ts +141 -0
  634. package/src/workspace/flags.ts +157 -0
  635. package/src/workspace/graph-cli.ts +1 -1
  636. package/src/workspace/identity-gate-rule.ts +91 -7
  637. package/src/workspace/identity.ts +14 -1
  638. package/src/workspace/intent-cli.ts +46 -5
  639. package/src/workspace/intent-trace.test.ts +173 -0
  640. package/src/workspace/intent-trace.ts +484 -0
  641. package/src/workspace/intent.schema.json +702 -6
  642. package/src/workspace/intent.ts +196 -26
  643. package/src/workspace/ls.schema.json +3 -1
  644. package/src/workspace/ls.ts +6 -0
  645. package/src/workspace/member-pipeline.test.ts +17 -0
  646. package/src/workspace/member-pipeline.ts +9 -0
  647. package/src/workspace/older-records.test.ts +412 -0
  648. package/src/workspace/overlap.test.ts +165 -0
  649. package/src/workspace/overlap.ts +340 -0
  650. package/src/workspace/promotion-order.ts +97 -0
  651. package/src/workspace/read-contract.e2e.test.ts +5 -1
  652. package/src/workspace/reason-codes.test.ts +7 -1
  653. package/src/workspace/reason-codes.ts +7 -1
  654. package/src/workspace/record-assets.test.ts +2 -1
  655. package/src/workspace/records-contract.e2e.test.ts +2 -1
  656. package/src/workspace/records.schema.json +21 -2
  657. package/src/workspace/records.ts +36 -2
  658. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  659. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  660. package/src/workspace/releases.schema.json +513 -0
  661. package/src/workspace/releases.test.ts +108 -0
  662. package/src/workspace/releases.ts +133 -0
  663. package/src/workspace/run-event.schema.json +295 -0
  664. package/src/workspace/status-contract.test.ts +81 -1
  665. package/src/workspace/status-gates.ts +64 -1
  666. package/src/workspace/status-mixed.test.ts +152 -0
  667. package/src/workspace/status-revoked.test.ts +153 -0
  668. package/src/workspace/status-stewards.ts +6 -0
  669. package/src/workspace/status.schema.json +268 -2
  670. package/src/workspace/status.ts +334 -4
  671. package/src/workspace/trust/ci-tag-statement.ts +151 -0
  672. package/src/workspace/trust/op-run-statement.ts +148 -0
  673. package/src/workspace/trust/rotation.ts +61 -14
  674. package/src/workspace/trust/seal.ts +10 -0
  675. package/src/workspace/trust/ssh-commit.ts +10 -9
  676. package/src/workspace/wip.ts +11 -2
  677. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
  678. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
  679. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
  680. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
  681. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
@@ -9,12 +9,19 @@ import { CapabilityRegistry, type DeployContext } from "../../components/capabil
9
9
  import type { DriverComponent } from "../../components/driver";
10
10
  import type { ReleaseRecord } from "../../lifecycle/release-ledger";
11
11
  import type { ParsedArgs } from "../registry";
12
+ import { makeRelease } from "../../lifecycle/release-store";
13
+ import { contentDigest } from "../../content-digest";
12
14
 
13
15
  const appendReleaseRecordMock = vi.fn();
14
16
  const readReleaseLedgerMock = vi.fn();
15
17
  const pushLifecycleMock = vi.fn();
16
18
  const resolveTargetsMock = vi.fn();
17
19
  const ran: string[] = [];
20
+ const stepInputs: Array<{ kind: string; input: unknown }> = [];
21
+ let releaseFixture: { release: import("../../lifecycle/release-store").Release; label: string | null } | null = null;
22
+ let storedTemplate: string | null = null;
23
+ const writtenTemplates: unknown[] = [];
24
+ let configEnvironments: unknown[] | undefined;
18
25
  let publishedDigest = "sha256:api";
19
26
  let failApplyFor: string | undefined;
20
27
 
@@ -35,7 +42,7 @@ vi.mock("../../lifecycle/release-ledger", async () => {
35
42
 
36
43
  vi.mock("../../config", async () => {
37
44
  const actual = await vi.importActual<typeof import("../../config")>("../../config");
38
- return { ...actual, loadChantConfig: async () => ({ config: {} }) };
45
+ return { ...actual, loadChantConfig: async () => ({ config: configEnvironments ? { environments: configEnvironments } : {} }) };
39
46
  });
40
47
 
41
48
  vi.mock("../../components/cli-support", () => ({
@@ -47,13 +54,25 @@ vi.mock("../../components/fan-out-support", () => ({
47
54
  fanOutRegistry: async () => {
48
55
  const registry = new CapabilityRegistry();
49
56
  const step = (kind: string, output: (ctx: DeployContext) => unknown) =>
50
- registry.register({ kind, async run(ctx: DeployContext) { ran.push(`${ctx.env}:${kind}`); return output(ctx); } } as never);
57
+ registry.register({ kind, async run(ctx: DeployContext, input: unknown) { ran.push(`${ctx.env}:${kind}`); stepInputs.push({ kind, input }); return output(ctx); } } as never);
51
58
  step("docker-build", () => ({ digest: "sha256:rebuilt" }));
52
59
  step("publish-image", () => ({ digest: publishedDigest, uri: `reg/api@${publishedDigest}` }));
53
60
  step("apply", (ctx) => {
54
61
  if (ctx.component === failApplyFor) throw new Error("apply failed");
55
62
  return {};
56
63
  });
64
+ // #4045: a planner whose member is provisional.
65
+ registry.register({
66
+ kind: "plan-provisional",
67
+ async run() { return {}; },
68
+ async plan() {
69
+ return {
70
+ member: "estate",
71
+ planDigest: "sha256:" + "3".repeat(64),
72
+ changeSet: { member: { member: "estate", lexicon: "terraform", planner: "terraform", status: "planned", planDigest: "sha256:" + "3".repeat(64), provisional: true, holes: [] }, entries: [] },
73
+ };
74
+ },
75
+ } as never);
57
76
  return registry;
58
77
  },
59
78
  }));
@@ -62,14 +81,84 @@ vi.mock("../../components/fan-out-support", () => ({
62
81
  vi.mock("../../op/gate", async () => {
63
82
  const actual = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
64
83
  const port = actual.memoryGateLedgerPort();
65
- return { ...actual, gitGateLedgerPort: () => port, gateIsSealed: async (gate: string) => sealedGates.has(gate) };
84
+ return { ...actual, gitGateLedgerPort: () => releaseGatePort ?? port, gateIsSealed: async (gate: string) => sealedGates.has(gate) };
66
85
  });
67
86
  const sealedGates = new Set<string>();
87
+ /** #4045: a fresh ledger per release gate test, so their approvals don't carry between tests. */
88
+ let releaseGatePort: ReturnType<typeof import("../../op/gate").memoryGateLedgerPort> | null = null;
89
+
90
+ // #4045: stored change sets live in memory, keyed by digest.
91
+ const storedChangeSets = new Map<string, import("../../change-set").ChangeSetDocument>();
92
+ let storeFails = false;
93
+ vi.mock("../../lifecycle/gate-change-set", () => ({
94
+ storeGateChangeSet: async ({ doc }: { doc: import("../../change-set").ChangeSetDocument }) => {
95
+ if (storeFails) throw new Error("the branch is read-only");
96
+ const written = !storedChangeSets.has(doc.digest);
97
+ storedChangeSets.set(doc.digest, doc);
98
+ return { digest: doc.digest, written };
99
+ },
100
+ }));
101
+ vi.mock("../../lifecycle/change-set-store", async () => {
102
+ const actual = await vi.importActual<typeof import("../../lifecycle/change-set-store")>("../../lifecycle/change-set-store");
103
+ return {
104
+ ...actual,
105
+ readStoredChangeSet: async (digest: string) => {
106
+ const doc = storedChangeSets.get(digest);
107
+ return doc ? actual.parseStoredChangeSet(JSON.stringify(doc), digest) : { status: "missing" };
108
+ },
109
+ };
110
+ });
111
+ const knownReleases = new Map<string, import("../../lifecycle/release-store").Release>();
112
+ const removedTemplates: unknown[] = [];
113
+
114
+ // #4026: the release store holds one release and one template's bytes.
115
+ vi.mock("../../lifecycle/release-store", async () => {
116
+ const actual = await vi.importActual<typeof import("../../lifecycle/release-store")>("../../lifecycle/release-store");
117
+ return {
118
+ ...actual,
119
+ resolveRelease: async (ref: string) => (releaseFixture && (ref === releaseFixture.label || ref === releaseFixture.release.releaseDigest) ? releaseFixture : null),
120
+ readReleaseTemplate: async () => storedTemplate,
121
+ readRelease: async (digest: string) => knownReleases.get(digest) ?? null,
122
+ // #4035: labels come from the fixture's cuts, never from this checkout's branch.
123
+ readCuts: async () => ({
124
+ cuts: [...knownLabels].map(([releaseDigest, label]) => ({ version: 1, releaseDigest, label, actor: "alice", timestamp: "2026-01-01T00:00:00.000Z", runOrigin: null, signature: null })),
125
+ malformed: 0,
126
+ }),
127
+ };
128
+ });
129
+ const knownLabels = new Map<string, string>();
130
+
131
+ // #4035: deploy outcomes live in memory.
132
+ const outcomes: import("../../lifecycle/deploy-outcome").DeployOutcome[] = [];
133
+ vi.mock("../../lifecycle/deploy-outcome", async () => {
134
+ const actual = await vi.importActual<typeof import("../../lifecycle/deploy-outcome")>("../../lifecycle/deploy-outcome");
135
+ return {
136
+ ...actual,
137
+ appendDeployOutcome: async (o: import("../../lifecycle/deploy-outcome").DeployOutcome) => { outcomes.push(o); return "c0ffee1"; },
138
+ readDeployOutcomes: async (env: string) => outcomes.filter((o) => o.env === env),
139
+ };
140
+ });
141
+ vi.mock("../../components/release-promote", async () => {
142
+ const actual = await vi.importActual<typeof import("../../components/release-promote")>("../../components/release-promote");
143
+ return {
144
+ ...actual,
145
+ writePinnedTemplates: (_dir: string, files: unknown[]) => writtenTemplates.push(...files),
146
+ removePinnedTemplates: (_dir: string, files: unknown[]) => removedTemplates.push(...files),
147
+ };
148
+ });
149
+
150
+ // #4033: the revoked commits come from a map, not from this checkout's tags.
151
+ const revokedCommits = new Map<string, import("../../lifecycle/release-revocation").ReleaseRevocation>();
152
+ vi.mock("../../lifecycle/release-revocation", async () => {
153
+ const actual = await vi.importActual<typeof import("../../lifecycle/release-revocation")>("../../lifecycle/release-revocation");
154
+ return { ...actual, readRevokedCommits: async () => revokedCommits };
155
+ });
68
156
 
69
157
  // The run handler pulls in every runtime; the promote needs only its exit code.
70
158
  vi.mock("./run", () => ({ GATED_EXIT_CODE: 3 }));
71
159
 
72
160
  const { runComponentsPromote, runComponentsRollback, runComponentsRedeploy } = await import("./promote");
161
+ const { checkRunEventLines, stepTrail } = await import("../../workspace/__fixtures__/run-event-check");
73
162
 
74
163
  const api: DriverComponent = {
75
164
  name: "api",
@@ -116,7 +205,20 @@ let logs: string[];
116
205
 
117
206
  beforeEach(() => {
118
207
  sealedGates.clear();
208
+ releaseGatePort = null;
209
+ storedChangeSets.clear();
210
+ storeFails = false;
211
+ knownReleases.clear();
212
+ knownLabels.clear();
213
+ outcomes.length = 0;
214
+ revokedCommits.clear();
215
+ removedTemplates.length = 0;
119
216
  ran.length = 0;
217
+ stepInputs.length = 0;
218
+ writtenTemplates.length = 0;
219
+ releaseFixture = null;
220
+ storedTemplate = null;
221
+ configEnvironments = undefined;
120
222
  publishedDigest = "sha256:api";
121
223
  failApplyFor = undefined;
122
224
  errors = [];
@@ -135,9 +237,52 @@ afterEach(() => {
135
237
  });
136
238
 
137
239
  describe("chant components promote", () => {
138
- test("--from and --to are required", async () => {
240
+ test("--to is required", async () => {
139
241
  expect(await runComponentsPromote(ctx({ migrateTo: undefined }))).toBe(1);
140
- expect(errors.join("\n")).toMatch(/--from <env> and --to <env> are required/);
242
+ expect(errors.join("\n")).toMatch(/--to <env> is required/);
243
+ });
244
+
245
+ test("--from is required when the member declares no environments, and nothing is refused", async () => {
246
+ expect(await runComponentsPromote(ctx({ migrateFrom: undefined }))).toBe(1);
247
+ expect(errors.join("\n")).toMatch(/--from <env> is required: this member declares no environments/);
248
+ errors.length = 0;
249
+ expect(await runComponentsPromote(ctx({ migrateFrom: "dev", migrateTo: "prod" }))).toBe(0);
250
+ });
251
+
252
+ test("the declared order defaults --from to the previous environment (#4030)", async () => {
253
+ configEnvironments = ["dev", { name: "staging", canary: true }, "prod"];
254
+ expect(await runComponentsPromote(ctx({ migrateFrom: undefined }))).toBe(0);
255
+ expect(readReleaseLedgerMock).toHaveBeenCalledWith("staging");
256
+ expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ env: "prod", promotedFrom: { env: "staging" } });
257
+ });
258
+
259
+ test("a promotion that skips an environment is refused with a code and a fix, and deploys nothing (#4030)", async () => {
260
+ configEnvironments = ["dev", "staging", "prod"];
261
+ expect(await runComponentsPromote(ctx({ migrateFrom: "dev", migrateTo: "prod" }))).toBe(1);
262
+ const out = errors.join("\n");
263
+ expect(out).toMatch(/promoting dev to prod skips staging/);
264
+ expect(out).toMatch(/\[promote-skips-environment\]/);
265
+ expect(out).toMatch(/--allow-skip/);
266
+ expect(ran).toEqual([]);
267
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
268
+ });
269
+
270
+ test("--allow-skip promotes past a declared environment (#4030)", async () => {
271
+ configEnvironments = ["dev", "staging", "prod"];
272
+ readReleaseLedgerMock.mockResolvedValue({ records: [{ ...staging, env: "dev" }], malformed: 0 });
273
+ expect(await runComponentsPromote(ctx({ migrateFrom: "dev", migrateTo: "prod", allowSkip: true }))).toBe(0);
274
+ expect(readReleaseLedgerMock).toHaveBeenCalledWith("dev");
275
+ expect(appendReleaseRecordMock).toHaveBeenCalledTimes(1);
276
+ });
277
+
278
+ test("the first environment of the order has nothing to default --from to (#4030)", async () => {
279
+ configEnvironments = ["dev", "staging", "prod"];
280
+ expect(await runComponentsPromote(ctx({ migrateFrom: undefined, migrateTo: "dev" }))).toBe(1);
281
+ expect(errors.join("\n")).toMatch(/\[promote-no-previous-environment\]/);
282
+ errors.length = 0;
283
+ expect(await runComponentsPromote(ctx({ migrateFrom: undefined, migrateTo: "qa" }))).toBe(1);
284
+ expect(errors.join("\n")).toMatch(/\[promote-to-not-in-order\]/);
285
+ expect(ran).toEqual([]);
141
286
  });
142
287
 
143
288
  test("deploys the staging digest to prod without building, and records the promotion", async () => {
@@ -255,6 +400,230 @@ describe("chant components promote", () => {
255
400
  });
256
401
  });
257
402
 
403
+ describe("chant components promote --release (#4026)", () => {
404
+ const prodTemplate = `{"env":"prod"}\n`;
405
+ const withTemplate: DriverComponent = {
406
+ ...api,
407
+ deploy: [...api.deploy.slice(0, 2), { phase: "Apply", steps: [{ kind: "apply", template: "dist/api.template.json", imageRef: "@Publish.digest" }] }],
408
+ };
409
+ const release = makeRelease({
410
+ member: null,
411
+ commit: "abc123".padEnd(40, "0"),
412
+ artifactDigests: [{ component: "api", kind: "image", path: "api.tar", digest: "sha256:api" }],
413
+ targets: [
414
+ { env: "staging", templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(`{"env":"staging"}\n`) }], params: [] },
415
+ { env: "prod", templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(prodTemplate) }], params: [] },
416
+ ],
417
+ });
418
+
419
+ beforeEach(() => {
420
+ resolveTargetsMock.mockResolvedValue({ success: true, targets: [withTemplate] });
421
+ releaseFixture = { release, label: "v1.0.0" };
422
+ storedTemplate = prodTemplate;
423
+ });
424
+
425
+ test("deploys the template the release recorded for --to, and names the release on the record", async () => {
426
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
427
+ expect(ran).toEqual(["prod:publish-image", "prod:apply"]);
428
+ const prodPath = `.chant/release-templates/${release.targets.find((t) => t.env === "prod")!.templates[0].digest.replace(/:/g, "_")}/api.template.json`;
429
+ expect(stepInputs.find((s) => s.kind === "apply")?.input).toMatchObject({ template: prodPath });
430
+ expect(writtenTemplates).toEqual([{ path: prodPath, content: prodTemplate }]);
431
+ expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ env: "prod", digest: "sha256:api", releaseDigest: release.releaseDigest });
432
+ });
433
+
434
+ test("refuses a template whose digest differs from the target's, and deploys nothing", async () => {
435
+ storedTemplate = `{"env":"prod","edited":true}\n`;
436
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(1);
437
+ expect(errors.join("\n")).toMatch(/hashes to sha256:[0-9a-f]{64}, but the release's target for "prod" records/);
438
+ expect(errors.join("\n")).toMatch(/nothing was deployed/);
439
+ expect(ran).toEqual([]);
440
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
441
+ });
442
+
443
+ test("refuses a release it can't find, and one with no target for --to", async () => {
444
+ expect(await runComponentsPromote(ctx({ release: "v9" }))).toBe(1);
445
+ expect(errors.join("\n")).toMatch(/no release is labelled or digested "v9"/);
446
+ errors.length = 0;
447
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0", migrateTo: "qa" }))).toBe(1);
448
+ expect(errors.join("\n")).toMatch(/has no target for "qa"/);
449
+ expect(ran).toEqual([]);
450
+ });
451
+
452
+ test("a promote without --release reads the checkout's template as before", async () => {
453
+ expect(await runComponentsPromote(ctx({}))).toBe(0);
454
+ expect(stepInputs.find((s) => s.kind === "apply")?.input).toMatchObject({ template: "dist/api.template.json" });
455
+ expect(appendReleaseRecordMock.mock.calls[0][0].releaseDigest).toBeUndefined();
456
+ });
457
+ });
458
+
459
+ describe("chant components promote --release binds its gate (#4045)", () => {
460
+ const prodTemplate = `{"env":"prod"}\n`;
461
+ const stagingTemplate = `{"env":"staging"}\n`;
462
+ const gated: DriverComponent = {
463
+ ...api,
464
+ deploy: [
465
+ ...api.deploy.slice(0, 2),
466
+ { phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/api.template.json", imageRef: "@Publish.digest" }] },
467
+ ],
468
+ };
469
+ const releaseAt = (commit: string) =>
470
+ makeRelease({
471
+ member: null,
472
+ commit: commit.padEnd(40, "0"),
473
+ artifactDigests: [{ component: "api", kind: "image", path: "api.tar", digest: "sha256:api" }],
474
+ targets: [
475
+ { env: "staging", templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(stagingTemplate) }], params: [] },
476
+ { env: "prod", templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(prodTemplate) }], params: [] },
477
+ ],
478
+ });
479
+ const r1 = releaseAt("abc123");
480
+ const r2 = releaseAt("def456");
481
+ const dev: ReleaseRecord = { ...staging, env: "dev" };
482
+ let ledgers: Record<string, ReleaseRecord[]>;
483
+
484
+ beforeEach(async () => {
485
+ const { memoryGateLedgerPort } = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
486
+ releaseGatePort = memoryGateLedgerPort();
487
+ resolveTargetsMock.mockResolvedValue({ success: true, targets: [gated] });
488
+ releaseFixture = { release: r1, label: "v1.0.0" };
489
+ storedTemplate = prodTemplate;
490
+ knownReleases.set(r1.releaseDigest, r1);
491
+ knownReleases.set(r2.releaseDigest, r2);
492
+ ledgers = { dev: [dev], staging: [staging], prod: [] };
493
+ readReleaseLedgerMock.mockImplementation(async (env: string) => ({ records: ledgers[env] ?? [], malformed: 0 }));
494
+ });
495
+
496
+ const onlyStored = () => [...storedChangeSets.values()];
497
+
498
+ test("a non-empty change set stops at a release gate that binds the release, the environment and the stored document", async () => {
499
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
500
+ expect(ran).not.toContain("prod:apply");
501
+ const [doc] = onlyStored();
502
+ expect(doc).toMatchObject({ releaseDigest: r1.releaseDigest, environment: "prod" });
503
+ expect(doc.entries[0]).toMatchObject({ address: "api", action: "create" });
504
+ expect(releaseGatePort!.appended).toEqual([
505
+ expect.objectContaining({ op: "release", gate: "prod-release", environment: "prod", planDigest: doc.digest, releaseDigest: r1.releaseDigest }),
506
+ ]);
507
+ expect(errors.join("\n")).toMatch(/chant approve release prod-release --env prod/);
508
+ expect(errors.join("\n")).toMatch(/release gate: change set jcs1-sha256:[0-9a-f]{64} for "prod", 1 entry/);
509
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
510
+ expect(removedTemplates).toHaveLength(1);
511
+ });
512
+
513
+ test("an approval of that release passes the gate, and the deploy record names the release and the gate's resolution", async () => {
514
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
515
+ const pending = releaseGatePort!.appended[0];
516
+ await releaseGatePort!.appendResolution!({
517
+ op: "release", gate: "prod-release", resolvedBy: "alice", timestamp: new Date(Date.now() + 1000).toISOString(),
518
+ planDigest: pending.planDigest, environment: "prod", releaseDigest: r1.releaseDigest,
519
+ });
520
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
521
+ expect(ran).toContain("prod:apply");
522
+ expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({
523
+ releaseDigest: r1.releaseDigest,
524
+ approver: "alice",
525
+ admission: { op: "release", gate: "prod-release" },
526
+ });
527
+ });
528
+
529
+ test("a release cut from a revoked commit is refused before anything runs, so its pending gate never passes (#4033)", async () => {
530
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
531
+ const pending = releaseGatePort!.appended[0];
532
+ await releaseGatePort!.appendResolution!({
533
+ op: "release", gate: "prod-release", resolvedBy: "alice", timestamp: new Date(Date.now() + 1000).toISOString(),
534
+ planDigest: pending.planDigest, environment: "prod", releaseDigest: r1.releaseDigest,
535
+ });
536
+ // The commit went red after the approval: a run from chant runs (#4044) names its run in opRun, with id null.
537
+ revokedCommits.set(r1.commit, {
538
+ commit: r1.commit,
539
+ tag: `ci/revoked/${r1.commit}`,
540
+ tagged: "2026-10-16T12:00:00.000Z",
541
+ phase: "e2e",
542
+ run: { name: "smoke", id: null, conclusion: "failure", url: null, opRun: { id: "r-12", env: "staging", signer: "runner:ci" } },
543
+ signer: "runner:ci",
544
+ });
545
+ ran.length = 0;
546
+ errors.length = 0;
547
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(1);
548
+ const said = errors.join("\n");
549
+ expect(said).toMatch(/release v1\.0\.0 was cut from commit abc123000000, which ci\/revoked\/abc1230{34} revokes \(phase e2e failed in smoke \(Op run r-12 in staging\)\)/);
550
+ expect(said).toMatch(/\[release-revoked\]/);
551
+ expect(said).toMatch(/chant ci last-green/);
552
+ expect(ran).toEqual([]);
553
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
554
+ });
555
+
556
+ test("an approval for one release can't be used by another", async () => {
557
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
558
+ const pending = releaseGatePort!.appended[0];
559
+ await releaseGatePort!.appendResolution!({
560
+ op: "release", gate: "prod-release", resolvedBy: "alice", timestamp: new Date(Date.now() + 1000).toISOString(),
561
+ planDigest: pending.planDigest, environment: "prod", releaseDigest: r1.releaseDigest,
562
+ });
563
+ releaseFixture = { release: r2, label: "v1.0.1" };
564
+ ran.length = 0;
565
+ expect(await runComponentsPromote(ctx({ release: "v1.0.1" }))).toBe(3);
566
+ expect(ran).not.toContain("prod:apply");
567
+ expect(releaseGatePort!.appended.at(-1)).toMatchObject({ releaseDigest: r2.releaseDigest });
568
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
569
+ });
570
+
571
+ test("an approval for one environment can't be used for another", async () => {
572
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
573
+ const pending = releaseGatePort!.appended[0];
574
+ await releaseGatePort!.appendResolution!({
575
+ op: "release", gate: "prod-release", resolvedBy: "alice", timestamp: new Date(Date.now() + 1000).toISOString(),
576
+ planDigest: pending.planDigest, environment: "prod", releaseDigest: r1.releaseDigest,
577
+ });
578
+ storedTemplate = stagingTemplate;
579
+ ran.length = 0;
580
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0", migrateFrom: "dev", migrateTo: "staging" }))).toBe(3);
581
+ expect(ran).not.toContain("staging:apply");
582
+ expect(releaseGatePort!.appended.at(-1)).toMatchObject({ environment: "staging", releaseDigest: r1.releaseDigest });
583
+ });
584
+
585
+ test("an empty change set passes without an approval: the promote writes an empty resolution and a deploy record naming the release", async () => {
586
+ ledgers.prod = [{ ...staging, env: "prod", releaseDigest: r1.releaseDigest, timestamp: "2026-01-03T00:00:00.000Z" }];
587
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
588
+ expect(ran).toContain("prod:apply");
589
+ const [doc] = onlyStored();
590
+ expect(doc.entries).toEqual([]);
591
+ expect(releaseGatePort!.appended).toEqual([]);
592
+ expect(releaseGatePort!.resolved).toEqual([
593
+ expect.objectContaining({ op: "release", gate: "prod-release", resolvedBy: "bob", empty: true, planDigest: doc.digest, environment: "prod", releaseDigest: r1.releaseDigest }),
594
+ ]);
595
+ expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ env: "prod", releaseDigest: r1.releaseDigest, approver: "bob", admission: { op: "release", gate: "prod-release" } });
596
+ expect(errors.join("\n")).toMatch(/empty: its gates pass without an approval/);
597
+ // Run again: the pass stands, and no second line is written.
598
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
599
+ expect(releaseGatePort!.resolved).toHaveLength(1);
600
+ });
601
+
602
+ test("an empty pass counts only once the stored document reads back empty", async () => {
603
+ ledgers.prod = [{ ...staging, env: "prod", releaseDigest: r1.releaseDigest, timestamp: "2026-01-03T00:00:00.000Z" }];
604
+ storeFails = true;
605
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
606
+ expect(errors.join("\n")).toMatch(/could not store the change set/);
607
+ expect(ran).not.toContain("prod:apply");
608
+ });
609
+
610
+ test("a change set with a provisional member is refused before anything deploys", async () => {
611
+ resolveTargetsMock.mockResolvedValue({
612
+ success: true,
613
+ targets: [{ ...gated, deploy: [...gated.deploy, { phase: "Plan", steps: [{ kind: "plan-provisional" }] }] }],
614
+ });
615
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(1);
616
+ expect(errors.join("\n")).toMatch(/provisional members \(estate\)/);
617
+ expect(ran).toEqual([]);
618
+ });
619
+
620
+ test("a release promote whose compositions declare no gate binds nothing and stores no change set", async () => {
621
+ resolveTargetsMock.mockResolvedValue({ success: true, targets: [{ ...gated, deploy: [...gated.deploy.slice(0, 2), { phase: "Apply", steps: [{ kind: "apply", template: "dist/api.template.json" }] }] }] });
622
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
623
+ expect(storedChangeSets.size).toBe(0);
624
+ });
625
+ });
626
+
258
627
  describe("chant components rollback", () => {
259
628
  const prod = (digest: string, timestamp: string, runId: string): ReleaseRecord =>
260
629
  ({ ...staging, env: "prod", digest, timestamp, runId });
@@ -270,7 +639,7 @@ describe("chant components rollback", () => {
270
639
 
271
640
  test("an environment and --component are required", async () => {
272
641
  expect(await runComponentsRollback(rb({ component: undefined }))).toBe(1);
273
- expect(errors.join("\n")).toMatch(/--component <name> are required/);
642
+ expect(errors.join("\n")).toMatch(/--component <name> or --release <label\|digest> are required/);
274
643
  });
275
644
 
276
645
  test("redeploys the previous release from its recorded digest and records what it restored", async () => {
@@ -287,6 +656,24 @@ describe("chant components rollback", () => {
287
656
  expect(appendReleaseRecordMock.mock.calls[0][0]).not.toHaveProperty("promotedFrom");
288
657
  });
289
658
 
659
+ test("copies the release off the record it restores, and writes none for a record of no release (#4043)", async () => {
660
+ const RELEASE = `jcs1-sha256:${"b".repeat(64)}`;
661
+ readReleaseLedgerMock.mockResolvedValue({
662
+ records: [{ ...prod("sha256:old", "2026-01-01T00:00:00.000Z", "run-1"), releaseDigest: RELEASE }, prod("sha256:new", "2026-01-02T00:00:00.000Z", "run-2")],
663
+ malformed: 0,
664
+ });
665
+ expect(await runComponentsRollback(rb({}))).toBe(0);
666
+ expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ digest: "sha256:old", releaseDigest: RELEASE });
667
+
668
+ appendReleaseRecordMock.mockClear();
669
+ readReleaseLedgerMock.mockResolvedValue({
670
+ records: [prod("sha256:old", "2026-01-01T00:00:00.000Z", "run-1"), prod("sha256:new", "2026-01-02T00:00:00.000Z", "run-2")],
671
+ malformed: 0,
672
+ });
673
+ expect(await runComponentsRollback(rb({}))).toBe(0);
674
+ expect(appendReleaseRecordMock.mock.calls[0][0]).not.toHaveProperty("releaseDigest");
675
+ });
676
+
290
677
  test("a comma list is refused: rollback acts on one component (#3950)", async () => {
291
678
  expect(await runComponentsRollback(rb({ component: "api,web" }))).toBe(1);
292
679
  expect(errors.join("\n")).toMatch(/--component takes one component name, not a list/);
@@ -353,6 +740,16 @@ describe("chant components redeploy", () => {
353
740
  expect(errors.join("\n")).toMatch(/Redeployed .*api.* in prod at sha256:new/);
354
741
  });
355
742
 
743
+ test("copies the release off the record it redeploys (#4043)", async () => {
744
+ const RELEASE = `jcs1-sha256:${"b".repeat(64)}`;
745
+ readReleaseLedgerMock.mockResolvedValue({
746
+ records: [{ ...prod("sha256:new", "2026-01-02T00:00:00.000Z", "run-2"), releaseDigest: RELEASE }],
747
+ malformed: 0,
748
+ });
749
+ expect(await runComponentsRedeploy(rd({}))).toBe(0);
750
+ expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ digest: "sha256:new", releaseDigest: RELEASE, redeploys: { runId: "run-2" } });
751
+ });
752
+
356
753
  test("the same digest rollback refuses is the one redeploy accepts", async () => {
357
754
  expect(await runComponentsRollback(rd({ path: "rollback", digest: "sha256:new" }))).toBe(1);
358
755
  expect(errors.join("\n")).toMatch(/already the current release/);
@@ -389,10 +786,275 @@ describe("chant components redeploy", () => {
389
786
  expect(errors.join("\n")).toMatch(/then run the same redeploy again/);
390
787
  });
391
788
 
392
- test("a failed redeploy records nothing and exits 1", async () => {
789
+ test("a failed redeploy records no release, records the component failed (#4035), and exits 1", async () => {
393
790
  failApplyFor = "api";
394
791
  expect(await runComponentsRedeploy(rd({}))).toBe(1);
395
792
  expect(appendReleaseRecordMock).not.toHaveBeenCalled();
793
+ expect(outcomes).toEqual([
794
+ expect.objectContaining({ env: "prod", verb: "redeploy", status: "fail", components: [{ component: "api", outcome: "failed", digest: "sha256:new", releaseDigest: null }] }),
795
+ ]);
396
796
  expect(errors.join("\n")).toMatch(/redeploy failed at "api"/);
397
797
  });
398
798
  });
799
+
800
+ describe("--events-json on promote, rollback and redeploy (#4041)", () => {
801
+ const prod = (digest: string, timestamp: string, runId: string): ReleaseRecord =>
802
+ ({ ...staging, env: "prod", digest, timestamp, runId });
803
+ let out: string[];
804
+
805
+ beforeEach(() => {
806
+ out = [];
807
+ vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array) => { out.push(String(chunk)); return true; });
808
+ });
809
+
810
+ /** The stream's events, checked, and the release record's run id beside them. */
811
+ // Only the stream's lines: anything else a module writes to stdout is not this test's.
812
+ const streamed = () => checkRunEventLines(out.filter((l) => l.startsWith('{"$schema"')));
813
+
814
+ test("promote streams the deploy as a component run named by the release record's run id", async () => {
815
+ expect(await runComponentsPromote(ctx({ eventsJson: true }))).toBe(0);
816
+ const events = streamed();
817
+ expect(events[0]).toMatchObject({ type: "run-start", kind: "component", name: "promote api in prod", runId: "run-9", waves: [["api"]] });
818
+ expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ runId: "run-9" });
819
+ expect(stepTrail(events)).toEqual([
820
+ ["step-start", "publish-image"], ["step-settle", "publish-image", "ok"],
821
+ ["step-start", "apply"], ["step-settle", "apply", "ok"],
822
+ ]);
823
+ expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "ok" });
824
+ });
825
+
826
+ test("rollback streams a gated run: the gate's step settles gated, and the run with it", async () => {
827
+ readReleaseLedgerMock.mockResolvedValue({
828
+ records: [prod("sha256:old", "2026-01-01T00:00:00.000Z", "run-1"), prod("sha256:new", "2026-01-02T00:00:00.000Z", "run-2")],
829
+ malformed: 0,
830
+ });
831
+ const gated: DriverComponent = {
832
+ ...api,
833
+ deploy: [...api.deploy.slice(0, 2), { phase: "Apply", steps: [{ kind: "gate", gate: "rollback-ok" }, { kind: "apply" }] }],
834
+ };
835
+ resolveTargetsMock.mockResolvedValue({ success: true, targets: [gated] });
836
+ expect(await runComponentsRollback(ctx({ migrateFrom: undefined, migrateTo: undefined, path: "rollback", extraPositional: "prod", component: "api", eventsJson: true }))).toBe(3);
837
+ const events = streamed();
838
+ expect(events[0]).toMatchObject({ name: "rollback api in prod" });
839
+ // A rollback deploys the recorded digest in place of the publish step.
840
+ expect(stepTrail(events)).toEqual([
841
+ ["step-start", "recorded-digest"], ["step-settle", "recorded-digest", "ok"],
842
+ ["step-start", "gate:rollback-ok"], ["gate-waiting", "gate:rollback-ok"], ["step-settle", "gate:rollback-ok", "gated"],
843
+ ["step-settle", "apply", "skipped:not-run"],
844
+ ]);
845
+ expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "gated" });
846
+ });
847
+
848
+ test("redeploy streams a failed run with the failing step's error", async () => {
849
+ readReleaseLedgerMock.mockResolvedValue({
850
+ records: [prod("sha256:old", "2026-01-01T00:00:00.000Z", "run-1"), prod("sha256:new", "2026-01-02T00:00:00.000Z", "run-2")],
851
+ malformed: 0,
852
+ });
853
+ failApplyFor = "api";
854
+ expect(await runComponentsRedeploy(ctx({ migrateFrom: undefined, migrateTo: undefined, path: "redeploy", extraPositional: "prod", component: "api", eventsJson: true }))).toBe(1);
855
+ const events = streamed();
856
+ expect(events[0]).toMatchObject({ name: "redeploy api in prod" });
857
+ expect(events.find((e) => e.type === "step-settle" && e.status === "failed")).toMatchObject({ step: "apply", error: "apply failed" });
858
+ expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed" });
859
+ });
860
+
861
+ test("--events-json beside --progress-json is refused before anything deploys", async () => {
862
+ expect(await runComponentsPromote(ctx({ eventsJson: true, progressJson: true }))).toBe(1);
863
+ expect(errors.join("\n")).toMatch(/--events-json and --progress-json both stream to stdout/);
864
+ expect(ran).toEqual([]);
865
+ expect(out.filter((l) => l.startsWith('{"$schema"'))).toEqual([]);
866
+ });
867
+ });
868
+
869
+ describe("chant components rollback --release (#4035)", () => {
870
+ const prodTemplate = `{"env":"prod"}\n`;
871
+ const gated: DriverComponent = {
872
+ ...api,
873
+ deploy: [
874
+ ...api.deploy.slice(0, 2),
875
+ { phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/api.template.json", imageRef: "@Publish.digest" }] },
876
+ ],
877
+ };
878
+ const releaseAt = (commit: string) =>
879
+ makeRelease({
880
+ member: null,
881
+ commit: commit.padEnd(40, "0"),
882
+ artifactDigests: [{ component: "api", kind: "image", path: "api.tar", digest: "sha256:api" }],
883
+ targets: [{ env: "prod", templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(prodTemplate) }], params: [] }],
884
+ });
885
+ const r1 = releaseAt("abc123");
886
+ const r2 = releaseAt("def456");
887
+ const prod = (digest: string, timestamp: string, runId: string, release: { releaseDigest: string }): ReleaseRecord =>
888
+ ({ ...staging, env: "prod", digest, timestamp, runId, releaseDigest: release.releaseDigest });
889
+ let ledger: ReleaseRecord[];
890
+ const revoke = (r: { commit: string }) =>
891
+ revokedCommits.set(r.commit, { commit: r.commit, tag: `ci/revoked/${r.commit}`, tagged: "2026-10-16T12:00:00.000Z", phase: "e2e", run: null, signer: null });
892
+
893
+ beforeEach(async () => {
894
+ const { memoryGateLedgerPort } = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
895
+ releaseGatePort = memoryGateLedgerPort();
896
+ resolveTargetsMock.mockResolvedValue({ success: true, targets: [gated] });
897
+ releaseFixture = { release: r1, label: "v1.0.0" };
898
+ storedTemplate = prodTemplate;
899
+ for (const [r, label] of [[r1, "v1.0.0"], [r2, "v1.1.0"]] as const) {
900
+ knownReleases.set(r.releaseDigest, r);
901
+ knownLabels.set(r.releaseDigest, label);
902
+ }
903
+ ledger = [prod("sha256:old", "2026-01-01T00:00:00.000Z", "run-1", r1), prod("sha256:new", "2026-01-02T00:00:00.000Z", "run-2", r2)];
904
+ readReleaseLedgerMock.mockImplementation(async (env: string) => ({ records: env === "prod" ? ledger : [], malformed: 0 }));
905
+ });
906
+
907
+ const rb = (a: Partial<ParsedArgs>) => ctx({ migrateFrom: undefined, migrateTo: undefined, path: "rollback", extraPositional: "prod", ...a });
908
+
909
+ test("restores every component of the release through the release gate, and records the release it restored", async () => {
910
+ expect(await runComponentsRollback(rb({ release: "v1.0.0" }))).toBe(3);
911
+ expect(ran).toEqual([]);
912
+ const [doc] = [...storedChangeSets.values()];
913
+ expect(doc).toMatchObject({ releaseDigest: r1.releaseDigest, environment: "prod" });
914
+ expect(doc.entries[0]).toMatchObject({ address: "api", action: "update" });
915
+ expect(releaseGatePort!.appended).toEqual([
916
+ expect.objectContaining({ op: "release", gate: "prod-release", environment: "prod", planDigest: doc.digest, releaseDigest: r1.releaseDigest }),
917
+ ]);
918
+ expect(errors.join("\n")).toMatch(/chant approve release prod-release --env prod/);
919
+ expect(errors.join("\n")).toMatch(/then run the same rollback again/);
920
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
921
+ // Stopped at its gate with nothing deployed: the environment is unchanged, so no outcome.
922
+ expect(outcomes).toEqual([]);
923
+
924
+ await releaseGatePort!.appendResolution!({
925
+ op: "release", gate: "prod-release", resolvedBy: "alice", timestamp: new Date(Date.now() + 1000).toISOString(),
926
+ planDigest: doc.digest, environment: "prod", releaseDigest: r1.releaseDigest,
927
+ });
928
+ expect(await runComponentsRollback(rb({ release: "v1.0.0" }))).toBe(0);
929
+ // No build and no publish: the recorded digest of the release, and the release's own template.
930
+ expect(ran).toEqual(["prod:apply"]);
931
+ const pinned = `.chant/release-templates/${r1.targets[0].templates[0].digest.replace(/:/g, "_")}/api.template.json`;
932
+ expect(stepInputs.find((x) => x.kind === "apply")?.input).toMatchObject({ template: pinned, imageRef: "sha256:old" });
933
+ expect(appendReleaseRecordMock).toHaveBeenCalledTimes(1);
934
+ expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({
935
+ component: "api",
936
+ env: "prod",
937
+ digest: "sha256:old",
938
+ releaseDigest: r1.releaseDigest,
939
+ restores: { env: "prod", runId: "run-1", timestamp: "2026-01-01T00:00:00.000Z" },
940
+ approver: "alice",
941
+ admission: { op: "release", gate: "prod-release" },
942
+ });
943
+ expect(errors.join("\n")).toMatch(/Rolled back .*api.* in prod to sha256:old/);
944
+ });
945
+
946
+ test("--release takes neither --component nor --digest", async () => {
947
+ expect(await runComponentsRollback(rb({ release: "v1.0.0", component: "api" }))).toBe(1);
948
+ expect(errors.join("\n")).toMatch(/--release rolls back every component of a release, so it does not take --component/);
949
+ expect(await runComponentsRollback(rb({ release: "v1.0.0", digest: "sha256:old" }))).toBe(1);
950
+ expect(errors.join("\n")).toMatch(/--digest picks one release of one component/);
951
+ expect(ran).toEqual([]);
952
+ });
953
+
954
+ test("the release the environment runs is refused, unless a run left one of its components unsettled", async () => {
955
+ releaseFixture = { release: r2, label: "v1.1.0" };
956
+ expect(await runComponentsRollback(rb({ release: "v1.1.0" }))).toBe(1);
957
+ expect(errors.join("\n")).toMatch(/"prod" already runs release v1\.1\.0; nothing to roll back/);
958
+ expect(storedChangeSets.size).toBe(0);
959
+
960
+ outcomes.push({
961
+ version: 1, env: "prod", verb: "promote", runId: "run-3", timestamp: "2026-01-03T00:00:00.000Z", actor: "bob",
962
+ releaseDigest: `jcs1-sha256:${"9".repeat(64)}`, status: "fail",
963
+ components: [{ component: "api", outcome: "failed", digest: "sha256:newer", releaseDigest: `jcs1-sha256:${"9".repeat(64)}` }],
964
+ });
965
+ // The ledger records v1.1.0, but the failed run left api unknown: a hole, so the gate waits for an approval.
966
+ expect(await runComponentsRollback(rb({ release: "v1.1.0" }))).toBe(3);
967
+ const [doc] = [...storedChangeSets.values()];
968
+ expect(doc.entries).toEqual([]);
969
+ expect(doc.members[0].holes).toEqual([expect.objectContaining({ address: "api", reason: expect.stringMatching(/the promote run run-3 left api failed in "prod"/) })]);
970
+ expect(releaseGatePort!.resolved).toEqual([]);
971
+ expect(releaseGatePort!.appended.at(-1)).toMatchObject({ releaseDigest: r2.releaseDigest, environment: "prod", planDigest: doc.digest });
972
+ });
973
+
974
+ test("a release that never deployed a component to the environment is refused", async () => {
975
+ ledger = [ledger[1]];
976
+ expect(await runComponentsRollback(rb({ release: "v1.0.0" }))).toBe(1);
977
+ expect(errors.join("\n")).toMatch(/release v1\.0\.0 never deployed "api" to "prod", so no recorded digest can be restored; promote it instead: chant components promote --to prod --release v1\.0\.0/);
978
+ expect(ran).toEqual([]);
979
+ });
980
+
981
+ test("a revoked release is refused before anything runs, naming a release to restore instead", async () => {
982
+ revoke(r1);
983
+ expect(await runComponentsRollback(rb({ release: "v1.0.0" }))).toBe(1);
984
+ const said = errors.join("\n");
985
+ expect(said).toMatch(/release v1\.0\.0 was cut from commit abc123000000, which ci\/revoked\/abc1230{34} revokes .*\[release-revoked\]/);
986
+ expect(said).toMatch(/chant components rollback prod --release v1\.1\.0/);
987
+ expect(ran).toEqual([]);
988
+ expect(storedChangeSets.size).toBe(0);
989
+ });
990
+
991
+ test("rollback --component and redeploy refuse to put back a revoked release (#4033)", async () => {
992
+ revoke(r2);
993
+ const rd = ctx({ migrateFrom: undefined, migrateTo: undefined, path: "redeploy", extraPositional: "prod", component: "api" });
994
+ expect(await runComponentsRedeploy(rd)).toBe(1);
995
+ expect(errors.join("\n")).toMatch(/release v1\.1\.0 was cut from commit def456000000, .*\[release-revoked\]/);
996
+ expect(errors.join("\n")).toMatch(/Restore a release cut from a green commit instead: chant components rollback prod --release v1\.0\.0/);
997
+
998
+ revokedCommits.clear();
999
+ revoke(r1);
1000
+ errors.length = 0;
1001
+ expect(await runComponentsRollback(rb({ component: "api" }))).toBe(1);
1002
+ expect(errors.join("\n")).toMatch(/release v1\.0\.0 was cut from commit abc123000000, .*\[release-revoked\]/);
1003
+ expect(ran).toEqual([]);
1004
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
1005
+ });
1006
+ });
1007
+
1008
+ describe("a promote that applies partly records what the environment runs (#4035)", () => {
1009
+ const web: DriverComponent = { ...api, name: "web" };
1010
+ const release = makeRelease({
1011
+ member: null,
1012
+ commit: "abc123".padEnd(40, "0"),
1013
+ artifactDigests: ["api", "web"].map((component) => ({ component, kind: "image", path: `${component}.tar`, digest: `sha256:${component}` })),
1014
+ targets: [{ env: "prod", templates: [], params: [] }],
1015
+ });
1016
+
1017
+ beforeEach(() => {
1018
+ resolveTargetsMock.mockResolvedValue({ success: true, targets: [api, web] });
1019
+ readReleaseLedgerMock.mockResolvedValue({ records: [staging, { ...staging, component: "web" }], malformed: 0 });
1020
+ releaseFixture = { release, label: "v2.0.0" };
1021
+ });
1022
+
1023
+ test("the component that deployed is recorded, the one that failed is recorded failed, and the lines that settle it are printed", async () => {
1024
+ failApplyFor = "web";
1025
+ expect(await runComponentsPromote(ctx({ release: "v2.0.0" }))).toBe(1);
1026
+ expect(appendReleaseRecordMock).toHaveBeenCalledTimes(1);
1027
+ const [record] = appendReleaseRecordMock.mock.calls[0];
1028
+ expect(record).toMatchObject({ component: "api", env: "prod", releaseDigest: release.releaseDigest });
1029
+ expect(outcomes).toEqual([{
1030
+ version: 1,
1031
+ env: "prod",
1032
+ verb: "promote",
1033
+ runId: "run-9",
1034
+ timestamp: record.timestamp,
1035
+ actor: "bob",
1036
+ releaseDigest: release.releaseDigest,
1037
+ status: "fail",
1038
+ components: [
1039
+ { component: "api", outcome: "deployed", digest: "sha256:api", releaseDigest: release.releaseDigest },
1040
+ { component: "web", outcome: "failed", digest: "sha256:api", releaseDigest: release.releaseDigest },
1041
+ ],
1042
+ }]);
1043
+ expect(pushLifecycleMock).toHaveBeenCalledTimes(1);
1044
+ const said = errors.join("\n");
1045
+ expect(said).toMatch(/"prod" now runs a mix: api deployed, web failed/);
1046
+ expect(said).toMatch(/settle web: chant components redeploy prod --component web, or run the same promote again/);
1047
+ });
1048
+
1049
+ test("--json carries the outcome", async () => {
1050
+ failApplyFor = "web";
1051
+ expect(await runComponentsPromote(ctx({ release: "v2.0.0", json: true }))).toBe(1);
1052
+ expect(JSON.parse(logs.join("\n")).outcome).toMatchObject({ status: "fail", components: [{ outcome: "deployed" }, { outcome: "failed" }] });
1053
+ });
1054
+
1055
+ test("a promote that deploys everything records no outcome", async () => {
1056
+ expect(await runComponentsPromote(ctx({ release: "v2.0.0" }))).toBe(0);
1057
+ expect(appendReleaseRecordMock).toHaveBeenCalledTimes(2);
1058
+ expect(outcomes).toEqual([]);
1059
+ });
1060
+ });