@intentius/chant 0.124.28 → 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 (395) hide show
  1. package/dist/build-params.d.ts +34 -1
  2. package/dist/build-params.d.ts.map +1 -1
  3. package/dist/build-params.js +31 -1
  4. package/dist/change-set-graph.d.ts +8 -1
  5. package/dist/change-set-graph.d.ts.map +1 -1
  6. package/dist/change-set-graph.js +4 -2
  7. package/dist/change-set.d.ts +10 -0
  8. package/dist/change-set.d.ts.map +1 -1
  9. package/dist/change-set.js +4 -3
  10. package/dist/cli/build-params-cli.d.ts +7 -1
  11. package/dist/cli/build-params-cli.d.ts.map +1 -1
  12. package/dist/cli/build-params-cli.js +17 -8
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/build.js +6 -3
  15. package/dist/cli/handlers/approve.d.ts.map +1 -1
  16. package/dist/cli/handlers/approve.js +45 -0
  17. package/dist/cli/handlers/build.d.ts.map +1 -1
  18. package/dist/cli/handlers/build.js +15 -4
  19. package/dist/cli/handlers/fan-out.d.ts +9 -0
  20. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  21. package/dist/cli/handlers/fan-out.js +40 -3
  22. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  23. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  24. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  25. package/dist/cli/handlers/pr.d.ts +11 -3
  26. package/dist/cli/handlers/pr.d.ts.map +1 -1
  27. package/dist/cli/handlers/pr.js +134 -26
  28. package/dist/cli/handlers/promote.d.ts +7 -0
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/promote.js +210 -33
  31. package/dist/cli/handlers/release.d.ts +62 -5
  32. package/dist/cli/handlers/release.d.ts.map +1 -1
  33. package/dist/cli/handlers/release.js +414 -28
  34. package/dist/cli/handlers/run.d.ts +11 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +67 -6
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +61 -10
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +24 -2
  41. package/dist/cli/registry.d.ts +15 -3
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/run-events.d.ts +3 -2
  44. package/dist/cli/run-events.d.ts.map +1 -1
  45. package/dist/cli/run-events.js +5 -3
  46. package/dist/components/auto-release.d.ts +2 -0
  47. package/dist/components/auto-release.d.ts.map +1 -1
  48. package/dist/components/auto-release.js +1 -0
  49. package/dist/components/capability.d.ts +9 -0
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/driver.d.ts +8 -0
  52. package/dist/components/driver.d.ts.map +1 -1
  53. package/dist/components/driver.js +35 -0
  54. package/dist/components/fan-out-inputs.d.ts +69 -0
  55. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  56. package/dist/components/fan-out-inputs.js +169 -0
  57. package/dist/components/fan-out-record.d.ts +13 -0
  58. package/dist/components/fan-out-record.d.ts.map +1 -1
  59. package/dist/components/fan-out-record.js +16 -0
  60. package/dist/components/fan-out-run.d.ts.map +1 -1
  61. package/dist/components/fan-out-run.js +50 -0
  62. package/dist/components/fan-out.d.ts +4 -0
  63. package/dist/components/fan-out.d.ts.map +1 -1
  64. package/dist/components/pr-pipeline.d.ts +25 -0
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +22 -4
  67. package/dist/components/promote.d.ts +23 -0
  68. package/dist/components/promote.d.ts.map +1 -1
  69. package/dist/components/promote.js +60 -2
  70. package/dist/components/release-compile.d.ts +9 -0
  71. package/dist/components/release-compile.d.ts.map +1 -1
  72. package/dist/components/release-compile.js +9 -3
  73. package/dist/components/release-gate.d.ts +11 -1
  74. package/dist/components/release-gate.d.ts.map +1 -1
  75. package/dist/components/release-gate.js +10 -4
  76. package/dist/components/wave-plan.d.ts +5 -0
  77. package/dist/components/wave-plan.d.ts.map +1 -1
  78. package/dist/components/wave-plan.js +10 -2
  79. package/dist/config.d.ts +38 -0
  80. package/dist/config.d.ts.map +1 -1
  81. package/dist/config.js +2 -0
  82. package/dist/flag-params.d.ts +54 -0
  83. package/dist/flag-params.d.ts.map +1 -0
  84. package/dist/flag-params.js +130 -0
  85. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  86. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  87. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  88. package/dist/lifecycle/change-set-store.d.ts +2 -1
  89. package/dist/lifecycle/change-set-store.d.ts.map +1 -1
  90. package/dist/lifecycle/change-set-store.js +2 -1
  91. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  92. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  93. package/dist/lifecycle/delivery-metrics.js +145 -0
  94. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  95. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  96. package/dist/lifecycle/deploy-outcome.js +101 -0
  97. package/dist/lifecycle/gate-change-set.d.ts +10 -1
  98. package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
  99. package/dist/lifecycle/gate-change-set.js +35 -3
  100. package/dist/lifecycle/gate-ledger.d.ts +2 -3
  101. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  102. package/dist/lifecycle/git.d.ts +30 -1
  103. package/dist/lifecycle/git.d.ts.map +1 -1
  104. package/dist/lifecycle/git.js +31 -5
  105. package/dist/lifecycle/index.d.ts +4 -0
  106. package/dist/lifecycle/index.d.ts.map +1 -1
  107. package/dist/lifecycle/index.js +4 -0
  108. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  109. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  110. package/dist/lifecycle/mixed-environment.js +154 -0
  111. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  112. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  113. package/dist/lifecycle/publish-attachments.js +179 -0
  114. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  115. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  116. package/dist/lifecycle/publish-credentials.js +69 -0
  117. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  118. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  119. package/dist/lifecycle/publish-declaration.js +83 -0
  120. package/dist/lifecycle/publish-forge.d.ts +53 -0
  121. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  122. package/dist/lifecycle/publish-forge.js +298 -0
  123. package/dist/lifecycle/publish-npm.d.ts +61 -0
  124. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  125. package/dist/lifecycle/publish-npm.js +242 -0
  126. package/dist/lifecycle/publish-oci.d.ts +64 -0
  127. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  128. package/dist/lifecycle/publish-oci.js +383 -0
  129. package/dist/lifecycle/publish-record.d.ts +66 -0
  130. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  131. package/dist/lifecycle/publish-record.js +84 -0
  132. package/dist/lifecycle/publish-targets.d.ts +45 -0
  133. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  134. package/dist/lifecycle/publish-targets.js +85 -0
  135. package/dist/lifecycle/release-cut.d.ts +48 -1
  136. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  137. package/dist/lifecycle/release-cut.js +116 -23
  138. package/dist/lifecycle/release-join.d.ts +95 -0
  139. package/dist/lifecycle/release-join.d.ts.map +1 -0
  140. package/dist/lifecycle/release-join.js +139 -0
  141. package/dist/lifecycle/release-publish.d.ts +312 -0
  142. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  143. package/dist/lifecycle/release-publish.js +335 -0
  144. package/dist/lifecycle/release-revocation.d.ts +80 -0
  145. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  146. package/dist/lifecycle/release-revocation.js +123 -0
  147. package/dist/lifecycle/release-store.d.ts +12 -2
  148. package/dist/lifecycle/release-store.d.ts.map +1 -1
  149. package/dist/lifecycle/release-store.js +4 -2
  150. package/dist/lifecycle/release-verify.d.ts +61 -0
  151. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  152. package/dist/lifecycle/release-verify.js +92 -0
  153. package/dist/lifecycle/release-workstation.d.ts +91 -0
  154. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  155. package/dist/lifecycle/release-workstation.js +180 -0
  156. package/dist/lifecycle/retention-policy.d.ts +45 -0
  157. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  158. package/dist/lifecycle/retention-policy.js +43 -0
  159. package/dist/lifecycle/retention.d.ts +185 -0
  160. package/dist/lifecycle/retention.d.ts.map +1 -0
  161. package/dist/lifecycle/retention.js +400 -0
  162. package/dist/plan-summary.d.ts.map +1 -1
  163. package/dist/plan-summary.js +8 -2
  164. package/dist/pr-loop.d.ts +7 -0
  165. package/dist/pr-loop.d.ts.map +1 -1
  166. package/dist/pr-loop.js +23 -4
  167. package/dist/pr-report-url.d.ts +82 -0
  168. package/dist/pr-report-url.d.ts.map +1 -0
  169. package/dist/pr-report-url.js +174 -0
  170. package/dist/provenance.d.ts +2 -2
  171. package/dist/provenance.d.ts.map +1 -1
  172. package/dist/run-event-store.d.ts +29 -2
  173. package/dist/run-event-store.d.ts.map +1 -1
  174. package/dist/run-event-store.js +50 -3
  175. package/dist/run-event-stream.d.ts +2 -1
  176. package/dist/run-event-stream.d.ts.map +1 -1
  177. package/dist/run-event-stream.js +5 -1
  178. package/dist/run-event.d.ts +19 -1
  179. package/dist/run-event.d.ts.map +1 -1
  180. package/dist/run-event.js +2 -0
  181. package/dist/wave-runtime.mjs +260 -229
  182. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  183. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  184. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  185. package/dist/workspace/change-set.schema.json +5 -1
  186. package/dist/workspace/check.schema.json +4 -2
  187. package/dist/workspace/checks/records.d.ts +9 -1
  188. package/dist/workspace/checks/records.d.ts.map +1 -1
  189. package/dist/workspace/checks/records.js +35 -1
  190. package/dist/workspace/checks.d.ts +1 -0
  191. package/dist/workspace/checks.d.ts.map +1 -1
  192. package/dist/workspace/checks.js +2 -1
  193. package/dist/workspace/ci-green.d.ts +8 -0
  194. package/dist/workspace/ci-green.d.ts.map +1 -1
  195. package/dist/workspace/ci-green.js +10 -3
  196. package/dist/workspace/declaration.d.ts +16 -0
  197. package/dist/workspace/declaration.d.ts.map +1 -1
  198. package/dist/workspace/declaration.js +26 -0
  199. package/dist/workspace/declaration.schema.json +64 -0
  200. package/dist/workspace/declared-kinds.d.ts +16 -0
  201. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  202. package/dist/workspace/declared-kinds.js +25 -0
  203. package/dist/workspace/delivery-metrics.d.ts +91 -0
  204. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  205. package/dist/workspace/delivery-metrics.js +217 -0
  206. package/dist/workspace/delivery-metrics.schema.json +519 -0
  207. package/dist/workspace/flags-read.d.ts +37 -0
  208. package/dist/workspace/flags-read.d.ts.map +1 -0
  209. package/dist/workspace/flags-read.js +103 -0
  210. package/dist/workspace/flags.d.ts +100 -0
  211. package/dist/workspace/flags.d.ts.map +1 -0
  212. package/dist/workspace/flags.js +98 -0
  213. package/dist/workspace/graph-cli.js +1 -1
  214. package/dist/workspace/intent-cli.d.ts +11 -2
  215. package/dist/workspace/intent-cli.d.ts.map +1 -1
  216. package/dist/workspace/intent-cli.js +54 -5
  217. package/dist/workspace/intent-trace.d.ts +219 -0
  218. package/dist/workspace/intent-trace.d.ts.map +1 -0
  219. package/dist/workspace/intent-trace.js +314 -0
  220. package/dist/workspace/intent.d.ts +30 -6
  221. package/dist/workspace/intent.d.ts.map +1 -1
  222. package/dist/workspace/intent.js +141 -21
  223. package/dist/workspace/intent.schema.json +702 -6
  224. package/dist/workspace/overlap.d.ts +146 -0
  225. package/dist/workspace/overlap.d.ts.map +1 -0
  226. package/dist/workspace/overlap.js +276 -0
  227. package/dist/workspace/reason-codes.d.ts +2 -0
  228. package/dist/workspace/reason-codes.d.ts.map +1 -1
  229. package/dist/workspace/reason-codes.js +3 -0
  230. package/dist/workspace/records.d.ts +15 -2
  231. package/dist/workspace/records.d.ts.map +1 -1
  232. package/dist/workspace/records.js +32 -0
  233. package/dist/workspace/records.schema.json +21 -2
  234. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  235. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  236. package/dist/workspace/releases.d.ts +9 -0
  237. package/dist/workspace/releases.d.ts.map +1 -1
  238. package/dist/workspace/releases.js +9 -0
  239. package/dist/workspace/releases.schema.json +123 -1
  240. package/dist/workspace/run-event.schema.json +10 -3
  241. package/dist/workspace/status-gates.d.ts +8 -0
  242. package/dist/workspace/status-gates.d.ts.map +1 -1
  243. package/dist/workspace/status.d.ts +78 -0
  244. package/dist/workspace/status.d.ts.map +1 -1
  245. package/dist/workspace/status.js +170 -3
  246. package/dist/workspace/status.schema.json +198 -1
  247. package/dist/workspace/wip.d.ts.map +1 -1
  248. package/dist/workspace/wip.js +11 -3
  249. package/package.json +1 -1
  250. package/src/build-params.test.ts +46 -0
  251. package/src/build-params.ts +55 -1
  252. package/src/change-set-graph.ts +10 -2
  253. package/src/change-set.ts +14 -3
  254. package/src/cli/build-params-cli.ts +22 -9
  255. package/src/cli/commands/build.ts +6 -3
  256. package/src/cli/handlers/approve-release.test.ts +5 -1
  257. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  258. package/src/cli/handlers/approve.ts +47 -0
  259. package/src/cli/handlers/build.test.ts +15 -0
  260. package/src/cli/handlers/build.ts +17 -4
  261. package/src/cli/handlers/fan-out.test.ts +180 -1
  262. package/src/cli/handlers/fan-out.ts +38 -3
  263. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  264. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  265. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  266. package/src/cli/handlers/pr.test.ts +148 -1
  267. package/src/cli/handlers/pr.ts +139 -28
  268. package/src/cli/handlers/promote.test.ts +280 -2
  269. package/src/cli/handlers/promote.ts +210 -34
  270. package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
  271. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  272. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  273. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  274. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  275. package/src/cli/handlers/release.e2e.test.ts +146 -32
  276. package/src/cli/handlers/release.ts +415 -28
  277. package/src/cli/handlers/run-auto-release.test.ts +9 -0
  278. package/src/cli/handlers/run-release.test.ts +55 -0
  279. package/src/cli/handlers/run.ts +69 -5
  280. package/src/cli/main.test.ts +1 -0
  281. package/src/cli/main.ts +61 -10
  282. package/src/cli/parse-args.ts +19 -2
  283. package/src/cli/registry.ts +15 -3
  284. package/src/cli/run-events.ts +5 -2
  285. package/src/components/auto-release.test.ts +9 -0
  286. package/src/components/auto-release.ts +3 -0
  287. package/src/components/capability.ts +9 -0
  288. package/src/components/driver.test.ts +24 -0
  289. package/src/components/driver.ts +36 -0
  290. package/src/components/fan-out-inputs.test.ts +85 -0
  291. package/src/components/fan-out-inputs.ts +192 -0
  292. package/src/components/fan-out-record.test.ts +10 -0
  293. package/src/components/fan-out-record.ts +19 -0
  294. package/src/components/fan-out-run.ts +48 -0
  295. package/src/components/fan-out.ts +4 -0
  296. package/src/components/pr-pipeline.ts +35 -4
  297. package/src/components/promote.ts +68 -2
  298. package/src/components/release-compile.ts +17 -3
  299. package/src/components/release-gate.ts +18 -4
  300. package/src/components/wave-plan.ts +15 -2
  301. package/src/config.ts +8 -0
  302. package/src/flag-params.test.ts +144 -0
  303. package/src/flag-params.ts +134 -0
  304. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  305. package/src/lifecycle/change-set-store.ts +2 -1
  306. package/src/lifecycle/delivery-metrics.ts +263 -0
  307. package/src/lifecycle/deploy-outcome.ts +147 -0
  308. package/src/lifecycle/gate-change-set.ts +37 -6
  309. package/src/lifecycle/gate-ledger.ts +2 -2
  310. package/src/lifecycle/git.ts +61 -8
  311. package/src/lifecycle/index.ts +4 -0
  312. package/src/lifecycle/mixed-environment.test.ts +97 -0
  313. package/src/lifecycle/mixed-environment.ts +204 -0
  314. package/src/lifecycle/publish-attachments.ts +196 -0
  315. package/src/lifecycle/publish-credentials.ts +84 -0
  316. package/src/lifecycle/publish-declaration.ts +95 -0
  317. package/src/lifecycle/publish-forge.ts +325 -0
  318. package/src/lifecycle/publish-npm.ts +259 -0
  319. package/src/lifecycle/publish-oci.ts +396 -0
  320. package/src/lifecycle/publish-record.ts +113 -0
  321. package/src/lifecycle/publish-resume.test.ts +168 -0
  322. package/src/lifecycle/publish-targets.test.ts +494 -0
  323. package/src/lifecycle/publish-targets.ts +100 -0
  324. package/src/lifecycle/release-cut.test.ts +74 -2
  325. package/src/lifecycle/release-cut.ts +158 -22
  326. package/src/lifecycle/release-flag.test.ts +112 -0
  327. package/src/lifecycle/release-join.test.ts +68 -0
  328. package/src/lifecycle/release-join.ts +182 -0
  329. package/src/lifecycle/release-publish.test.ts +225 -0
  330. package/src/lifecycle/release-publish.ts +568 -0
  331. package/src/lifecycle/release-revocation.ts +147 -0
  332. package/src/lifecycle/release-store.ts +15 -4
  333. package/src/lifecycle/release-verify.test.ts +110 -0
  334. package/src/lifecycle/release-verify.ts +120 -0
  335. package/src/lifecycle/release-workstation.test.ts +204 -0
  336. package/src/lifecycle/release-workstation.ts +216 -0
  337. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  338. package/src/lifecycle/retention-policy.ts +63 -0
  339. package/src/lifecycle/retention.test.ts +363 -0
  340. package/src/lifecycle/retention.ts +508 -0
  341. package/src/plan-summary.test.ts +20 -0
  342. package/src/plan-summary.ts +8 -1
  343. package/src/pr-loop.ts +25 -4
  344. package/src/pr-report-url.test.ts +131 -0
  345. package/src/pr-report-url.ts +209 -0
  346. package/src/provenance.ts +2 -2
  347. package/src/run-event-store.ts +63 -3
  348. package/src/run-event-stream.ts +4 -1
  349. package/src/run-event.ts +21 -1
  350. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  351. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  352. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  353. package/src/workspace/change-set.schema.json +5 -1
  354. package/src/workspace/check.schema.json +4 -2
  355. package/src/workspace/checks/records.ts +35 -1
  356. package/src/workspace/checks.ts +2 -1
  357. package/src/workspace/ci-green.ts +14 -3
  358. package/src/workspace/declaration.schema.json +64 -0
  359. package/src/workspace/declaration.test.ts +28 -0
  360. package/src/workspace/declaration.ts +38 -0
  361. package/src/workspace/declared-kinds.ts +40 -1
  362. package/src/workspace/delivery-metrics.schema.json +519 -0
  363. package/src/workspace/delivery-metrics.test.ts +236 -0
  364. package/src/workspace/delivery-metrics.ts +286 -0
  365. package/src/workspace/flags-read.ts +121 -0
  366. package/src/workspace/flags.test.ts +141 -0
  367. package/src/workspace/flags.ts +157 -0
  368. package/src/workspace/graph-cli.ts +1 -1
  369. package/src/workspace/intent-cli.ts +46 -5
  370. package/src/workspace/intent-trace.test.ts +173 -0
  371. package/src/workspace/intent-trace.ts +484 -0
  372. package/src/workspace/intent.schema.json +702 -6
  373. package/src/workspace/intent.ts +148 -25
  374. package/src/workspace/older-records.test.ts +412 -0
  375. package/src/workspace/overlap.test.ts +165 -0
  376. package/src/workspace/overlap.ts +340 -0
  377. package/src/workspace/read-contract.e2e.test.ts +2 -1
  378. package/src/workspace/reason-codes.test.ts +5 -0
  379. package/src/workspace/reason-codes.ts +3 -0
  380. package/src/workspace/record-assets.test.ts +2 -1
  381. package/src/workspace/records-contract.e2e.test.ts +2 -1
  382. package/src/workspace/records.schema.json +21 -2
  383. package/src/workspace/records.ts +36 -2
  384. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  385. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  386. package/src/workspace/releases.schema.json +123 -1
  387. package/src/workspace/releases.ts +11 -0
  388. package/src/workspace/run-event.schema.json +10 -3
  389. package/src/workspace/status-contract.test.ts +29 -0
  390. package/src/workspace/status-gates.ts +8 -0
  391. package/src/workspace/status-mixed.test.ts +152 -0
  392. package/src/workspace/status-revoked.test.ts +153 -0
  393. package/src/workspace/status.schema.json +198 -1
  394. package/src/workspace/status.ts +254 -3
  395. package/src/workspace/wip.ts +11 -2
@@ -233,7 +233,8 @@ describe("proof: a plan-only invocation prints the derivation and dispatches not
233
233
 
234
234
  ran = [];
235
235
  stderrLines = [];
236
- expect(await runComponentsFanOut(ctx({ base: "main", resume, dryRun: true }))).toBe(0);
236
+ // The same environment: progress is bound to it (#4081).
237
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, dryRun: true }))).toBe(0);
237
238
 
238
239
  expect(ran).toEqual([]);
239
240
  const out = stderr();
@@ -656,6 +657,46 @@ describe("--events-json (#4041)", () => {
656
657
  expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed" });
657
658
  });
658
659
 
660
+ test("a component behind a failure settles blocked, naming it, after the failing wave and before the next starts (#4007)", async () => {
661
+ failing = ["cluster-a"];
662
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", eventsJson: true }))).toBe(1);
663
+ const events = checkRunEventLines(lines());
664
+ const blocked = events.filter((e) => e.type === "component-settle" && e.status === "blocked");
665
+ expect(blocked.map((e) => ({ wave: (e as { wave: number }).wave, component: (e as { component: string }).component, blockedBy: (e as { blockedBy?: string }).blockedBy }))).toEqual([
666
+ { wave: 3, component: "app-one", blockedBy: "cluster-a" },
667
+ { wave: 3, component: "app-two", blockedBy: "cluster-a" },
668
+ ]);
669
+ const wave2Settle = events.findIndex((e) => e.type === "wave-settle" && e.wave === 2);
670
+ const wave3Start = events.findIndex((e) => e.type === "wave-start" && e.wave === 3);
671
+ expect(events.indexOf(blocked[0]!)).toBeGreaterThan(wave2Settle);
672
+ expect(events.indexOf(blocked[1]!)).toBeLessThan(wave3Start);
673
+ // A blocked component never starts.
674
+ expect(events.some((e) => e.type === "component-start" && e.component === "app-one")).toBe(false);
675
+ });
676
+
677
+ test("a resumed fan-out settles what the earlier attempt applied as already-applied, in the derivation's waves (#4007)", async () => {
678
+ const dir = await mkdtemp(join(tmpdir(), "fan-out-"));
679
+ try {
680
+ const resume = join(dir, "attempt.json");
681
+ failing = ["cluster-a"];
682
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(1);
683
+ failing = [];
684
+ stdoutLines = [];
685
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, eventsJson: true }))).toBe(0);
686
+ const events = checkRunEventLines(lines());
687
+ const firstWave = events.findIndex((e) => e.type === "wave-start");
688
+ const applied = events.filter((e) => e.type === "component-settle" && e.status === "already-applied");
689
+ expect(applied.map((e) => [(e as { component: string }).component, (e as { wave: number }).wave])).toEqual([["app-three", 3], ["cluster-b", 2], ["net", 1]]);
690
+ for (const e of applied) expect(events.indexOf(e)).toBeLessThan(firstWave);
691
+ // --progress-json never printed them, and still does not.
692
+ stdoutLines = [];
693
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, progressJson: true }))).toBe(0);
694
+ expect(stdoutLines.filter((l) => l.startsWith('{"type"')).map((l) => JSON.parse(l).type)).toEqual(["run-start", "run-done"]);
695
+ } finally {
696
+ await rm(dir, { recursive: true, force: true });
697
+ }
698
+ });
699
+
659
700
  test("--progress-json on a set the gate stops now prints run-start and run-done gated (a level-0 change)", async () => {
660
701
  expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", progressJson: true }))).toBe(3);
661
702
  const progress = stdoutLines.filter((l) => l.startsWith('{"type"')).map((l) => JSON.parse(l));
@@ -670,3 +711,141 @@ describe("--events-json (#4041)", () => {
670
711
  expect(stderr()).toContain("--events-json and --progress-json both stream to stdout");
671
712
  });
672
713
  });
714
+
715
+ describe("progress carries only for an unchanged input (#4081)", () => {
716
+ /** The estate with each component deploying a built template from `templates/`, read relative to the project. */
717
+ const templated = ESTATE.map((c) => ({
718
+ ...c,
719
+ deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", stack: `${c.name}-stack`, template: `templates/${c.name}.json` }] }],
720
+ }));
721
+ let dir: string;
722
+ const writeTemplate = (name: string, body: unknown) =>
723
+ writeFile(join(dir, "templates", `${name}.json`), JSON.stringify(body));
724
+
725
+ beforeEach(async () => {
726
+ dir = await mkdtemp(join(tmpdir(), "fan-out-inputs-"));
727
+ const { mkdir } = await import("node:fs/promises");
728
+ await mkdir(join(dir, "templates"));
729
+ for (const c of templated) await writeTemplate(c.name, { Resources: { [c.name]: { Type: "AWS::SNS::Topic" } } });
730
+ // The project is the temp directory, so the templates are read from there and nothing touches the checkout.
731
+ vi.spyOn(process, "cwd").mockReturnValue(dir);
732
+ discoverComponentsMock.mockResolvedValue({
733
+ components: new Map(templated.map((c) => [c.name, { component: c, exportName: c.name, filePath: `${c.name}.component.ts` }])),
734
+ sourceFiles: [],
735
+ errors: [],
736
+ });
737
+ });
738
+ afterEach(async () => {
739
+ await rm(dir, { recursive: true, force: true });
740
+ });
741
+
742
+ test("same shape, changed content: a stale record does not skip the components whose input moved", async () => {
743
+ const resume = join(dir, "attempt.json");
744
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
745
+ const first = JSON.parse(await readFile(resume, "utf8"));
746
+ expect(first.completed).toEqual(["app-one", "app-three", "app-two", "cluster-a", "cluster-b", "net"]);
747
+ expect(Object.keys(first.inputs).sort()).toEqual(first.completed);
748
+ const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())?.[1];
749
+
750
+ // The next change: net's topic gets a display name and cluster-a a queue.
751
+ // Same components, same edges, so the same plan digest.
752
+ await writeTemplate("net", { Resources: { net: { Type: "AWS::SNS::Topic", Properties: { DisplayName: "estate" } } } });
753
+ await writeTemplate("cluster-a", { Resources: { "cluster-a": { Type: "AWS::SNS::Topic" }, events: { Type: "AWS::SQS::Queue" } } });
754
+ ran = [];
755
+ stderrLines = [];
756
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
757
+
758
+ expect([...ran].sort()).toEqual(["app-one", "app-three", "app-two", "cluster-a", "cluster-b", "net"]);
759
+ const out = stderr();
760
+ expect(out).toContain(`plan: ${digest}`);
761
+ expect(out).toContain("net: changed since the earlier attempt, so it runs again");
762
+ expect(out).toContain("cluster-a: changed since the earlier attempt, so it runs again");
763
+ // Downstream of net, so its parameters read a changed upstream.
764
+ expect(out).toContain("app-three: changed since the earlier attempt, so it runs again");
765
+ expect(out).not.toMatch(/already-applied/);
766
+ expect(out).toContain("fan-out completed: 6 applied, 0 failed, 0 blocked");
767
+ const second = JSON.parse(await readFile(resume, "utf8"));
768
+ expect(second.inputs.net).not.toBe(first.inputs.net);
769
+ expect(second.inputs["cluster-b"]).not.toBe(first.inputs["cluster-b"]);
770
+ });
771
+
772
+ test("a change below the root reruns only its own subtree", async () => {
773
+ const resume = join(dir, "attempt.json");
774
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
775
+ await writeTemplate("cluster-a", { Resources: { "cluster-a": { Type: "AWS::SNS::Topic" }, events: { Type: "AWS::SQS::Queue" } } });
776
+ ran = [];
777
+ stderrLines = [];
778
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
779
+ expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
780
+ expect(stderr()).toMatch(/cluster-b +already-applied/);
781
+ expect(stderr()).not.toContain("net: changed");
782
+ });
783
+
784
+ test("the README's fail-then-fix: the fix reruns the failed subtree and the rest stay already-applied", async () => {
785
+ const resume = join(dir, "attempt.json");
786
+ failing = ["cluster-a"];
787
+ await writeTemplate("cluster-a", { Resources: { events: { Type: "AWS::SQS::Queue", Properties: { FifoQueue: true, QueueName: "events" } } } });
788
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(1);
789
+ const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())?.[1];
790
+
791
+ // The fix changes cluster-a's template, which never applied.
792
+ failing = [];
793
+ await writeTemplate("cluster-a", { Resources: { events: { Type: "AWS::SQS::Queue", Properties: { FifoQueue: true, QueueName: "events.fifo" } } } });
794
+ ran = [];
795
+ stderrLines = [];
796
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
797
+ expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
798
+ const out = stderr();
799
+ expect(out).toContain(`plan: ${digest}`);
800
+ expect(out).not.toContain("changed since the earlier attempt");
801
+ for (const name of ["net", "cluster-b", "app-three"]) expect(out).toMatch(new RegExp(`${name} +already-applied`));
802
+ expect(out).toContain("fan-out completed: 3 applied, 0 failed, 0 blocked");
803
+ });
804
+
805
+ test("the fix keeps the approval: approval binds the derivation, progress binds the inputs", async () => {
806
+ const resume = join(dir, "attempt.json");
807
+ failing = ["cluster-a"];
808
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(3);
809
+ const pending = gateLedger.appended[0]!;
810
+ gateLedger = memoryGateLedgerPort({
811
+ pending: [pending],
812
+ resolutions: [
813
+ {
814
+ version: 1,
815
+ kind: "resolution",
816
+ op: "fan-out",
817
+ gate: "release",
818
+ resolvedBy: "operator",
819
+ timestamp: new Date(Date.parse(pending.timestamp) + 1000).toISOString(),
820
+ planDigest: pending.planDigest,
821
+ },
822
+ ],
823
+ });
824
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(1);
825
+
826
+ failing = [];
827
+ await writeTemplate("cluster-a", { Resources: { fixed: { Type: "AWS::SQS::Queue" } } });
828
+ ran = [];
829
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(0);
830
+ expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
831
+ });
832
+
833
+ test("the input digest is per environment: progress against one does not carry into another", async () => {
834
+ const resume = join(dir, "attempt.json");
835
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
836
+ ran = [];
837
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "prod", resume }))).toBe(0);
838
+ expect(ran).toHaveLength(6);
839
+ });
840
+
841
+ test("a record without input digests carries nothing, and says so", async () => {
842
+ const resume = join(dir, "attempt.json");
843
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", dryRun: true }))).toBe(0);
844
+ const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())![1]!;
845
+ await writeFile(resume, JSON.stringify({ digest, completed: ["net", "cluster-b"], failed: [], outputs: {} }));
846
+ stderrLines = [];
847
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
848
+ expect(ran).toHaveLength(6);
849
+ expect(stderr()).toContain("does not record what each component applied, so none of its progress carries");
850
+ });
851
+ });
@@ -34,6 +34,15 @@
34
34
  * the plan before the gate is decided, and the digest is carried rather than
35
35
  * recomputed, so the approval still stands.
36
36
  *
37
+ * The approval and the progress are bound to different things (#4081). The
38
+ * approval is bound to the plan digest, the derivation: who runs, in what
39
+ * order, off which edges. The progress is bound to each component's input
40
+ * digest (../../components/fan-out-inputs.ts): a component the earlier
41
+ * attempt applied stays `already-applied` only while what it would apply is
42
+ * the same, and one whose template, step inputs or upstream moved runs again
43
+ * with a line saying so. The fix after a failure keeps the approval, and a
44
+ * different change of the same shape does not inherit the progress.
45
+ *
37
46
  * `--wave-gate <name>` (#3049) swaps the one gate for one per wave: the same
38
47
  * repeated command stops at each wave's gate in turn, each planned after the
39
48
  * wave before it applied, and `chant approve fan-out <name>-wave-<n>` answers
@@ -49,6 +58,7 @@ import { changedFilesBetween, lexiconChangedUnits } from "../../components/chang
49
58
  import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support";
50
59
  import { EarlierWaveNotAppliedError, runFanOut } from "../../components/fan-out-run";
51
60
  import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt } from "../../components/fan-out-record";
61
+ import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs";
52
62
  import { describeChangedWave, withWaveRecord, type WaveRecord } from "../../gated-waves";
53
63
  import { renderFanOutHuman, renderFanOutJson, renderFanOutPlan } from "../../components/fan-out-output";
54
64
  import { ndjsonProgressSink } from "../../components/run-progress";
@@ -253,7 +263,12 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
253
263
  // real run happens inside `runFanOut`, before the gate is decided, so an
254
264
  // approval survives the resume rather than being re-asked for.
255
265
  const resumePath = args.resume ? resolve(args.resume) : undefined;
266
+ // What each component would apply now (#4081). The record keeps it for
267
+ // every component that applies, and a later attempt carries progress only
268
+ // where it is unchanged.
269
+ const inputsNow = resumePath ? componentInputDigests(derived.components, { projectRoot: projectPath, env: args.env ?? "local" }) : {};
256
270
  let priorCompleted: string[] = [];
271
+ let priorInputs: Record<string, string> = {};
257
272
  let priorOutputs: Record<string, Record<string, unknown>> = {};
258
273
  let priorWaves: WaveRecord[] = [];
259
274
  let priorCarried: Record<string, unknown> = {};
@@ -277,11 +292,28 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
277
292
  // as progress would block their subtrees again on the very run that
278
293
  // exists to retry them — the operator repeated the command precisely
279
294
  // because the thing that failed is now expected to work.
280
- priorCompleted = attempt.completed;
295
+ //
296
+ // And only the part of `completed` whose input is the same now (#4081).
297
+ // The plan digest identifies the derivation, which a different change
298
+ // of the same shape shares; the input digest says what was applied.
299
+ const split = progressThatCarries(attempt, inputsNow);
300
+ if (split.unrecorded && split.changed.length > 0) {
301
+ console.error(formatWarning({
302
+ message: `--resume: "${args.resume}" does not record what each component applied, so none of its progress carries`,
303
+ hint: "It was written before chant recorded component inputs. Everything selected will run.",
304
+ }));
305
+ } else {
306
+ for (const name of split.changed) {
307
+ console.error(formatInfo(`${name}: changed since the earlier attempt, so it runs again`));
308
+ }
309
+ }
310
+ const carried = new Set(split.carried);
311
+ priorCompleted = split.carried;
312
+ priorInputs = Object.fromEntries(split.carried.map((name) => [name, inputsNow[name]!]));
281
313
  priorOutputs = Object.fromEntries(
282
- Object.entries(attempt.outputs).filter(([name]) => attempt.completed.includes(name)),
314
+ Object.entries(attempt.outputs).filter(([name]) => carried.has(name)),
283
315
  );
284
- progress = { completed: attempt.completed };
316
+ progress = { completed: split.carried };
285
317
  priorWaves = attempt.waves ?? [];
286
318
  // What steps kept per member (#3459), such as a choudoufu root's wave
287
319
  // resume file. Each step decides whether what it kept still applies.
@@ -319,6 +351,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
319
351
  // runner, ctrl-c) then still knows that the first two applied, and the
320
352
  // repeated command picks up from there instead of redoing them.
321
353
  const completedSoFar = new Set(priorCompleted);
354
+ const inputsSoFar: Record<string, string> = { ...priorInputs };
322
355
  const failedSoFar: string[] = [];
323
356
  const outputsSoFar = { ...priorOutputs };
324
357
  let wavesSoFar = priorWaves;
@@ -330,6 +363,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
330
363
  completed: [...completedSoFar].sort(),
331
364
  failed: [...failedSoFar].sort(),
332
365
  outputs: outputsSoFar,
366
+ inputs: Object.fromEntries(Object.entries(inputsSoFar).sort(([a], [b]) => a.localeCompare(b))),
333
367
  ...(wavesSoFar.length > 0 ? { waves: wavesSoFar } : {}),
334
368
  ...(Object.keys(carriedSoFar).length > 0 ? { carried: carriedSoFar } : {}),
335
369
  };
@@ -377,6 +411,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
377
411
  onComponentSettled: (settled, outputs) => {
378
412
  if (settled.status === "ok") {
379
413
  completedSoFar.add(settled.component);
414
+ if (inputsNow[settled.component] !== undefined) inputsSoFar[settled.component] = inputsNow[settled.component]!;
380
415
  if (outputs) outputsSoFar[settled.component] = outputs;
381
416
  } else if (settled.status === "fail") {
382
417
  failedSoFar.push(settled.component);
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The why chain of `graph --intent` (#4023), from work item to production
3
+ * release. Everything runs in a temp repo with no remote, so nothing is
4
+ * pushed: a workspace with a work item, a commit whose trailer links it, a
5
+ * green tag, a real cut, a real `components promote --release` into prod that
6
+ * stops at its release gate, a real `chant approve`, and the promote that
7
+ * follows. Only the capability registry is replaced, so no deploy step
8
+ * reaches a cloud, and the member's graph, so the resource node resolves to
9
+ * its component file without building the member.
10
+ */
11
+
12
+ import { afterEach, describe, expect, test, vi } from "vitest";
13
+ import { withTestDir } from "@intentius/chant-test-utils";
14
+ import { spawnSync } from "node:child_process";
15
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { CapabilityRegistry, type DeployContext } from "../../components/capability";
18
+ import type { DriverComponent } from "../../components/driver";
19
+ import type { ParsedArgs } from "../registry";
20
+ import { appendReleaseRecord, readReleaseLedger } from "../../lifecycle/release-ledger";
21
+ import { readGateLedger } from "../../lifecycle/gate-ledger";
22
+ import { cutRelease } from "../../lifecycle/release-cut";
23
+ import { resetGateOrigin } from "../../lifecycle/gate-origin";
24
+ import { contract, REPO } from "../../workspace/__fixtures__/contract-repo";
25
+ import { intentGraph, type IntentDocument } from "../../workspace/intent";
26
+ import intentSchema from "../../workspace/intent.schema.json";
27
+ import type { ReleaseArtifact } from "../../lifecycle/release-store";
28
+ import type { CompiledTarget, TargetCompiler } from "../../components/release-compile";
29
+
30
+ const DIGEST = `sha256:${"0123456789abcdef".repeat(4)}`;
31
+
32
+ const api: DriverComponent = {
33
+ name: "api",
34
+ deploy: [
35
+ { phase: "Publish", steps: [{ kind: "publish-image", from: "archive:api.tar" }] },
36
+ { phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/api.template.json", imageRef: "@Publish.digest" }] },
37
+ ],
38
+ };
39
+
40
+ vi.mock("../../config", async () => {
41
+ const actual = await vi.importActual<typeof import("../../config")>("../../config");
42
+ return { ...actual, loadChantConfig: async () => ({ config: {} }) };
43
+ });
44
+ vi.mock("../../components/cli-support", () => ({
45
+ isComponentList: () => false,
46
+ resolveComponentTargets: async () => ({ success: true, targets: [api] }),
47
+ }));
48
+ vi.mock("../../components/fan-out-support", () => ({
49
+ fanOutRegistry: async () => {
50
+ const registry = new CapabilityRegistry();
51
+ registry.register({ kind: "publish-image", async run(_ctx: DeployContext) { return { digest: DIGEST, uri: `reg/api@${DIGEST}` }; } } as never);
52
+ registry.register({ kind: "apply", async run() { return {}; } } as never);
53
+ return registry;
54
+ },
55
+ }));
56
+ vi.mock("../../op/discover", () => ({ discoverOps: async () => ({ errors: [], ops: new Map() }) }));
57
+ vi.mock("./run", () => ({ GATED_EXIT_CODE: 3 }));
58
+
59
+ const { runComponentsPromote } = await import("./promote");
60
+ const { recordGateApproval } = await import("./approve");
61
+
62
+ function git(dir: string, ...args: string[]): string {
63
+ const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
64
+ if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
65
+ return r.stdout.trim();
66
+ }
67
+
68
+ /** A compiler that compiles each environment to its own template and builds one image. */
69
+ const compiler: TargetCompiler = {
70
+ async compile(env): Promise<CompiledTarget> {
71
+ return {
72
+ env,
73
+ params: [],
74
+ components: ["api"],
75
+ buildSteps: { api: [{ kind: "docker-build", context: "api", into: "api.tar" }] },
76
+ buildInputs: { api: ["api"] },
77
+ componentFiles: { api: "api.component.ts" },
78
+ templates: [{ component: "api", path: "dist/api.template.json", content: `{"env":"${env}"}\n` }],
79
+ };
80
+ },
81
+ async buildArtifacts(_env, components): Promise<ReleaseArtifact[]> {
82
+ return components.map((c) => ({ component: c, kind: "image" as const, path: `${c}.tar`, digest: DIGEST }));
83
+ },
84
+ };
85
+
86
+ function args(overrides: Partial<ParsedArgs>): ParsedArgs {
87
+ return {
88
+ command: "components", path: "promote", format: "", fix: false, watch: false, verbose: false, help: false, live: false,
89
+ migrateFrom: "staging", migrateTo: "prod", actor: "bob", runId: "run-9", ...overrides,
90
+ } as ParsedArgs;
91
+ }
92
+ const ctx = (a: Partial<ParsedArgs>) => ({ args: args(a), plugins: [] }) as never;
93
+
94
+ const REF = join(REPO, "reference-workspace");
95
+ const KIND_FILES: Record<string, string> = {
96
+ "work/work.kind.mjs": readFileSync(join(REF, "work", "work.kind.mjs"), "utf-8"),
97
+ "work/work.schema.json": readFileSync(join(REF, "work", "work.schema.json"), "utf-8"),
98
+ "decisions/decision.kind.mjs": readFileSync(join(REF, "decisions", "decision.kind.mjs"), "utf-8"),
99
+ "decisions/decision.schema.json": readFileSync(join(REF, "decisions", "decision.schema.json"), "utf-8"),
100
+ };
101
+ const W001 = `---\n${JSON.stringify({ schema: 1, id: "W-001", title: "Ship the api to prod", state: "open", implements: [], needs: [], constrains: ["path:api.component.ts"], evidence: [], opened_on: "2026-10-16", supersedes: [] }, null, 2)}\n---\n\n# W-001\n`;
102
+ const DECLARATION = JSON.stringify({ name: "shop", schema: 1, members: [{ name: "api", dir: ".", kind: "other", because: "the api and its component" }] });
103
+ const intent = contract(intentSchema);
104
+ type Result = Exclude<IntentDocument, { error: unknown }>;
105
+
106
+ describe("the why chain from work item to production release (#4023)", () => {
107
+ const cwd = process.cwd();
108
+ afterEach(() => {
109
+ process.chdir(cwd);
110
+ resetGateOrigin();
111
+ vi.restoreAllMocks();
112
+ });
113
+
114
+ test(
115
+ "the trace from the api's resource node walks W-001, the linked commit, its green tag, the cut, the release gate approval and the prod deploy",
116
+ async () => {
117
+ vi.spyOn(console, "error").mockImplementation(() => {});
118
+ vi.spyOn(console, "log").mockImplementation(() => {});
119
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
120
+ await withTestDir(async (dir) => {
121
+ git(dir, "init", "-q", "-b", "main");
122
+ git(dir, "config", "user.email", "t@chant.dev");
123
+ git(dir, "config", "user.name", "T");
124
+ const files: Record<string, string> = {
125
+ "chant.workspace.json": DECLARATION,
126
+ "chant.config.ts": `export default { environments: ["dev", "staging", "prod"] };\n`,
127
+ "api.component.ts": "export const api = {};\n",
128
+ "work/W-001-ship.md": W001,
129
+ ...KIND_FILES,
130
+ };
131
+ for (const [path, text] of Object.entries(files)) {
132
+ mkdirSync(join(dir, path, ".."), { recursive: true });
133
+ writeFileSync(join(dir, path), text);
134
+ }
135
+ git(dir, "add", ".");
136
+ git(dir, "commit", "-q", "-m", "the api and W-001");
137
+ // The work: a commit whose trailer names W-001, made green.
138
+ writeFileSync(join(dir, "api.component.ts"), "export const api = { port: 8081 };\n");
139
+ git(dir, "commit", "-q", "-am", "serve the api on 8081", "--trailer", "Chant-Record: work:W-001");
140
+ const sha = git(dir, "rev-parse", "HEAD");
141
+ git(dir, "tag", `ci/green/${sha}`, sha);
142
+ process.chdir(dir);
143
+
144
+ // The release is cut from the green commit, and promoted to prod through its release gate.
145
+ const cut = await cutRelease({ cwd: dir, compiler, label: "v1.0.0", actor: "alice", env: {}, now: () => new Date("2026-10-16T10:00:00.000Z") });
146
+ const releaseDigest = cut.release.releaseDigest;
147
+ await appendReleaseRecord({ component: "api", env: "staging", digest: DIGEST, gitSha: sha, runId: "run-1", timestamp: "2026-10-16T10:30:00.000Z", actor: "ci" }, { cwd: dir });
148
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
149
+ expect((await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod" })).ok).toBe(true);
150
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
151
+ const [deploy] = (await readReleaseLedger("prod", { cwd: dir })).records;
152
+ const [resolution] = (await readGateLedger("release", { cwd: dir })).resolutions;
153
+
154
+ const walk = async (region: string): Promise<Result> => {
155
+ const { doc } = await intentGraph({
156
+ cwd: dir,
157
+ region,
158
+ kinds: [join(dir, "work/work.kind.mjs")],
159
+ resolveNode: async (_cwd, _at, member, id) => (member === "api" && id === "api/api" ? { file: "api.component.ts", line: null } : null),
160
+ });
161
+ intent.expectValid(doc);
162
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
163
+ return doc;
164
+ };
165
+
166
+ // From the resource node: who asked for it, and who let it ship, in one read.
167
+ const doc = await walk("api/api");
168
+ expect(doc.nodes[0]).toMatchObject({ kind: "region", path: "api.component.ts", node: "api/api" });
169
+ const trace = doc.trace!;
170
+ expect(trace.reasons).toEqual([]);
171
+ const c = trace.commits.find((x) => x.sha === sha)!;
172
+ expect(c.work).toEqual({ state: "linked", items: [{ id: "work:W-001", node: "record:work/W-001", source: { ref: sha, field: "Chant-Record", value: "work:W-001" } }], problems: [] });
173
+ expect(c.tag).toMatchObject({ state: "green", green: { tag: `ci/green/${sha}` } });
174
+ expect(c.releases).toEqual([{ release: releaseDigest, link: "cut-from", source: { ref: `chant/lifecycle:_releases/${releaseDigest.replace(/:/g, "_")}.json`, field: "commit", value: sha } }]);
175
+ expect(c.deploys.map((d) => [d.env, d.link])).toEqual([["prod", "built-from"], ["staging", "built-from"]]);
176
+
177
+ const [release] = trace.releases;
178
+ expect(trace.releases).toHaveLength(1);
179
+ expect(release).toMatchObject({ releaseDigest, label: "v1.0.0", commit: sha, tag: { state: "green" }, cuts: [{ label: "v1.0.0", actor: "alice" }] });
180
+ // The gate approval: who, on which channel, when, and the change set the promote stored for it (#4022).
181
+ expect(release.gates).toEqual([
182
+ expect.objectContaining({ op: "release", gate: "prod-release", environment: "prod", resolvedBy: "alice", channel: "cli", at: resolution.timestamp, planDigest: resolution.planDigest, changeSet: resolution.planDigest, empty: false }),
183
+ ]);
184
+ expect(release.gates[0].changeSetSource).toEqual({ ref: `chant/lifecycle:_change-sets/${resolution.planDigest!.replace(/:/g, "_")}.json`, field: "planDigest", value: resolution.planDigest });
185
+
186
+ // The prod deploy record, admitted by that resolution.
187
+ const prod = trace.deploys.find((d) => d.env === "prod")!;
188
+ expect(release.deploys).toEqual([prod.id]);
189
+ expect(prod).toMatchObject({ component: "api", runId: deploy.runId, gitSha: sha, release: releaseDigest, approver: "alice", admittedBy: { op: "release", gate: "prod-release", resolvedBy: "alice", at: resolution.timestamp } });
190
+ // Staging was a plain deploy of the same commit: it names no release.
191
+ expect(trace.deploys.find((d) => d.env === "staging")).toMatchObject({ release: null, admittedBy: null });
192
+
193
+ // The same chain from the commit itself.
194
+ const byCommit = await walk(sha);
195
+ expect(byCommit.trace!.commits).toEqual([c]);
196
+ expect(byCommit.trace!.releases).toEqual(trace.releases);
197
+ });
198
+ },
199
+ 240_000,
200
+ );
201
+ });
@@ -0,0 +1,126 @@
1
+ /** #4036: `chant lifecycle gc [--yes] [--push] [--json]`, in temp repos. */
2
+
3
+ import { execFileSync } from "node:child_process";
4
+ import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
8
+ import type { ParsedArgs } from "../registry";
9
+ import { runLifecycleGc } from "./lifecycle-gc";
10
+ import { writeLedgerFiles } from "../../lifecycle/git";
11
+ import { makeRelease } from "../../lifecycle/release-store";
12
+ import type { GcReport } from "../../lifecycle/retention";
13
+
14
+ function args(o: Partial<ParsedArgs>): ParsedArgs {
15
+ return { command: "lifecycle", path: ".", format: "", fix: false, watch: false, verbose: false, help: false, live: false, ...o };
16
+ }
17
+
18
+ const git = (cwd: string, ...a: string[]) => execFileSync("git", a, { cwd, encoding: "utf-8" }).trim();
19
+
20
+ let dir: string;
21
+ let cwd: string;
22
+ let out: string[];
23
+ let err: string[];
24
+ beforeEach(() => {
25
+ cwd = process.cwd();
26
+ dir = realpathSync(mkdtempSync(join(tmpdir(), "lifecycle-gc-")));
27
+ vi.stubEnv("CHANT_RUN_EVENTS_DIR", join(dir, ".events"));
28
+ git(dir, "init", "-q", "-b", "main");
29
+ git(dir, "config", "user.email", "t@t");
30
+ git(dir, "config", "user.name", "t");
31
+ process.chdir(dir);
32
+ out = [];
33
+ err = [];
34
+ vi.spyOn(console, "log").mockImplementation((s: string) => void out.push(String(s)));
35
+ vi.spyOn(console, "error").mockImplementation((s: string) => void err.push(String(s)));
36
+ });
37
+ afterEach(() => {
38
+ vi.restoreAllMocks();
39
+ vi.unstubAllEnvs();
40
+ process.chdir(cwd);
41
+ rmSync(dir, { recursive: true, force: true });
42
+ });
43
+
44
+ function commitAll(): string {
45
+ writeFileSync(join(dir, "README.md"), "x\n");
46
+ git(dir, "add", ".");
47
+ git(dir, "commit", "-q", "-m", "init");
48
+ return git(dir, "rev-parse", "HEAD");
49
+ }
50
+
51
+ const changeSet = `_change-sets/jcs1-sha256_${"1".repeat(64)}.json`;
52
+ const ctx = (o: Partial<ParsedArgs>) => ({ args: args(o), plugins: [], serializers: [] });
53
+ const report = (): GcReport => JSON.parse(out.join("\n")) as GcReport;
54
+
55
+ describe("chant lifecycle gc", () => {
56
+ test("a dry run by default; --yes prunes in one local commit and pushes nothing", async () => {
57
+ writeFileSync(join(dir, "chant.workspace.json"), JSON.stringify({ name: "w", schema: 1, members: [], retention: { changeSets: { days: 0 } } }));
58
+ commitAll();
59
+ await writeLedgerFiles([{ path: changeSet, content: "{}\n", expectPriorSha: null }], "fixture", { cwd: dir });
60
+ const tip = git(dir, "rev-parse", "chant/lifecycle");
61
+
62
+ expect(await runLifecycleGc(ctx({}))).toBe(0);
63
+ expect(out[0]).toMatch(/^chant\/lifecycle at [0-9a-f]{12}: would remove 1 change set\(s\), 0 release\(s\) and 0 template\(s\)/);
64
+ expect(err.join("\n")).toContain("Dry run: nothing was removed");
65
+ expect(git(dir, "rev-parse", "chant/lifecycle")).toBe(tip);
66
+
67
+ out = [];
68
+ expect(await runLifecycleGc(ctx({ yes: true, json: true }))).toBe(0);
69
+ const r = report();
70
+ expect(r.dryRun).toBe(false);
71
+ expect(r.policy.changeSets.days).toBe(0);
72
+ expect(r.lifecycle.remove.map((f) => f.path)).toEqual([changeSet]);
73
+ expect(r.lifecycle.commit).toBe(git(dir, "rev-parse", "chant/lifecycle"));
74
+ expect(git(dir, "rev-parse", "chant/lifecycle^")).toBe(tip);
75
+ expect(r.lifecycle.pushed).toBe(false);
76
+ });
77
+
78
+ test("outside a workspace the defaults apply, and a release of a revoked commit is not promotable", async () => {
79
+ const head = commitAll();
80
+ const cut = (commit: string) => {
81
+ const r = makeRelease({ member: null, commit, artifactDigests: [], targets: [{ env: "prod", templates: [], params: [] }] });
82
+ return { r, line: JSON.stringify({ version: 1, releaseDigest: r.releaseDigest, label: null, actor: "a", timestamp: new Date().toISOString(), runOrigin: null, signature: null }) };
83
+ };
84
+ const revoked = cut(head);
85
+ const fine = cut("b".repeat(40));
86
+ const file = (d: string) => `_releases/${d.replace(/:/g, "_")}.json`;
87
+ await writeLedgerFiles(
88
+ [
89
+ { path: changeSet, content: "{}\n" },
90
+ { path: file(revoked.r.releaseDigest), content: `${JSON.stringify(revoked.r)}\n` },
91
+ { path: file(fine.r.releaseDigest), content: `${JSON.stringify(fine.r)}\n` },
92
+ { path: "_releases/cuts.jsonl", content: `${revoked.line}\n${fine.line}\n` },
93
+ ],
94
+ "fixture",
95
+ { cwd: dir },
96
+ );
97
+ git(dir, "tag", `ci/revoked/${head}`, head);
98
+
99
+ expect(await runLifecycleGc(ctx({ json: true }))).toBe(0);
100
+ const r = report();
101
+ expect(r.policy.releases).toEqual({ days: 14, keep: 10 });
102
+ expect(r.lifecycle.remove.map((f) => f.path)).toEqual([file(revoked.r.releaseDigest)]);
103
+ expect(r.lifecycle.keep.map((f) => [f.path, f.reasons.map((x) => x.code)])).toEqual(
104
+ expect.arrayContaining([[changeSet, ["within-window"]], [file(fine.r.releaseDigest), ["newest", "within-window"]]]),
105
+ );
106
+ });
107
+
108
+ test("a workspace whose declaration can't be read prunes nothing", async () => {
109
+ writeFileSync(join(dir, "chant.workspace.json"), JSON.stringify({ name: "w", schema: 1, members: [], retention: { changeSets: { days: -1 } } }));
110
+ commitAll();
111
+ expect(await runLifecycleGc(ctx({ yes: true }))).toBe(1);
112
+ expect(err.join("\n")).toContain("its retention policy is unknown and nothing is pruned");
113
+ });
114
+
115
+ test("a directory outside git is refused", async () => {
116
+ const plain = realpathSync(mkdtempSync(join(tmpdir(), "lifecycle-gc-plain-")));
117
+ try {
118
+ process.chdir(plain);
119
+ expect(await runLifecycleGc(ctx({}))).toBe(1);
120
+ expect(err.join("\n")).toContain("not in a git repository");
121
+ } finally {
122
+ process.chdir(dir);
123
+ rmSync(plain, { recursive: true, force: true });
124
+ }
125
+ });
126
+ });