@intentius/chant 0.124.28 → 0.124.30

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 (449) 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 +18 -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 +67 -8
  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 +91 -10
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +73 -2
  41. package/dist/cli/registry.d.ts +32 -4
  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 +49 -0
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +38 -4
  67. package/dist/components/pr-run.d.ts +20 -2
  68. package/dist/components/pr-run.d.ts.map +1 -1
  69. package/dist/components/pr-run.js +26 -13
  70. package/dist/components/promote.d.ts +23 -0
  71. package/dist/components/promote.d.ts.map +1 -1
  72. package/dist/components/promote.js +60 -2
  73. package/dist/components/release-compile.d.ts +9 -0
  74. package/dist/components/release-compile.d.ts.map +1 -1
  75. package/dist/components/release-compile.js +9 -3
  76. package/dist/components/release-gate.d.ts +11 -1
  77. package/dist/components/release-gate.d.ts.map +1 -1
  78. package/dist/components/release-gate.js +10 -4
  79. package/dist/components/wave-plan.d.ts +16 -0
  80. package/dist/components/wave-plan.d.ts.map +1 -1
  81. package/dist/components/wave-plan.js +36 -2
  82. package/dist/config.d.ts +38 -0
  83. package/dist/config.d.ts.map +1 -1
  84. package/dist/config.js +2 -0
  85. package/dist/flag-params.d.ts +54 -0
  86. package/dist/flag-params.d.ts.map +1 -0
  87. package/dist/flag-params.js +130 -0
  88. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  89. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  90. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  91. package/dist/lifecycle/change-set-store.d.ts +2 -1
  92. package/dist/lifecycle/change-set-store.d.ts.map +1 -1
  93. package/dist/lifecycle/change-set-store.js +2 -1
  94. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  95. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  96. package/dist/lifecycle/delivery-metrics.js +145 -0
  97. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  98. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  99. package/dist/lifecycle/deploy-outcome.js +101 -0
  100. package/dist/lifecycle/gate-change-set.d.ts +10 -1
  101. package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
  102. package/dist/lifecycle/gate-change-set.js +35 -3
  103. package/dist/lifecycle/gate-ledger.d.ts +2 -3
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +30 -1
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/git.js +31 -5
  108. package/dist/lifecycle/index.d.ts +4 -0
  109. package/dist/lifecycle/index.d.ts.map +1 -1
  110. package/dist/lifecycle/index.js +4 -0
  111. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  112. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  113. package/dist/lifecycle/mixed-environment.js +154 -0
  114. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  115. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  116. package/dist/lifecycle/publish-attachments.js +179 -0
  117. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  118. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  119. package/dist/lifecycle/publish-credentials.js +69 -0
  120. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  121. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  122. package/dist/lifecycle/publish-declaration.js +83 -0
  123. package/dist/lifecycle/publish-forge.d.ts +53 -0
  124. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  125. package/dist/lifecycle/publish-forge.js +298 -0
  126. package/dist/lifecycle/publish-npm.d.ts +61 -0
  127. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  128. package/dist/lifecycle/publish-npm.js +242 -0
  129. package/dist/lifecycle/publish-oci.d.ts +64 -0
  130. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  131. package/dist/lifecycle/publish-oci.js +383 -0
  132. package/dist/lifecycle/publish-record.d.ts +66 -0
  133. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  134. package/dist/lifecycle/publish-record.js +84 -0
  135. package/dist/lifecycle/publish-targets.d.ts +45 -0
  136. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  137. package/dist/lifecycle/publish-targets.js +85 -0
  138. package/dist/lifecycle/release-cut.d.ts +48 -1
  139. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  140. package/dist/lifecycle/release-cut.js +116 -23
  141. package/dist/lifecycle/release-join.d.ts +95 -0
  142. package/dist/lifecycle/release-join.d.ts.map +1 -0
  143. package/dist/lifecycle/release-join.js +139 -0
  144. package/dist/lifecycle/release-ledger.d.ts +8 -3
  145. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  146. package/dist/lifecycle/release-ledger.js +2 -0
  147. package/dist/lifecycle/release-publish.d.ts +312 -0
  148. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  149. package/dist/lifecycle/release-publish.js +335 -0
  150. package/dist/lifecycle/release-revocation.d.ts +80 -0
  151. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  152. package/dist/lifecycle/release-revocation.js +123 -0
  153. package/dist/lifecycle/release-store.d.ts +12 -2
  154. package/dist/lifecycle/release-store.d.ts.map +1 -1
  155. package/dist/lifecycle/release-store.js +4 -2
  156. package/dist/lifecycle/release-verify.d.ts +61 -0
  157. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  158. package/dist/lifecycle/release-verify.js +92 -0
  159. package/dist/lifecycle/release-workstation.d.ts +91 -0
  160. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  161. package/dist/lifecycle/release-workstation.js +180 -0
  162. package/dist/lifecycle/retention-policy.d.ts +45 -0
  163. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  164. package/dist/lifecycle/retention-policy.js +43 -0
  165. package/dist/lifecycle/retention.d.ts +185 -0
  166. package/dist/lifecycle/retention.d.ts.map +1 -0
  167. package/dist/lifecycle/retention.js +400 -0
  168. package/dist/lifecycle/run-ledger.d.ts +1 -1
  169. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  170. package/dist/lifecycle/run-ledger.js +9 -0
  171. package/dist/op/activities/activity-contracts.d.ts +1 -0
  172. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  173. package/dist/op/activities/activity-contracts.js +1 -0
  174. package/dist/op/activities/shell.d.ts +6 -0
  175. package/dist/op/activities/shell.d.ts.map +1 -1
  176. package/dist/op/activities/shell.js +1 -1
  177. package/dist/op/local-executor.d.ts +23 -1
  178. package/dist/op/local-executor.d.ts.map +1 -1
  179. package/dist/op/local-executor.js +9 -3
  180. package/dist/op/runtime.d.ts +27 -0
  181. package/dist/op/runtime.d.ts.map +1 -1
  182. package/dist/op/runtime.js +20 -0
  183. package/dist/plan-summary.d.ts.map +1 -1
  184. package/dist/plan-summary.js +8 -2
  185. package/dist/pr-forge.d.ts +23 -0
  186. package/dist/pr-forge.d.ts.map +1 -1
  187. package/dist/pr-forge.js +44 -0
  188. package/dist/pr-loop.d.ts +17 -0
  189. package/dist/pr-loop.d.ts.map +1 -1
  190. package/dist/pr-loop.js +30 -4
  191. package/dist/pr-report-url.d.ts +82 -0
  192. package/dist/pr-report-url.d.ts.map +1 -0
  193. package/dist/pr-report-url.js +174 -0
  194. package/dist/provenance.d.ts +2 -2
  195. package/dist/provenance.d.ts.map +1 -1
  196. package/dist/run-event-store.d.ts +29 -2
  197. package/dist/run-event-store.d.ts.map +1 -1
  198. package/dist/run-event-store.js +50 -3
  199. package/dist/run-event-stream.d.ts +4 -3
  200. package/dist/run-event-stream.d.ts.map +1 -1
  201. package/dist/run-event-stream.js +7 -3
  202. package/dist/run-event.d.ts +34 -1
  203. package/dist/run-event.d.ts.map +1 -1
  204. package/dist/run-event.js +4 -0
  205. package/dist/wave-runtime.mjs +260 -229
  206. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  207. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  208. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  209. package/dist/workspace/change-set.schema.json +5 -1
  210. package/dist/workspace/check.schema.json +4 -2
  211. package/dist/workspace/checks/records.d.ts +9 -1
  212. package/dist/workspace/checks/records.d.ts.map +1 -1
  213. package/dist/workspace/checks/records.js +35 -1
  214. package/dist/workspace/checks.d.ts +1 -0
  215. package/dist/workspace/checks.d.ts.map +1 -1
  216. package/dist/workspace/checks.js +2 -1
  217. package/dist/workspace/ci-green-forge.d.ts +2 -0
  218. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  219. package/dist/workspace/ci-green.d.ts +10 -0
  220. package/dist/workspace/ci-green.d.ts.map +1 -1
  221. package/dist/workspace/ci-green.js +14 -5
  222. package/dist/workspace/ci-run.d.ts +222 -0
  223. package/dist/workspace/ci-run.d.ts.map +1 -0
  224. package/dist/workspace/ci-run.js +464 -0
  225. package/dist/workspace/ci-serve.d.ts +214 -0
  226. package/dist/workspace/ci-serve.d.ts.map +1 -0
  227. package/dist/workspace/ci-serve.js +412 -0
  228. package/dist/workspace/declaration.d.ts +16 -0
  229. package/dist/workspace/declaration.d.ts.map +1 -1
  230. package/dist/workspace/declaration.js +26 -0
  231. package/dist/workspace/declaration.schema.json +64 -0
  232. package/dist/workspace/declared-kinds.d.ts +16 -0
  233. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  234. package/dist/workspace/declared-kinds.js +25 -0
  235. package/dist/workspace/delivery-metrics.d.ts +91 -0
  236. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  237. package/dist/workspace/delivery-metrics.js +217 -0
  238. package/dist/workspace/delivery-metrics.schema.json +519 -0
  239. package/dist/workspace/flags-read.d.ts +37 -0
  240. package/dist/workspace/flags-read.d.ts.map +1 -0
  241. package/dist/workspace/flags-read.js +103 -0
  242. package/dist/workspace/flags.d.ts +100 -0
  243. package/dist/workspace/flags.d.ts.map +1 -0
  244. package/dist/workspace/flags.js +98 -0
  245. package/dist/workspace/graph-cli.js +1 -1
  246. package/dist/workspace/intent-cli.d.ts +11 -2
  247. package/dist/workspace/intent-cli.d.ts.map +1 -1
  248. package/dist/workspace/intent-cli.js +54 -5
  249. package/dist/workspace/intent-trace.d.ts +219 -0
  250. package/dist/workspace/intent-trace.d.ts.map +1 -0
  251. package/dist/workspace/intent-trace.js +314 -0
  252. package/dist/workspace/intent.d.ts +30 -6
  253. package/dist/workspace/intent.d.ts.map +1 -1
  254. package/dist/workspace/intent.js +141 -21
  255. package/dist/workspace/intent.schema.json +702 -6
  256. package/dist/workspace/overlap.d.ts +146 -0
  257. package/dist/workspace/overlap.d.ts.map +1 -0
  258. package/dist/workspace/overlap.js +276 -0
  259. package/dist/workspace/pr-report.schema.json +5 -0
  260. package/dist/workspace/reason-codes.d.ts +2 -0
  261. package/dist/workspace/reason-codes.d.ts.map +1 -1
  262. package/dist/workspace/reason-codes.js +3 -0
  263. package/dist/workspace/records.d.ts +15 -2
  264. package/dist/workspace/records.d.ts.map +1 -1
  265. package/dist/workspace/records.js +32 -0
  266. package/dist/workspace/records.schema.json +21 -2
  267. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  268. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  269. package/dist/workspace/releases.d.ts +9 -0
  270. package/dist/workspace/releases.d.ts.map +1 -1
  271. package/dist/workspace/releases.js +9 -0
  272. package/dist/workspace/releases.schema.json +123 -1
  273. package/dist/workspace/run-event.schema.json +24 -6
  274. package/dist/workspace/status-gates.d.ts +8 -0
  275. package/dist/workspace/status-gates.d.ts.map +1 -1
  276. package/dist/workspace/status-stewards.d.ts +2 -0
  277. package/dist/workspace/status-stewards.d.ts.map +1 -1
  278. package/dist/workspace/status-stewards.js +1 -0
  279. package/dist/workspace/status.d.ts +78 -0
  280. package/dist/workspace/status.d.ts.map +1 -1
  281. package/dist/workspace/status.js +170 -3
  282. package/dist/workspace/status.schema.json +210 -2
  283. package/dist/workspace/wip.d.ts.map +1 -1
  284. package/dist/workspace/wip.js +11 -3
  285. package/package.json +1 -1
  286. package/src/build-params.test.ts +46 -0
  287. package/src/build-params.ts +55 -1
  288. package/src/change-set-graph.ts +10 -2
  289. package/src/change-set.ts +14 -3
  290. package/src/cli/build-params-cli.ts +22 -9
  291. package/src/cli/commands/build.ts +6 -3
  292. package/src/cli/handlers/approve-release.test.ts +5 -1
  293. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  294. package/src/cli/handlers/approve.ts +47 -0
  295. package/src/cli/handlers/build.test.ts +30 -0
  296. package/src/cli/handlers/build.ts +20 -4
  297. package/src/cli/handlers/fan-out.test.ts +262 -1
  298. package/src/cli/handlers/fan-out.ts +72 -8
  299. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  300. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  301. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  302. package/src/cli/handlers/pr.test.ts +148 -1
  303. package/src/cli/handlers/pr.ts +144 -28
  304. package/src/cli/handlers/promote.test.ts +280 -2
  305. package/src/cli/handlers/promote.ts +210 -34
  306. package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
  307. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  308. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  309. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  310. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  311. package/src/cli/handlers/release.e2e.test.ts +146 -32
  312. package/src/cli/handlers/release.ts +415 -28
  313. package/src/cli/handlers/run-auto-release.test.ts +9 -0
  314. package/src/cli/handlers/run-release.test.ts +55 -0
  315. package/src/cli/handlers/run.ts +69 -5
  316. package/src/cli/main.test.ts +1 -0
  317. package/src/cli/main.ts +91 -10
  318. package/src/cli/parse-args.ts +54 -2
  319. package/src/cli/registry.ts +32 -4
  320. package/src/cli/run-events.ts +5 -2
  321. package/src/components/auto-release.test.ts +9 -0
  322. package/src/components/auto-release.ts +3 -0
  323. package/src/components/capability.ts +9 -0
  324. package/src/components/driver.test.ts +24 -0
  325. package/src/components/driver.ts +36 -0
  326. package/src/components/fan-out-inputs.test.ts +85 -0
  327. package/src/components/fan-out-inputs.ts +192 -0
  328. package/src/components/fan-out-record.test.ts +10 -0
  329. package/src/components/fan-out-record.ts +19 -0
  330. package/src/components/fan-out-run.ts +48 -0
  331. package/src/components/fan-out.ts +4 -0
  332. package/src/components/pr-pipeline.ts +67 -4
  333. package/src/components/pr-run.test.ts +127 -0
  334. package/src/components/pr-run.ts +32 -12
  335. package/src/components/promote.ts +68 -2
  336. package/src/components/release-compile.ts +17 -3
  337. package/src/components/release-gate.ts +18 -4
  338. package/src/components/wave-plan.ts +45 -2
  339. package/src/config.ts +8 -0
  340. package/src/flag-params.test.ts +144 -0
  341. package/src/flag-params.ts +134 -0
  342. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  343. package/src/lifecycle/change-set-store.ts +2 -1
  344. package/src/lifecycle/delivery-metrics.ts +263 -0
  345. package/src/lifecycle/deploy-outcome.ts +147 -0
  346. package/src/lifecycle/gate-change-set.ts +37 -6
  347. package/src/lifecycle/gate-ledger.ts +2 -2
  348. package/src/lifecycle/git.ts +61 -8
  349. package/src/lifecycle/index.ts +4 -0
  350. package/src/lifecycle/mixed-environment.test.ts +97 -0
  351. package/src/lifecycle/mixed-environment.ts +204 -0
  352. package/src/lifecycle/publish-attachments.ts +196 -0
  353. package/src/lifecycle/publish-credentials.ts +84 -0
  354. package/src/lifecycle/publish-declaration.ts +95 -0
  355. package/src/lifecycle/publish-forge.ts +325 -0
  356. package/src/lifecycle/publish-npm.ts +259 -0
  357. package/src/lifecycle/publish-oci.ts +396 -0
  358. package/src/lifecycle/publish-record.ts +113 -0
  359. package/src/lifecycle/publish-resume.test.ts +168 -0
  360. package/src/lifecycle/publish-targets.test.ts +494 -0
  361. package/src/lifecycle/publish-targets.ts +100 -0
  362. package/src/lifecycle/release-cut.test.ts +74 -2
  363. package/src/lifecycle/release-cut.ts +158 -22
  364. package/src/lifecycle/release-flag.test.ts +112 -0
  365. package/src/lifecycle/release-join.test.ts +68 -0
  366. package/src/lifecycle/release-join.ts +182 -0
  367. package/src/lifecycle/release-ledger.ts +9 -3
  368. package/src/lifecycle/release-publish.test.ts +225 -0
  369. package/src/lifecycle/release-publish.ts +568 -0
  370. package/src/lifecycle/release-revocation.ts +147 -0
  371. package/src/lifecycle/release-store.ts +15 -4
  372. package/src/lifecycle/release-verify.test.ts +110 -0
  373. package/src/lifecycle/release-verify.ts +120 -0
  374. package/src/lifecycle/release-workstation.test.ts +204 -0
  375. package/src/lifecycle/release-workstation.ts +216 -0
  376. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  377. package/src/lifecycle/retention-policy.ts +63 -0
  378. package/src/lifecycle/retention.test.ts +363 -0
  379. package/src/lifecycle/retention.ts +508 -0
  380. package/src/lifecycle/run-ledger.ts +7 -1
  381. package/src/op/activities/activity-contracts.ts +1 -0
  382. package/src/op/activities/shell.ts +7 -1
  383. package/src/op/local-executor.ts +27 -4
  384. package/src/op/runtime.ts +42 -0
  385. package/src/plan-summary.test.ts +20 -0
  386. package/src/plan-summary.ts +8 -1
  387. package/src/pr-forge.test.ts +25 -0
  388. package/src/pr-forge.ts +62 -0
  389. package/src/pr-loop.test.ts +13 -1
  390. package/src/pr-loop.ts +41 -4
  391. package/src/pr-report-url.test.ts +131 -0
  392. package/src/pr-report-url.ts +209 -0
  393. package/src/provenance.ts +2 -2
  394. package/src/run-event-store.ts +63 -3
  395. package/src/run-event-stream.ts +8 -4
  396. package/src/run-event.ts +38 -1
  397. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  398. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  399. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  400. package/src/workspace/change-set.schema.json +5 -1
  401. package/src/workspace/check.schema.json +4 -2
  402. package/src/workspace/checks/records.ts +35 -1
  403. package/src/workspace/checks.ts +2 -1
  404. package/src/workspace/ci-green-forge.ts +2 -0
  405. package/src/workspace/ci-green.ts +20 -5
  406. package/src/workspace/ci-run.test.ts +416 -0
  407. package/src/workspace/ci-run.ts +571 -0
  408. package/src/workspace/ci-serve.test.ts +268 -0
  409. package/src/workspace/ci-serve.ts +521 -0
  410. package/src/workspace/declaration.schema.json +64 -0
  411. package/src/workspace/declaration.test.ts +28 -0
  412. package/src/workspace/declaration.ts +38 -0
  413. package/src/workspace/declared-kinds.ts +40 -1
  414. package/src/workspace/delivery-metrics.schema.json +519 -0
  415. package/src/workspace/delivery-metrics.test.ts +236 -0
  416. package/src/workspace/delivery-metrics.ts +286 -0
  417. package/src/workspace/flags-read.ts +121 -0
  418. package/src/workspace/flags.test.ts +141 -0
  419. package/src/workspace/flags.ts +157 -0
  420. package/src/workspace/graph-cli.ts +1 -1
  421. package/src/workspace/intent-cli.ts +46 -5
  422. package/src/workspace/intent-trace.test.ts +173 -0
  423. package/src/workspace/intent-trace.ts +484 -0
  424. package/src/workspace/intent.schema.json +702 -6
  425. package/src/workspace/intent.ts +148 -25
  426. package/src/workspace/older-records.test.ts +412 -0
  427. package/src/workspace/overlap.test.ts +165 -0
  428. package/src/workspace/overlap.ts +340 -0
  429. package/src/workspace/pr-report.schema.json +5 -0
  430. package/src/workspace/read-contract.e2e.test.ts +2 -1
  431. package/src/workspace/reason-codes.test.ts +5 -0
  432. package/src/workspace/reason-codes.ts +3 -0
  433. package/src/workspace/record-assets.test.ts +2 -1
  434. package/src/workspace/records-contract.e2e.test.ts +2 -1
  435. package/src/workspace/records.schema.json +21 -2
  436. package/src/workspace/records.ts +36 -2
  437. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  438. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  439. package/src/workspace/releases.schema.json +123 -1
  440. package/src/workspace/releases.ts +11 -0
  441. package/src/workspace/run-event.schema.json +24 -6
  442. package/src/workspace/status-contract.test.ts +29 -0
  443. package/src/workspace/status-gates.ts +8 -0
  444. package/src/workspace/status-mixed.test.ts +152 -0
  445. package/src/workspace/status-revoked.test.ts +153 -0
  446. package/src/workspace/status-stewards.ts +3 -0
  447. package/src/workspace/status.schema.json +210 -2
  448. package/src/workspace/status.ts +254 -3
  449. package/src/workspace/wip.ts +11 -2
@@ -84,6 +84,13 @@ let inputs: Record<string, Record<string, unknown>> = {};
84
84
  let stepOutputs: Record<string, Record<string, unknown>> = {};
85
85
  /** Called as a component's step starts, before it applies. */
86
86
  let onStart: (component: string) => void = () => {};
87
+ /**
88
+ * What each stack exposes in the deployed estate, read through the step's
89
+ * `outputs` as cfn-deploy's `describeStack` does (#4104). `undefined` leaves
90
+ * the capability with no `outputs`; an `Error` makes the read fail.
91
+ */
92
+ let liveOutputs: Record<string, Record<string, unknown> | Error> | undefined;
93
+ let outputsRead: string[] = [];
87
94
 
88
95
  function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
89
96
  return {
@@ -114,6 +121,8 @@ beforeEach(() => {
114
121
  inputs = {};
115
122
  stepOutputs = {};
116
123
  onStart = () => {};
124
+ liveOutputs = undefined;
125
+ outputsRead = [];
117
126
  gateLedger = memoryGateLedgerPort();
118
127
  stderrLines = [];
119
128
  stdoutLines = [];
@@ -149,6 +158,16 @@ beforeEach(() => {
149
158
  if (failing.includes(runCtx.component)) throw new Error(`${runCtx.component} failed`);
150
159
  return { ok: true, ...(stepOutputs[runCtx.component] ? { outputs: stepOutputs[runCtx.component] } : {}) };
151
160
  },
161
+ ...(liveOutputs
162
+ ? {
163
+ async outputs(readCtx: { component: string }) {
164
+ outputsRead.push(readCtx.component);
165
+ const live = liveOutputs![readCtx.component];
166
+ if (live instanceof Error) throw live;
167
+ return { ...(live ?? {}) };
168
+ },
169
+ }
170
+ : {}),
152
171
  } as never);
153
172
  return Promise.resolve(registry);
154
173
  });
@@ -233,7 +252,8 @@ describe("proof: a plan-only invocation prints the derivation and dispatches not
233
252
 
234
253
  ran = [];
235
254
  stderrLines = [];
236
- expect(await runComponentsFanOut(ctx({ base: "main", resume, dryRun: true }))).toBe(0);
255
+ // The same environment: progress is bound to it (#4081).
256
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, dryRun: true }))).toBe(0);
237
257
 
238
258
  expect(ran).toEqual([]);
239
259
  const out = stderr();
@@ -656,6 +676,46 @@ describe("--events-json (#4041)", () => {
656
676
  expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed" });
657
677
  });
658
678
 
679
+ test("a component behind a failure settles blocked, naming it, after the failing wave and before the next starts (#4007)", async () => {
680
+ failing = ["cluster-a"];
681
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", eventsJson: true }))).toBe(1);
682
+ const events = checkRunEventLines(lines());
683
+ const blocked = events.filter((e) => e.type === "component-settle" && e.status === "blocked");
684
+ expect(blocked.map((e) => ({ wave: (e as { wave: number }).wave, component: (e as { component: string }).component, blockedBy: (e as { blockedBy?: string }).blockedBy }))).toEqual([
685
+ { wave: 3, component: "app-one", blockedBy: "cluster-a" },
686
+ { wave: 3, component: "app-two", blockedBy: "cluster-a" },
687
+ ]);
688
+ const wave2Settle = events.findIndex((e) => e.type === "wave-settle" && e.wave === 2);
689
+ const wave3Start = events.findIndex((e) => e.type === "wave-start" && e.wave === 3);
690
+ expect(events.indexOf(blocked[0]!)).toBeGreaterThan(wave2Settle);
691
+ expect(events.indexOf(blocked[1]!)).toBeLessThan(wave3Start);
692
+ // A blocked component never starts.
693
+ expect(events.some((e) => e.type === "component-start" && e.component === "app-one")).toBe(false);
694
+ });
695
+
696
+ test("a resumed fan-out settles what the earlier attempt applied as already-applied, in the derivation's waves (#4007)", async () => {
697
+ const dir = await mkdtemp(join(tmpdir(), "fan-out-"));
698
+ try {
699
+ const resume = join(dir, "attempt.json");
700
+ failing = ["cluster-a"];
701
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(1);
702
+ failing = [];
703
+ stdoutLines = [];
704
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, eventsJson: true }))).toBe(0);
705
+ const events = checkRunEventLines(lines());
706
+ const firstWave = events.findIndex((e) => e.type === "wave-start");
707
+ const applied = events.filter((e) => e.type === "component-settle" && e.status === "already-applied");
708
+ expect(applied.map((e) => [(e as { component: string }).component, (e as { wave: number }).wave])).toEqual([["app-three", 3], ["cluster-b", 2], ["net", 1]]);
709
+ for (const e of applied) expect(events.indexOf(e)).toBeLessThan(firstWave);
710
+ // --progress-json never printed them, and still does not.
711
+ stdoutLines = [];
712
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume, progressJson: true }))).toBe(0);
713
+ expect(stdoutLines.filter((l) => l.startsWith('{"type"')).map((l) => JSON.parse(l).type)).toEqual(["run-start", "run-done"]);
714
+ } finally {
715
+ await rm(dir, { recursive: true, force: true });
716
+ }
717
+ });
718
+
659
719
  test("--progress-json on a set the gate stops now prints run-start and run-done gated (a level-0 change)", async () => {
660
720
  expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", progressJson: true }))).toBe(3);
661
721
  const progress = stdoutLines.filter((l) => l.startsWith('{"type"')).map((l) => JSON.parse(l));
@@ -670,3 +730,204 @@ describe("--events-json (#4041)", () => {
670
730
  expect(stderr()).toContain("--events-json and --progress-json both stream to stdout");
671
731
  });
672
732
  });
733
+
734
+ describe("progress carries only for an unchanged input (#4081)", () => {
735
+ /** The estate with each component deploying a built template from `templates/`, read relative to the project. */
736
+ const templated = ESTATE.map((c) => ({
737
+ ...c,
738
+ deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", stack: `${c.name}-stack`, template: `templates/${c.name}.json` }] }],
739
+ }));
740
+ let dir: string;
741
+ const writeTemplate = (name: string, body: unknown) =>
742
+ writeFile(join(dir, "templates", `${name}.json`), JSON.stringify(body));
743
+
744
+ beforeEach(async () => {
745
+ dir = await mkdtemp(join(tmpdir(), "fan-out-inputs-"));
746
+ const { mkdir } = await import("node:fs/promises");
747
+ await mkdir(join(dir, "templates"));
748
+ for (const c of templated) await writeTemplate(c.name, { Resources: { [c.name]: { Type: "AWS::SNS::Topic" } } });
749
+ // The project is the temp directory, so the templates are read from there and nothing touches the checkout.
750
+ vi.spyOn(process, "cwd").mockReturnValue(dir);
751
+ discoverComponentsMock.mockResolvedValue({
752
+ components: new Map(templated.map((c) => [c.name, { component: c, exportName: c.name, filePath: `${c.name}.component.ts` }])),
753
+ sourceFiles: [],
754
+ errors: [],
755
+ });
756
+ });
757
+ afterEach(async () => {
758
+ await rm(dir, { recursive: true, force: true });
759
+ });
760
+
761
+ test("same shape, changed content: a stale record does not skip the components whose input moved", async () => {
762
+ const resume = join(dir, "attempt.json");
763
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
764
+ const first = JSON.parse(await readFile(resume, "utf8"));
765
+ expect(first.completed).toEqual(["app-one", "app-three", "app-two", "cluster-a", "cluster-b", "net"]);
766
+ expect(Object.keys(first.inputs).sort()).toEqual(first.completed);
767
+ const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())?.[1];
768
+
769
+ // The next change: net's topic gets a display name and cluster-a a queue.
770
+ // Same components, same edges, so the same plan digest.
771
+ await writeTemplate("net", { Resources: { net: { Type: "AWS::SNS::Topic", Properties: { DisplayName: "estate" } } } });
772
+ await writeTemplate("cluster-a", { Resources: { "cluster-a": { Type: "AWS::SNS::Topic" }, events: { Type: "AWS::SQS::Queue" } } });
773
+ ran = [];
774
+ stderrLines = [];
775
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
776
+
777
+ expect([...ran].sort()).toEqual(["app-one", "app-three", "app-two", "cluster-a", "cluster-b", "net"]);
778
+ const out = stderr();
779
+ expect(out).toContain(`plan: ${digest}`);
780
+ expect(out).toContain("net: changed since the earlier attempt, so it runs again");
781
+ expect(out).toContain("cluster-a: changed since the earlier attempt, so it runs again");
782
+ // Downstream of net, so its parameters read a changed upstream.
783
+ expect(out).toContain("app-three: changed since the earlier attempt, so it runs again");
784
+ expect(out).not.toMatch(/already-applied/);
785
+ expect(out).toContain("fan-out completed: 6 applied, 0 failed, 0 blocked");
786
+ const second = JSON.parse(await readFile(resume, "utf8"));
787
+ expect(second.inputs.net).not.toBe(first.inputs.net);
788
+ expect(second.inputs["cluster-b"]).not.toBe(first.inputs["cluster-b"]);
789
+ });
790
+
791
+ test("a change below the root reruns only its own subtree", async () => {
792
+ const resume = join(dir, "attempt.json");
793
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
794
+ await writeTemplate("cluster-a", { Resources: { "cluster-a": { Type: "AWS::SNS::Topic" }, events: { Type: "AWS::SQS::Queue" } } });
795
+ ran = [];
796
+ stderrLines = [];
797
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
798
+ expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
799
+ expect(stderr()).toMatch(/cluster-b +already-applied/);
800
+ expect(stderr()).not.toContain("net: changed");
801
+ });
802
+
803
+ test("the README's fail-then-fix: the fix reruns the failed subtree and the rest stay already-applied", async () => {
804
+ const resume = join(dir, "attempt.json");
805
+ failing = ["cluster-a"];
806
+ await writeTemplate("cluster-a", { Resources: { events: { Type: "AWS::SQS::Queue", Properties: { FifoQueue: true, QueueName: "events" } } } });
807
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(1);
808
+ const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())?.[1];
809
+
810
+ // The fix changes cluster-a's template, which never applied.
811
+ failing = [];
812
+ await writeTemplate("cluster-a", { Resources: { events: { Type: "AWS::SQS::Queue", Properties: { FifoQueue: true, QueueName: "events.fifo" } } } });
813
+ ran = [];
814
+ stderrLines = [];
815
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
816
+ expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
817
+ const out = stderr();
818
+ expect(out).toContain(`plan: ${digest}`);
819
+ expect(out).not.toContain("changed since the earlier attempt");
820
+ for (const name of ["net", "cluster-b", "app-three"]) expect(out).toMatch(new RegExp(`${name} +already-applied`));
821
+ expect(out).toContain("fan-out completed: 3 applied, 0 failed, 0 blocked");
822
+ });
823
+
824
+ test("the fix keeps the approval: approval binds the derivation, progress binds the inputs", async () => {
825
+ const resume = join(dir, "attempt.json");
826
+ failing = ["cluster-a"];
827
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(3);
828
+ const pending = gateLedger.appended[0]!;
829
+ gateLedger = memoryGateLedgerPort({
830
+ pending: [pending],
831
+ resolutions: [
832
+ {
833
+ version: 1,
834
+ kind: "resolution",
835
+ op: "fan-out",
836
+ gate: "release",
837
+ resolvedBy: "operator",
838
+ timestamp: new Date(Date.parse(pending.timestamp) + 1000).toISOString(),
839
+ planDigest: pending.planDigest,
840
+ },
841
+ ],
842
+ });
843
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(1);
844
+
845
+ failing = [];
846
+ await writeTemplate("cluster-a", { Resources: { fixed: { Type: "AWS::SQS::Queue" } } });
847
+ ran = [];
848
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", resume }))).toBe(0);
849
+ expect(ran).toEqual(["cluster-a", "app-one", "app-two"]);
850
+ });
851
+
852
+ test("the input digest is per environment: progress against one does not carry into another", async () => {
853
+ const resume = join(dir, "attempt.json");
854
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
855
+ ran = [];
856
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "prod", resume }))).toBe(0);
857
+ expect(ran).toHaveLength(6);
858
+ });
859
+
860
+ test("a record without input digests carries nothing, and says so", async () => {
861
+ const resume = join(dir, "attempt.json");
862
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", dryRun: true }))).toBe(0);
863
+ const digest = /plan: (jcs1-sha256:[0-9a-f]{64})/.exec(stderr())![1]!;
864
+ await writeFile(resume, JSON.stringify({ digest, completed: ["net", "cluster-b"], failed: [], outputs: {} }));
865
+ stderrLines = [];
866
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", resume }))).toBe(0);
867
+ expect(ran).toHaveLength(6);
868
+ expect(stderr()).toContain("does not record what each component applied, so none of its progress carries");
869
+ });
870
+ });
871
+
872
+ describe("a seed's outputs, read from the deployed estate (#4104)", () => {
873
+ /** cluster-a reads net's TopicArn, and the change touches cluster-a alone. */
874
+ function wireClusterA(): void {
875
+ const wired: Component = {
876
+ name: "cluster-a",
877
+ dependsOn: ["net"],
878
+ deploy: [
879
+ {
880
+ phase: "Apply",
881
+ steps: [{ kind: "cfn-deploy", stack: "cluster-a-stack", inputs: { topic: { stackOutput: { stack: "net", name: "TopicArn" } } } }],
882
+ },
883
+ ],
884
+ };
885
+ const estate = ESTATE.map((c) => (c.name === "cluster-a" ? wired : c));
886
+ discoverComponentsMock.mockResolvedValue({
887
+ components: new Map(estate.map((c) => [c.name, { component: c, exportName: c.name, filePath: `${c.name}.component.ts` }])),
888
+ sourceFiles: [],
889
+ errors: [],
890
+ });
891
+ affectedStacksMock.mockResolvedValue({ changed: ["cluster-a-stack"], dependents: [], indeterminate: [] });
892
+ }
893
+
894
+ test("a cluster-a-only change runs cluster-a with net's deployed outputs, and says it read them", async () => {
895
+ wireClusterA();
896
+ liveOutputs = { net: { TopicArn: "arn:aws:sns:us-east-1:000000000000:net", QueueUrl: "http://q" } };
897
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test" }))).toBe(0);
898
+ expect(ran).not.toContain("net");
899
+ expect(outputsRead).toEqual(["net"]);
900
+ expect(inputs["cluster-a"]).toEqual(expect.objectContaining({ inputs: { topic: "arn:aws:sns:us-east-1:000000000000:net" } }));
901
+ expect(stderr()).toContain("net: outputs read from the deployed estate (QueueUrl, TopicArn)");
902
+ });
903
+
904
+ test("--seed-outputs wins: a seed it gives is not read", async () => {
905
+ wireClusterA();
906
+ liveOutputs = { net: { TopicArn: "arn:deployed" } };
907
+ const dir = await mkdtemp(join(tmpdir(), "fan-out-"));
908
+ try {
909
+ const seed = join(dir, "seed.json");
910
+ await writeFile(seed, JSON.stringify({ net: { TopicArn: "arn:seeded" } }));
911
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", seedOutputs: [seed] }))).toBe(0);
912
+ expect(outputsRead).toEqual([]);
913
+ expect(inputs["cluster-a"]).toEqual(expect.objectContaining({ inputs: { topic: "arn:seeded" } }));
914
+ } finally {
915
+ await rm(dir, { recursive: true, force: true });
916
+ }
917
+ });
918
+
919
+ test("a read that fails stops the fan-out before anything runs, naming the seed", async () => {
920
+ wireClusterA();
921
+ liveOutputs = { net: new Error("ExpiredToken") };
922
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test" }))).toBe(1);
923
+ expect(ran).toEqual([]);
924
+ expect(stderr()).toContain("could not read the deployed outputs of net: ExpiredToken");
925
+ });
926
+
927
+ test("--dry-run reads nothing", async () => {
928
+ wireClusterA();
929
+ liveOutputs = { net: { TopicArn: "arn:deployed" } };
930
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", dryRun: true }))).toBe(0);
931
+ expect(outputsRead).toEqual([]);
932
+ });
933
+ });
@@ -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";
@@ -57,6 +67,8 @@ import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary
57
67
  import { approveCommand, gateIsSealed } from "../../op/gate";
58
68
  import { getHeadCommit } from "../../lifecycle/git";
59
69
  import { remainingFanOut, type ChangedUnits, type FanOutProgress } from "../../components/fan-out";
70
+ import { readDeployedOutputs } from "../../components/wave-plan";
71
+ import { deployContext } from "../../components/driver";
60
72
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
61
73
  import { formatError, formatInfo, formatWarning } from "../format";
62
74
  import { FAN_OUT_GATE_OP } from "../../op/gate-name";
@@ -253,7 +265,12 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
253
265
  // real run happens inside `runFanOut`, before the gate is decided, so an
254
266
  // approval survives the resume rather than being re-asked for.
255
267
  const resumePath = args.resume ? resolve(args.resume) : undefined;
268
+ // What each component would apply now (#4081). The record keeps it for
269
+ // every component that applies, and a later attempt carries progress only
270
+ // where it is unchanged.
271
+ const inputsNow = resumePath ? componentInputDigests(derived.components, { projectRoot: projectPath, env: args.env ?? "local" }) : {};
256
272
  let priorCompleted: string[] = [];
273
+ let priorInputs: Record<string, string> = {};
257
274
  let priorOutputs: Record<string, Record<string, unknown>> = {};
258
275
  let priorWaves: WaveRecord[] = [];
259
276
  let priorCarried: Record<string, unknown> = {};
@@ -277,11 +294,28 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
277
294
  // as progress would block their subtrees again on the very run that
278
295
  // exists to retry them — the operator repeated the command precisely
279
296
  // because the thing that failed is now expected to work.
280
- priorCompleted = attempt.completed;
297
+ //
298
+ // And only the part of `completed` whose input is the same now (#4081).
299
+ // The plan digest identifies the derivation, which a different change
300
+ // of the same shape shares; the input digest says what was applied.
301
+ const split = progressThatCarries(attempt, inputsNow);
302
+ if (split.unrecorded && split.changed.length > 0) {
303
+ console.error(formatWarning({
304
+ message: `--resume: "${args.resume}" does not record what each component applied, so none of its progress carries`,
305
+ hint: "It was written before chant recorded component inputs. Everything selected will run.",
306
+ }));
307
+ } else {
308
+ for (const name of split.changed) {
309
+ console.error(formatInfo(`${name}: changed since the earlier attempt, so it runs again`));
310
+ }
311
+ }
312
+ const carried = new Set(split.carried);
313
+ priorCompleted = split.carried;
314
+ priorInputs = Object.fromEntries(split.carried.map((name) => [name, inputsNow[name]!]));
281
315
  priorOutputs = Object.fromEntries(
282
- Object.entries(attempt.outputs).filter(([name]) => attempt.completed.includes(name)),
316
+ Object.entries(attempt.outputs).filter(([name]) => carried.has(name)),
283
317
  );
284
- progress = { completed: attempt.completed };
318
+ progress = { completed: split.carried };
285
319
  priorWaves = attempt.waves ?? [];
286
320
  // What steps kept per member (#3459), such as a choudoufu root's wave
287
321
  // resume file. Each step decides whether what it kept still applies.
@@ -311,6 +345,36 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
311
345
  }
312
346
 
313
347
  const registry = await fanOutRegistry(projectPath, config);
348
+ const env = args.env ?? "local";
349
+
350
+ // A completed component's recorded outputs win over a `--seed-outputs`
351
+ // entry for the same name, as a fresh apply's outputs merge over a seed in
352
+ // the driver: the record holds what this fan-out itself applied. A seed
353
+ // neither gives is read from the deployed estate (#4104), through its
354
+ // capability's `outputs`, as pr-plan reads a dependency outside the change.
355
+ // One that cannot be read leaves its dependents' references unresolved.
356
+ const componentOutputs: Record<string, Record<string, unknown>> = { ...seededOutputs, ...priorOutputs };
357
+ const seeds = (progress ? remainingFanOut(derived.plan, derived.components, progress) : derived.plan).seeds;
358
+ let deployedSeeds: Record<string, string[]>;
359
+ try {
360
+ deployedSeeds = await readDeployedOutputs(
361
+ seeds,
362
+ new Map(derived.components.map((c) => [c.name, c])),
363
+ registry,
364
+ (name) => deployContext({ env }, name),
365
+ componentOutputs,
366
+ );
367
+ } catch (err) {
368
+ console.error(formatError({
369
+ message: `could not read the deployed outputs of ${seeds.filter((s) => !(s in componentOutputs)).join(", ")}: ${err instanceof Error ? err.message : String(err)}`,
370
+ hint: "Nothing ran. --seed-outputs <file> gives a component's outputs instead of reading them.",
371
+ }));
372
+ return 1;
373
+ }
374
+ for (const [name, keys] of Object.entries(deployedSeeds)) {
375
+ console.error(formatInfo(`${name}: outputs read from the deployed estate (${keys.length > 0 ? keys.join(", ") : "none yet"})`));
376
+ }
377
+
314
378
  // #3061: the commit this run deploys, for the workloads' vcs.ref.head.revision.
315
379
  const revision = await getHeadCommit().catch(() => undefined);
316
380
 
@@ -319,6 +383,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
319
383
  // runner, ctrl-c) then still knows that the first two applied, and the
320
384
  // repeated command picks up from there instead of redoing them.
321
385
  const completedSoFar = new Set(priorCompleted);
386
+ const inputsSoFar: Record<string, string> = { ...priorInputs };
322
387
  const failedSoFar: string[] = [];
323
388
  const outputsSoFar = { ...priorOutputs };
324
389
  let wavesSoFar = priorWaves;
@@ -330,6 +395,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
330
395
  completed: [...completedSoFar].sort(),
331
396
  failed: [...failedSoFar].sort(),
332
397
  outputs: outputsSoFar,
398
+ inputs: Object.fromEntries(Object.entries(inputsSoFar).sort(([a], [b]) => a.localeCompare(b))),
333
399
  ...(wavesSoFar.length > 0 ? { waves: wavesSoFar } : {}),
334
400
  ...(Object.keys(carriedSoFar).length > 0 ? { carried: carriedSoFar } : {}),
335
401
  };
@@ -344,12 +410,9 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
344
410
  let result;
345
411
  try {
346
412
  result = await runFanOut(derived.plan, derived.components, registry, {
347
- env: args.env ?? "local",
413
+ env,
348
414
  releaseIdentity: () => (revision ? { revision } : {}),
349
- // A completed component's recorded outputs win over a `--seed-outputs`
350
- // entry for the same name, as a fresh apply's outputs merge over a seed in
351
- // the driver: the record holds what this fan-out itself applied.
352
- componentOutputs: { ...seededOutputs, ...priorOutputs },
415
+ componentOutputs,
353
416
  ...(gate ? { gate } : {}),
354
417
  ...(waveGate ? { waveGate } : {}),
355
418
  ...(waveGate
@@ -377,6 +440,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
377
440
  onComponentSettled: (settled, outputs) => {
378
441
  if (settled.status === "ok") {
379
442
  completedSoFar.add(settled.component);
443
+ if (inputsNow[settled.component] !== undefined) inputsSoFar[settled.component] = inputsNow[settled.component]!;
380
444
  if (outputs) outputsSoFar[settled.component] = outputs;
381
445
  } else if (settled.status === "fail") {
382
446
  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
+ });