@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
@@ -50,7 +50,9 @@ import { loadKindRegistry, resolveMemberFields, type MemberFields } from "./kind
50
50
  import { factoryView, listingView, plantability, treeBytes, type FactoryView, type ListingView, type Plantable } from "./box-factory";
51
51
  import { declaredRecordKinds, extensionsAt, readDeclaration, readerVersion, WorkspaceReadError, type Declaration, type ErrorLocation, type Member } from "./declaration";
52
52
  import type { ReasonCode } from "./reason-codes";
53
- import { gateAdmissionFrom } from "./identity";
53
+ import { readMemberPromotionOrder, type PromotionStep } from "./promotion-order";
54
+ import { gateAdmissionFrom, releaseRuleFrom } from "./identity";
55
+ import { checkReleaseSignature, type GateAdmissionRule, type ReleaseSignatureCheck } from "./identity-gate-rule";
54
56
  import { scopeSource } from "./write-scope";
55
57
  import { GATE_REASON_CODES, readMemberGates, type GateLedgerReader, type StatusGate, type StatusGateLedger } from "./status-gates";
56
58
  import { STEWARD_REASON_CODES, readMemberStewards, type StatusSteward, type StewardReasonCode } from "./status-stewards";
@@ -120,6 +122,10 @@ export interface StatusRelease {
120
122
  runId: string;
121
123
  timestamp: string;
122
124
  actor: string;
125
+ /** Who approved the change at its gate (#1035), when it had one. Null for an ungated release. */
126
+ approver: string | null;
127
+ /** The gate resolution that admitted the release (#4020): the gate and when it was resolved. Null for an ungated release and for one recorded before chant 0.124.28. */
128
+ admission: { op: string; gate: string; resolvedAt: string } | null;
123
129
  /** Read flags, such as `legacy-digest` (#2514). */
124
130
  flags: string[];
125
131
  /**
@@ -185,6 +191,34 @@ export interface StatusMember {
185
191
  stewards: StatusSteward[];
186
192
  /** Why a steward or a last run may be missing from `stewards`. */
187
193
  stewardReasons: { code: StewardReasonCode; message: string }[];
194
+ /**
195
+ * The member's promotion order (#4030): the literal entries of the
196
+ * `environments` array in its chant.config.ts, in order, each with whether
197
+ * it is marked canary. A release moves from each step to the next. `[]`
198
+ * when the config declares no environments, null when it could not be read.
199
+ * Added in contract 1 by chant 0.124.28.
200
+ */
201
+ promotionOrder: PromotionStep[] | null;
202
+ /**
203
+ * The member's release cuts (#4026, ws-104), oldest first, each with
204
+ * whether its signature verifies for its actor against the signers at
205
+ * base and, when the declaration names a `release` gate with a class,
206
+ * whether the actor is in it (#4031). `[]` when none is cut, null when the
207
+ * cuts could not be read. Added in contract 1 by chant 0.124.28.
208
+ */
209
+ releaseCuts: StatusReleaseCut[] | null;
210
+ }
211
+
212
+ /** One cut of a member's release (#4031), as `status --json` lists it. */
213
+ export interface StatusReleaseCut {
214
+ releaseDigest: string;
215
+ label: string | null;
216
+ /** The release's commit, or null when its release file can't be read. */
217
+ commit: string | null;
218
+ actor: string;
219
+ timestamp: string;
220
+ signature: string | null;
221
+ signatureCheck: ReleaseSignatureCheck;
188
222
  }
189
223
 
190
224
  /** The most paths `ship.pending.paths` lists; `files` still counts them all. */
@@ -318,6 +352,8 @@ export interface StatusQuery {
318
352
  now?: string;
319
353
  /** Reads one member's stewards; the declaration and ledger reader unless a test swaps it. */
320
354
  readStewards?: typeof readMemberStewards;
355
+ /** Reads one member's promotion order from its directory; its chant.config.ts unless a test swaps it. */
356
+ readPromotionOrder?: (dir: string) => Promise<PromotionStep[] | null>;
321
357
  }
322
358
 
323
359
  /** A factory view with the declaration's x- keys on it and on its check, publish and tiers (#3595). */
@@ -443,6 +479,8 @@ async function readEnvironment(member: Member, env: string, cwd: string, read: L
443
479
  runId: r.runId,
444
480
  timestamp: r.timestamp,
445
481
  actor: r.actor,
482
+ approver: r.approver ?? null,
483
+ admission: r.admission ? { op: r.admission.op, gate: r.admission.gate, resolvedAt: r.admission.resolvedAt } : null,
446
484
  flags: [...(r.flags ?? [])],
447
485
  plan: await readReleasePlan(r.digest, { cwd, prefix: planPrefix }),
448
486
  })),
@@ -506,6 +544,7 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
506
544
  // #3163: the declaration's identity.gates at base, read once for every member's gates.
507
545
  const ruleSource = scopeSource(found.dir);
508
546
  const rules = (gate: string) => gateAdmissionFrom(ruleSource, gate);
547
+ const releaseRule = releaseRuleFrom(ruleSource).rule;
509
548
  const members: StatusMember[] = [];
510
549
  for (const m of declaration.members) {
511
550
  const environments: StatusEnvironment[] = [];
@@ -553,6 +592,8 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
553
592
  },
554
593
  stewards: stewards.stewards,
555
594
  stewardReasons: stewards.reasons,
595
+ promotionOrder: await (query.readPromotionOrder ?? readMemberPromotionOrder)(resolve(found.dir, m.dir)),
596
+ releaseCuts: await readReleaseCuts(resolve(found.dir, m.dir), releaseRule),
556
597
  ...x(m.pointer),
557
598
  });
558
599
  }
@@ -595,6 +636,35 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
595
636
  }
596
637
  }
597
638
 
639
+ /**
640
+ * A member's release cuts with their signatures judged (#4031), read from
641
+ * its ledger on the local `chant/lifecycle`. Null when they can't be read.
642
+ */
643
+ async function readReleaseCuts(memberDir: string, rule: GateAdmissionRule): Promise<StatusReleaseCut[] | null> {
644
+ try {
645
+ const { readCuts, readRelease } = await import("../lifecycle/release-store");
646
+ const loc = { cwd: memberDir };
647
+ const { cuts } = await readCuts(loc);
648
+ const commits = new Map<string, string | null>();
649
+ const out: StatusReleaseCut[] = [];
650
+ for (const c of cuts) {
651
+ if (!commits.has(c.releaseDigest)) commits.set(c.releaseDigest, (await readRelease(c.releaseDigest, loc))?.commit ?? null);
652
+ out.push({
653
+ releaseDigest: c.releaseDigest,
654
+ label: c.label,
655
+ commit: commits.get(c.releaseDigest) ?? null,
656
+ actor: c.actor,
657
+ timestamp: c.timestamp,
658
+ signature: c.signature,
659
+ signatureCheck: checkReleaseSignature(rule, c),
660
+ });
661
+ }
662
+ return out;
663
+ } catch {
664
+ return null;
665
+ }
666
+ }
667
+
598
668
  /**
599
669
  * The work leases of the flat ledger and of each member's (#2732). Leases are
600
670
  * env-independent, so the environment asked for doesn't narrow them. Never
@@ -715,6 +785,15 @@ export function formatStatus(doc: Extract<StatusDocument, { members: unknown }>)
715
785
  for (const a of doc.acceptance) rows.push([a.member ? `${a.member}/${a.item}` : a.item, a.state ?? "-", `${a.met}/${a.total}`]);
716
786
  lines.push(...table(rows));
717
787
  }
788
+ const cut = doc.members.filter((m) => (m.releaseCuts ?? []).length > 0);
789
+ if (cut.length > 0) {
790
+ lines.push("");
791
+ const rows: string[][] = [["MEMBER", "RELEASE", "COMMIT", "CUT BY", "SIGNATURE"]];
792
+ for (const m of cut) {
793
+ for (const c of m.releaseCuts!) rows.push([m.name, c.label ?? c.releaseDigest.slice(0, 24), c.commit?.slice(0, 12) ?? "-", c.actor, c.signatureCheck.status]);
794
+ }
795
+ lines.push(...table(rows));
796
+ }
718
797
  const boxes = doc.members.filter((m) => m.box?.isolation);
719
798
  if (boxes.length > 0) {
720
799
  lines.push("");
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The statement a ci tag carries (#4031): a runner's signed word that a
3
+ * commit went green, or went red after it was green.
4
+ *
5
+ * `ci/green/<sha>` and `ci/revoked/<sha>` are plain git tags (ws-103), so
6
+ * anyone who may push a tag could make one by hand and go around CI. When the
7
+ * declaration sets `ci.green.signedSince` to a commit, a tag on any commit
8
+ * that is not a strict first-parent ancestor of it counts only when its
9
+ * annotation holds a statement signed by a runner key, and readers ignore
10
+ * and report a tag without a valid one.
11
+ *
12
+ * The statement reuses what runner evidence (#2553, ws-069) and the agent-run
13
+ * statement (#3192, ws-090) already use: an in-toto Statement v1 in a DSSE
14
+ * envelope (./dsse.ts), signed with an Ed25519 key that `.chant/trust.json`
15
+ * at base lists under `runners`. The key's principal must also hold the
16
+ * `runner` role there, since a tag says CI ran. A key the signers file lists
17
+ * is never a runner key (./policy.ts).
18
+ *
19
+ * - `subject`: the tagged commit, by `digest.gitCommit`.
20
+ * - `predicate`: the signer's principal and the tag's JSON record as the
21
+ * annotation holds it (`tag`, `commit`, `branch`, `at`, and the phases'
22
+ * runs for a green tag or the failed phase and run for a revoked one).
23
+ *
24
+ * The envelope sits in the annotation's JSON record as `statement`, beside
25
+ * the fields it signs, so a reader that does not know it reads the record as
26
+ * before. Verifying needs the tag, the envelope and the runner keys at base,
27
+ * so it works offline, and every read verifies again.
28
+ */
29
+
30
+ import { z } from "zod";
31
+ import { canonicalJson } from "../../effect-receipt";
32
+ import { IN_TOTO_PAYLOAD_TYPE, signEnvelope, verifyEnvelope, type DsseEnvelope } from "./dsse";
33
+ import { STATEMENT_TYPE } from "./evidence";
34
+ import type { KeyObject } from "node:crypto";
35
+ import type { RunnerKey } from "./policy";
36
+
37
+ export const CI_TAG_PREDICATE = "https://intentius.io/chant/ci-tag/v1";
38
+
39
+ /** The role a tag's signer must hold in `.chant/trust.json` (#4031): the core class `runner`. */
40
+ export const CI_TAG_SIGNER_ROLE = "runner";
41
+
42
+ const fullCommit = z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/);
43
+ const text = z.string().min(1);
44
+
45
+ /** The statement a ci tag carries. Strict at the top, so a verifier never acts on a field it does not know. */
46
+ export const ciTagStatementSchema = z
47
+ .object({
48
+ _type: z.literal(STATEMENT_TYPE),
49
+ subject: z
50
+ .array(
51
+ z
52
+ .object({ name: fullCommit, digest: z.object({ gitCommit: fullCommit }).strict() })
53
+ .strict()
54
+ .refine((s) => s.name === s.digest.gitCommit, "a commit subject is named by its own id"),
55
+ )
56
+ .length(1),
57
+ predicateType: z.literal(CI_TAG_PREDICATE),
58
+ predicate: z
59
+ .object({
60
+ signer: text,
61
+ record: z
62
+ .object({ schema: z.literal(1), tag: z.enum(["green", "revoked"]), commit: fullCommit, branch: text, at: text })
63
+ .passthrough(),
64
+ })
65
+ .strict(),
66
+ })
67
+ .strict();
68
+ export type CiTagStatement = z.infer<typeof ciTagStatementSchema>;
69
+
70
+ /** A tag's JSON record, without the statement. */
71
+ export type CiTagRecord = Record<string, unknown> & { tag: "green" | "revoked"; commit: string; branch: string };
72
+
73
+ /** Whoever signs the tags a tick makes: a runner key and the principal `.chant/trust.json` lists it for. */
74
+ export interface CiTagSigner {
75
+ principal: string;
76
+ key: KeyObject;
77
+ publicKey: string;
78
+ }
79
+
80
+ export function buildCiTagStatement(record: CiTagRecord, signer: string): CiTagStatement {
81
+ return {
82
+ _type: STATEMENT_TYPE,
83
+ subject: [{ name: record.commit, digest: { gitCommit: record.commit } }],
84
+ predicateType: CI_TAG_PREDICATE,
85
+ predicate: { signer, record: record as CiTagStatement["predicate"]["record"] },
86
+ };
87
+ }
88
+
89
+ /** The envelope a tag's record carries as `statement`, signed by `signer`. */
90
+ export function signCiTagRecord(record: CiTagRecord, signer: CiTagSigner): DsseEnvelope {
91
+ const statement = buildCiTagStatement(record, signer.principal);
92
+ return signEnvelope(IN_TOTO_PAYLOAD_TYPE, Buffer.from(canonicalJson(statement), "utf8"), signer.key, signer.publicKey);
93
+ }
94
+
95
+ /**
96
+ * Why a tag is ignored: it has no statement, the statement is not one, no
97
+ * runner key at base signed it, or it signs another tag's facts. The same
98
+ * words a run's attestation uses (`./run-statement.ts`).
99
+ */
100
+ export type CiTagFailure = "unsigned" | "invalid" | "untrusted" | "mismatch";
101
+
102
+ export type CiTagVerdict =
103
+ | { status: "signed"; signer: string; keyid: string; reason: string }
104
+ | { status: CiTagFailure; reason: string };
105
+
106
+ const normalise = (name: string): string => name.normalize("NFKC").trim().toLowerCase();
107
+
108
+ /**
109
+ * Verify the statement in a tag's record. `expect` is what the tag itself
110
+ * says: the commit its name gives, its kind and the declared branch. The
111
+ * signer's key must be a runner key at base and its principal must hold the
112
+ * `runner` role in `roles`. Never throws.
113
+ */
114
+ export function verifyCiTagStatement(
115
+ record: Record<string, unknown> | null,
116
+ expect: { sha: string; kind: "green" | "revoked"; branch: string },
117
+ runners: readonly RunnerKey[],
118
+ roles: Readonly<Record<string, readonly string[]>>,
119
+ ): CiTagVerdict {
120
+ const tag = `ci/${expect.kind}/${expect.sha}`;
121
+ if (record === null) return { status: "unsigned", reason: `${tag} has no JSON record in its annotation, so it carries no statement (a lightweight tag, or one made by hand)` };
122
+ const { statement: envelope, ...fields } = record;
123
+ if (envelope === undefined) return { status: "unsigned", reason: `${tag} carries no statement signed by a runner key` };
124
+ const v = verifyEnvelope(envelope, runners);
125
+ if (!v.ok) return { status: v.code === "envelope-untrusted" ? "untrusted" : "invalid", reason: `${tag}: ${v.reason}` };
126
+ if (!(roles[CI_TAG_SIGNER_ROLE] ?? []).some((p) => normalise(p) === normalise(v.principal))) {
127
+ return { status: "untrusted", reason: `${tag} is signed by ${v.principal}'s runner key, and ${v.principal} holds no ${CI_TAG_SIGNER_ROLE} role in .chant/trust.json at base` };
128
+ }
129
+ if (v.payloadType !== IN_TOTO_PAYLOAD_TYPE) return { status: "invalid", reason: `${tag}: payload type ${v.payloadType} is not ${IN_TOTO_PAYLOAD_TYPE}` };
130
+ let raw: unknown;
131
+ try {
132
+ raw = JSON.parse(v.payload.toString("utf8"));
133
+ } catch {
134
+ return { status: "invalid", reason: `${tag}: the statement's payload is not JSON` };
135
+ }
136
+ const parsed = ciTagStatementSchema.safeParse(raw);
137
+ if (!parsed.success) {
138
+ const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "/"}: ${i.message}`).join("; ");
139
+ return { status: "invalid", reason: `${tag}: the payload is not a ${CI_TAG_PREDICATE} statement: ${detail}` };
140
+ }
141
+ const p = parsed.data.predicate;
142
+ if (p.signer !== v.principal) return { status: "invalid", reason: `${tag}: the statement names signer ${JSON.stringify(p.signer)}, but ${v.principal}'s key signed it` };
143
+ const differs: string[] = [];
144
+ if (parsed.data.subject[0]!.name !== expect.sha) differs.push(`it is for commit ${parsed.data.subject[0]!.name.slice(0, 12)}`);
145
+ if (p.record.commit !== expect.sha) differs.push(`its record names commit ${p.record.commit.slice(0, 12)}`);
146
+ if (p.record.tag !== expect.kind) differs.push(`it signs a ${p.record.tag} tag`);
147
+ if (p.record.branch !== expect.branch) differs.push(`it is for branch ${p.record.branch}, and ci.green names ${expect.branch}`);
148
+ if (canonicalJson(p.record) !== canonicalJson(fields)) differs.push("the annotation's record is not the one signed");
149
+ if (differs.length > 0) return { status: "mismatch", reason: `${tag} is signed by ${v.principal}, but the statement does not match the tag: ${differs.join("; ")}` };
150
+ return { status: "signed", signer: v.principal, keyid: v.keyid, reason: `signed by ${v.principal} (${CI_TAG_SIGNER_ROLE})` };
151
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The Op-run statement (#4044): a runner's signed word that an Op ran on a
3
+ * commit and settled as its run record says.
4
+ *
5
+ * `chant ci tick` can take chant's own Op runs as its status source in place
6
+ * of a forge's check runs (`ci.green.source: "runs"`), so a project whose
7
+ * pipelines run as chant Ops gets green and revoked tags with no forge check
8
+ * runs. The run ledger on `chant/lifecycle` is written by whatever ran the
9
+ * Op, so the tick counts a run only when its record carries a statement
10
+ * signed by a runner key, the same keys and role a ci tag needs (#4031):
11
+ * `.chant/trust.json` at base lists the key under `runners`, and its
12
+ * principal holds the `runner` role there. A key the signers file lists is
13
+ * never a runner key (./policy.ts).
14
+ *
15
+ * The statement is an in-toto Statement v1 in a DSSE envelope (./dsse.ts),
16
+ * as runner evidence (#2553), the agent-run statement (#3192) and the ci tag
17
+ * statement (#4031) are:
18
+ *
19
+ * - `subject`: the commit the run built from (`gitSha`), by `digest.gitCommit`.
20
+ * - `predicate`: the signer's principal, and the run's id, Op, env, status
21
+ * and the SHA-256 of its record without the statement.
22
+ *
23
+ * The runner signs at the end of the run when it is given a key
24
+ * (`chant run <op> --key <file>` or `CHANT_RUNNER_KEY`), and the envelope is
25
+ * stored on the record itself as `statement`, so the record and its
26
+ * signature are one ledger line. Verifying needs the record and the runner
27
+ * keys at base, so it works offline, and every read verifies again.
28
+ */
29
+
30
+ import { createHash } from "node:crypto";
31
+ import { z } from "zod";
32
+ import { canonicalJson } from "../../effect-receipt";
33
+ import type { OpRunRecord } from "../../op/runtime";
34
+ import { IN_TOTO_PAYLOAD_TYPE, signEnvelope, verifyEnvelope, type DsseEnvelope } from "./dsse";
35
+ import { STATEMENT_TYPE } from "./evidence";
36
+ import { CI_TAG_SIGNER_ROLE, type CiTagFailure, type CiTagSigner } from "./ci-tag-statement";
37
+ import type { RunnerKey } from "./policy";
38
+
39
+ export const OP_RUN_PREDICATE = "https://intentius.io/chant/op-run/v1";
40
+
41
+ const fullCommit = z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/);
42
+ const text = z.string().min(1);
43
+
44
+ /** The statement a runner signs for an Op run. Strict, so a verifier never acts on a field it does not know. */
45
+ export const opRunStatementSchema = z
46
+ .object({
47
+ _type: z.literal(STATEMENT_TYPE),
48
+ subject: z
49
+ .array(
50
+ z
51
+ .object({ name: fullCommit, digest: z.object({ gitCommit: fullCommit }).strict() })
52
+ .strict()
53
+ .refine((s) => s.name === s.digest.gitCommit, "a commit subject is named by its own id"),
54
+ )
55
+ .length(1),
56
+ predicateType: z.literal(OP_RUN_PREDICATE),
57
+ predicate: z
58
+ .object({
59
+ signer: text,
60
+ run: z
61
+ .object({
62
+ id: text,
63
+ op: text,
64
+ env: text,
65
+ status: z.enum(["ok", "fail", "gated", "waiting"]),
66
+ record: z.object({ sha256: z.string().regex(/^[0-9a-f]{64}$/) }).strict(),
67
+ })
68
+ .strict(),
69
+ })
70
+ .strict(),
71
+ })
72
+ .strict();
73
+ export type OpRunStatement = z.infer<typeof opRunStatementSchema>;
74
+
75
+ /**
76
+ * The SHA-256 of a run record: the canonical JSON (keys sorted, no
77
+ * whitespace) of the record without its `statement`, so storing the
78
+ * statement never changes the hash it signs.
79
+ */
80
+ export function opRunRecordDigest(record: OpRunRecord): string {
81
+ const { statement: _statement, ...rest } = record;
82
+ return createHash("sha256").update(canonicalJson(rest), "utf8").digest("hex");
83
+ }
84
+
85
+ /** The statement for a settled run, as `signer` would sign it. Throws for a run that knew no commit. */
86
+ export function buildOpRunStatement(record: OpRunRecord, signer: string): OpRunStatement {
87
+ const sha = record.gitSha;
88
+ if (!sha || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(sha)) {
89
+ throw new Error(`run ${record.id} of ${record.op} names no full commit sha, so there is no commit for its statement to sign`);
90
+ }
91
+ return {
92
+ _type: STATEMENT_TYPE,
93
+ subject: [{ name: sha, digest: { gitCommit: sha } }],
94
+ predicateType: OP_RUN_PREDICATE,
95
+ predicate: { signer, run: { id: record.id, op: record.op, env: record.env, status: record.status, record: { sha256: opRunRecordDigest(record) } } },
96
+ };
97
+ }
98
+
99
+ /** The envelope a run record carries as `statement`, signed by `signer`. */
100
+ export function signOpRunRecord(record: OpRunRecord, signer: CiTagSigner): DsseEnvelope {
101
+ const statement = buildOpRunStatement(record, signer.principal);
102
+ return signEnvelope(IN_TOTO_PAYLOAD_TYPE, Buffer.from(canonicalJson(statement), "utf8"), signer.key, signer.publicKey);
103
+ }
104
+
105
+ /** One run record judged: signed by a runner holding the `runner` role, or why not, in the words a ci tag uses. */
106
+ export type OpRunVerdict = { status: "signed"; signer: string; keyid: string; reason: string } | { status: CiTagFailure; reason: string };
107
+
108
+ const normalise = (name: string): string => name.normalize("NFKC").trim().toLowerCase();
109
+
110
+ /**
111
+ * Verify the statement a run record carries against the runner keys and
112
+ * roles at base: a signature by a runner key whose principal holds the
113
+ * `runner` role, over a statement for this run, its commit and its record as
114
+ * the ledger holds it. Never throws.
115
+ */
116
+ export function verifyOpRunStatement(record: OpRunRecord, runners: readonly RunnerKey[], roles: Readonly<Record<string, readonly string[]>>): OpRunVerdict {
117
+ const run = `run ${record.id} of ${record.op}`;
118
+ if (record.statement === undefined) return { status: "unsigned", reason: `${run} carries no statement signed by a runner key` };
119
+ const v = verifyEnvelope(record.statement, runners);
120
+ if (!v.ok) return { status: v.code === "envelope-untrusted" ? "untrusted" : "invalid", reason: `${run}: ${v.reason}` };
121
+ if (!(roles[CI_TAG_SIGNER_ROLE] ?? []).some((p) => normalise(p) === normalise(v.principal))) {
122
+ return { status: "untrusted", reason: `${run} is signed by ${v.principal}'s runner key, and ${v.principal} holds no ${CI_TAG_SIGNER_ROLE} role in .chant/trust.json at base` };
123
+ }
124
+ if (v.payloadType !== IN_TOTO_PAYLOAD_TYPE) return { status: "invalid", reason: `${run}: payload type ${v.payloadType} is not ${IN_TOTO_PAYLOAD_TYPE}` };
125
+ let raw: unknown;
126
+ try {
127
+ raw = JSON.parse(v.payload.toString("utf8"));
128
+ } catch {
129
+ return { status: "invalid", reason: `${run}: the statement's payload is not JSON` };
130
+ }
131
+ const parsed = opRunStatementSchema.safeParse(raw);
132
+ if (!parsed.success) {
133
+ const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "/"}: ${i.message}`).join("; ");
134
+ return { status: "invalid", reason: `${run}: the payload is not a ${OP_RUN_PREDICATE} statement: ${detail}` };
135
+ }
136
+ const p = parsed.data.predicate;
137
+ if (p.signer !== v.principal) return { status: "invalid", reason: `${run}: the statement names signer ${JSON.stringify(p.signer)}, but ${v.principal}'s key signed it` };
138
+ const differs: string[] = [];
139
+ if (parsed.data.subject[0]!.name !== record.gitSha) differs.push(`it is for commit ${parsed.data.subject[0]!.name.slice(0, 12)}, and the record names ${record.gitSha ? record.gitSha.slice(0, 12) : "none"}`);
140
+ if (p.run.id !== record.id) differs.push(`it is for run ${p.run.id}`);
141
+ if (p.run.op !== record.op) differs.push(`it is for Op ${p.run.op}`);
142
+ if (p.run.env !== record.env) differs.push(`it is for env ${p.run.env}`);
143
+ if (p.run.status !== record.status) differs.push(`it says ${p.run.status}, and the record says ${record.status}`);
144
+ const digest = opRunRecordDigest(record);
145
+ if (p.run.record.sha256 !== digest) differs.push(`the record hashed ${p.run.record.sha256.slice(0, 12)} when signed and ${digest.slice(0, 12)} now`);
146
+ if (differs.length > 0) return { status: "mismatch", reason: `${run} is signed by ${v.principal}, but the statement does not match its record: ${differs.join("; ")}` };
147
+ return { status: "signed", signer: v.principal, keyid: v.keyid, reason: `signed by ${v.principal} (${CI_TAG_SIGNER_ROLE})` };
148
+ }
@@ -256,15 +256,24 @@ export function signerHistory(repo: string, base: string, signersPath: string):
256
256
  return { versions };
257
257
  }
258
258
 
259
+ /** A full commit id, as git prints it: lowercase hex, sha1 or sha256. */
260
+ const FULL_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
261
+
259
262
  /**
260
263
  * Finds the signer set in effect for a commit, by its position in the base's
261
- * history. Built once per base; the first-parent line is read once and
262
- * each lookup is a binary search.
264
+ * history. Built once per base. The first-parent line and the parents of
265
+ * every commit the base reaches are read once, in two `git rev-list` walks,
266
+ * and each lookup of a full commit id is then a map read (#4032). Before,
267
+ * each lookup ran `git merge-base --is-ancestor` about log2(n) times, and a
268
+ * trace over a busy file spent most of its time starting those processes. A
269
+ * commit named any other way (a ref, an abbreviated id) is still looked up
270
+ * with `git merge-base`, as before.
263
271
  */
264
272
  export class SignerPositions {
265
273
  private line: string[] | undefined;
266
274
  private index = new Map<string, number>();
267
275
  private memo = new Map<string, SignerVersion | null>();
276
+ private entries: Map<string, number> | undefined;
268
277
 
269
278
  constructor(
270
279
  private readonly repo: string,
@@ -291,21 +300,39 @@ export class SignerPositions {
291
300
  }
292
301
 
293
302
  /**
294
- * The version that judges `commit`, or null when no signer set was in
295
- * effect there (before the first one, or the chain is broken).
303
+ * For every commit the base reaches, the index on the first-parent line
304
+ * (newest first) of the oldest first-parent commit that contains it: where
305
+ * it came in. The line is walked oldest first, and each first-parent commit
306
+ * marks what it reaches that nothing older reached. A marked commit's
307
+ * ancestors are all marked, so the walk stops at one.
296
308
  */
297
- versionFor(commit: string): SignerVersion | null {
298
- const hit = this.memo.get(commit);
299
- if (hit !== undefined) return hit;
300
- const answer = this.lookup(commit);
301
- this.memo.set(commit, answer);
302
- return answer;
309
+ private entryIndex(): Map<string, number> {
310
+ if (!this.entries) {
311
+ const line = this.firstParents();
312
+ const parents = new Map<string, string[]>();
313
+ for (const row of git(this.repo, ["rev-list", "--parents", this.base]).split("\n")) {
314
+ const [commit, ...ps] = row.trim().split(" ");
315
+ if (commit) parents.set(commit, ps);
316
+ }
317
+ const entries = new Map<string, number>();
318
+ for (let i = line.length - 1; i >= 0; i--) {
319
+ const stack = [line[i]];
320
+ while (stack.length > 0) {
321
+ const c = stack.pop()!;
322
+ if (entries.has(c)) continue;
323
+ entries.set(c, i);
324
+ for (const p of parents.get(c) ?? []) if (!entries.has(p)) stack.push(p);
325
+ }
326
+ }
327
+ this.entries = entries;
328
+ }
329
+ return this.entries;
303
330
  }
304
331
 
305
- private lookup(commit: string): SignerVersion | null {
306
- if (this.history.broken) return null;
307
- const latest = this.history.versions.at(-1) ?? null;
308
- if (!this.isAncestor(commit, this.base)) return latest;
332
+ /** Where `commit` came in on the first-parent line, or null when the base doesn't reach it. */
333
+ private entryOf(commit: string): number | null {
334
+ if (FULL_ID.test(commit)) return this.entryIndex().get(commit) ?? null;
335
+ if (!this.isAncestor(commit, this.base)) return null;
309
336
  const line = this.firstParents();
310
337
  // The oldest first-parent commit that contains `commit` is where it came in.
311
338
  let lo = 0;
@@ -320,6 +347,26 @@ export class SignerPositions {
320
347
  hi = mid - 1;
321
348
  }
322
349
  }
350
+ return at;
351
+ }
352
+
353
+ /**
354
+ * The version that judges `commit`, or null when no signer set was in
355
+ * effect there (before the first one, or the chain is broken).
356
+ */
357
+ versionFor(commit: string): SignerVersion | null {
358
+ const hit = this.memo.get(commit);
359
+ if (hit !== undefined) return hit;
360
+ const answer = this.lookup(commit);
361
+ this.memo.set(commit, answer);
362
+ return answer;
363
+ }
364
+
365
+ private lookup(commit: string): SignerVersion | null {
366
+ if (this.history.broken) return null;
367
+ const latest = this.history.versions.at(-1) ?? null;
368
+ const at = this.entryOf(commit);
369
+ if (at === null) return latest;
323
370
  // The newest version made strictly before that commit.
324
371
  let best: SignerVersion | null = null;
325
372
  let bestIndex = Infinity;
@@ -31,6 +31,7 @@ import { type TrustPolicy } from "./policy";
31
31
  import type { Signer } from "./trust-files";
32
32
  import { sshKeygen, verifySshSignature } from "./ssh-commit";
33
33
  import { GATE_SEAL_NAMESPACE, gateSealPayload, namespaceAllowed, sshSignatureKey, verifySshSignatureWith, type SealedGateApproval } from "./gate-seal";
34
+ import { RELEASE_SEAL_NAMESPACE, releaseSealPayload } from "../identity-gate-rule";
34
35
 
35
36
  export { GATE_SEAL_NAMESPACE, gateSealPayload, gateSealVersion, sealedFieldsOf, type SealedGateApproval } from "./gate-seal";
36
37
 
@@ -321,6 +322,15 @@ export function sealGateApproval(keyFile: string, a: SealedGateApproval): Verdic
321
322
  return sign(keyFile, gateSealPayload(a), GATE_SEAL_NAMESPACE, a.resolvedBy);
322
323
  }
323
324
 
325
+ /**
326
+ * Sign a release cut (#4031) with the key in `keyFile`, over
327
+ * `releaseSealPayload` in the `chant-release` namespace. The cut line keeps
328
+ * the armored signature; its actor is the signer. Throws a {@link SealError}.
329
+ */
330
+ export function sealReleaseCut(keyFile: string, cut: { releaseDigest: string; label: string | null; actor: string; timestamp: string }): VerdictSeal {
331
+ return sign(keyFile, releaseSealPayload(cut), RELEASE_SEAL_NAMESPACE, cut.actor);
332
+ }
333
+
324
334
  function sign(keyFile: string, payload: Buffer, namespace: string, signer: string): VerdictSeal {
325
335
  const signed = sshKeygen(["-q", "-Y", "sign", "-n", namespace, "-f", keyFile], payload);
326
336
  if (signed.missing) throw new SealError("--sign needs ssh-keygen, and it is not installed here");
@@ -57,12 +57,14 @@ export function splitSignedCommit(raw: Buffer, header = "gpgsig"): SignedCommit
57
57
  return { payload, signature: sig.length > 0 ? sig.join("\n") + "\n" : undefined };
58
58
  }
59
59
 
60
- function objectFormat(repo: string): string {
61
- try {
62
- return execFileSync("git", ["rev-parse", "--show-object-format"], { cwd: repo, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
63
- } catch {
64
- return "sha1";
65
- }
60
+ /**
61
+ * The signature header of a raw commit object, by its object format, which
62
+ * shows in the length of its tree id. Read from the object itself, so judging
63
+ * a commit starts no `git rev-parse` (#4032).
64
+ */
65
+ function signatureHeader(raw: Buffer): "gpgsig" | "gpgsig-sha256" {
66
+ const tree = /^tree ([0-9a-f]+)$/m.exec(raw.subarray(0, 200).toString("latin1"))?.[1];
67
+ return tree?.length === 64 ? "gpgsig-sha256" : "gpgsig";
66
68
  }
67
69
 
68
70
  /** Run ssh-keygen; `missing` is true when it is not installed. Shared with the verdict seals (./seal.ts). */
@@ -117,14 +119,13 @@ export const sshCommitAttestor: CommitAttestor = {
117
119
  const hit = cache.get(memo);
118
120
  if (hit) return hit;
119
121
  const raw = execFileSync("git", ["cat-file", "commit", commit], { cwd: ctx.repo, maxBuffer: 64 * 1024 * 1024 });
120
- const answer = judgeRaw(raw, objectFormat(ctx.repo) === "sha256" ? "gpgsig-sha256" : "gpgsig", signers);
122
+ const answer = judgeRaw(raw, signatureHeader(raw), signers);
121
123
  cache.set(memo, answer);
122
124
  return answer;
123
125
  },
124
126
  attestRaw(ctx: AttestorContext, raw: Buffer): CommitAttestation {
125
127
  // A commit from another repository: its object format shows in its tree line.
126
- const tree = /^tree ([0-9a-f]+)$/m.exec(raw.subarray(0, 200).toString("latin1"))?.[1];
127
- return judgeRaw(raw, tree?.length === 64 ? "gpgsig-sha256" : "gpgsig", ctx.policy.signers);
128
+ return judgeRaw(raw, signatureHeader(raw), ctx.policy.signers);
128
129
  },
129
130
  };
130
131
 
@@ -1,14 +0,0 @@
1
- // @ts-check
2
- import { defineConfig } from 'astro/config';
3
- import starlight from '@astrojs/starlight';
4
-
5
- export default defineConfig({
6
- integrations: [
7
- starlight({
8
- title: 'Fixture',
9
- sidebar: [
10
- { label: 'Overview', slug: '' },
11
- ],
12
- }),
13
- ],
14
- });
@@ -1,16 +0,0 @@
1
- {
2
- "name": "@intentius/chant-lexicon-fixture-docs",
3
- "type": "module",
4
- "version": "0.0.1",
5
- "private": true,
6
- "scripts": {
7
- "dev": "astro dev",
8
- "build": "astro build",
9
- "preview": "astro preview"
10
- },
11
- "dependencies": {
12
- "@astrojs/starlight": "^0.37.6",
13
- "astro": "^5.6.1",
14
- "sharp": "^0.34.2"
15
- }
16
- }
@@ -1,8 +0,0 @@
1
- ---
2
- title: Overview
3
- description: Fixture lexicon for chant
4
- ---
5
-
6
- Welcome to the Fixture lexicon documentation.
7
-
8
- Run `just generate` to populate this site with generated reference pages.