@intentius/chant 0.124.26 → 0.124.28

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 (459) hide show
  1. package/README.md +1 -1
  2. package/dist/__golden__/wave-loop/fan-out-gated.json +8 -0
  3. package/dist/__golden__/wave-loop/fan-out-no-members.json +4 -0
  4. package/dist/__golden__/wave-loop/fan-out-only.json +6 -0
  5. package/dist/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  6. package/dist/__golden__/wave-loop/op-cost-member.json +3 -0
  7. package/dist/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  8. package/dist/__golden__/wave-loop/op-held-on.json +2 -0
  9. package/dist/__golden__/wave-loop/op-hooks-held.json +2 -0
  10. package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  11. package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  12. package/dist/__golden__/wave-loop/op-hooks-share.json +3 -0
  13. package/dist/__golden__/wave-loop/op-per-run.json +7 -0
  14. package/dist/__golden__/wave-loop/op-pr-review.json +7 -0
  15. package/dist/__golden__/wave-loop/op-sealed.json +3 -0
  16. package/dist/__golden__/wave-loop/op-spend.json +3 -0
  17. package/dist/__golden__/wave-loop/op-wave-commands.json +7 -0
  18. package/dist/__golden__/wave-loop/op-wide-waiting.json +4 -0
  19. package/dist/change-set-graph.d.ts +56 -0
  20. package/dist/change-set-graph.d.ts.map +1 -0
  21. package/dist/change-set-graph.js +128 -0
  22. package/dist/change-set.d.ts +52 -3
  23. package/dist/change-set.d.ts.map +1 -1
  24. package/dist/change-set.js +35 -4
  25. package/dist/chant-version.d.ts +9 -0
  26. package/dist/chant-version.d.ts.map +1 -0
  27. package/dist/chant-version.js +16 -0
  28. package/dist/cli/commands/build.d.ts +8 -0
  29. package/dist/cli/commands/build.d.ts.map +1 -1
  30. package/dist/cli/commands/build.js +9 -8
  31. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  32. package/dist/cli/commands/check-lexicon-docs.js +30 -2
  33. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  34. package/dist/cli/commands/check-lexicon.js +13 -2
  35. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
  36. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  37. package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
  38. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  39. package/dist/cli/commands/init-lexicon.js +17 -8
  40. package/dist/cli/handlers/approve.d.ts.map +1 -1
  41. package/dist/cli/handlers/approve.js +19 -4
  42. package/dist/cli/handlers/build.d.ts.map +1 -1
  43. package/dist/cli/handlers/build.js +28 -0
  44. package/dist/cli/handlers/change-set.d.ts +10 -0
  45. package/dist/cli/handlers/change-set.d.ts.map +1 -1
  46. package/dist/cli/handlers/change-set.js +51 -1
  47. package/dist/cli/handlers/fan-out.d.ts +1 -1
  48. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  49. package/dist/cli/handlers/fan-out.js +14 -3
  50. package/dist/cli/handlers/pr.d.ts +8 -2
  51. package/dist/cli/handlers/pr.d.ts.map +1 -1
  52. package/dist/cli/handlers/pr.js +148 -24
  53. package/dist/cli/handlers/promote.d.ts.map +1 -1
  54. package/dist/cli/handlers/promote.js +256 -21
  55. package/dist/cli/handlers/release.d.ts +34 -0
  56. package/dist/cli/handlers/release.d.ts.map +1 -0
  57. package/dist/cli/handlers/release.js +203 -0
  58. package/dist/cli/handlers/run.d.ts +28 -2
  59. package/dist/cli/handlers/run.d.ts.map +1 -1
  60. package/dist/cli/handlers/run.js +170 -13
  61. package/dist/cli/main.d.ts.map +1 -1
  62. package/dist/cli/main.js +88 -13
  63. package/dist/cli/mcp/workspace-tools.js +1 -1
  64. package/dist/cli/parse-args.d.ts.map +1 -1
  65. package/dist/cli/parse-args.js +49 -3
  66. package/dist/cli/registry.d.ts +29 -3
  67. package/dist/cli/registry.d.ts.map +1 -1
  68. package/dist/cli/run-events.d.ts +29 -0
  69. package/dist/cli/run-events.d.ts.map +1 -0
  70. package/dist/cli/run-events.js +47 -0
  71. package/dist/cli/version.d.ts +3 -5
  72. package/dist/cli/version.d.ts.map +1 -1
  73. package/dist/cli/version.js +3 -13
  74. package/dist/codegen/docs-in-site.d.ts +71 -0
  75. package/dist/codegen/docs-in-site.d.ts.map +1 -0
  76. package/dist/codegen/docs-in-site.js +177 -0
  77. package/dist/codegen/docs-types.d.ts +14 -1
  78. package/dist/codegen/docs-types.d.ts.map +1 -1
  79. package/dist/codegen/docs.d.ts +18 -1
  80. package/dist/codegen/docs.d.ts.map +1 -1
  81. package/dist/codegen/docs.js +40 -7
  82. package/dist/codegen/rehype-base-url.mjs +15 -9
  83. package/dist/components/auto-release.d.ts +4 -1
  84. package/dist/components/auto-release.d.ts.map +1 -1
  85. package/dist/components/auto-release.js +2 -0
  86. package/dist/components/cli-support.d.ts +6 -0
  87. package/dist/components/cli-support.d.ts.map +1 -1
  88. package/dist/components/cli-support.js +10 -9
  89. package/dist/components/driver.d.ts +42 -9
  90. package/dist/components/driver.d.ts.map +1 -1
  91. package/dist/components/driver.js +81 -54
  92. package/dist/components/fan-out-run.d.ts +7 -0
  93. package/dist/components/fan-out-run.d.ts.map +1 -1
  94. package/dist/components/fan-out-run.js +35 -26
  95. package/dist/components/pr-run.d.ts +11 -0
  96. package/dist/components/pr-run.d.ts.map +1 -1
  97. package/dist/components/pr-run.js +20 -1
  98. package/dist/components/promote-order.d.ts +38 -0
  99. package/dist/components/promote-order.d.ts.map +1 -0
  100. package/dist/components/promote-order.js +62 -0
  101. package/dist/components/promote.d.ts +15 -2
  102. package/dist/components/promote.d.ts.map +1 -1
  103. package/dist/components/promote.js +17 -0
  104. package/dist/components/release-compile.d.ts +133 -0
  105. package/dist/components/release-compile.d.ts.map +1 -0
  106. package/dist/components/release-compile.js +376 -0
  107. package/dist/components/release-gate.d.ts +125 -0
  108. package/dist/components/release-gate.d.ts.map +1 -0
  109. package/dist/components/release-gate.js +282 -0
  110. package/dist/components/release-promote.d.ts +96 -0
  111. package/dist/components/release-promote.d.ts.map +1 -0
  112. package/dist/components/release-promote.js +204 -0
  113. package/dist/components/run-progress.d.ts +7 -0
  114. package/dist/components/run-progress.d.ts.map +1 -1
  115. package/dist/components/wave-plan.d.ts +3 -1
  116. package/dist/components/wave-plan.d.ts.map +1 -1
  117. package/dist/components/wave-plan.js +1 -1
  118. package/dist/config.d.ts +7 -0
  119. package/dist/config.d.ts.map +1 -1
  120. package/dist/config.js +1 -0
  121. package/dist/index.d.ts +1 -0
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +2 -0
  124. package/dist/lifecycle/change-set-store.d.ts +67 -0
  125. package/dist/lifecycle/change-set-store.d.ts.map +1 -0
  126. package/dist/lifecycle/change-set-store.js +96 -0
  127. package/dist/lifecycle/commit-sha.d.ts +13 -0
  128. package/dist/lifecycle/commit-sha.d.ts.map +1 -0
  129. package/dist/lifecycle/commit-sha.js +21 -0
  130. package/dist/lifecycle/gate-change-set.d.ts +69 -0
  131. package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
  132. package/dist/lifecycle/gate-change-set.js +106 -0
  133. package/dist/lifecycle/gate-ledger.d.ts +49 -1
  134. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  135. package/dist/lifecycle/gate-ledger.js +18 -0
  136. package/dist/lifecycle/release-cut.d.ts +107 -0
  137. package/dist/lifecycle/release-cut.d.ts.map +1 -0
  138. package/dist/lifecycle/release-cut.js +273 -0
  139. package/dist/lifecycle/release-ledger.d.ts +54 -0
  140. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  141. package/dist/lifecycle/release-ledger.js +53 -20
  142. package/dist/lifecycle/release-store.d.ts +190 -0
  143. package/dist/lifecycle/release-store.d.ts.map +1 -0
  144. package/dist/lifecycle/release-store.js +318 -0
  145. package/dist/lifecycle/run-ledger.d.ts +4 -0
  146. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  147. package/dist/lifecycle/run-ledger.js +2 -0
  148. package/dist/op/activities/change-coverage.d.ts.map +1 -1
  149. package/dist/op/activities/change-coverage.js +4 -2
  150. package/dist/op/activities/source-release.d.ts.map +1 -1
  151. package/dist/op/activities/source-release.js +7 -3
  152. package/dist/op/activities/source-rollback.d.ts.map +1 -1
  153. package/dist/op/activities/source-rollback.js +7 -3
  154. package/dist/op/activity-registry.d.ts +6 -3
  155. package/dist/op/activity-registry.d.ts.map +1 -1
  156. package/dist/op/activity-registry.js +7 -5
  157. package/dist/op/discover.d.ts.map +1 -1
  158. package/dist/op/discover.js +14 -7
  159. package/dist/op/gate-name.d.ts +11 -2
  160. package/dist/op/gate-name.d.ts.map +1 -1
  161. package/dist/op/gate-name.js +12 -3
  162. package/dist/op/gate-run-locator.d.ts +22 -0
  163. package/dist/op/gate-run-locator.d.ts.map +1 -1
  164. package/dist/op/gate-run-locator.js +31 -0
  165. package/dist/op/gate.d.ts +27 -3
  166. package/dist/op/gate.d.ts.map +1 -1
  167. package/dist/op/gate.js +57 -3
  168. package/dist/op/local-executor.d.ts +36 -1
  169. package/dist/op/local-executor.d.ts.map +1 -1
  170. package/dist/op/local-executor.js +158 -52
  171. package/dist/op/op-waves-run.d.ts.map +1 -1
  172. package/dist/op/op-waves-run.js +4 -0
  173. package/dist/op/run-live.d.ts +2 -0
  174. package/dist/op/run-live.d.ts.map +1 -1
  175. package/dist/op/run-live.js +1 -1
  176. package/dist/op/runtime.d.ts +46 -0
  177. package/dist/op/runtime.d.ts.map +1 -1
  178. package/dist/op/runtimes/local.d.ts.map +1 -1
  179. package/dist/op/runtimes/local.js +12 -2
  180. package/dist/point-decision.d.ts +2 -0
  181. package/dist/point-decision.d.ts.map +1 -1
  182. package/dist/point-decision.js +9 -2
  183. package/dist/run-event-store.d.ts +77 -0
  184. package/dist/run-event-store.d.ts.map +1 -0
  185. package/dist/run-event-store.js +204 -0
  186. package/dist/run-event-stream.d.ts +132 -0
  187. package/dist/run-event-stream.d.ts.map +1 -0
  188. package/dist/run-event-stream.js +238 -0
  189. package/dist/run-event.d.ts +267 -0
  190. package/dist/run-event.d.ts.map +1 -0
  191. package/dist/run-event.js +207 -0
  192. package/dist/wave-runtime.mjs +225 -242
  193. package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
  194. package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
  195. package/dist/workspace/__fixtures__/run-event-check.js +59 -0
  196. package/dist/workspace/change-set-show.d.ts +56 -0
  197. package/dist/workspace/change-set-show.d.ts.map +1 -0
  198. package/dist/workspace/change-set-show.js +54 -0
  199. package/dist/workspace/change-set-show.schema.json +102 -0
  200. package/dist/workspace/change-set.schema.json +28 -1
  201. package/dist/workspace/changes-cli.d.ts.map +1 -1
  202. package/dist/workspace/changes-cli.js +10 -1
  203. package/dist/workspace/changes.d.ts +37 -0
  204. package/dist/workspace/changes.d.ts.map +1 -1
  205. package/dist/workspace/changes.js +49 -2
  206. package/dist/workspace/changes.schema.json +135 -0
  207. package/dist/workspace/ci-green-cli.d.ts +19 -4
  208. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  209. package/dist/workspace/ci-green-cli.js +75 -12
  210. package/dist/workspace/ci-green-forge.d.ts +20 -3
  211. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  212. package/dist/workspace/ci-green-forge.js +6 -4
  213. package/dist/workspace/ci-green-workflow.d.ts +11 -0
  214. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  215. package/dist/workspace/ci-green-workflow.js +6 -0
  216. package/dist/workspace/ci-green.d.ts +273 -13
  217. package/dist/workspace/ci-green.d.ts.map +1 -1
  218. package/dist/workspace/ci-green.js +449 -38
  219. package/dist/workspace/ci-last-green.schema.json +32 -2
  220. package/dist/workspace/ci-list.schema.json +525 -0
  221. package/dist/workspace/commit-links.d.ts +85 -0
  222. package/dist/workspace/commit-links.d.ts.map +1 -0
  223. package/dist/workspace/commit-links.js +163 -0
  224. package/dist/workspace/declaration.d.ts +19 -0
  225. package/dist/workspace/declaration.d.ts.map +1 -1
  226. package/dist/workspace/declaration.js +1 -1
  227. package/dist/workspace/declaration.schema.json +12 -0
  228. package/dist/workspace/identity-gate-rule.d.ts +37 -0
  229. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  230. package/dist/workspace/identity-gate-rule.js +72 -7
  231. package/dist/workspace/identity.d.ts +12 -0
  232. package/dist/workspace/identity.d.ts.map +1 -1
  233. package/dist/workspace/identity.js +13 -1
  234. package/dist/workspace/intent.d.ts +6 -1
  235. package/dist/workspace/intent.d.ts.map +1 -1
  236. package/dist/workspace/intent.js +51 -1
  237. package/dist/workspace/ls.d.ts +4 -0
  238. package/dist/workspace/ls.d.ts.map +1 -1
  239. package/dist/workspace/ls.js +2 -0
  240. package/dist/workspace/ls.schema.json +3 -1
  241. package/dist/workspace/member-pipeline.d.ts +6 -0
  242. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  243. package/dist/workspace/member-pipeline.js +6 -0
  244. package/dist/workspace/promotion-order.d.ts +41 -0
  245. package/dist/workspace/promotion-order.d.ts.map +1 -0
  246. package/dist/workspace/promotion-order.js +95 -0
  247. package/dist/workspace/reason-codes.d.ts +2 -0
  248. package/dist/workspace/reason-codes.d.ts.map +1 -1
  249. package/dist/workspace/reason-codes.js +4 -1
  250. package/dist/workspace/releases.d.ts +72 -0
  251. package/dist/workspace/releases.d.ts.map +1 -0
  252. package/dist/workspace/releases.js +94 -0
  253. package/dist/workspace/releases.schema.json +391 -0
  254. package/dist/workspace/run-event.schema.json +288 -0
  255. package/dist/workspace/status-gates.d.ts +26 -1
  256. package/dist/workspace/status-gates.d.ts.map +1 -1
  257. package/dist/workspace/status-gates.js +31 -2
  258. package/dist/workspace/status-stewards.d.ts +4 -0
  259. package/dist/workspace/status-stewards.d.ts.map +1 -1
  260. package/dist/workspace/status-stewards.js +2 -0
  261. package/dist/workspace/status.d.ts +39 -0
  262. package/dist/workspace/status.d.ts.map +1 -1
  263. package/dist/workspace/status.js +48 -1
  264. package/dist/workspace/status.schema.json +70 -1
  265. package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
  266. package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
  267. package/dist/workspace/trust/ci-tag-statement.js +123 -0
  268. package/dist/workspace/trust/op-run-statement.d.ts +91 -0
  269. package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
  270. package/dist/workspace/trust/op-run-statement.js +143 -0
  271. package/dist/workspace/trust/rotation.d.ts +18 -2
  272. package/dist/workspace/trust/rotation.d.ts.map +1 -1
  273. package/dist/workspace/trust/rotation.js +65 -16
  274. package/dist/workspace/trust/seal.d.ts +11 -0
  275. package/dist/workspace/trust/seal.d.ts.map +1 -1
  276. package/dist/workspace/trust/seal.js +9 -0
  277. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  278. package/dist/workspace/trust/ssh-commit.js +10 -10
  279. package/package.json +1 -1
  280. package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
  281. package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
  282. package/src/__golden__/wave-loop/fan-out-only.json +6 -0
  283. package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  284. package/src/__golden__/wave-loop/op-cost-member.json +3 -0
  285. package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  286. package/src/__golden__/wave-loop/op-held-on.json +2 -0
  287. package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
  288. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  289. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  290. package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
  291. package/src/__golden__/wave-loop/op-per-run.json +7 -0
  292. package/src/__golden__/wave-loop/op-pr-review.json +7 -0
  293. package/src/__golden__/wave-loop/op-sealed.json +3 -0
  294. package/src/__golden__/wave-loop/op-spend.json +3 -0
  295. package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
  296. package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
  297. package/src/change-set-graph.test.ts +72 -0
  298. package/src/change-set-graph.ts +133 -0
  299. package/src/change-set.ts +76 -6
  300. package/src/chant-version.ts +16 -0
  301. package/src/cli/commands/build.ts +19 -8
  302. package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
  303. package/src/cli/commands/check-lexicon-docs.ts +39 -2
  304. package/src/cli/commands/check-lexicon.ts +14 -2
  305. package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
  306. package/src/cli/commands/init-lexicon.test.ts +25 -0
  307. package/src/cli/commands/init-lexicon.ts +18 -8
  308. package/src/cli/compiled-cli.e2e.test.ts +31 -6
  309. package/src/cli/handlers/approve-release.test.ts +80 -0
  310. package/src/cli/handlers/approve.ts +18 -2
  311. package/src/cli/handlers/build.test.ts +52 -0
  312. package/src/cli/handlers/build.ts +29 -0
  313. package/src/cli/handlers/change-set.ts +49 -1
  314. package/src/cli/handlers/fan-out.test.ts +48 -0
  315. package/src/cli/handlers/fan-out.ts +15 -3
  316. package/src/cli/handlers/operator.test.ts +37 -0
  317. package/src/cli/handlers/pr.test.ts +194 -0
  318. package/src/cli/handlers/pr.ts +168 -34
  319. package/src/cli/handlers/promote.test.ts +389 -5
  320. package/src/cli/handlers/promote.ts +279 -20
  321. package/src/cli/handlers/release-implicit.e2e.test.ts +153 -0
  322. package/src/cli/handlers/release.e2e.test.ts +85 -0
  323. package/src/cli/handlers/release.ts +206 -0
  324. package/src/cli/handlers/run-auto-release.test.ts +71 -0
  325. package/src/cli/handlers/run-events-cmd.test.ts +92 -0
  326. package/src/cli/handlers/run.test.ts +30 -0
  327. package/src/cli/handlers/run.ts +176 -15
  328. package/src/cli/main.test.ts +7 -0
  329. package/src/cli/main.ts +88 -13
  330. package/src/cli/mcp/never-over-mcp.test.ts +2 -0
  331. package/src/cli/mcp/workspace-tools.ts +1 -1
  332. package/src/cli/parse-args.ts +37 -3
  333. package/src/cli/registry.ts +29 -3
  334. package/src/cli/run-events.ts +54 -0
  335. package/src/cli/version.ts +3 -13
  336. package/src/codegen/docs-in-site.test.ts +166 -0
  337. package/src/codegen/docs-in-site.ts +182 -0
  338. package/src/codegen/docs-site/AgentPrompt.astro +26 -1
  339. package/src/codegen/docs-types.ts +14 -1
  340. package/src/codegen/docs.ts +60 -7
  341. package/src/codegen/rehype-base-url.d.mts +3 -1
  342. package/src/codegen/rehype-base-url.mjs +15 -9
  343. package/src/codegen/rehype-base-url.test.ts +34 -0
  344. package/src/components/auto-release.test.ts +21 -0
  345. package/src/components/auto-release.ts +6 -1
  346. package/src/components/cli-support.test.ts +82 -0
  347. package/src/components/cli-support.ts +16 -9
  348. package/src/components/driver.ts +129 -56
  349. package/src/components/fan-out-run.ts +56 -35
  350. package/src/components/pr-run.ts +30 -1
  351. package/src/components/promote-order.test.ts +90 -0
  352. package/src/components/promote-order.ts +75 -0
  353. package/src/components/promote.test.ts +10 -1
  354. package/src/components/promote.ts +26 -1
  355. package/src/components/release-compile.test.ts +147 -0
  356. package/src/components/release-compile.ts +445 -0
  357. package/src/components/release-gate.test.ts +215 -0
  358. package/src/components/release-gate.ts +341 -0
  359. package/src/components/release-promote.test.ts +159 -0
  360. package/src/components/release-promote.ts +232 -0
  361. package/src/components/run-progress.ts +7 -0
  362. package/src/components/wave-plan.ts +1 -1
  363. package/src/config.ts +7 -1
  364. package/src/index.ts +2 -0
  365. package/src/lifecycle/change-set-store.test.ts +217 -0
  366. package/src/lifecycle/change-set-store.ts +112 -0
  367. package/src/lifecycle/commit-sha.ts +23 -0
  368. package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
  369. package/src/lifecycle/gate-change-set.ts +133 -0
  370. package/src/lifecycle/gate-ledger.test.ts +33 -0
  371. package/src/lifecycle/gate-ledger.ts +66 -1
  372. package/src/lifecycle/release-cut-signed.test.ts +180 -0
  373. package/src/lifecycle/release-cut.test.ts +411 -0
  374. package/src/lifecycle/release-cut.ts +367 -0
  375. package/src/lifecycle/release-ledger.test.ts +14 -0
  376. package/src/lifecycle/release-ledger.ts +85 -17
  377. package/src/lifecycle/release-store.ts +423 -0
  378. package/src/lifecycle/run-ledger.test.ts +10 -0
  379. package/src/lifecycle/run-ledger.ts +6 -0
  380. package/src/op/activities/change-coverage.ts +4 -1
  381. package/src/op/activities/source-release.ts +8 -3
  382. package/src/op/activities/source-rollback.ts +8 -4
  383. package/src/op/activity-registry.ts +7 -5
  384. package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
  385. package/src/op/discover-root.test.ts +39 -0
  386. package/src/op/discover.ts +15 -8
  387. package/src/op/gate-name.ts +13 -3
  388. package/src/op/gate-release.test.ts +126 -0
  389. package/src/op/gate-run-locator.test.ts +110 -0
  390. package/src/op/gate-run-locator.ts +35 -0
  391. package/src/op/gate.ts +88 -6
  392. package/src/op/local-executor.ts +207 -52
  393. package/src/op/op-waves-run.ts +4 -0
  394. package/src/op/run-events.test.ts +166 -0
  395. package/src/op/run-live.ts +1 -1
  396. package/src/op/runtime.ts +39 -0
  397. package/src/op/runtimes/local.test.ts +16 -0
  398. package/src/op/runtimes/local.ts +13 -2
  399. package/src/point-decision.ts +12 -2
  400. package/src/run-event-store.test.ts +144 -0
  401. package/src/run-event-store.ts +213 -0
  402. package/src/run-event-stream.test.ts +175 -0
  403. package/src/run-event-stream.ts +306 -0
  404. package/src/run-event.test.ts +277 -0
  405. package/src/run-event.ts +451 -0
  406. package/src/wave-loop-golden.test.ts +9 -0
  407. package/src/workspace/__fixtures__/run-event-check.ts +58 -0
  408. package/src/workspace/ancestry-index.test.ts +93 -0
  409. package/src/workspace/change-set-show.schema.json +102 -0
  410. package/src/workspace/change-set-show.ts +72 -0
  411. package/src/workspace/change-set.schema.json +28 -1
  412. package/src/workspace/changes-cli.ts +10 -1
  413. package/src/workspace/changes.schema.json +135 -0
  414. package/src/workspace/changes.test.ts +1 -1
  415. package/src/workspace/changes.ts +84 -2
  416. package/src/workspace/ci-green-cli.ts +75 -13
  417. package/src/workspace/ci-green-forge.ts +24 -4
  418. package/src/workspace/ci-green-runs.test.ts +242 -0
  419. package/src/workspace/ci-green-signed.test.ts +302 -0
  420. package/src/workspace/ci-green-workflow.test.ts +16 -0
  421. package/src/workspace/ci-green-workflow.ts +12 -0
  422. package/src/workspace/ci-green.test.ts +131 -2
  423. package/src/workspace/ci-green.ts +624 -42
  424. package/src/workspace/ci-last-green.schema.json +32 -2
  425. package/src/workspace/ci-list.schema.json +525 -0
  426. package/src/workspace/commit-links.test.ts +279 -0
  427. package/src/workspace/commit-links.ts +183 -0
  428. package/src/workspace/declaration.schema.json +12 -0
  429. package/src/workspace/declaration.ts +22 -2
  430. package/src/workspace/identity-gate-rule.ts +91 -7
  431. package/src/workspace/identity.ts +14 -1
  432. package/src/workspace/intent.ts +48 -1
  433. package/src/workspace/ls.schema.json +3 -1
  434. package/src/workspace/ls.ts +6 -0
  435. package/src/workspace/member-pipeline.test.ts +17 -0
  436. package/src/workspace/member-pipeline.ts +9 -0
  437. package/src/workspace/promotion-order.ts +97 -0
  438. package/src/workspace/read-contract.e2e.test.ts +4 -1
  439. package/src/workspace/reason-codes.test.ts +2 -1
  440. package/src/workspace/reason-codes.ts +4 -1
  441. package/src/workspace/releases.schema.json +391 -0
  442. package/src/workspace/releases.test.ts +108 -0
  443. package/src/workspace/releases.ts +122 -0
  444. package/src/workspace/run-event.schema.json +288 -0
  445. package/src/workspace/status-contract.test.ts +52 -1
  446. package/src/workspace/status-gates.ts +56 -1
  447. package/src/workspace/status-stewards.ts +6 -0
  448. package/src/workspace/status.schema.json +70 -1
  449. package/src/workspace/status.ts +80 -1
  450. package/src/workspace/trust/ci-tag-statement.ts +151 -0
  451. package/src/workspace/trust/op-run-statement.ts +148 -0
  452. package/src/workspace/trust/rotation.ts +61 -14
  453. package/src/workspace/trust/seal.ts +10 -0
  454. package/src/workspace/trust/ssh-commit.ts +10 -9
  455. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
  456. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
  457. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
  458. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
  459. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Stored change sets mapped onto a real graph (#4022).
3
+ *
4
+ * The estate is the aws source `__fixtures__/change-set/lifecycle-v2.ts.txt`
5
+ * as a workspace member, and the change set is
6
+ * `__fixtures__/change-set/lifecycle-plan.floci.json`, chant's real plan of
7
+ * that source against floci (see `../change-set.test.ts`). The graph is the
8
+ * member's own `chant graph --format ir`, run by `storeGateChangeSet` as a
9
+ * pull-request apply runs it, and the ids are checked against `chant
10
+ * workspace graph`, which is what behold reads. The stored document is then
11
+ * read back through `chant change-set show --json`, and the gate that binds
12
+ * it through `workspace status`'s gate reader.
13
+ */
14
+
15
+ import { spawnSync } from "node:child_process";
16
+ import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
20
+ import { composeChangeSet, lifecyclePlanPart } from "../change-set";
21
+ import type { ChangeSet as LifecycleChangeSet } from "./change-set";
22
+ import { storeGateChangeSet } from "./gate-change-set";
23
+ import { appendPendingGate } from "./gate-ledger";
24
+ import { clearMemberLedgerCache } from "./member-ledger";
25
+ import { readMemberGates } from "../workspace/status-gates";
26
+
27
+ const REPO = realpathSync(join(import.meta.dirname, "..", "..", "..", ".."));
28
+ const LOADER = join(REPO, "node_modules", "tsx", "dist", "loader.mjs");
29
+ const MAIN = join(REPO, "packages", "core", "src", "cli", "main.ts");
30
+ const FIXTURES = join(import.meta.dirname, "..", "__fixtures__", "change-set");
31
+ const TIMEOUT = 240_000;
32
+
33
+ function chant(cwd: string, ...args: string[]): { code: number | null; stdout: string; stderr: string } {
34
+ const r = spawnSync(process.execPath, ["--import", `file://${LOADER}`, MAIN, ...args], { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
35
+ return { code: r.status, stdout: r.stdout, stderr: r.stderr };
36
+ }
37
+
38
+ describe("a change set stored by a gate, on a real estate's graph", () => {
39
+ let root: string;
40
+ let member: string;
41
+ const git = (...a: string[]) => {
42
+ const r = spawnSync("git", a, { cwd: root, encoding: "utf8" });
43
+ if (r.status !== 0) throw new Error(`git ${a.join(" ")}: ${r.stderr}`);
44
+ return r.stdout.trim();
45
+ };
46
+ const plan = JSON.parse(readFileSync(join(FIXTURES, "lifecycle-plan.floci.json"), "utf8")) as LifecycleChangeSet;
47
+ const doc = composeChangeSet([lifecyclePlanPart({ member: "queues", plan })]);
48
+
49
+ beforeAll(() => {
50
+ root = realpathSync(mkdtempSync(join(tmpdir(), "chant-change-set-graph-")));
51
+ member = join(root, "delivery");
52
+ mkdirSync(member);
53
+ symlinkSync(join(REPO, "node_modules"), join(root, "node_modules"));
54
+ writeFileSync(join(root, "chant.workspace.json"), JSON.stringify({ name: "acme", schema: 1, members: [{ name: "delivery", dir: "delivery", kind: "chant" }] }));
55
+ writeFileSync(join(member, "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
56
+ writeFileSync(join(member, "queues.ts"), readFileSync(join(FIXTURES, "lifecycle-v2.ts.txt"), "utf8"));
57
+ writeFileSync(join(root, ".gitignore"), "node_modules\n");
58
+ git("init", "-q", "-b", "main");
59
+ git("config", "user.email", "ci@example.test");
60
+ git("config", "user.name", "ci");
61
+ git("add", "-A");
62
+ git("commit", "-qm", "queues");
63
+ clearMemberLedgerCache();
64
+ });
65
+
66
+ afterAll(() => {
67
+ if (root) rmSync(root, { recursive: true, force: true });
68
+ });
69
+
70
+ test("each entry carries the node chant workspace graph gives its address", async () => {
71
+ const head = git("rev-parse", "HEAD");
72
+ const stored = await storeGateChangeSet({ doc, cwd: member, at: head });
73
+ expect(stored.warning).toBeUndefined();
74
+ expect(stored).toMatchObject({ digest: doc.digest, written: true, graph: { member: "delivery", at: head } });
75
+
76
+ const graph = chant(root, "workspace", "graph");
77
+ expect(graph.code, graph.stderr).toBe(0);
78
+ const nodeIds = new Set((JSON.parse(graph.stdout) as { nodes: Array<{ id: string }> }).nodes.map((n) => n.id));
79
+
80
+ const shown = chant(member, "change-set", "show", doc.digest, "--json");
81
+ expect(shown.code, shown.stderr).toBe(0);
82
+ const out = JSON.parse(shown.stdout);
83
+ expect(out).toMatchObject({ contract: 1, member: "delivery", digest: doc.digest, reason: null });
84
+ expect(out.changeSet.digest).toBe(doc.digest);
85
+ expect(out.changeSet.graph).toEqual({ member: "delivery", at: head });
86
+ const nodes = Object.fromEntries((out.changeSet.entries as Array<{ address: string; node: string | null }>).map((e) => [e.address, e.node]));
87
+ // jobs, events and dlq are declared; alerts is a live topic v2 no longer declares, so it has no node.
88
+ expect(nodes).toEqual({ alerts: null, dlq: "delivery/dlq", events: "delivery/events", jobs: "delivery/jobs" });
89
+ for (const node of Object.values(nodes)) if (node !== null) expect(nodeIds.has(node), node).toBe(true);
90
+
91
+ // From the workspace root, by --member.
92
+ const fromRoot = chant(root, "change-set", "show", doc.digest, "--member", "delivery", "--json");
93
+ expect(JSON.parse(fromRoot.stdout)).toEqual(out);
94
+
95
+ // Without --json: the grouped summary, then each change on its node.
96
+ const text = chant(member, "change-set", "show", doc.digest);
97
+ expect(text.code, text.stderr).toBe(0);
98
+ expect(text.stdout).toContain(`Nodes from the graph of delivery at ${head.slice(0, 12)}:`);
99
+ expect(text.stdout).toMatch(/create\s+queues {2}dlq {2}delivery\/dlq/);
100
+ }, TIMEOUT);
101
+
102
+ test("the gate that binds it links to it in workspace status", async () => {
103
+ const now = new Date().toISOString();
104
+ const later = new Date(Date.now() + 3_600_000).toISOString();
105
+ await appendPendingGate({ op: "pr-7-delivery", gate: "pr-apply", planDigest: doc.digest, timestamp: now, expiresAt: later }, { cwd: member });
106
+ const { gates } = await readMemberGates("_members/delivery/_gates", "members", git("rev-parse", "chant/lifecycle"), [], root, now);
107
+ expect(gates).toEqual([expect.objectContaining({ component: "pr-7-delivery", state: "pending", planDigest: doc.digest, changeSet: doc.digest })]);
108
+
109
+ const status = chant(root, "workspace", "status", "dev", "--json");
110
+ expect(status.stdout, status.stderr).not.toBe("");
111
+ const gate = (JSON.parse(status.stdout).members as Array<{ name: string; gates?: Array<{ component: string; changeSet?: string }> }>)
112
+ .find((m) => m.name === "delivery")
113
+ ?.gates?.find((g) => g.component === "pr-7-delivery");
114
+ expect(gate?.changeSet, status.stderr).toBe(doc.digest);
115
+ }, TIMEOUT);
116
+
117
+ test("a digest nothing is stored under shows no change set and exits 1", () => {
118
+ const missing = chant(member, "change-set", "show", `jcs1-sha256:${"0".repeat(64)}`, "--json");
119
+ expect(missing.code).toBe(1);
120
+ expect(JSON.parse(missing.stdout)).toMatchObject({ member: "delivery", changeSet: null, reason: null });
121
+ }, TIMEOUT);
122
+ });
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Store the change set a gate binds (#4022), mapped onto the member's graph.
3
+ *
4
+ * {@link storeGateChangeSet} is what a gate that binds a change-set document's
5
+ * digest calls before it decides the gate: `components pr-apply`, and
6
+ * `components promote --release` before it decides a release gate or writes
7
+ * an `empty` resolution (#4045). It reads the member's
8
+ * graph the way `chant workspace graph` reads it, from the member's own
9
+ * `chant graph --format ir`, maps each entry onto a node
10
+ * (`../change-set-graph.ts`), and stores the document under its digest
11
+ * (`./change-set-store.ts`).
12
+ *
13
+ * The graph is read only when the digest is not stored yet. A graph that
14
+ * can't be read never stops the gate: the document is stored with `graph:
15
+ * null` and every `node` null, and the caller gets a warning to print.
16
+ */
17
+
18
+ import { spawn } from "node:child_process";
19
+ import { mapChangeSetToGraph, type ChangeSetGraphNodes } from "../change-set-graph";
20
+ import type { ChangeSetDocument, ChangeSetGraph } from "../change-set";
21
+ import { persistChangeSet, readStoredChangeSet } from "./change-set-store";
22
+ import { resolveMemberLedger } from "./member-ledger";
23
+
24
+ /** Reads the node ids of the graph of the project at `projectDir`. */
25
+ export type GraphIdReader = (projectDir: string) => Promise<{ ids: string[] } | { error: string }>;
26
+
27
+ const GRAPH_TIMEOUT_MS = 300_000;
28
+
29
+ /**
30
+ * The node ids of `chant graph --format ir` in `projectDir`, run under the
31
+ * chant that directory resolves (its own `node_modules/.bin/chant`, else this
32
+ * one), as `chant workspace graph` runs each member. `args` are passed after
33
+ * `graph --format ir`, such as the run's `--param` flags.
34
+ */
35
+ export async function readGraphIds(projectDir: string, args: readonly string[] = []): Promise<{ ids: string[] } | { error: string }> {
36
+ const { readerToolchain, resolveToolchain } = await import("../workspace/member-commands");
37
+ const { readMemberIr } = await import("../workspace/compose-graph");
38
+ const { findWorkspaceRoot } = await import("../project-root");
39
+ const root = findWorkspaceRoot(projectDir)?.dir ?? projectDir;
40
+ const toolchain = resolveToolchain(projectDir, root, readerToolchain());
41
+ const [command, ...pre] = toolchain.command;
42
+ const out = await new Promise<{ code: number | null; stdout: string; stderr: string }>((resolveRun) => {
43
+ const child = spawn(command, [...pre, "graph", "--format", "ir", ...args], { cwd: projectDir, stdio: ["ignore", "pipe", "pipe"] });
44
+ let stdout = "";
45
+ let stderr = "";
46
+ const timer = setTimeout(() => child.kill("SIGKILL"), GRAPH_TIMEOUT_MS);
47
+ child.stdout.on("data", (d) => (stdout += d));
48
+ child.stderr.on("data", (d) => (stderr += d));
49
+ child.on("error", (err) => {
50
+ clearTimeout(timer);
51
+ resolveRun({ code: null, stdout, stderr: err.message });
52
+ });
53
+ child.on("close", (code) => {
54
+ clearTimeout(timer);
55
+ resolveRun({ code, stdout, stderr });
56
+ });
57
+ });
58
+ if (out.code !== 0) {
59
+ const line = out.stderr.split("\n").map((l) => l.trim()).find(Boolean);
60
+ return { error: `chant graph --format ir exited ${out.code ?? "on a signal"}${line ? `: ${line}` : ""}` };
61
+ }
62
+ const read = readMemberIr(out.stdout);
63
+ if ("reason" in read) return { error: read.reason.message };
64
+ return { ids: read.ir.nodes.map((n) => n.id) };
65
+ }
66
+
67
+ let graphReaderOverride: GraphIdReader | undefined;
68
+
69
+ /**
70
+ * Replace how a gate reads the graph wherever the default is taken, such as
71
+ * in `components pr-apply`; `undefined` restores {@link readGraphIds}. For
72
+ * tests that drive a gate and read node ids in process, so they don't start a
73
+ * `chant graph` child process (`gate-change-set.e2e.test.ts` covers that one).
74
+ */
75
+ export function setGraphIdReader(reader: GraphIdReader | undefined): void {
76
+ graphReaderOverride = reader;
77
+ }
78
+
79
+ /** The graph reader a gate uses: the one {@link setGraphIdReader} set, else {@link readGraphIds} with `args`. */
80
+ export function gateGraphReader(args: readonly string[] = []): GraphIdReader {
81
+ return graphReaderOverride ?? ((dir) => readGraphIds(dir, args));
82
+ }
83
+
84
+ export interface StoreGateChangeSetInput {
85
+ /** The document whose `digest` the gate binds. */
86
+ doc: ChangeSetDocument;
87
+ /** The member's directory: where the graph is read and whose ledger it is stored in. */
88
+ cwd: string;
89
+ /** The commit the document was planned at, recorded as the graph's `at`. */
90
+ at?: string | null;
91
+ /** How to read the graph. Defaults to {@link gateGraphReader}. */
92
+ readGraph?: GraphIdReader;
93
+ }
94
+
95
+ export interface StoreGateChangeSetResult {
96
+ digest: string;
97
+ /** True when this call wrote the document; false when it was stored already. */
98
+ written: boolean;
99
+ /** The graph the stored document's nodes come from, null when none could be read. Absent when it was stored already. */
100
+ graph?: ChangeSetGraph | null;
101
+ /** Why no graph was read, when none was. */
102
+ warning?: string;
103
+ }
104
+
105
+ /**
106
+ * Store `doc` under its digest in the member's ledger at `cwd`, mapped onto
107
+ * the member's graph, unless it is stored already. Does not push; the gate's
108
+ * pending fact or resolution pushes the branch after it.
109
+ */
110
+ export async function storeGateChangeSet(input: StoreGateChangeSetInput): Promise<StoreGateChangeSetResult> {
111
+ const { doc, cwd } = input;
112
+ const loc = { cwd };
113
+ if ((await readStoredChangeSet(doc.digest, loc)).status !== "missing") return { digest: doc.digest, written: false };
114
+ const ledger = await resolveMemberLedger(cwd);
115
+ const member = ledger.members.length > 0 ? ledger.members.join("/") : null;
116
+ let nodes: ChangeSetGraphNodes | null = null;
117
+ let warning: string | undefined;
118
+ try {
119
+ const read = await (input.readGraph ?? gateGraphReader())(cwd);
120
+ if ("error" in read) warning = read.error;
121
+ else nodes = { member, at: input.at ?? null, ids: read.ids };
122
+ } catch (err) {
123
+ warning = err instanceof Error ? err.message : String(err);
124
+ }
125
+ const mapped = mapChangeSetToGraph(doc, nodes);
126
+ const { written } = await persistChangeSet(mapped, loc);
127
+ return {
128
+ digest: doc.digest,
129
+ written,
130
+ graph: mapped.graph ?? null,
131
+ ...(warning ? { warning: `the change set is stored without graph nodes: ${warning}` } : {}),
132
+ };
133
+ }
@@ -503,3 +503,36 @@ describe("pending fact wave and heldBy (#3886, #3887)", () => {
503
503
  expect(pending[0]!.heldBy).toBeUndefined();
504
504
  });
505
505
  });
506
+
507
+ describe("commit and run on gate records (#4020)", () => {
508
+ const base = { version: 1, op: "tg", gate: "g", timestamp: "2026-10-01T00:00:00Z" };
509
+ const parse = (...lines: Record<string, unknown>[]) => parseGateLedger(lines.map((l) => JSON.stringify(l)).join("\n"));
510
+ test("a pending fact and a resolution read back with their commit and run", () => {
511
+ const { pending, resolutions, malformed } = parse(
512
+ { ...base, kind: "pending", expiresAt: "2026-10-02T00:00:00Z", gitSha: "a".repeat(40), runId: "9001" },
513
+ { ...base, resolvedBy: "alice", gitSha: "a".repeat(40), runId: "9001" },
514
+ );
515
+ expect(malformed).toBe(0);
516
+ expect(pending[0]).toMatchObject({ gitSha: "a".repeat(40), runId: "9001" });
517
+ expect(resolutions[0]).toMatchObject({ gitSha: "a".repeat(40), runId: "9001" });
518
+ });
519
+ test("lines written before the fields existed still read, with neither", () => {
520
+ const { pending, resolutions, malformed } = parse(
521
+ { ...base, kind: "pending", expiresAt: "2026-10-02T00:00:00Z" },
522
+ { ...base, resolvedBy: "alice" },
523
+ );
524
+ expect(malformed).toBe(0);
525
+ expect(pending[0]).not.toHaveProperty("gitSha");
526
+ expect(resolutions[0]).not.toHaveProperty("runId");
527
+ });
528
+ test("a commit or run of the wrong type is dropped and the line is kept", () => {
529
+ const { pending, resolutions, malformed } = parse(
530
+ { ...base, kind: "pending", expiresAt: "2026-10-02T00:00:00Z", gitSha: 7, runId: { id: 1 } },
531
+ { ...base, resolvedBy: "alice", gitSha: ["x"] },
532
+ );
533
+ expect(malformed).toBe(0);
534
+ expect(pending[0]!.gitSha).toBeUndefined();
535
+ expect(pending[0]!.runId).toBeUndefined();
536
+ expect(resolutions[0]!.gitSha).toBeUndefined();
537
+ });
538
+ });
@@ -138,6 +138,16 @@ export interface GateResolutionRecord {
138
138
  * such a record proves is that somebody approved *something*.
139
139
  */
140
140
  planDigest?: string;
141
+ /**
142
+ * The commit the gate was waiting on (#4020), copied off the pending fact the
143
+ * approval answers, or the run's own commit for a resolution a run writes
144
+ * itself (a decision point's answer, a review, a carried approval). It is not
145
+ * the approver's checkout, and a seal does not cover it. Absent on a
146
+ * resolution written before 0.124.28 and when no commit was known.
147
+ */
148
+ gitSha?: string;
149
+ /** The run the gate was waiting on (#4020), copied the same way as {@link GateResolutionRecord.gitSha}. */
150
+ runId?: string;
141
151
  /**
142
152
  * The checkpoint this approval lets a run resume from (#3947), copied off
143
153
  * the pending fact it answers. Set only on an approval of a gate that
@@ -151,6 +161,26 @@ export interface GateResolutionRecord {
151
161
  * recorded for its own environment and plan.
152
162
  */
153
163
  environment?: string;
164
+ /**
165
+ * The release this approval is for (#4045, ws-104 question 2), copied off
166
+ * the pending fact it answers. Set on a release gate, the gate `chant
167
+ * components promote --release` passes for an environment. A release gate
168
+ * counts only a resolution recorded for its own release, environment and
169
+ * change-set document digest (`planDigest`), so it names the release it
170
+ * admitted. Absent on every other gate, and on a resolution written before
171
+ * 0.124.28.
172
+ */
173
+ releaseDigest?: string;
174
+ /**
175
+ * The resolution passes a release gate over an empty change set (#4045,
176
+ * ws-104 question 6): the promote wrote it itself, with the actor running
177
+ * it in `resolvedBy`, because the change set its `planDigest` names changes
178
+ * nothing. A gate counts it only after it reads the stored document under
179
+ * `planDigest` and finds it empty, and neither a quorum nor a signing rule
180
+ * applies to it. It never counts as a person's approval. Absent on every
181
+ * other resolution.
182
+ */
183
+ empty?: true;
154
184
  /**
155
185
  * The channel this resolution was authored on (chant#2384) — set by the
156
186
  * writer, never by the caller. `chant approve` records `"cli"`, the
@@ -292,8 +322,19 @@ export interface PendingGateRecord {
292
322
  gate: string;
293
323
  /** The gate's human-readable description, when it declared one — what `chant operator status` shows a reader who wasn't there for the run. */
294
324
  description?: string;
295
- /** The run that reached the gate, when the caller identifies its runs. */
325
+ /**
326
+ * The run that reached the gate. An Op run records its own id; a run with no
327
+ * id of its own (a component gate) records the CI run id when it ran in CI
328
+ * (#4020). Absent outside CI for a run that mints none.
329
+ */
296
330
  runId?: string;
331
+ /**
332
+ * The commit the run reached the gate at (#4020): the checkout's `HEAD`, else
333
+ * the commit the CI job started for. It is the commit the gate waits on, and
334
+ * `chant approve` copies it onto the resolution. Absent on a record written
335
+ * before 0.124.28 and when the run knew no commit.
336
+ */
337
+ gitSha?: string;
297
338
  /** ISO-8601 timestamp, caller-supplied (library code never calls `Date.now()` internally). */
298
339
  timestamp: string;
299
340
  /** ISO-8601 expiry, from the gate's `timeout` (default {@link DEFAULT_GATE_EXPIRY}). Past it, the fact is stale and a run re-records it. */
@@ -334,6 +375,13 @@ export interface PendingGateRecord {
334
375
  * can pick one.
335
376
  */
336
377
  environment?: string;
378
+ /**
379
+ * The release the run reached this gate for (#4045): on a release gate, the
380
+ * `releaseDigest` of the release `chant components promote --release`
381
+ * deploys. `chant approve` copies it onto the approval with the plan.
382
+ * Absent on every other gate, and on a fact written before 0.124.28.
383
+ */
384
+ releaseDigest?: string;
337
385
  /**
338
386
  * The channel the run that reached this gate was driven from (chant#2384).
339
387
  *
@@ -519,12 +567,29 @@ export function parseGateLedger(
519
567
  malformed++;
520
568
  continue;
521
569
  }
570
+ // #4045: and for the release a release gate approval is bound to.
571
+ const rawRelease: unknown = (parsed as { releaseDigest?: unknown }).releaseDigest;
572
+ if (rawRelease !== undefined && typeof rawRelease !== "string") {
573
+ malformed++;
574
+ continue;
575
+ }
576
+ // #4045: an `empty` that is not a boolean can't be told from a pass or an approval.
577
+ const rawEmpty: unknown = (parsed as { empty?: unknown }).empty;
578
+ if (rawEmpty !== undefined && typeof rawEmpty !== "boolean") {
579
+ malformed++;
580
+ continue;
581
+ }
522
582
  // #3947: and for the checkpoint an approval lets a run resume from.
523
583
  const rawCheckpoint: unknown = (parsed as { checkpointDigest?: unknown }).checkpointDigest;
524
584
  if (rawCheckpoint !== undefined && typeof rawCheckpoint !== "string") {
525
585
  malformed++;
526
586
  continue;
527
587
  }
588
+ // #4020: a commit or run id of the wrong type is dropped, so a reader can trust the type without losing the line.
589
+ for (const key of ["gitSha", "runId"] as const) {
590
+ const v: unknown = (parsed as Record<string, unknown>)[key];
591
+ if (v !== undefined && typeof v !== "string") delete (parsed as Record<string, unknown>)[key];
592
+ }
528
593
  if (parsed.kind === "pending") {
529
594
  if (typeof parsed.expiresAt !== "string") {
530
595
  malformed++;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * #4031: who may cut a release, and the cut's signature. The declaration's
3
+ * `identity.gates.release` names the class that may cut; a cut is signed
4
+ * with the actor's ssh key over the release digest, label, actor and time,
5
+ * and `release list` and `workspace status` verify it on every read. With
6
+ * `ci.green.signedSince`, a hand-pushed green tag cuts nothing.
7
+ */
8
+
9
+ import { spawnSync } from "node:child_process";
10
+ import { afterAll, describe, expect, test } from "vitest";
11
+ import { cutRelease, ReleaseCutError } from "./release-cut";
12
+ import { readCuts, type ReleaseArtifact } from "./release-store";
13
+ import type { CompiledTarget, TargetCompiler } from "../components/release-compile";
14
+ import { hasSshKeygen, TestRepo, type Key } from "../workspace/trust/test-repo";
15
+ import { releasesDocument } from "../workspace/releases";
16
+ import { workspaceStatus, type StatusDocument } from "../workspace/status";
17
+ import { checkReleaseSignature, type GateAdmissionRule } from "../workspace/identity-gate-rule";
18
+ import { contract } from "../workspace/__fixtures__/contract-repo";
19
+ import releasesSchema from "../workspace/releases.schema.json";
20
+ import statusSchema from "../workspace/status.schema.json";
21
+ import { rmSync } from "node:fs";
22
+ import { dirname } from "node:path";
23
+
24
+ const repos: TestRepo[] = [];
25
+ afterAll(() => {
26
+ for (const r of repos) rmSync(dirname(r.dir), { recursive: true, force: true });
27
+ });
28
+
29
+ function compiler(): TargetCompiler {
30
+ return {
31
+ async compile(env): Promise<CompiledTarget> {
32
+ return {
33
+ env,
34
+ params: [],
35
+ components: ["api"],
36
+ buildSteps: { api: [{ kind: "docker-build", context: "api", into: "api.tar" }] },
37
+ buildInputs: { api: ["api"] },
38
+ componentFiles: { api: "api.component.ts" },
39
+ templates: [{ component: "api", path: "dist/api.json", content: `{"env":"${env}"}\n` }],
40
+ };
41
+ },
42
+ async buildArtifacts(_env, components): Promise<ReleaseArtifact[]> {
43
+ return components.map((c) => ({ component: c, kind: "image" as const, path: `${c}.tar`, digest: `sha256:${"b".repeat(64)}` }));
44
+ },
45
+ };
46
+ }
47
+
48
+ /** A domain class, `releaser`, from a path pin, granted by the `releaser` role. */
49
+ const PLUGIN = {
50
+ "classes/package.json": JSON.stringify({ name: "@acme/classes", version: "1.0.0", exports: { "./workspace-principals": "./principals.json" } }),
51
+ "classes/principals.json": JSON.stringify({ schema: 1, classes: [{ name: "releaser", description: "people who may cut a release", role: "releaser" }] }),
52
+ };
53
+
54
+ /**
55
+ * A workspace whose root member is a chant project with three environments,
56
+ * signed by alice and bob, with alice holding the releaser role. `extra`
57
+ * goes into the declaration.
58
+ */
59
+ function setup(extra: Record<string, unknown>): { r: TestRepo; alice: Key; bob: Key; head: string } {
60
+ const r = new TestRepo("release-cut");
61
+ repos.push(r);
62
+ const alice = r.key("alice");
63
+ const bob = r.key("bob");
64
+ r.write("chant.workspace.json", JSON.stringify({ name: "acme", schema: 1, pins: [{ path: "classes" }], members: [{ name: "root", dir: ".", kind: "other", because: "the release fixture" }], ...extra }, null, 2));
65
+ for (const [path, text] of Object.entries(PLUGIN)) r.write(path, text);
66
+ r.write(".chant/allowed_signers", `alice ${alice.pub}\nbob ${bob.pub}\n`);
67
+ r.write(".chant/trust.json", JSON.stringify({ schema: 1, roles: { releaser: ["alice"] } }));
68
+ r.write("chant.config.ts", `export default { environments: ["dev", "prod"] };\n`);
69
+ r.write("api.component.ts", "export const api = {};\n");
70
+ r.write("api/Dockerfile", "FROM scratch\n");
71
+ const head = r.commit("init");
72
+ return { r, alice, bob, head };
73
+ }
74
+
75
+ const NOW = () => new Date("2026-10-16T10:00:00.000Z");
76
+ const cut = (r: TestRepo, actor: string, sign?: Key, label: string | null = "v1.0.0") =>
77
+ cutRelease({ cwd: r.dir, compiler: compiler(), label, actor, env: {}, now: NOW, ...(sign ? { sign: sign.file } : {}) });
78
+ const code = async (p: Promise<unknown>) => ((await p.catch((e: unknown) => e)) as ReleaseCutError).code;
79
+ const noLedger = (r: TestRepo) => spawnSync("git", ["rev-parse", "--verify", "--quiet", "refs/heads/chant/lifecycle"], { cwd: r.dir }).status !== 0;
80
+
81
+ describe.skipIf(!hasSshKeygen)("who may cut a release (#4031)", () => {
82
+ const RELEASE_GATE = { identity: { gates: { release: { class: "releaser" } } } };
83
+
84
+ test("cutting as someone outside the release gate's class is refused, and nothing is written", async () => {
85
+ const { r, bob } = setup(RELEASE_GATE);
86
+ r.git(["tag", `ci/green/${r.git(["rev-parse", "HEAD"]).trim()}`]);
87
+ const err = await cut(r, "bob", bob).catch((e: unknown) => e);
88
+ expect(err).toBeInstanceOf(ReleaseCutError);
89
+ expect((err as ReleaseCutError).code).toBe("release-signer-refused");
90
+ expect((err as ReleaseCutError).message).toMatch(/bob is not in the class releaser that gate release needs/);
91
+ expect(noLedger(r)).toBe(true);
92
+ });
93
+
94
+ test("an unsigned cut, or one signed with another signer's key, is refused under the gate", async () => {
95
+ const { r, alice, bob } = setup(RELEASE_GATE);
96
+ r.git(["tag", `ci/green/${r.git(["rev-parse", "HEAD"]).trim()}`]);
97
+ expect(await code(cut(r, "alice"))).toBe("release-unsigned");
98
+ const forged = await cut(r, "alice", bob).catch((e: unknown) => e);
99
+ expect((forged as ReleaseCutError).code).toBe("release-signer-refused");
100
+ expect((forged as ReleaseCutError).message).toMatch(/does not verify for alice/);
101
+ expect(noLedger(r)).toBe(true);
102
+ expect(alice.name).toBe("alice");
103
+ });
104
+
105
+ test("a cut by a releaser carries a signature that release list and workspace status verify", async () => {
106
+ const { r, alice } = setup(RELEASE_GATE);
107
+ const head = r.git(["rev-parse", "HEAD"]).trim();
108
+ r.git(["tag", `ci/green/${head}`]);
109
+ const result = await cut(r, "alice", alice);
110
+ expect(result.cut?.signature).toMatch(/^-----BEGIN SSH SIGNATURE-----/);
111
+ const { cuts } = await readCuts({ cwd: r.dir });
112
+ expect(cuts).toHaveLength(1);
113
+ expect(cuts[0]).toMatchObject({ label: "v1.0.0", actor: "alice", signature: result.cut!.signature });
114
+
115
+ const list = await releasesDocument({ cwd: r.dir });
116
+ contract(releasesSchema).expectValid(list);
117
+ const entry = (list as Extract<typeof list, { releases: unknown }>).releases[0];
118
+ expect(entry.cuts[0].signatureCheck).toMatchObject({ status: "verified", key: expect.stringMatching(/^SHA256:/), reason: "signed by alice, in the class releaser" });
119
+
120
+ const status = (await workspaceStatus({ cwd: r.dir, env: "dev" })) as Extract<StatusDocument, { members: unknown }>;
121
+ contract(statusSchema).expectValid(status);
122
+ expect(status.members[0].releaseCuts).toEqual([
123
+ {
124
+ releaseDigest: result.release.releaseDigest,
125
+ label: "v1.0.0",
126
+ commit: head,
127
+ actor: "alice",
128
+ timestamp: "2026-10-16T10:00:00.000Z",
129
+ signature: result.cut!.signature,
130
+ signatureCheck: { status: "verified", key: entry.cuts[0].signatureCheck.key, reason: "signed by alice, in the class releaser" },
131
+ },
132
+ ]);
133
+
134
+ // A cut line edited after the fact no longer verifies: the label is signed.
135
+ const rule: GateAdmissionRule = { gate: "release", class: null, signers: { signersPath: ".chant/allowed_signers", active: true, signers: [{ principal: "alice", key: alice.pub, line: 1 }] }, roles: {}, classes: [] };
136
+ expect(checkReleaseSignature(rule, { ...cuts[0] }).status).toBe("verified");
137
+ expect(checkReleaseSignature(rule, { ...cuts[0], label: "v9.9.9" })).toMatchObject({ status: "unverified", reason: expect.stringMatching(/does not verify for alice/) });
138
+ expect(checkReleaseSignature(rule, { ...cuts[0], signature: null }).status).toBe("unsigned");
139
+ });
140
+
141
+ test("without a release gate a cut may be unsigned, and a signed one is still verified on read", async () => {
142
+ const { r, bob } = setup({});
143
+ r.git(["tag", `ci/green/${r.git(["rev-parse", "HEAD"]).trim()}`]);
144
+ const plain = await cut(r, "carol", undefined, "v1");
145
+ expect(plain.cut?.signature).toBeNull();
146
+ r.write("README.md", "next\n");
147
+ const next = r.commit("next");
148
+ r.git(["tag", `ci/green/${next}`]);
149
+ const signed = await cut(r, "bob", bob, "v2");
150
+ expect(signed.cut?.signature).toMatch(/BEGIN SSH SIGNATURE/);
151
+ const list = (await releasesDocument({ cwd: r.dir })) as Extract<Awaited<ReturnType<typeof releasesDocument>>, { releases: unknown }>;
152
+ // Both cuts share one time, so the list's order between them is the digests'.
153
+ expect(list.releases.map((x) => [x.label, x.cuts[0].signatureCheck.status]).sort()).toEqual([
154
+ ["v1", "unsigned"],
155
+ ["v2", "verified"],
156
+ ]);
157
+ });
158
+
159
+ test("with ci.green.signedSince, a hand-pushed green tag cuts nothing", async () => {
160
+ const green = { branch: "main", phases: { test: ["test"] }, require: ["test"] };
161
+ const { r, alice, head: pivot } = setup({ ...RELEASE_GATE, ci: { green } });
162
+ // Tags on commits from the pivot on need a statement; the declaration says so in the next commit.
163
+ const decl = JSON.parse(r.git(["show", "HEAD:chant.workspace.json"]));
164
+ r.write("chant.workspace.json", JSON.stringify({ ...decl, ci: { green: { ...green, signedSince: pivot } } }, null, 2));
165
+ const head = r.commit("require signed ci tags");
166
+ r.git(["tag", "-a", "-m", "ci: by hand", `ci/green/${head}`, head]);
167
+ const err = await cut(r, "alice", alice).catch((e: unknown) => e);
168
+ expect((err as ReleaseCutError).code).toBe("release-commit-not-green");
169
+ expect((err as ReleaseCutError).message).toMatch(/no ci\/green tag that counts.*ignored: ci\/green\/[0-9a-f]+ \(unsigned\)/);
170
+ expect(noLedger(r)).toBe(true);
171
+
172
+ // A signedSince that names no commit here refuses the cut.
173
+ r.write("chant.workspace.json", JSON.stringify({ ...decl, ci: { green: { ...green, signedSince: "f".repeat(40) } } }, null, 2));
174
+ const broken = r.commit("a signedSince from another clone");
175
+ r.git(["tag", `ci/green/${broken}`, broken]);
176
+ const refused = await cut(r, "alice", alice).catch((e: unknown) => e);
177
+ expect((refused as ReleaseCutError).code).toBe("declaration-invalid");
178
+ expect(noLedger(r)).toBe(true);
179
+ });
180
+ });