@intentius/chant 0.124.28 → 0.124.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/dist/build-params.d.ts +34 -1
  2. package/dist/build-params.d.ts.map +1 -1
  3. package/dist/build-params.js +31 -1
  4. package/dist/change-set-graph.d.ts +8 -1
  5. package/dist/change-set-graph.d.ts.map +1 -1
  6. package/dist/change-set-graph.js +4 -2
  7. package/dist/change-set.d.ts +10 -0
  8. package/dist/change-set.d.ts.map +1 -1
  9. package/dist/change-set.js +4 -3
  10. package/dist/cli/build-params-cli.d.ts +7 -1
  11. package/dist/cli/build-params-cli.d.ts.map +1 -1
  12. package/dist/cli/build-params-cli.js +17 -8
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/build.js +6 -3
  15. package/dist/cli/handlers/approve.d.ts.map +1 -1
  16. package/dist/cli/handlers/approve.js +45 -0
  17. package/dist/cli/handlers/build.d.ts.map +1 -1
  18. package/dist/cli/handlers/build.js +15 -4
  19. package/dist/cli/handlers/fan-out.d.ts +9 -0
  20. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  21. package/dist/cli/handlers/fan-out.js +40 -3
  22. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  23. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  24. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  25. package/dist/cli/handlers/pr.d.ts +11 -3
  26. package/dist/cli/handlers/pr.d.ts.map +1 -1
  27. package/dist/cli/handlers/pr.js +134 -26
  28. package/dist/cli/handlers/promote.d.ts +7 -0
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/promote.js +210 -33
  31. package/dist/cli/handlers/release.d.ts +62 -5
  32. package/dist/cli/handlers/release.d.ts.map +1 -1
  33. package/dist/cli/handlers/release.js +414 -28
  34. package/dist/cli/handlers/run.d.ts +11 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +67 -6
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +61 -10
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +24 -2
  41. package/dist/cli/registry.d.ts +15 -3
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/run-events.d.ts +3 -2
  44. package/dist/cli/run-events.d.ts.map +1 -1
  45. package/dist/cli/run-events.js +5 -3
  46. package/dist/components/auto-release.d.ts +2 -0
  47. package/dist/components/auto-release.d.ts.map +1 -1
  48. package/dist/components/auto-release.js +1 -0
  49. package/dist/components/capability.d.ts +9 -0
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/driver.d.ts +8 -0
  52. package/dist/components/driver.d.ts.map +1 -1
  53. package/dist/components/driver.js +35 -0
  54. package/dist/components/fan-out-inputs.d.ts +69 -0
  55. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  56. package/dist/components/fan-out-inputs.js +169 -0
  57. package/dist/components/fan-out-record.d.ts +13 -0
  58. package/dist/components/fan-out-record.d.ts.map +1 -1
  59. package/dist/components/fan-out-record.js +16 -0
  60. package/dist/components/fan-out-run.d.ts.map +1 -1
  61. package/dist/components/fan-out-run.js +50 -0
  62. package/dist/components/fan-out.d.ts +4 -0
  63. package/dist/components/fan-out.d.ts.map +1 -1
  64. package/dist/components/pr-pipeline.d.ts +25 -0
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +22 -4
  67. package/dist/components/promote.d.ts +23 -0
  68. package/dist/components/promote.d.ts.map +1 -1
  69. package/dist/components/promote.js +60 -2
  70. package/dist/components/release-compile.d.ts +9 -0
  71. package/dist/components/release-compile.d.ts.map +1 -1
  72. package/dist/components/release-compile.js +9 -3
  73. package/dist/components/release-gate.d.ts +11 -1
  74. package/dist/components/release-gate.d.ts.map +1 -1
  75. package/dist/components/release-gate.js +10 -4
  76. package/dist/components/wave-plan.d.ts +5 -0
  77. package/dist/components/wave-plan.d.ts.map +1 -1
  78. package/dist/components/wave-plan.js +10 -2
  79. package/dist/config.d.ts +38 -0
  80. package/dist/config.d.ts.map +1 -1
  81. package/dist/config.js +2 -0
  82. package/dist/flag-params.d.ts +54 -0
  83. package/dist/flag-params.d.ts.map +1 -0
  84. package/dist/flag-params.js +130 -0
  85. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  86. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  87. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  88. package/dist/lifecycle/change-set-store.d.ts +2 -1
  89. package/dist/lifecycle/change-set-store.d.ts.map +1 -1
  90. package/dist/lifecycle/change-set-store.js +2 -1
  91. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  92. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  93. package/dist/lifecycle/delivery-metrics.js +145 -0
  94. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  95. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  96. package/dist/lifecycle/deploy-outcome.js +101 -0
  97. package/dist/lifecycle/gate-change-set.d.ts +10 -1
  98. package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
  99. package/dist/lifecycle/gate-change-set.js +35 -3
  100. package/dist/lifecycle/gate-ledger.d.ts +2 -3
  101. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  102. package/dist/lifecycle/git.d.ts +30 -1
  103. package/dist/lifecycle/git.d.ts.map +1 -1
  104. package/dist/lifecycle/git.js +31 -5
  105. package/dist/lifecycle/index.d.ts +4 -0
  106. package/dist/lifecycle/index.d.ts.map +1 -1
  107. package/dist/lifecycle/index.js +4 -0
  108. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  109. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  110. package/dist/lifecycle/mixed-environment.js +154 -0
  111. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  112. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  113. package/dist/lifecycle/publish-attachments.js +179 -0
  114. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  115. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  116. package/dist/lifecycle/publish-credentials.js +69 -0
  117. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  118. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  119. package/dist/lifecycle/publish-declaration.js +83 -0
  120. package/dist/lifecycle/publish-forge.d.ts +53 -0
  121. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  122. package/dist/lifecycle/publish-forge.js +298 -0
  123. package/dist/lifecycle/publish-npm.d.ts +61 -0
  124. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  125. package/dist/lifecycle/publish-npm.js +242 -0
  126. package/dist/lifecycle/publish-oci.d.ts +64 -0
  127. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  128. package/dist/lifecycle/publish-oci.js +383 -0
  129. package/dist/lifecycle/publish-record.d.ts +66 -0
  130. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  131. package/dist/lifecycle/publish-record.js +84 -0
  132. package/dist/lifecycle/publish-targets.d.ts +45 -0
  133. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  134. package/dist/lifecycle/publish-targets.js +85 -0
  135. package/dist/lifecycle/release-cut.d.ts +48 -1
  136. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  137. package/dist/lifecycle/release-cut.js +116 -23
  138. package/dist/lifecycle/release-join.d.ts +95 -0
  139. package/dist/lifecycle/release-join.d.ts.map +1 -0
  140. package/dist/lifecycle/release-join.js +139 -0
  141. package/dist/lifecycle/release-publish.d.ts +312 -0
  142. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  143. package/dist/lifecycle/release-publish.js +335 -0
  144. package/dist/lifecycle/release-revocation.d.ts +80 -0
  145. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  146. package/dist/lifecycle/release-revocation.js +123 -0
  147. package/dist/lifecycle/release-store.d.ts +12 -2
  148. package/dist/lifecycle/release-store.d.ts.map +1 -1
  149. package/dist/lifecycle/release-store.js +4 -2
  150. package/dist/lifecycle/release-verify.d.ts +61 -0
  151. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  152. package/dist/lifecycle/release-verify.js +92 -0
  153. package/dist/lifecycle/release-workstation.d.ts +91 -0
  154. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  155. package/dist/lifecycle/release-workstation.js +180 -0
  156. package/dist/lifecycle/retention-policy.d.ts +45 -0
  157. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  158. package/dist/lifecycle/retention-policy.js +43 -0
  159. package/dist/lifecycle/retention.d.ts +185 -0
  160. package/dist/lifecycle/retention.d.ts.map +1 -0
  161. package/dist/lifecycle/retention.js +400 -0
  162. package/dist/plan-summary.d.ts.map +1 -1
  163. package/dist/plan-summary.js +8 -2
  164. package/dist/pr-loop.d.ts +7 -0
  165. package/dist/pr-loop.d.ts.map +1 -1
  166. package/dist/pr-loop.js +23 -4
  167. package/dist/pr-report-url.d.ts +82 -0
  168. package/dist/pr-report-url.d.ts.map +1 -0
  169. package/dist/pr-report-url.js +174 -0
  170. package/dist/provenance.d.ts +2 -2
  171. package/dist/provenance.d.ts.map +1 -1
  172. package/dist/run-event-store.d.ts +29 -2
  173. package/dist/run-event-store.d.ts.map +1 -1
  174. package/dist/run-event-store.js +50 -3
  175. package/dist/run-event-stream.d.ts +2 -1
  176. package/dist/run-event-stream.d.ts.map +1 -1
  177. package/dist/run-event-stream.js +5 -1
  178. package/dist/run-event.d.ts +19 -1
  179. package/dist/run-event.d.ts.map +1 -1
  180. package/dist/run-event.js +2 -0
  181. package/dist/wave-runtime.mjs +260 -229
  182. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  183. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  184. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  185. package/dist/workspace/change-set.schema.json +5 -1
  186. package/dist/workspace/check.schema.json +4 -2
  187. package/dist/workspace/checks/records.d.ts +9 -1
  188. package/dist/workspace/checks/records.d.ts.map +1 -1
  189. package/dist/workspace/checks/records.js +35 -1
  190. package/dist/workspace/checks.d.ts +1 -0
  191. package/dist/workspace/checks.d.ts.map +1 -1
  192. package/dist/workspace/checks.js +2 -1
  193. package/dist/workspace/ci-green.d.ts +8 -0
  194. package/dist/workspace/ci-green.d.ts.map +1 -1
  195. package/dist/workspace/ci-green.js +10 -3
  196. package/dist/workspace/declaration.d.ts +16 -0
  197. package/dist/workspace/declaration.d.ts.map +1 -1
  198. package/dist/workspace/declaration.js +26 -0
  199. package/dist/workspace/declaration.schema.json +64 -0
  200. package/dist/workspace/declared-kinds.d.ts +16 -0
  201. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  202. package/dist/workspace/declared-kinds.js +25 -0
  203. package/dist/workspace/delivery-metrics.d.ts +91 -0
  204. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  205. package/dist/workspace/delivery-metrics.js +217 -0
  206. package/dist/workspace/delivery-metrics.schema.json +519 -0
  207. package/dist/workspace/flags-read.d.ts +37 -0
  208. package/dist/workspace/flags-read.d.ts.map +1 -0
  209. package/dist/workspace/flags-read.js +103 -0
  210. package/dist/workspace/flags.d.ts +100 -0
  211. package/dist/workspace/flags.d.ts.map +1 -0
  212. package/dist/workspace/flags.js +98 -0
  213. package/dist/workspace/graph-cli.js +1 -1
  214. package/dist/workspace/intent-cli.d.ts +11 -2
  215. package/dist/workspace/intent-cli.d.ts.map +1 -1
  216. package/dist/workspace/intent-cli.js +54 -5
  217. package/dist/workspace/intent-trace.d.ts +219 -0
  218. package/dist/workspace/intent-trace.d.ts.map +1 -0
  219. package/dist/workspace/intent-trace.js +314 -0
  220. package/dist/workspace/intent.d.ts +30 -6
  221. package/dist/workspace/intent.d.ts.map +1 -1
  222. package/dist/workspace/intent.js +141 -21
  223. package/dist/workspace/intent.schema.json +702 -6
  224. package/dist/workspace/overlap.d.ts +146 -0
  225. package/dist/workspace/overlap.d.ts.map +1 -0
  226. package/dist/workspace/overlap.js +276 -0
  227. package/dist/workspace/reason-codes.d.ts +2 -0
  228. package/dist/workspace/reason-codes.d.ts.map +1 -1
  229. package/dist/workspace/reason-codes.js +3 -0
  230. package/dist/workspace/records.d.ts +15 -2
  231. package/dist/workspace/records.d.ts.map +1 -1
  232. package/dist/workspace/records.js +32 -0
  233. package/dist/workspace/records.schema.json +21 -2
  234. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  235. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  236. package/dist/workspace/releases.d.ts +9 -0
  237. package/dist/workspace/releases.d.ts.map +1 -1
  238. package/dist/workspace/releases.js +9 -0
  239. package/dist/workspace/releases.schema.json +123 -1
  240. package/dist/workspace/run-event.schema.json +10 -3
  241. package/dist/workspace/status-gates.d.ts +8 -0
  242. package/dist/workspace/status-gates.d.ts.map +1 -1
  243. package/dist/workspace/status.d.ts +78 -0
  244. package/dist/workspace/status.d.ts.map +1 -1
  245. package/dist/workspace/status.js +170 -3
  246. package/dist/workspace/status.schema.json +198 -1
  247. package/dist/workspace/wip.d.ts.map +1 -1
  248. package/dist/workspace/wip.js +11 -3
  249. package/package.json +1 -1
  250. package/src/build-params.test.ts +46 -0
  251. package/src/build-params.ts +55 -1
  252. package/src/change-set-graph.ts +10 -2
  253. package/src/change-set.ts +14 -3
  254. package/src/cli/build-params-cli.ts +22 -9
  255. package/src/cli/commands/build.ts +6 -3
  256. package/src/cli/handlers/approve-release.test.ts +5 -1
  257. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  258. package/src/cli/handlers/approve.ts +47 -0
  259. package/src/cli/handlers/build.test.ts +15 -0
  260. package/src/cli/handlers/build.ts +17 -4
  261. package/src/cli/handlers/fan-out.test.ts +180 -1
  262. package/src/cli/handlers/fan-out.ts +38 -3
  263. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  264. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  265. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  266. package/src/cli/handlers/pr.test.ts +148 -1
  267. package/src/cli/handlers/pr.ts +139 -28
  268. package/src/cli/handlers/promote.test.ts +280 -2
  269. package/src/cli/handlers/promote.ts +210 -34
  270. package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
  271. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  272. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  273. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  274. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  275. package/src/cli/handlers/release.e2e.test.ts +146 -32
  276. package/src/cli/handlers/release.ts +415 -28
  277. package/src/cli/handlers/run-auto-release.test.ts +9 -0
  278. package/src/cli/handlers/run-release.test.ts +55 -0
  279. package/src/cli/handlers/run.ts +69 -5
  280. package/src/cli/main.test.ts +1 -0
  281. package/src/cli/main.ts +61 -10
  282. package/src/cli/parse-args.ts +19 -2
  283. package/src/cli/registry.ts +15 -3
  284. package/src/cli/run-events.ts +5 -2
  285. package/src/components/auto-release.test.ts +9 -0
  286. package/src/components/auto-release.ts +3 -0
  287. package/src/components/capability.ts +9 -0
  288. package/src/components/driver.test.ts +24 -0
  289. package/src/components/driver.ts +36 -0
  290. package/src/components/fan-out-inputs.test.ts +85 -0
  291. package/src/components/fan-out-inputs.ts +192 -0
  292. package/src/components/fan-out-record.test.ts +10 -0
  293. package/src/components/fan-out-record.ts +19 -0
  294. package/src/components/fan-out-run.ts +48 -0
  295. package/src/components/fan-out.ts +4 -0
  296. package/src/components/pr-pipeline.ts +35 -4
  297. package/src/components/promote.ts +68 -2
  298. package/src/components/release-compile.ts +17 -3
  299. package/src/components/release-gate.ts +18 -4
  300. package/src/components/wave-plan.ts +15 -2
  301. package/src/config.ts +8 -0
  302. package/src/flag-params.test.ts +144 -0
  303. package/src/flag-params.ts +134 -0
  304. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  305. package/src/lifecycle/change-set-store.ts +2 -1
  306. package/src/lifecycle/delivery-metrics.ts +263 -0
  307. package/src/lifecycle/deploy-outcome.ts +147 -0
  308. package/src/lifecycle/gate-change-set.ts +37 -6
  309. package/src/lifecycle/gate-ledger.ts +2 -2
  310. package/src/lifecycle/git.ts +61 -8
  311. package/src/lifecycle/index.ts +4 -0
  312. package/src/lifecycle/mixed-environment.test.ts +97 -0
  313. package/src/lifecycle/mixed-environment.ts +204 -0
  314. package/src/lifecycle/publish-attachments.ts +196 -0
  315. package/src/lifecycle/publish-credentials.ts +84 -0
  316. package/src/lifecycle/publish-declaration.ts +95 -0
  317. package/src/lifecycle/publish-forge.ts +325 -0
  318. package/src/lifecycle/publish-npm.ts +259 -0
  319. package/src/lifecycle/publish-oci.ts +396 -0
  320. package/src/lifecycle/publish-record.ts +113 -0
  321. package/src/lifecycle/publish-resume.test.ts +168 -0
  322. package/src/lifecycle/publish-targets.test.ts +494 -0
  323. package/src/lifecycle/publish-targets.ts +100 -0
  324. package/src/lifecycle/release-cut.test.ts +74 -2
  325. package/src/lifecycle/release-cut.ts +158 -22
  326. package/src/lifecycle/release-flag.test.ts +112 -0
  327. package/src/lifecycle/release-join.test.ts +68 -0
  328. package/src/lifecycle/release-join.ts +182 -0
  329. package/src/lifecycle/release-publish.test.ts +225 -0
  330. package/src/lifecycle/release-publish.ts +568 -0
  331. package/src/lifecycle/release-revocation.ts +147 -0
  332. package/src/lifecycle/release-store.ts +15 -4
  333. package/src/lifecycle/release-verify.test.ts +110 -0
  334. package/src/lifecycle/release-verify.ts +120 -0
  335. package/src/lifecycle/release-workstation.test.ts +204 -0
  336. package/src/lifecycle/release-workstation.ts +216 -0
  337. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  338. package/src/lifecycle/retention-policy.ts +63 -0
  339. package/src/lifecycle/retention.test.ts +363 -0
  340. package/src/lifecycle/retention.ts +508 -0
  341. package/src/plan-summary.test.ts +20 -0
  342. package/src/plan-summary.ts +8 -1
  343. package/src/pr-loop.ts +25 -4
  344. package/src/pr-report-url.test.ts +131 -0
  345. package/src/pr-report-url.ts +209 -0
  346. package/src/provenance.ts +2 -2
  347. package/src/run-event-store.ts +63 -3
  348. package/src/run-event-stream.ts +4 -1
  349. package/src/run-event.ts +21 -1
  350. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  351. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  352. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  353. package/src/workspace/change-set.schema.json +5 -1
  354. package/src/workspace/check.schema.json +4 -2
  355. package/src/workspace/checks/records.ts +35 -1
  356. package/src/workspace/checks.ts +2 -1
  357. package/src/workspace/ci-green.ts +14 -3
  358. package/src/workspace/declaration.schema.json +64 -0
  359. package/src/workspace/declaration.test.ts +28 -0
  360. package/src/workspace/declaration.ts +38 -0
  361. package/src/workspace/declared-kinds.ts +40 -1
  362. package/src/workspace/delivery-metrics.schema.json +519 -0
  363. package/src/workspace/delivery-metrics.test.ts +236 -0
  364. package/src/workspace/delivery-metrics.ts +286 -0
  365. package/src/workspace/flags-read.ts +121 -0
  366. package/src/workspace/flags.test.ts +141 -0
  367. package/src/workspace/flags.ts +157 -0
  368. package/src/workspace/graph-cli.ts +1 -1
  369. package/src/workspace/intent-cli.ts +46 -5
  370. package/src/workspace/intent-trace.test.ts +173 -0
  371. package/src/workspace/intent-trace.ts +484 -0
  372. package/src/workspace/intent.schema.json +702 -6
  373. package/src/workspace/intent.ts +148 -25
  374. package/src/workspace/older-records.test.ts +412 -0
  375. package/src/workspace/overlap.test.ts +165 -0
  376. package/src/workspace/overlap.ts +340 -0
  377. package/src/workspace/read-contract.e2e.test.ts +2 -1
  378. package/src/workspace/reason-codes.test.ts +5 -0
  379. package/src/workspace/reason-codes.ts +3 -0
  380. package/src/workspace/record-assets.test.ts +2 -1
  381. package/src/workspace/records-contract.e2e.test.ts +2 -1
  382. package/src/workspace/records.schema.json +21 -2
  383. package/src/workspace/records.ts +36 -2
  384. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  385. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  386. package/src/workspace/releases.schema.json +123 -1
  387. package/src/workspace/releases.ts +11 -0
  388. package/src/workspace/run-event.schema.json +10 -3
  389. package/src/workspace/status-contract.test.ts +29 -0
  390. package/src/workspace/status-gates.ts +8 -0
  391. package/src/workspace/status-mixed.test.ts +152 -0
  392. package/src/workspace/status-revoked.test.ts +153 -0
  393. package/src/workspace/status.schema.json +198 -1
  394. package/src/workspace/status.ts +254 -3
  395. package/src/workspace/wip.ts +11 -2
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `--report-url` (#4007): the URL forms, the key, and the upload of a run's
3
+ * events as it goes: one upload in flight at a time, a failure that warns
4
+ * and never stops the run, and a last upload that carries everything. The
5
+ * s3 scheme runs a fake AWS CLI; nothing here reaches a network.
6
+ */
7
+
8
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { pathToFileURL } from "node:url";
12
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
13
+ import { openPrReportUpload, parseReportUrl, reportKey, reportPut, type ReportPut } from "./pr-report-url";
14
+
15
+ const line = (type: string, extra: Record<string, unknown> = {}): string => JSON.stringify({ type, ...extra }) + "\n";
16
+
17
+ let dir: string;
18
+ beforeEach(async () => {
19
+ dir = await mkdtemp(join(tmpdir(), "pr-report-url-"));
20
+ });
21
+ afterEach(async () => {
22
+ await rm(dir, { recursive: true, force: true });
23
+ });
24
+
25
+ describe("parseReportUrl", () => {
26
+ test("reads s3 and file URLs and refuses anything else", () => {
27
+ expect(parseReportUrl("s3://bucket/a/b/")).toEqual({ scheme: "s3", url: "s3://bucket/a/b", bucket: "bucket", prefix: "a/b" });
28
+ expect(parseReportUrl("s3://bucket")).toEqual({ scheme: "s3", url: "s3://bucket", bucket: "bucket", prefix: "" });
29
+ expect(parseReportUrl(pathToFileURL(dir).href)).toMatchObject({ scheme: "file", dir });
30
+ expect(parseReportUrl("https://example.invalid/x")).toHaveProperty("error");
31
+ });
32
+
33
+ test("keys by pull request and commit, and by member after them", () => {
34
+ expect(reportKey(12, "abc")).toBe("12/abc");
35
+ expect(reportKey(null, "abc")).toBe("unknown/abc");
36
+ expect(reportKey(12, "abc", "api")).toBe("12/abc/api");
37
+ });
38
+ });
39
+
40
+ describe("openPrReportUpload", () => {
41
+ /** A put whose uploads finish only when the test says, recording what each carried. */
42
+ function heldPut() {
43
+ const calls: Array<{ key: string; body: string; finish: (err?: Error) => void }> = [];
44
+ let inFlight = 0;
45
+ let most = 0;
46
+ const put: ReportPut = (key, body) =>
47
+ new Promise<void>((resolve, reject) => {
48
+ inFlight++;
49
+ most = Math.max(most, inFlight);
50
+ calls.push({ key, body, finish: (err) => { inFlight--; if (err) reject(err); else resolve(); } });
51
+ });
52
+ return { put, calls, most: () => most };
53
+ }
54
+
55
+ test("uploads the events on each settle, never two at once, and the settles that came meanwhile in one more", async () => {
56
+ const held = heldPut();
57
+ const upload = openPrReportUpload({ target: { scheme: "s3", url: "s3://b", bucket: "b", prefix: "" }, key: "12/abc", stage: "apply", warn: () => {}, put: held.put });
58
+ upload.line(line("run-start"));
59
+ expect(held.calls).toHaveLength(0);
60
+ upload.line(line("wave-start", { wave: 1 }));
61
+ upload.line(line("component-settle", { wave: 1 }));
62
+ expect(held.calls).toHaveLength(1);
63
+ upload.line(line("wave-settle", { wave: 1 }));
64
+ upload.line(line("wave-start", { wave: 2 }));
65
+ upload.line(line("component-settle", { wave: 2 }));
66
+ // Both settles wait behind the one in flight, and go up as one.
67
+ expect(held.calls).toHaveLength(1);
68
+ held.calls[0]!.finish();
69
+ await new Promise((r) => setImmediate(r));
70
+ expect(held.calls).toHaveLength(2);
71
+ expect(held.calls[1]!.body.split("\n").filter(Boolean)).toHaveLength(6);
72
+ expect(held.calls[1]!.key).toBe("12/abc/pr-apply.events.jsonl");
73
+
74
+ upload.file("pr-apply.json", "{}\n");
75
+ const closing = upload.close();
76
+ await new Promise((r) => setImmediate(r));
77
+ // The last upload waits for the one in flight.
78
+ expect(held.calls).toHaveLength(2);
79
+ held.calls[1]!.finish();
80
+ await new Promise((r) => setImmediate(r));
81
+ held.calls[2]!.finish();
82
+ await new Promise((r) => setImmediate(r));
83
+ held.calls[3]!.finish();
84
+ await closing;
85
+ expect(held.calls.map((c) => c.key)).toEqual(["12/abc/pr-apply.events.jsonl", "12/abc/pr-apply.events.jsonl", "12/abc/pr-apply.events.jsonl", "12/abc/pr-apply.json"]);
86
+ expect(held.most()).toBe(1);
87
+ });
88
+
89
+ test("a failed upload as the run goes warns once and the run goes on; the last one is tried again", async () => {
90
+ const warnings: string[] = [];
91
+ let fail = true;
92
+ const put: ReportPut = async (key) => {
93
+ if (fail) throw new Error(`no bucket for ${key}`);
94
+ };
95
+ const upload = openPrReportUpload({ target: { scheme: "s3", url: "s3://b", bucket: "b", prefix: "" }, key: "12/abc", stage: "apply", warn: (m) => warnings.push(m), put });
96
+ upload.line(line("component-settle"));
97
+ await new Promise((r) => setImmediate(r));
98
+ upload.line(line("wave-settle"));
99
+ await new Promise((r) => setImmediate(r));
100
+ expect(warnings).toHaveLength(1);
101
+ expect(warnings[0]).toMatch(/as the run went: no bucket .*The run goes on/);
102
+ fail = false;
103
+ await upload.close();
104
+ expect(warnings).toHaveLength(1);
105
+ });
106
+
107
+ test("file://: each settle replaces the events file whole, so it holds a wave's settle before the next wave starts", async () => {
108
+ const target = parseReportUrl(pathToFileURL(dir).href);
109
+ if ("error" in target) throw new Error(target.error);
110
+ const upload = openPrReportUpload({ target, key: "12/abc", stage: "apply", warn: () => {} });
111
+ upload.line(line("run-start"));
112
+ upload.line(line("wave-settle", { wave: 1 }));
113
+ await new Promise((r) => setImmediate(r));
114
+ const events = join(dir, "12", "abc", "pr-apply.events.jsonl");
115
+ expect(await readFile(events, "utf8")).toBe(line("run-start") + line("wave-settle", { wave: 1 }));
116
+ upload.line(line("run-settle"));
117
+ upload.file("change-set.json", "{}\n");
118
+ await upload.close();
119
+ expect((await readFile(events, "utf8")).split("\n").filter(Boolean)).toHaveLength(3);
120
+ expect(await readFile(join(dir, "12", "abc", "change-set.json"), "utf8")).toBe("{}\n");
121
+ });
122
+
123
+ test("s3://: the AWS CLI copies each object from its stdin, with the content type, and chant hands it no credential", async () => {
124
+ const runs: Array<{ args: string[]; stdin: string }> = [];
125
+ const put = reportPut({ scheme: "s3", url: "s3://b/p", bucket: "b", prefix: "p" }, async (args, stdin) => {
126
+ runs.push({ args, stdin });
127
+ });
128
+ await put("12/abc/pr-plan.json", "{}\n", "application/json");
129
+ expect(runs).toEqual([{ args: ["s3", "cp", "-", "s3://b/p/12/abc/pr-plan.json", "--content-type", "application/json", "--only-show-errors"], stdin: "{}\n" }]);
130
+ });
131
+ });
@@ -0,0 +1,209 @@
1
+ /**
2
+ * `pr-plan --report-url` and `pr-apply --report-url` (#4007): copy a stage's
3
+ * report, its change set and its run events where a reader outside CI can
4
+ * fetch them, keyed by pull request and commit.
5
+ *
6
+ * ```
7
+ * <url>/<pr>/<commit>/pr-<stage>.json
8
+ * <url>/<pr>/<commit>/change-set.json
9
+ * <url>/<pr>/<commit>/pr-<stage>.events.jsonl (with --events-json)
10
+ * ```
11
+ *
12
+ * `<pr>` is the pull or merge request number, or `unknown` when pr-apply
13
+ * found none, and `<commit>` is the full sha the stage ran on: the pull
14
+ * request's head for pr-plan, the pushed commit for pr-apply. A workspace
15
+ * member's loop (`--member`) adds `/<member>` after the commit. There is no
16
+ * index: a reader that knows the pull request and the commit knows the key.
17
+ * The events file is named for its stage because pr-plan and pr-apply run on
18
+ * the same commit when a pull request merges by fast-forward.
19
+ *
20
+ * The events file is uploaded again, whole, each time a wave, a component or
21
+ * the run settles, so a reader polling the URL sees each wave as it applies.
22
+ * One upload of it is in flight at a time: a settle while one is in flight
23
+ * asks for one more after it, with everything written by then. A failed
24
+ * upload is a warning and the run goes on. The last upload, after the run
25
+ * settles, carries the whole stream, and the report and the change set go
26
+ * with it.
27
+ *
28
+ * Two schemes. `file://<dir>` writes under a directory, each file through a
29
+ * temporary name and a rename, so a reader never sees half a file. `s3://
30
+ * <bucket>/<prefix>` runs the AWS CLI (`aws s3 cp - <s3 url>`, the body on
31
+ * its stdin), which reads its credentials, region and endpoint from the
32
+ * environment the job gives it, the way the aws lexicon's `s3-sync` does.
33
+ * chant passes it no credential and prints none: a failed upload prints the
34
+ * CLI's own error line.
35
+ */
36
+
37
+ import { spawn } from "node:child_process";
38
+ import { mkdirSync, renameSync, writeFileSync } from "node:fs";
39
+ import { dirname, isAbsolute, join } from "node:path";
40
+ import { fileURLToPath } from "node:url";
41
+
42
+ /** Where `--report-url` points. */
43
+ export type ReportTarget =
44
+ | { scheme: "file"; url: string; dir: string }
45
+ | { scheme: "s3"; url: string; bucket: string; prefix: string };
46
+
47
+ /** `--report-url` read, or why it can't be. */
48
+ export function parseReportUrl(raw: string): ReportTarget | { error: string } {
49
+ const url = raw.trim();
50
+ if (url.startsWith("file://")) {
51
+ let dir: string;
52
+ try {
53
+ dir = fileURLToPath(url);
54
+ } catch (err) {
55
+ return { error: `--report-url ${url}: ${err instanceof Error ? err.message : String(err)}` };
56
+ }
57
+ if (!isAbsolute(dir)) return { error: `--report-url ${url}: a file URL names an absolute directory` };
58
+ return { scheme: "file", url: url.replace(/\/+$/, ""), dir };
59
+ }
60
+ const s3 = /^s3:\/\/([^/]+)\/?(.*)$/.exec(url);
61
+ if (s3) {
62
+ const prefix = s3[2]!.replace(/\/+$/, "");
63
+ return { scheme: "s3", url: `s3://${s3[1]}${prefix ? `/${prefix}` : ""}`, bucket: s3[1]!, prefix };
64
+ }
65
+ return { error: `--report-url ${url}: takes an s3://<bucket>/<prefix> or file://<directory> URL` };
66
+ }
67
+
68
+ /** The key a stage's files are kept under, below the URL: `<pr>/<commit>[/<member>]`. */
69
+ export function reportKey(pr: number | null, commit: string, member?: string): string {
70
+ return [pr === null ? "unknown" : String(pr), commit, ...(member ? [member] : [])].join("/");
71
+ }
72
+
73
+ /** The events file of a stage. */
74
+ export const reportEventsFile = (stage: "plan" | "apply"): string => `pr-${stage}.events.jsonl`;
75
+
76
+ /** Puts one object: `key` below the URL. Rejects with a line a person can read. */
77
+ export type ReportPut = (key: string, body: string, contentType: string) => Promise<void>;
78
+
79
+ /** Runs the AWS CLI with `args` and `stdin`, rejecting with its error line on a non-zero exit. */
80
+ export type AwsCli = (args: string[], stdin: string) => Promise<void>;
81
+
82
+ const AWS_BINARY = "aws";
83
+
84
+ /** The AWS CLI on PATH. */
85
+ export const realAwsCli: AwsCli = (args, stdin) =>
86
+ new Promise((resolvePut, reject) => {
87
+ const child = spawn(AWS_BINARY, args, { stdio: ["pipe", "ignore", "pipe"] });
88
+ let stderr = "";
89
+ child.stderr.on("data", (chunk: Buffer) => {
90
+ if (stderr.length < 4096) stderr += chunk.toString("utf8");
91
+ });
92
+ child.on("error", (err: NodeJS.ErrnoException) =>
93
+ reject(new Error(err.code === "ENOENT" ? "the AWS CLI (aws) is not on PATH; an s3 report URL is uploaded with it" : err.message)),
94
+ );
95
+ child.on("close", (code) => {
96
+ if (code === 0) return resolvePut();
97
+ const line = stderr.trim().split("\n").filter(Boolean).at(-1) ?? "";
98
+ reject(new Error(`aws s3 cp exited ${code}${line ? `: ${line.slice(0, 300)}` : ""}`));
99
+ });
100
+ child.stdin.on("error", () => {
101
+ // The CLI exited before reading its input; its exit says why.
102
+ });
103
+ child.stdin.end(stdin);
104
+ });
105
+
106
+ /** A put for `target`. `aws` replaces the AWS CLI in a test. */
107
+ export function reportPut(target: ReportTarget, aws: AwsCli = realAwsCli): ReportPut {
108
+ if (target.scheme === "file") {
109
+ return async (key, body) => {
110
+ const path = join(target.dir, ...key.split("/"));
111
+ mkdirSync(dirname(path), { recursive: true });
112
+ const temp = `${path}.${process.pid}.tmp`;
113
+ writeFileSync(temp, body);
114
+ renameSync(temp, path);
115
+ };
116
+ }
117
+ return (key, body, contentType) => {
118
+ const object = `s3://${target.bucket}/${target.prefix ? `${target.prefix}/` : ""}${key}`;
119
+ return aws(["s3", "cp", "-", object, "--content-type", contentType, "--only-show-errors"], body);
120
+ };
121
+ }
122
+
123
+ /** A stage's upload: its events as they are written, and its files at the end. */
124
+ export interface PrReportUpload {
125
+ /** The folder the stage's files go to: `<url>/<key>`. */
126
+ readonly where: string;
127
+ /** One line of the stage's run-event stream. A wave, component or run settle uploads the events. */
128
+ line(line: string): void;
129
+ /** A file to upload when the stage closes, replacing one of the same name. */
130
+ file(name: string, body: string): void;
131
+ /** Wait for the upload in flight, then upload the whole stream and the files. Never throws. */
132
+ close(): Promise<void>;
133
+ }
134
+
135
+ const UPLOAD_ON = new Set(["wave-settle", "component-settle", "run-settle"]);
136
+ const NDJSON = "application/x-ndjson";
137
+ const JSON_TYPE = "application/json";
138
+
139
+ const messageOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));
140
+
141
+ /** Open a stage's upload to `target` under `key`. `warn` prints a warning line. */
142
+ export function openPrReportUpload(input: {
143
+ target: ReportTarget;
144
+ key: string;
145
+ stage: "plan" | "apply";
146
+ warn: (message: string) => void;
147
+ put?: ReportPut;
148
+ }): PrReportUpload {
149
+ const put = input.put ?? reportPut(input.target);
150
+ const where = `${input.target.url}/${input.key}`;
151
+ const eventsKey = `${input.key}/${reportEventsFile(input.stage)}`;
152
+ const files = new Map<string, string>();
153
+ let events = "";
154
+ let inFlight: Promise<void> | undefined;
155
+ let again = false;
156
+ let warned = false;
157
+
158
+ const pushEvents = (): void => {
159
+ if (inFlight) {
160
+ again = true;
161
+ return;
162
+ }
163
+ inFlight = put(eventsKey, events, NDJSON)
164
+ .catch((err) => {
165
+ if (warned) return;
166
+ warned = true;
167
+ input.warn(`could not upload the run's events to ${where} as the run went: ${messageOf(err)}. The run goes on, and its end tries again.`);
168
+ })
169
+ .finally(() => {
170
+ inFlight = undefined;
171
+ if (again) {
172
+ again = false;
173
+ pushEvents();
174
+ }
175
+ });
176
+ };
177
+
178
+ return {
179
+ where,
180
+ line(line) {
181
+ events += line;
182
+ let type: unknown;
183
+ try {
184
+ type = (JSON.parse(line) as { type?: unknown }).type;
185
+ } catch {
186
+ return;
187
+ }
188
+ if (typeof type === "string" && UPLOAD_ON.has(type)) pushEvents();
189
+ },
190
+ file(name, body) {
191
+ files.set(name, body);
192
+ },
193
+ async close() {
194
+ again = false;
195
+ while (inFlight) await inFlight;
196
+ const uploads: Array<[string, string, string]> = [
197
+ ...(events ? [[eventsKey, events, NDJSON] as [string, string, string]] : []),
198
+ ...[...files].map(([name, body]): [string, string, string] => [`${input.key}/${name}`, body, JSON_TYPE]),
199
+ ];
200
+ for (const [key, body, type] of uploads) {
201
+ try {
202
+ await put(key, body, type);
203
+ } catch (err) {
204
+ input.warn(`could not upload ${key.slice(input.key.length + 1)} to ${where}: ${messageOf(err)}`);
205
+ }
206
+ }
207
+ },
208
+ };
209
+ }
package/src/provenance.ts CHANGED
@@ -297,6 +297,6 @@ export interface BuildParamProvenance {
297
297
  name: string;
298
298
  /** The resolved, type-coerced value actually bound to `params.<name>` for this build. */
299
299
  value: string | number | boolean;
300
- /** Which input supplied the value. */
301
- source: "cli" | "params-file" | "env" | "default";
300
+ /** Which input supplied the value: `flag:<name>` names the workspace flag record it came from (#4024). */
301
+ source: "cli" | "params-file" | "env" | "default" | `flag:${string}`;
302
302
  }
@@ -8,13 +8,14 @@
8
8
  * as `--events-json` prints them. It sits beside the in-flight records
9
9
  * (`./op/run-live.ts`), outside the working tree and off `chant/lifecycle`:
10
10
  * the run ledger's growth is #4036's concern and these files add none to it.
11
- * Unlike the in-flight record they are kept after the run settles. They are
12
- * never pruned here; retention is #4036's.
11
+ * Unlike the in-flight record they are kept after the run settles, until
12
+ * `chant lifecycle gc --yes` deletes the ones its retention policy lets go
13
+ * (#4036, {@link planRunEventPrune}).
13
14
  *
14
15
  * Every write is best-effort. A run whose events cannot be kept still runs.
15
16
  */
16
17
 
17
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
18
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
18
19
  import { join } from "node:path";
19
20
  import { gitCommonDir } from "./op/run-live";
20
21
  import type { RunEvent } from "./run-event";
@@ -211,3 +212,62 @@ export async function readRunEvents(
211
212
  }
212
213
  }
213
214
  }
215
+
216
+ /** One run's event file, as retention sees it (#4036). */
217
+ export interface RunEventFile {
218
+ file: string;
219
+ bytes: number;
220
+ /** The file's last write, ISO-8601. */
221
+ modified: string;
222
+ }
223
+
224
+ /** What {@link planRunEventPrune} would delete and keep. */
225
+ export interface RunEventPrune {
226
+ /** The directory read, or null outside a checkout. */
227
+ dir: string | null;
228
+ remove: RunEventFile[];
229
+ keep: RunEventFile[];
230
+ }
231
+
232
+ /**
233
+ * Which run event files the policy lets go (#4036): every file older than
234
+ * `days` by its last write, apart from the newest `keep`. A run still going
235
+ * writes its file as it goes, so it is never older than the window. Reads
236
+ * only; {@link pruneRunEvents} deletes.
237
+ */
238
+ export async function planRunEventPrune(cwd: string, policy: { days: number; keep: number }, now: Date): Promise<RunEventPrune> {
239
+ const dir = await runEventsDir(cwd);
240
+ if (!dir || !existsSync(dir)) return { dir, remove: [], keep: [] };
241
+ const files: Array<RunEventFile & { ms: number }> = [];
242
+ for (const entry of readdirSync(dir)) {
243
+ if (!entry.endsWith(".jsonl")) continue;
244
+ const file = join(dir, entry);
245
+ try {
246
+ const st = statSync(file);
247
+ if (!st.isFile()) continue;
248
+ files.push({ file, bytes: st.size, modified: new Date(st.mtimeMs).toISOString(), ms: st.mtimeMs });
249
+ } catch {
250
+ // Gone between the listing and the stat.
251
+ }
252
+ }
253
+ files.sort((a, b) => b.ms - a.ms || (a.file < b.file ? -1 : 1));
254
+ const cutoff = now.getTime() - policy.days * 86_400_000;
255
+ const remove: RunEventFile[] = [];
256
+ const keep: RunEventFile[] = [];
257
+ files.forEach(({ ms, ...f }, i) => (i >= policy.keep && ms < cutoff ? remove : keep).push(f));
258
+ return { dir, remove, keep };
259
+ }
260
+
261
+ /** Delete the files a plan lets go. Returns how many were deleted; a file already gone is not counted. */
262
+ export function pruneRunEvents(plan: RunEventPrune): number {
263
+ let n = 0;
264
+ for (const f of plan.remove) {
265
+ try {
266
+ rmSync(f.file);
267
+ n++;
268
+ } catch {
269
+ // Already gone.
270
+ }
271
+ }
272
+ return n;
273
+ }
@@ -46,7 +46,8 @@ const GATE_PREFIX = "gate:";
46
46
  /**
47
47
  * A run-event body as the older `RunProgressEvent`, or undefined when that
48
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
49
+ * a phase outside a component, a gate's decision or a component the run did
50
+ * not start (`blocked`, `already-applied`, #4007), so those map to nothing
50
51
  * and `--progress-json` prints what it printed before #4041.
51
52
  */
52
53
  export function progressOfRunEvent(body: RunEventBody): RunProgressEvent | undefined {
@@ -79,6 +80,8 @@ export function progressOfRunEvent(body: RunEventBody): RunProgressEvent | undef
79
80
  ? undefined
80
81
  : { type: "phase-done", component: body.component, phase: body.phase, status: progressStatus(body.status) };
81
82
  case "component-settle":
83
+ // A component the run did not start (#4007) was never in the older stream.
84
+ if (body.status === "blocked" || body.status === "already-applied") return undefined;
82
85
  return { type: "component-done", wave: body.wave, component: body.component, status: progressStatus(body.status) };
83
86
  case "wave-settle":
84
87
  return { type: "wave-done", wave: body.wave, status: progressStatus(body.status) };
package/src/run-event.ts CHANGED
@@ -126,11 +126,31 @@ export interface RunEventComponentStart {
126
126
  component: string;
127
127
  }
128
128
 
129
+ /**
130
+ * How a component settled: a {@link RunEventSettleStatus}, or one of two
131
+ * statuses for a component the run did not start (#4007), added within
132
+ * contract 1 by chant 0.124.29. `blocked`: a dependency failed or stopped at
133
+ * a gate, so it never ran. `already-applied`: an earlier attempt that this
134
+ * run resumed applied it. Neither has a `component-start` before it. A reader
135
+ * that knows only the four settle statuses reads either as a component that
136
+ * did not run in this run.
137
+ */
138
+ export type RunEventComponentSettleStatus = RunEventSettleStatus | "blocked" | "already-applied";
139
+
140
+ /** The first chant that writes `blocked`, `already-applied` and `blockedBy` on `component-settle` (#4007). */
141
+ export const RUN_EVENT_COMPONENT_SKIP_FLOOR = "0.124.29";
142
+
129
143
  export interface RunEventComponentSettle {
130
144
  type: "component-settle";
145
+ /**
146
+ * 1-based, into `run-start`'s `waves`. On an `already-applied` settle it is
147
+ * the wave the component had in the derivation the earlier attempt ran.
148
+ */
131
149
  wave: number;
132
150
  component: string;
133
- status: RunEventSettleStatus;
151
+ status: RunEventComponentSettleStatus;
152
+ /** With `blocked`: the failed or gated component the run reached this one from. */
153
+ blockedBy?: string;
134
154
  }
135
155
 
136
156
  /** An Op's phase, a component composition's phase, or a pipeline's stage. `component` is null outside a component run. */
@@ -0,0 +1,132 @@
1
+ /**
2
+ * A workspace with feature flags (#4024), for the flag tests: a throwaway git
3
+ * repository with a decision kind, the reference workspace's work kind, and
4
+ * the canonical flag kind chant ships at `reference-kinds/flag`.
5
+ *
6
+ * - The decision kind closes `confirmed` and `superseded`. d-001 is
7
+ * confirmed, d-002 is only decided.
8
+ * - W-001 implements d-001, so its work is settled. W-002 implements d-002.
9
+ * - The flag `old-banner` hides W-001, so it is due for removal (stale).
10
+ * - The flag `new-checkout` hides W-002 and backs the build parameter
11
+ * `newCheckout` of member `shop`: on in dev, off in prod, and no value in
12
+ * staging.
13
+ * - Member `shop` has a component whose deploy declares a gate, so every
14
+ * environment it builds for has one. Member `blog` has a component with
15
+ * no gate.
16
+ */
17
+
18
+ import { readFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { git, repo } from "./contract-repo";
21
+
22
+ const HERE = import.meta.dirname;
23
+ const REPO_ROOT = join(HERE, "..", "..", "..", "..", "..");
24
+ const REF = join(REPO_ROOT, "reference-workspace");
25
+ const ref = (path: string) => readFileSync(join(REF, path), "utf-8");
26
+ const canonical = (file: string) => readFileSync(join(HERE, "..", "reference-kinds", "flag", file), "utf-8");
27
+
28
+ const DECISION_KIND = `export const recordKind = {
29
+ name: "decision",
30
+ location: { dir: ".", match: "^d-[0-9]{3}-.+\\\\.md$" },
31
+ format: "markdown-front-matter",
32
+ schema: { id: "urn:test:decision:1", path: "decision.schema.json" },
33
+ idField: "id",
34
+ stateField: "state",
35
+ states: ["proposed", "decided", "confirmed", "superseded"],
36
+ closedStates: ["confirmed", "superseded"],
37
+ approval: { proposed: 0, decided: 1, confirmed: 2, superseded: 2 },
38
+ supersedes: { field: "supersedes" },
39
+ };
40
+ `;
41
+
42
+ const DECISION_SCHEMA = JSON.stringify(
43
+ {
44
+ $schema: "http://json-schema.org/draft-07/schema#",
45
+ $id: "urn:test:decision:1",
46
+ type: "object",
47
+ required: ["id", "state"],
48
+ properties: { id: { type: "string" }, state: { type: "string" }, supersedes: { type: "array", items: { type: "string" } } },
49
+ },
50
+ null,
51
+ 2,
52
+ );
53
+
54
+ const decision = (id: string, state: string) => `---\nid: "${id}"\nstate: "${state}"\nsupersedes: []\n---\n\n# ${id}\n`;
55
+
56
+ export function workItem(id: string, implementsIds: string[], state = "in-progress"): string {
57
+ const data = {
58
+ schema: 1,
59
+ id,
60
+ title: `Work ${id}`,
61
+ state,
62
+ implements: implementsIds,
63
+ needs: [],
64
+ constrains: ["member:shop"],
65
+ evidence: [],
66
+ owner: "alice",
67
+ opened_on: "2026-10-01",
68
+ ...(state === "done" ? { closed_on: "2026-10-10" } : {}),
69
+ source: { kind: "workspace", member: "shop" },
70
+ supersedes: [],
71
+ };
72
+ return `---\n${JSON.stringify(data, null, 2)}\n---\n\n# ${id}\n`;
73
+ }
74
+
75
+ export function flagRecord(id: string, fields: { work: string; member: string; param: string; values: Record<string, string | number | boolean>; owner?: string }): string {
76
+ const data = { schema: 1, id, owner: fields.owner ?? "alice", work: fields.work, member: fields.member, param: fields.param, values: fields.values };
77
+ return `---\n${JSON.stringify(data, null, 2)}\n---\n\n# ${id}\n`;
78
+ }
79
+
80
+ /** A component file whose deploy declares a gate when `gated` is set. */
81
+ export function componentFile(name: string, gated: boolean): string {
82
+ const steps = gated ? `[{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/${name}.json" }]` : `[{ kind: "apply", template: "dist/${name}.json" }]`;
83
+ return `export const ${name} = { name: "${name}", dependsOn: [], deploy: [{ phase: "Apply", steps: ${steps} }] };\n`;
84
+ }
85
+
86
+ /** The flag workspace's files, with `overrides` laid over them (a null value removes a file). */
87
+ export function flagWorkspaceFiles(overrides: Record<string, string | null> = {}): Record<string, string> {
88
+ const files: Record<string, string | null> = {
89
+ "chant.workspace.json": JSON.stringify(
90
+ {
91
+ name: "flags",
92
+ schema: 1,
93
+ members: [
94
+ { name: "shop", dir: "shop", kind: "chant" },
95
+ { name: "blog", dir: "blog", kind: "chant" },
96
+ ],
97
+ records: [{ kind: "decisions/decision.kind.mjs" }, { kind: "work/work.kind.mjs" }, { kind: "flags/flag.kind.mjs" }],
98
+ },
99
+ null,
100
+ 2,
101
+ ),
102
+ "shop/chant.config.ts": `export default { environments: ["dev", "staging", "prod"] };\n`,
103
+ "shop/shop.component.ts": componentFile("shop", true),
104
+ "blog/chant.config.ts": `export default { environments: ["dev", "prod"] };\n`,
105
+ "blog/blog.component.ts": componentFile("blog", false),
106
+ "decisions/decision.kind.mjs": DECISION_KIND,
107
+ "decisions/decision.schema.json": DECISION_SCHEMA,
108
+ "decisions/d-001-the-banner.md": decision("d-001", "confirmed"),
109
+ "decisions/d-002-the-checkout.md": decision("d-002", "decided"),
110
+ "work/work.kind.mjs": ref("work/work.kind.mjs").replace(' answers: "../answers/answer.kind.mjs",\n', ""),
111
+ "work/work.schema.json": ref("work/work.schema.json"),
112
+ "work/W-001-the-banner.md": workItem("W-001", ["d-001"], "done"),
113
+ "work/W-002-the-checkout.md": workItem("W-002", ["d-002"]),
114
+ "flags/flag.kind.mjs": canonical("flag.kind.mjs"),
115
+ "flags/flag.schema.json": canonical("flag.schema.json"),
116
+ "flags/old-banner.flag.md": flagRecord("old-banner", { work: "W-001", member: "blog", param: "banner", values: { dev: true, prod: true } }),
117
+ "flags/new-checkout.flag.md": flagRecord("new-checkout", { work: "W-002", member: "shop", param: "newCheckout", values: { dev: true, prod: false } }),
118
+ ...overrides,
119
+ };
120
+ return Object.fromEntries(Object.entries(files).filter((e): e is [string, string] => e[1] !== null));
121
+ }
122
+
123
+ /** The flag workspace in a new git repository, committed. */
124
+ export function flagWorkspace(overrides: Record<string, string | null> = {}): string {
125
+ const root = repo(flagWorkspaceFiles(overrides));
126
+ git(root, "config", "user.name", "t");
127
+ git(root, "config", "user.email", "t@example.com");
128
+ git(root, "config", "commit.gpgsign", "false");
129
+ git(root, "add", "-A");
130
+ git(root, "commit", "-q", "-m", "flags");
131
+ return root;
132
+ }