@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,306 @@
1
+ /**
2
+ * The emitters' side of the run-event contract (#4041): what a runtime or a
3
+ * command needs to print a run's stream through the one writer in
4
+ * `./run-event.ts`.
5
+ *
6
+ * Every emitter hands bodies to a {@link RunEventSink}. The CLI opens a
7
+ * {@link RunEventStream} over a run's identity, which stamps each body
8
+ * through `createRunEventWriter` and makes sure the stream opens with
9
+ * `run-start` and ends with one `run-settle`, even when the run threw before
10
+ * it said so itself. The components driver emits bodies only, and
11
+ * {@link progressOfRunEvent} turns them back into the older
12
+ * `RunProgressEvent` lines that `--progress-json` still prints.
13
+ *
14
+ * Kept apart from `./run-event.ts`, which a level-0 import loads: this
15
+ * module reads git and the CI environment.
16
+ */
17
+
18
+ import type { RunProgressEvent } from "./components/run-progress";
19
+ import type { GateCheck } from "./op/gate";
20
+ import { resolveCommitSha } from "./lifecycle/commit-sha";
21
+ import { resolveCiRun, type RunOrigin } from "./lifecycle/release-ledger";
22
+ import {
23
+ createRunEventWriter,
24
+ type RunEventBody,
25
+ type RunEventKind,
26
+ type RunEventRun,
27
+ type RunEventRuntime,
28
+ type RunEventSettleStatus,
29
+ } from "./run-event";
30
+
31
+ /** Where an emitter hands a run's events, before the writer stamps them. */
32
+ export type RunEventSink = (body: RunEventBody) => void;
33
+
34
+ /** One sink that hands each body to every sink given, or undefined when none is. */
35
+ export function joinRunEventSinks(...sinks: Array<RunEventSink | undefined>): RunEventSink | undefined {
36
+ const live = sinks.filter((s): s is RunEventSink => s !== undefined);
37
+ if (live.length === 0) return undefined;
38
+ if (live.length === 1) return live[0];
39
+ return (body) => {
40
+ for (const sink of live) sink(body);
41
+ };
42
+ }
43
+
44
+ const GATE_PREFIX = "gate:";
45
+
46
+ /**
47
+ * A run-event body as the older `RunProgressEvent`, or undefined when that
48
+ * stream had no such event. It never printed a gate step, a skipped step,
49
+ * a phase outside a component or a gate's decision, so those map to nothing
50
+ * and `--progress-json` prints what it printed before #4041.
51
+ */
52
+ export function progressOfRunEvent(body: RunEventBody): RunProgressEvent | undefined {
53
+ const progressStatus = (s: RunEventSettleStatus): "ok" | "failed" | "gated" => (s === "waiting" ? "gated" : s);
54
+ switch (body.type) {
55
+ case "run-start":
56
+ return { type: "run-start", waves: body.waves ?? [] };
57
+ case "wave-start":
58
+ return { type: "wave-start", wave: body.wave, components: body.components };
59
+ case "component-start":
60
+ return { type: "component-start", wave: body.wave, component: body.component };
61
+ case "phase-start":
62
+ return body.component === null ? undefined : { type: "phase-start", component: body.component, phase: body.phase };
63
+ case "step-start":
64
+ if (body.component === null || body.step.startsWith(GATE_PREFIX)) return undefined;
65
+ return { type: "step", component: body.component, phase: body.phase, step: body.step, status: "running" };
66
+ case "step-settle":
67
+ if (body.component === null || body.step.startsWith(GATE_PREFIX)) return undefined;
68
+ if (body.status !== "ok" && body.status !== "failed") return undefined;
69
+ return {
70
+ type: "step",
71
+ component: body.component,
72
+ phase: body.phase,
73
+ step: body.step,
74
+ status: body.status,
75
+ ...(body.error !== undefined ? { error: body.error } : {}),
76
+ };
77
+ case "phase-settle":
78
+ return body.component === null
79
+ ? undefined
80
+ : { type: "phase-done", component: body.component, phase: body.phase, status: progressStatus(body.status) };
81
+ case "component-settle":
82
+ return { type: "component-done", wave: body.wave, component: body.component, status: progressStatus(body.status) };
83
+ case "wave-settle":
84
+ return { type: "wave-done", wave: body.wave, status: progressStatus(body.status) };
85
+ case "run-settle":
86
+ return { type: "run-done", status: progressStatus(body.status) };
87
+ case "gate-waiting":
88
+ case "gate-resolved":
89
+ return undefined;
90
+ }
91
+ }
92
+
93
+ /** A `RunProgressEvent` callback as a sink of run-event bodies, through {@link progressOfRunEvent}. */
94
+ export function progressSinkOf(onProgress: ((event: RunProgressEvent) => void) | undefined): RunEventSink | undefined {
95
+ if (!onProgress) return undefined;
96
+ return (body) => {
97
+ const event = progressOfRunEvent(body);
98
+ if (event) onProgress(event);
99
+ };
100
+ }
101
+
102
+ /** The CI run the environment names, as `resolveCiRun` gives it. */
103
+ type CiRun = { runId: string; runOrigin: RunOrigin };
104
+
105
+ /**
106
+ * A run's identity from what the emitter already knows. The runtime is `ci`
107
+ * inside a CI job, `steward` in a steward's turn and `local` otherwise; the
108
+ * origin is the CI run's, `op` for a steward's run, and `local` otherwise. An
109
+ * Op run keeps its own run id in CI, as its run record does (#4020).
110
+ */
111
+ export function runEventRunOf(input: {
112
+ kind: RunEventKind;
113
+ name: string;
114
+ runId: string;
115
+ gitSha?: string;
116
+ ci?: CiRun;
117
+ steward?: boolean;
118
+ runtime?: RunEventRuntime;
119
+ runOrigin?: RunOrigin;
120
+ }): RunEventRun {
121
+ return {
122
+ runId: input.runId,
123
+ kind: input.kind,
124
+ name: input.name,
125
+ gitSha: input.gitSha ?? null,
126
+ runtime: input.runtime ?? (input.ci ? "ci" : input.steward ? "steward" : "local"),
127
+ runOrigin: input.runOrigin ?? input.ci?.runOrigin ?? (input.steward ? { forge: "op" } : { forge: "local" }),
128
+ };
129
+ }
130
+
131
+ /**
132
+ * A run's identity, resolved: the commit from `resolveCommitSha` and the CI
133
+ * run from `resolveCiRun` (#4020). Without `runId`, a CI run is named by its
134
+ * CI run id and any other by a locally minted `local-<ms>` id, the spelling
135
+ * `resolveRunId` uses for a release record.
136
+ */
137
+ export async function resolveRunEventRun(input: {
138
+ kind: RunEventKind;
139
+ name: string;
140
+ runId?: string;
141
+ cwd?: string;
142
+ env?: NodeJS.ProcessEnv;
143
+ }): Promise<RunEventRun> {
144
+ const env = input.env ?? process.env;
145
+ const ci = resolveCiRun(env);
146
+ const gitSha = await resolveCommitSha({ ...(input.cwd ? { cwd: input.cwd } : {}), env });
147
+ const runId = input.runId ?? ci?.runId ?? `local-${Date.now()}`;
148
+ // An explicit id that is not the CI run's is one the caller minted, so the
149
+ // origin says local, as `resolveRunId` records it on a release.
150
+ const inCiSpace = ci !== undefined && runId === ci.runId;
151
+ return runEventRunOf({
152
+ kind: input.kind,
153
+ name: input.name,
154
+ runId,
155
+ ...(gitSha ? { gitSha } : {}),
156
+ ...(ci ? { ci } : {}),
157
+ ...(ci && !inCiSpace ? { runOrigin: { forge: "local" } } : {}),
158
+ });
159
+ }
160
+
161
+ /** One run's stream: the writer, held to the shape the contract promises. */
162
+ export interface RunEventStream {
163
+ /** The run every event names. */
164
+ readonly run: RunEventRun;
165
+ /** Write a body. */
166
+ emit: RunEventSink;
167
+ /**
168
+ * A sink for a part of the run that frames itself as a whole run, such as
169
+ * the fan-out a pull request's apply runs: its `run-start` and
170
+ * `run-settle` are dropped, and the caller settles the run.
171
+ */
172
+ nested: RunEventSink;
173
+ /** End the stream with `run-settle`, unless it has ended. */
174
+ settle(status: RunEventSettleStatus, error?: string): void;
175
+ /** Whether `run-settle` has been written. */
176
+ readonly settled: boolean;
177
+ }
178
+
179
+ /**
180
+ * Open a run's stream over `write`. The first event is always `run-start`
181
+ * (one with `waves: null` is written first when the emitter starts with
182
+ * something else), a second `run-start` is dropped, `run-settle` gets the
183
+ * time since `run-start` when the emitter did not time it, and nothing is
184
+ * written after `run-settle`.
185
+ */
186
+ export function openRunEventStream(
187
+ run: RunEventRun,
188
+ write: (line: string) => void,
189
+ opts: { now?: () => Date; chant?: string } = {},
190
+ ): RunEventStream {
191
+ const now = opts.now ?? (() => new Date());
192
+ const writer = createRunEventWriter({ run, write, now, ...(opts.chant ? { chant: opts.chant } : {}) });
193
+ let startedAt: number | undefined;
194
+ let settled = false;
195
+
196
+ const emit: RunEventSink = (body) => {
197
+ if (settled) return;
198
+ if (body.type === "run-start") {
199
+ if (startedAt !== undefined) return;
200
+ startedAt = now().getTime();
201
+ writer.emit(body);
202
+ return;
203
+ }
204
+ if (startedAt === undefined) emit({ type: "run-start", waves: null });
205
+ if (body.type === "run-settle") {
206
+ settled = true;
207
+ writer.emit(body.durationMs === null ? { ...body, durationMs: Math.max(0, now().getTime() - startedAt!) } : body);
208
+ return;
209
+ }
210
+ writer.emit(body);
211
+ };
212
+
213
+ return {
214
+ run,
215
+ emit,
216
+ nested: (body) => {
217
+ if (body.type !== "run-start" && body.type !== "run-settle") emit(body);
218
+ },
219
+ settle(status, error) {
220
+ if (settled) return;
221
+ emit({ type: "run-settle", status, durationMs: null, ...(error !== undefined ? { error } : {}) });
222
+ },
223
+ get settled() {
224
+ return settled;
225
+ },
226
+ };
227
+ }
228
+
229
+ /** Where a gate step sits, for {@link gateEventsOf}. */
230
+ export interface GateStepAt {
231
+ component: string | null;
232
+ phase: string;
233
+ /** `gate:<name>`. */
234
+ step: string;
235
+ index?: number;
236
+ /** The op the gate is recorded under. */
237
+ op: string;
238
+ gate: string;
239
+ }
240
+
241
+ /**
242
+ * A decided gate as events: `gate-resolved` and an `ok` settle when an
243
+ * approval stands, `gate-waiting` and a `gated` settle when none does.
244
+ * `message` says why it did not pass, for a person.
245
+ */
246
+ export function gateEventsOf(check: GateCheck, at: GateStepAt, opts: { durationMs: number | null; message?: string }): RunEventBody[] {
247
+ const where = { component: at.component, phase: at.phase, step: at.step, ...(at.index !== undefined ? { index: at.index } : {}) };
248
+ const message = opts.message !== undefined ? { message: opts.message } : {};
249
+ if (check.satisfied) {
250
+ return [
251
+ {
252
+ type: "gate-resolved",
253
+ ...where,
254
+ op: at.op,
255
+ gate: at.gate,
256
+ resolvedBy: check.resolution.resolvedBy,
257
+ resolvedAt: check.resolution.timestamp,
258
+ via: check.via ?? null,
259
+ ...(check.resolution.url ? { url: check.resolution.url } : {}),
260
+ },
261
+ { type: "step-settle", ...where, status: "ok", durationMs: opts.durationMs },
262
+ ];
263
+ }
264
+ return [
265
+ { type: "gate-waiting", ...where, op: check.pending.op, gate: at.gate, point: null, ...message },
266
+ { type: "step-settle", ...where, status: "gated", durationMs: opts.durationMs, ...message },
267
+ ];
268
+ }
269
+
270
+ /** The phase a gate outside any component is the one step of: a fan-out's, a wave's or a pull request's. */
271
+ export const GATE_PHASE = "gate";
272
+
273
+ /**
274
+ * Decide a gate that stands outside any component, and emit it as the one
275
+ * step of a {@link GATE_PHASE} phase: `phase-start`, `step-start`, the
276
+ * decision through {@link gateEventsOf}, the settle and `phase-settle`. A
277
+ * decision that throws settles the step `failed` and is rethrown. `message`
278
+ * says, for a person, why a gate that did not pass did not.
279
+ */
280
+ export async function emitGateStep(
281
+ emit: RunEventSink | undefined,
282
+ at: { op: string; gate: string },
283
+ decide: () => Promise<GateCheck>,
284
+ message?: (check: Extract<GateCheck, { satisfied: false }>) => string | undefined,
285
+ ): Promise<GateCheck> {
286
+ const where = { component: null, phase: GATE_PHASE, step: `gate:${at.gate}` };
287
+ emit?.({ type: "phase-start", component: null, phase: GATE_PHASE });
288
+ emit?.({ type: "step-start", ...where });
289
+ const start = Date.now();
290
+ let check: GateCheck;
291
+ try {
292
+ check = await decide();
293
+ } catch (err) {
294
+ emit?.({ type: "step-settle", ...where, status: "failed", durationMs: Date.now() - start, error: err instanceof Error ? err.message : String(err) });
295
+ emit?.({ type: "phase-settle", component: null, phase: GATE_PHASE, status: "failed" });
296
+ throw err;
297
+ }
298
+ const why = check.satisfied
299
+ ? undefined
300
+ : (message?.(check) ?? (check.mismatch ? "an approval stands for this gate, but for another plan" : undefined));
301
+ for (const body of gateEventsOf(check, { ...where, op: at.op, gate: at.gate }, { durationMs: Date.now() - start, ...(why !== undefined ? { message: why } : {}) })) {
302
+ emit?.(body);
303
+ }
304
+ emit?.({ type: "phase-settle", component: null, phase: GATE_PHASE, status: check.satisfied ? "ok" : "gated" });
305
+ return check;
306
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * #4019: the run-event contract. One schema for Op, component and pipeline
3
+ * runs, the one writer, and the two older streams mapped onto it. Every
4
+ * line the writer prints is checked against `run-event.schema.json`.
5
+ */
6
+
7
+ import { describe, expect, test } from "vitest";
8
+ import type { RunProgressEvent } from "./components/run-progress";
9
+ import type { StepRecord } from "./op/local-executor";
10
+ import { contract, validSchema } from "./workspace/__fixtures__/contract-repo";
11
+ import { READ_CONTRACT_FLOOR, READ_CONTRACT_VERSION } from "./workspace/reason-codes";
12
+ import {
13
+ buildRunEvent,
14
+ createRunEventWriter,
15
+ RUN_EVENT_CONTRACT_VERSION,
16
+ RUN_EVENT_FLOOR,
17
+ RUN_EVENT_SCHEMA_ID,
18
+ RUN_EVENT_TYPES,
19
+ runEventOfProgress,
20
+ runEventsOfStepRecord,
21
+ serializeRunEvent,
22
+ type RunEvent,
23
+ type RunEventBody,
24
+ type RunEventRun,
25
+ } from "./run-event";
26
+ import schema from "./workspace/run-event.schema.json";
27
+
28
+ const { validate, expectValid } = contract(schema);
29
+
30
+ const SHA = "0123456789abcdef0123456789abcdef01234567";
31
+
32
+ const OP_RUN: RunEventRun = {
33
+ runId: "6f1c2d4e-0000-4000-8000-000000000001",
34
+ kind: "op",
35
+ name: "deploy-api",
36
+ gitSha: SHA,
37
+ runtime: "local",
38
+ runOrigin: { forge: "local" },
39
+ };
40
+
41
+ /** A writer that keeps its lines, on a clock that ticks a second per event. */
42
+ function capture(run: RunEventRun) {
43
+ const lines: string[] = [];
44
+ let t = Date.parse("2026-10-16T12:00:00.000Z");
45
+ const writer = createRunEventWriter({ run, write: (l) => lines.push(l), now: () => new Date((t += 1000)), chant: "0.124.28" });
46
+ const events = () => lines.map((l) => JSON.parse(l) as RunEvent);
47
+ return { writer, lines, events };
48
+ }
49
+
50
+ describe("the run-event schema", () => {
51
+ test("is a valid draft 2020-12 document at the contract version, naming its floor and the read contract's", () => {
52
+ expect(validSchema(schema)).toBe(true);
53
+ expect(schema.$id).toBe(RUN_EVENT_SCHEMA_ID);
54
+ expect(RUN_EVENT_SCHEMA_ID).toBe(`https://intentius.io/chant/schemas/workspace/run-event/v${READ_CONTRACT_VERSION}/run-event.schema.json`);
55
+ const envelope = schema.$defs.envelope.properties;
56
+ expect(envelope.contract).toEqual({ const: RUN_EVENT_CONTRACT_VERSION });
57
+ expect(envelope.$schema).toEqual({ const: RUN_EVENT_SCHEMA_ID });
58
+ expect(schema.description).toContain(`written by chant ${RUN_EVENT_FLOOR} and newer`);
59
+ expect(schema.description).toContain(`chant ${READ_CONTRACT_FLOOR} and newer`);
60
+ });
61
+
62
+ test("names exactly the event types the code does, and every one requires the envelope", () => {
63
+ const defs = schema.$defs as Record<string, { allOf?: Array<{ $ref?: string; properties?: { type?: { const?: string } } }> }>;
64
+ const refs = schema.oneOf.map((o) => o.$ref.replace("#/$defs/", ""));
65
+ expect([...refs].sort()).toEqual([...RUN_EVENT_TYPES].sort());
66
+ for (const t of RUN_EVENT_TYPES) {
67
+ const parts = defs[t].allOf!;
68
+ expect(parts[0].$ref, t).toBe("#/$defs/envelope");
69
+ expect(parts[1].properties!.type!.const, t).toBe(t);
70
+ }
71
+ });
72
+
73
+ test("keeps the step statuses closed, with skipped and room for cached", () => {
74
+ const status = schema.$defs["step-settle"].allOf[1].properties!.status as { enum: string[] };
75
+ expect(status.enum).toEqual(["ok", "failed", "skipped", "cached", "gated", "waiting"]);
76
+ const one = buildRunEvent({ type: "step-settle", component: null, phase: "p", step: "s", status: "ok", durationMs: 1 }, { run: OP_RUN, seq: 3 });
77
+ expectValid(one);
78
+ for (const bad of ["fail", "running", "done"]) expect(validate({ ...one, status: bad }), bad).toBe(false);
79
+ expect(validate({ ...one, status: "skipped", skip: "receipt" })).toBe(true);
80
+ expect(validate({ ...one, status: "skipped", skip: "because" })).toBe(false);
81
+ });
82
+
83
+ test("refuses an event missing the run, the commit or the runtime, or at another contract", () => {
84
+ const one = buildRunEvent({ type: "run-start", waves: null }, { run: OP_RUN, seq: 0 });
85
+ expectValid(one);
86
+ for (const field of ["runId", "kind", "name", "gitSha", "runtime", "runOrigin", "seq", "at", "chant"] as const) {
87
+ const copy: Record<string, unknown> = { ...one };
88
+ delete copy[field];
89
+ expect(validate(copy), field).toBe(false);
90
+ }
91
+ expect(validate({ ...one, gitSha: "main" })).toBe(false);
92
+ expect(validate({ ...one, gitSha: null })).toBe(true);
93
+ expect(validate({ ...one, runtime: "cloud" })).toBe(false);
94
+ expect(validate({ ...one, runOrigin: { forge: "jenkins" } })).toBe(false);
95
+ expect(validate({ ...one, contract: 2 })).toBe(false);
96
+ expect(validate({ ...one, type: "run-done" })).toBe(false);
97
+ });
98
+
99
+ test("lets a reader ignore fields it does not know", () => {
100
+ expectValid({ ...buildRunEvent({ type: "run-start", waves: null }, { run: OP_RUN, seq: 0 }), later: true });
101
+ });
102
+ });
103
+
104
+ describe("the one writer", () => {
105
+ test("writes an Op run as one line per event, each valid, with the run on every line and seq counting from 0", () => {
106
+ const { writer, lines, events } = capture(OP_RUN);
107
+ const bodies: RunEventBody[] = [
108
+ { type: "run-start", waves: null },
109
+ { type: "phase-start", component: null, phase: "build" },
110
+ { type: "step-start", component: null, phase: "build", step: "shell" },
111
+ { type: "step-settle", component: null, phase: "build", step: "shell", status: "ok", durationMs: 412 },
112
+ { type: "step-start", component: null, phase: "build", step: "effect:schema" },
113
+ { type: "step-settle", component: null, phase: "build", step: "effect:schema", status: "skipped", skip: "receipt", durationMs: 3 },
114
+ { type: "step-settle", component: null, phase: "build", step: "cache-restore", status: "cached", durationMs: 0 },
115
+ { type: "phase-settle", component: null, phase: "build", status: "ok" },
116
+ { type: "phase-start", component: null, phase: "release" },
117
+ { type: "step-start", component: null, phase: "release", step: "gate:prod" },
118
+ { type: "gate-waiting", component: null, phase: "release", step: "gate:prod", op: "deploy-api", gate: "prod", point: null },
119
+ { type: "step-settle", component: null, phase: "release", step: "gate:prod", status: "gated", durationMs: 20 },
120
+ { type: "phase-settle", component: null, phase: "release", status: "gated" },
121
+ { type: "run-settle", status: "gated", durationMs: 1500 },
122
+ ];
123
+ for (const b of bodies) writer.emit(b);
124
+ expect(writer.next).toBe(bodies.length);
125
+ expect(lines.every((l) => l.endsWith("\n") && !l.slice(0, -1).includes("\n"))).toBe(true);
126
+ const evs = events();
127
+ evs.forEach((e, i) => {
128
+ expectValid(e);
129
+ expect(e.seq).toBe(i);
130
+ expect(e).toMatchObject({ $schema: RUN_EVENT_SCHEMA_ID, contract: 1, chant: "0.124.28", runId: OP_RUN.runId, kind: "op", name: "deploy-api", gitSha: SHA, runtime: "local", runOrigin: { forge: "local" } });
131
+ expect(e.type).toBe(bodies[i].type);
132
+ });
133
+ expect(evs[0].at).toBe("2026-10-16T12:00:01.000Z");
134
+ expect(Object.keys(evs[0]).slice(0, 5)).toEqual(["$schema", "contract", "chant", "seq", "at"]);
135
+ expect(evs.at(-1)).toMatchObject({ type: "run-settle", status: "gated" });
136
+ });
137
+
138
+ test("writes a pipeline run in forge CI, its stages as phases and its jobs as steps, with the CI locator", () => {
139
+ const { writer, events } = capture({
140
+ runId: "8812345",
141
+ kind: "pipeline",
142
+ name: "main",
143
+ gitSha: SHA,
144
+ runtime: "ci",
145
+ runOrigin: { forge: "gitlab", repo: "acme/api", url: "https://gitlab.example.com/acme/api/-/pipelines/8812345" },
146
+ });
147
+ writer.emit({ type: "run-start", waves: null });
148
+ writer.emit({ type: "phase-start", component: null, phase: "test" });
149
+ writer.emit({ type: "step-start", component: null, phase: "test", step: "unit" });
150
+ writer.emit({ type: "step-settle", component: null, phase: "test", step: "unit", status: "failed", durationMs: 9000, error: "exit 1" });
151
+ writer.emit({ type: "phase-settle", component: null, phase: "test", status: "failed" });
152
+ writer.emit({ type: "run-settle", status: "failed", durationMs: 9100, error: "job unit failed" });
153
+ for (const e of events()) expectValid(e);
154
+ });
155
+
156
+ test("writes a steward run with its approval and an open decision point", () => {
157
+ const { writer, events } = capture({ ...OP_RUN, runtime: "steward", runOrigin: { forge: "op", url: "https://fountain.example.com/threads/t-1" } });
158
+ writer.emit({ type: "gate-resolved", component: null, phase: "release", step: "gate:prod", op: "deploy-api", gate: "prod", resolvedBy: "alice", resolvedAt: "2026-10-16T11:00:00.000Z", via: "quorum" });
159
+ writer.emit({ type: "gate-waiting", component: null, phase: "release", step: "pick-region", op: "deploy-api", gate: null, point: "A-0042" });
160
+ writer.emit({ type: "step-settle", component: null, phase: "release", step: "pick-region", status: "waiting", durationMs: 5, message: "region?" });
161
+ writer.emit({ type: "run-settle", status: "waiting", durationMs: 40 });
162
+ for (const e of events()) expectValid(e);
163
+ });
164
+
165
+ test("serializes one event as one JSON line", () => {
166
+ const e = buildRunEvent({ type: "run-start", waves: [["a"], ["b", "c"]] }, { run: OP_RUN, seq: 0, at: "2026-10-16T12:00:00.000Z", chant: "1.0.0" });
167
+ expect(serializeRunEvent(e)).toBe(JSON.stringify(e) + "\n");
168
+ expect(e.at).toBe("2026-10-16T12:00:00.000Z");
169
+ });
170
+
171
+ test("is exported from @intentius/chant", async () => {
172
+ const root = await import("./index");
173
+ expect(root.createRunEventWriter).toBe(createRunEventWriter);
174
+ expect(root.RUN_EVENT_SCHEMA_ID).toBe(RUN_EVENT_SCHEMA_ID);
175
+ }, 60_000);
176
+ });
177
+
178
+ describe("RunProgressEvent in terms of the run-event contract", () => {
179
+ const progress: RunProgressEvent[] = [
180
+ { type: "run-start", waves: [["db"], ["api", "web"]] },
181
+ { type: "wave-start", wave: 1, components: ["db"] },
182
+ { type: "component-start", wave: 1, component: "db" },
183
+ { type: "phase-start", component: "db", phase: "deploy" },
184
+ { type: "step", component: "db", phase: "deploy", step: "apply", status: "running" },
185
+ { type: "step", component: "db", phase: "deploy", step: "apply", status: "failed", error: "boom" },
186
+ { type: "step", component: "db", phase: "deploy", step: "verify", status: "ok" },
187
+ { type: "phase-done", component: "db", phase: "deploy", status: "failed" },
188
+ { type: "component-done", wave: 1, component: "db", status: "gated" },
189
+ { type: "wave-done", wave: 1, status: "failed" },
190
+ { type: "run-done", status: "failed" },
191
+ ];
192
+
193
+ test("maps every variant to a valid event that keeps its fields", () => {
194
+ const { writer, events } = capture({ ...OP_RUN, kind: "component", name: "db,api,web" });
195
+ for (const p of progress) writer.emit(runEventOfProgress(p));
196
+ const evs = events();
197
+ for (const e of evs) expectValid(e);
198
+ expect(evs.map((e) => e.type)).toEqual([
199
+ "run-start",
200
+ "wave-start",
201
+ "component-start",
202
+ "phase-start",
203
+ "step-start",
204
+ "step-settle",
205
+ "step-settle",
206
+ "phase-settle",
207
+ "component-settle",
208
+ "wave-settle",
209
+ "run-settle",
210
+ ]);
211
+ expect(evs[0]).toMatchObject({ waves: [["db"], ["api", "web"]] });
212
+ expect(evs[5]).toMatchObject({ component: "db", step: "apply", status: "failed", error: "boom", durationMs: null });
213
+ expect(evs[8]).toMatchObject({ component: "db", status: "gated" });
214
+ });
215
+ });
216
+
217
+ describe("StepRecord in terms of the run-event contract", () => {
218
+ const ctx = { op: "deploy-api" };
219
+ const base = { phase: "release", args: {}, durationMs: 12 } as const;
220
+ const types = (r: StepRecord) => runEventsOfStepRecord(r, ctx).map((b) => b.type);
221
+ const settle = (r: StepRecord) => runEventsOfStepRecord(r, ctx).at(-1)!;
222
+
223
+ test("ok and fail settle ok and failed, with the error", () => {
224
+ expect(settle({ ...base, fn: "shell", status: "ok" })).toEqual({ type: "step-settle", component: null, phase: "release", step: "shell", status: "ok", durationMs: 12 });
225
+ expect(settle({ ...base, fn: "shell", status: "fail", error: "exit 2" })).toMatchObject({ status: "failed", error: "exit 2" });
226
+ });
227
+
228
+ test("an approved gate resolves before its step settles ok", () => {
229
+ const r: StepRecord = { ...base, fn: "gate:prod", status: "ok", approval: { gate: "prod", resolvedBy: "alice", timestamp: "2026-10-16T11:00:00.000Z", via: "policy", url: "https://x" } };
230
+ expect(types(r)).toEqual(["gate-resolved", "step-settle"]);
231
+ expect(runEventsOfStepRecord(r, ctx)[0]).toEqual({ type: "gate-resolved", component: null, phase: "release", step: "gate:prod", op: "deploy-api", gate: "prod", resolvedBy: "alice", resolvedAt: "2026-10-16T11:00:00.000Z", via: "policy", url: "https://x" });
232
+ });
233
+
234
+ test("a step waiting on a gate or a decision point settles gated or waiting, after gate-waiting", () => {
235
+ const own: StepRecord = { ...base, fn: "workspace-upgrade", status: "skipped", gate: { op: "workspace-upgrade", gate: "all" } };
236
+ expect(types(own)).toEqual(["gate-waiting", "step-settle"]);
237
+ expect(runEventsOfStepRecord(own, ctx)[0]).toMatchObject({ op: "workspace-upgrade", gate: "all", point: null });
238
+ expect(settle(own)).toMatchObject({ status: "gated" });
239
+
240
+ const point: StepRecord = { ...base, fn: "pick", status: "skipped", point: { id: "A-1", point: "region", state: "escalated", path: "answers/A-1.md", subject: null, steward: null } };
241
+ expect(runEventsOfStepRecord(point, ctx)[0]).toMatchObject({ type: "gate-waiting", gate: null, point: "A-1" });
242
+ expect(settle(point)).toMatchObject({ status: "waiting" });
243
+
244
+ const pointGate: StepRecord = { ...point, fn: "gate:prod" };
245
+ expect(runEventsOfStepRecord(pointGate, ctx)[0]).toMatchObject({ gate: "prod", point: "A-1" });
246
+
247
+ const mismatch: StepRecord = { ...base, fn: "gate:prod", status: "skipped", refusal: "approved for another plan" };
248
+ expect(runEventsOfStepRecord(mismatch, ctx)[0]).toMatchObject({ type: "gate-waiting", gate: "prod", message: "approved for another plan" });
249
+ expect(settle(mismatch)).toMatchObject({ status: "gated", message: "approved for another plan" });
250
+ });
251
+
252
+ test("skips say why when the record does, and null when it does not", () => {
253
+ expect(settle({ ...base, fn: "build", status: "skipped", resumed: { run: "r-1", gate: "prod" } })).toMatchObject({ status: "skipped", skip: "resumed" });
254
+ expect(settle({ ...base, fn: "workLease:claim", status: "skipped", refusal: "no item ready" })).toMatchObject({ status: "skipped", skip: "refused", message: "no item ready" });
255
+ expect(settle({ ...base, fn: "gate:prod", status: "skipped" })).toMatchObject({ status: "skipped", skip: null });
256
+ expect(settle({ ...base, fn: "migrate", status: "skipped", durationMs: 0 })).toMatchObject({ status: "skipped", skip: null, durationMs: 0 });
257
+ });
258
+
259
+ test("every mapped record writes valid events", () => {
260
+ const { writer, events } = capture(OP_RUN);
261
+ const records: StepRecord[] = [
262
+ { ...base, fn: "shell", status: "ok" },
263
+ { ...base, fn: "shell", status: "fail", error: "x" },
264
+ { ...base, fn: "gate:prod", status: "ok", approval: { gate: "prod", resolvedBy: "a", timestamp: "2026-10-16T11:00:00.000Z" } },
265
+ { ...base, fn: "gate:prod", status: "skipped", refusal: "r" },
266
+ { ...base, fn: "build", status: "skipped", resumed: { run: "r-1", gate: "prod" } },
267
+ { ...base, fn: "migrate", status: "skipped" },
268
+ ];
269
+ for (const r of records) for (const b of runEventsOfStepRecord(r, { ...ctx, component: "api" })) writer.emit(b);
270
+ const evs = events();
271
+ expect(evs.length).toBe(8);
272
+ for (const e of evs) {
273
+ expectValid(e);
274
+ expect(e).toMatchObject({ component: "api" });
275
+ }
276
+ });
277
+ });