@intentius/chant 0.102.0 → 0.104.1

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 (471) hide show
  1. package/dist/audit/core.d.ts +2 -0
  2. package/dist/audit/core.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/change-set.d.ts +232 -0
  5. package/dist/change-set.d.ts.map +1 -0
  6. package/dist/cli/commands/audit.d.ts +7 -0
  7. package/dist/cli/commands/audit.d.ts.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/handlers/build.d.ts.map +1 -1
  10. package/dist/cli/handlers/change-set.d.ts +11 -0
  11. package/dist/cli/handlers/change-set.d.ts.map +1 -0
  12. package/dist/cli/handlers/fan-out.d.ts +33 -0
  13. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  14. package/dist/cli/handlers/operator.d.ts +7 -0
  15. package/dist/cli/handlers/operator.d.ts.map +1 -1
  16. package/dist/cli/handlers/pr.d.ts +31 -0
  17. package/dist/cli/handlers/pr.d.ts.map +1 -0
  18. package/dist/cli/handlers/run.d.ts.map +1 -1
  19. package/dist/cli/main.d.ts.map +1 -1
  20. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  21. package/dist/cli/mcp/workspace-tools.d.ts +3 -2
  22. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  23. package/dist/cli/registry.d.ts +63 -1
  24. package/dist/cli/registry.d.ts.map +1 -1
  25. package/dist/components/auto-release.d.ts +16 -0
  26. package/dist/components/auto-release.d.ts.map +1 -1
  27. package/dist/components/capability.d.ts +53 -0
  28. package/dist/components/capability.d.ts.map +1 -1
  29. package/dist/components/changed-files.d.ts +31 -0
  30. package/dist/components/changed-files.d.ts.map +1 -0
  31. package/dist/components/cli-support.d.ts +9 -1
  32. package/dist/components/cli-support.d.ts.map +1 -1
  33. package/dist/components/component.d.ts +24 -0
  34. package/dist/components/component.d.ts.map +1 -1
  35. package/dist/components/deploy-units.d.ts.map +1 -1
  36. package/dist/components/driver.d.ts +13 -0
  37. package/dist/components/driver.d.ts.map +1 -1
  38. package/dist/components/fan-out-output.d.ts +6 -0
  39. package/dist/components/fan-out-output.d.ts.map +1 -1
  40. package/dist/components/fan-out-record.d.ts +41 -0
  41. package/dist/components/fan-out-record.d.ts.map +1 -0
  42. package/dist/components/fan-out-run.d.ts +65 -0
  43. package/dist/components/fan-out-run.d.ts.map +1 -1
  44. package/dist/components/fan-out-support.d.ts +2 -0
  45. package/dist/components/fan-out-support.d.ts.map +1 -1
  46. package/dist/components/fan-out.d.ts +6 -0
  47. package/dist/components/fan-out.d.ts.map +1 -1
  48. package/dist/components/gated-wave-pipeline.d.ts +50 -0
  49. package/dist/components/gated-wave-pipeline.d.ts.map +1 -0
  50. package/dist/components/index.d.ts +1 -1
  51. package/dist/components/index.d.ts.map +1 -1
  52. package/dist/components/layers.d.ts +25 -0
  53. package/dist/components/layers.d.ts.map +1 -0
  54. package/dist/components/pr-pipeline.d.ts +61 -0
  55. package/dist/components/pr-pipeline.d.ts.map +1 -0
  56. package/dist/components/pr-run.d.ts +119 -0
  57. package/dist/components/pr-run.d.ts.map +1 -0
  58. package/dist/components/promote.d.ts.map +1 -1
  59. package/dist/components/verbs/shell.d.ts.map +1 -1
  60. package/dist/components/wave-plan.d.ts +49 -0
  61. package/dist/components/wave-plan.d.ts.map +1 -0
  62. package/dist/gated-waves.d.ts +147 -0
  63. package/dist/gated-waves.d.ts.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/lexicon.d.ts +48 -0
  67. package/dist/lexicon.d.ts.map +1 -1
  68. package/dist/lifecycle/gate-ledger.d.ts +7 -0
  69. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  70. package/dist/lifecycle/git.d.ts +46 -1
  71. package/dist/lifecycle/git.d.ts.map +1 -1
  72. package/dist/lint/post-synth.d.ts +18 -0
  73. package/dist/lint/post-synth.d.ts.map +1 -1
  74. package/dist/op/activities/activity-contracts.d.ts +126 -0
  75. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  76. package/dist/op/activities/change-set.d.ts +62 -0
  77. package/dist/op/activities/change-set.d.ts.map +1 -0
  78. package/dist/op/activities/decide.d.ts +2 -0
  79. package/dist/op/activities/decide.d.ts.map +1 -1
  80. package/dist/op/activities/factory.d.ts +137 -0
  81. package/dist/op/activities/factory.d.ts.map +1 -0
  82. package/dist/op/activities/index.d.ts +4 -0
  83. package/dist/op/activities/index.d.ts.map +1 -1
  84. package/dist/op/activity-contract.d.ts.map +1 -1
  85. package/dist/op/builders.d.ts +11 -0
  86. package/dist/op/builders.d.ts.map +1 -1
  87. package/dist/op/decide-backend.d.ts +5 -1
  88. package/dist/op/decide-backend.d.ts.map +1 -1
  89. package/dist/op/decide-read-inputs.d.ts +4 -2
  90. package/dist/op/decide-read-inputs.d.ts.map +1 -1
  91. package/dist/op/factory.d.ts +83 -0
  92. package/dist/op/factory.d.ts.map +1 -0
  93. package/dist/op/index.d.ts +4 -1
  94. package/dist/op/index.d.ts.map +1 -1
  95. package/dist/op/op-ir.d.ts +2 -0
  96. package/dist/op/op-ir.d.ts.map +1 -1
  97. package/dist/op/op-verb-class.d.ts.map +1 -1
  98. package/dist/op/types.d.ts +8 -0
  99. package/dist/op/types.d.ts.map +1 -1
  100. package/dist/op/work-lease-run.d.ts.map +1 -1
  101. package/dist/plan-summary.d.ts +207 -0
  102. package/dist/plan-summary.d.ts.map +1 -0
  103. package/dist/pr-forge.d.ts +105 -0
  104. package/dist/pr-forge.d.ts.map +1 -0
  105. package/dist/pr-loop.d.ts +178 -0
  106. package/dist/pr-loop.d.ts.map +1 -0
  107. package/dist/telemetry-attribution.d.ts +31 -0
  108. package/dist/telemetry-attribution.d.ts.map +1 -1
  109. package/dist/terraform/hcl2json.d.ts +45 -0
  110. package/dist/terraform/hcl2json.d.ts.map +1 -0
  111. package/dist/terraform/parse.d.ts +4 -40
  112. package/dist/terraform/parse.d.ts.map +1 -1
  113. package/dist/vendor/js-yaml-load.d.ts +56 -0
  114. package/dist/vendor/js-yaml-load.d.ts.map +1 -0
  115. package/dist/workspace/box-cli.d.ts +12 -0
  116. package/dist/workspace/box-cli.d.ts.map +1 -0
  117. package/dist/workspace/box-factory.d.ts +27 -3
  118. package/dist/workspace/box-factory.d.ts.map +1 -1
  119. package/dist/workspace/box-listing.d.ts +85 -0
  120. package/dist/workspace/box-listing.d.ts.map +1 -0
  121. package/dist/workspace/box-publish.d.ts +147 -0
  122. package/dist/workspace/box-publish.d.ts.map +1 -0
  123. package/dist/workspace/broker-protocol.d.ts +190 -0
  124. package/dist/workspace/broker-protocol.d.ts.map +1 -0
  125. package/dist/workspace/checks.d.ts.map +1 -1
  126. package/dist/workspace/composites.d.ts +2 -2
  127. package/dist/workspace/composites.d.ts.map +1 -1
  128. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
  129. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
  130. package/dist/workspace/conformance/broker.d.ts +202 -0
  131. package/dist/workspace/conformance/broker.d.ts.map +1 -0
  132. package/dist/workspace/conformance/index.d.ts +2 -1
  133. package/dist/workspace/conformance/index.d.ts.map +1 -1
  134. package/dist/workspace/conformance/vitest.d.ts +9 -2
  135. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  136. package/dist/workspace/conformance/writer.d.ts +108 -14
  137. package/dist/workspace/conformance/writer.d.ts.map +1 -1
  138. package/dist/workspace/decide.d.ts +126 -12
  139. package/dist/workspace/decide.d.ts.map +1 -1
  140. package/dist/workspace/decision-points.schema.json +11 -4
  141. package/dist/workspace/declaration.d.ts +65 -0
  142. package/dist/workspace/declaration.d.ts.map +1 -1
  143. package/dist/workspace/declaration.schema.json +153 -4
  144. package/dist/workspace/environments.d.ts +36 -10
  145. package/dist/workspace/environments.d.ts.map +1 -1
  146. package/dist/workspace/factory-rules.d.ts +180 -0
  147. package/dist/workspace/factory-rules.d.ts.map +1 -0
  148. package/dist/workspace/init.d.ts +21 -0
  149. package/dist/workspace/init.d.ts.map +1 -1
  150. package/dist/workspace/intent-cli.d.ts +1 -1
  151. package/dist/workspace/intent-cli.d.ts.map +1 -1
  152. package/dist/workspace/intent-record.d.ts +28 -1
  153. package/dist/workspace/intent-record.d.ts.map +1 -1
  154. package/dist/workspace/intent.d.ts +102 -5
  155. package/dist/workspace/intent.d.ts.map +1 -1
  156. package/dist/workspace/json-edit.d.ts +24 -0
  157. package/dist/workspace/json-edit.d.ts.map +1 -0
  158. package/dist/workspace/jsonc.d.ts +11 -0
  159. package/dist/workspace/jsonc.d.ts.map +1 -1
  160. package/dist/workspace/kinds.d.ts +48 -0
  161. package/dist/workspace/kinds.d.ts.map +1 -1
  162. package/dist/workspace/lexicon-symbols.d.ts +29 -0
  163. package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
  164. package/dist/workspace/lineage-upgrade.d.ts +31 -2
  165. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  166. package/dist/workspace/point-answer.schema.json +42 -2
  167. package/dist/workspace/points-cli.d.ts +10 -3
  168. package/dist/workspace/points-cli.d.ts.map +1 -1
  169. package/dist/workspace/points.d.ts +66 -5
  170. package/dist/workspace/points.d.ts.map +1 -1
  171. package/dist/workspace/reason-codes.d.ts +33 -4
  172. package/dist/workspace/reason-codes.d.ts.map +1 -1
  173. package/dist/workspace/record-sessions.d.ts +5 -7
  174. package/dist/workspace/record-sessions.d.ts.map +1 -1
  175. package/dist/workspace/records-cli.d.ts +7 -0
  176. package/dist/workspace/records-cli.d.ts.map +1 -1
  177. package/dist/workspace/records-close.d.ts +7 -3
  178. package/dist/workspace/records-close.d.ts.map +1 -1
  179. package/dist/workspace/records-write.d.ts +66 -6
  180. package/dist/workspace/records-write.d.ts.map +1 -1
  181. package/dist/workspace/records.d.ts +28 -9
  182. package/dist/workspace/records.d.ts.map +1 -1
  183. package/dist/workspace/runs-cli.d.ts +138 -6
  184. package/dist/workspace/runs-cli.d.ts.map +1 -1
  185. package/dist/workspace/runs.d.ts +132 -11
  186. package/dist/workspace/runs.d.ts.map +1 -1
  187. package/dist/workspace/session-kinds.d.ts +1 -1
  188. package/dist/workspace/session-kinds.d.ts.map +1 -1
  189. package/dist/workspace/squash.d.ts +80 -0
  190. package/dist/workspace/squash.d.ts.map +1 -0
  191. package/dist/workspace/status-gates.d.ts +2 -0
  192. package/dist/workspace/status-gates.d.ts.map +1 -1
  193. package/dist/workspace/status.d.ts +54 -0
  194. package/dist/workspace/status.d.ts.map +1 -1
  195. package/dist/workspace/symbols.d.ts +16 -5
  196. package/dist/workspace/symbols.d.ts.map +1 -1
  197. package/dist/workspace/trust/run-commits.d.ts +46 -0
  198. package/dist/workspace/trust/run-commits.d.ts.map +1 -0
  199. package/dist/workspace/trust/run-statement.d.ts +128 -0
  200. package/dist/workspace/trust/run-statement.d.ts.map +1 -0
  201. package/dist/workspace/trust/seal.d.ts +6 -1
  202. package/dist/workspace/trust/seal.d.ts.map +1 -1
  203. package/dist/workspace/trust/verify-cli.d.ts +1 -1
  204. package/dist/workspace/trust/verify.d.ts +16 -2
  205. package/dist/workspace/trust/verify.d.ts.map +1 -1
  206. package/dist/workspace/wip-cli.d.ts +21 -0
  207. package/dist/workspace/wip-cli.d.ts.map +1 -0
  208. package/dist/workspace/wip.d.ts +268 -0
  209. package/dist/workspace/wip.d.ts.map +1 -0
  210. package/dist/workspace/work-cli.d.ts.map +1 -1
  211. package/dist/workspace/work-evidence.d.ts +1 -1
  212. package/dist/workspace/work-evidence.d.ts.map +1 -1
  213. package/dist/workspace/work-size.d.ts +55 -0
  214. package/dist/workspace/work-size.d.ts.map +1 -0
  215. package/dist/workspace/workspace-kinds.schema.json +122 -0
  216. package/dist/workspace/write-lock-cli.d.ts +60 -0
  217. package/dist/workspace/write-lock-cli.d.ts.map +1 -0
  218. package/dist/workspace/write-lock.d.ts +152 -0
  219. package/dist/workspace/write-lock.d.ts.map +1 -0
  220. package/dist/workspace/write-scope.d.ts +6 -2
  221. package/dist/workspace/write-scope.d.ts.map +1 -1
  222. package/dist/yaml.d.ts +1 -1
  223. package/dist/yaml.d.ts.map +1 -1
  224. package/package.json +6 -1
  225. package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
  226. package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
  227. package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
  228. package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
  229. package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
  230. package/src/__fixtures__/change-set/warden.plan.json +44 -0
  231. package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
  232. package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
  233. package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
  234. package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
  235. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
  236. package/src/audit/core.test.ts +15 -0
  237. package/src/audit/core.ts +8 -2
  238. package/src/audit/discover.test.ts +11 -0
  239. package/src/audit/discover.ts +7 -2
  240. package/src/change-set-bundle.test.ts +57 -0
  241. package/src/change-set.test.ts +215 -0
  242. package/src/change-set.ts +445 -0
  243. package/src/cli/commands/audit-opt-in.test.ts +54 -0
  244. package/src/cli/commands/audit.ts +29 -2
  245. package/src/cli/commands/build.test.ts +19 -0
  246. package/src/cli/commands/build.ts +3 -2
  247. package/src/cli/handlers/build.ts +13 -1
  248. package/src/cli/handlers/change-set.test.ts +56 -0
  249. package/src/cli/handlers/change-set.ts +44 -0
  250. package/src/cli/handlers/fan-out.test.ts +221 -2
  251. package/src/cli/handlers/fan-out.ts +165 -75
  252. package/src/cli/handlers/graph.ts +3 -0
  253. package/src/cli/handlers/operator.test.ts +21 -0
  254. package/src/cli/handlers/operator.ts +34 -3
  255. package/src/cli/handlers/pr.ts +373 -0
  256. package/src/cli/handlers/run.ts +1 -0
  257. package/src/cli/main.test.ts +20 -0
  258. package/src/cli/main.ts +185 -23
  259. package/src/cli/mcp/never-over-mcp.test.ts +101 -0
  260. package/src/cli/mcp/op-tools.ts +24 -2
  261. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  262. package/src/cli/mcp/workspace-tools.ts +34 -6
  263. package/src/cli/registry.ts +63 -1
  264. package/src/components/auto-release.ts +1 -1
  265. package/src/components/capability.ts +55 -0
  266. package/src/components/changed-files.ts +66 -0
  267. package/src/components/cli-support.ts +28 -5
  268. package/src/components/component.schema.json +17 -0
  269. package/src/components/component.ts +25 -0
  270. package/src/components/deploy-units.ts +3 -0
  271. package/src/components/driver.test.ts +52 -0
  272. package/src/components/driver.ts +31 -13
  273. package/src/components/fan-out-output.ts +42 -1
  274. package/src/components/fan-out-record.ts +69 -0
  275. package/src/components/fan-out-run.test.ts +17 -0
  276. package/src/components/fan-out-run.ts +304 -1
  277. package/src/components/fan-out-support.ts +3 -0
  278. package/src/components/fan-out-waves.test.ts +375 -0
  279. package/src/components/fan-out.ts +18 -17
  280. package/src/components/gated-wave-pipeline.ts +82 -0
  281. package/src/components/index.ts +1 -0
  282. package/src/components/layers.ts +30 -0
  283. package/src/components/pr-pipeline.ts +91 -0
  284. package/src/components/pr-run.test.ts +205 -0
  285. package/src/components/pr-run.ts +280 -0
  286. package/src/components/promote.ts +5 -0
  287. package/src/components/verbs/shell.test.ts +11 -0
  288. package/src/components/verbs/shell.ts +7 -3
  289. package/src/components/wave-plan.ts +124 -0
  290. package/src/discovery/sandbox/bundle.ts +1 -1
  291. package/src/gated-waves-bundle.test.ts +62 -0
  292. package/src/gated-waves.test.ts +118 -0
  293. package/src/gated-waves.ts +240 -0
  294. package/src/index.ts +1 -0
  295. package/src/lexicon.ts +51 -0
  296. package/src/lifecycle/gate-ledger.ts +7 -0
  297. package/src/lifecycle/git.ts +102 -16
  298. package/src/lint/post-synth.test.ts +19 -0
  299. package/src/lint/post-synth.ts +19 -1
  300. package/src/meta/declared-imports.test.ts +1 -1
  301. package/src/op/activities/activity-contracts.ts +84 -0
  302. package/src/op/activities/change-set.ts +114 -0
  303. package/src/op/activities/decide.ts +3 -0
  304. package/src/op/activities/factory.ts +675 -0
  305. package/src/op/activities/index.ts +6 -0
  306. package/src/op/activity-contract.test.ts +21 -1
  307. package/src/op/activity-contract.ts +19 -9
  308. package/src/op/builders.ts +23 -0
  309. package/src/op/decide-backend.ts +5 -1
  310. package/src/op/decide-read-inputs.ts +33 -5
  311. package/src/op/factory.e2e.test.ts +232 -0
  312. package/src/op/factory.ts +118 -0
  313. package/src/op/index.ts +9 -1
  314. package/src/op/op-ir.test.ts +9 -0
  315. package/src/op/op-ir.ts +4 -0
  316. package/src/op/op-verb-class.ts +3 -0
  317. package/src/op/types.ts +8 -0
  318. package/src/op/work-lease-run.ts +3 -0
  319. package/src/plan-summary.test.ts +263 -0
  320. package/src/plan-summary.ts +845 -0
  321. package/src/pr-forge.test.ts +237 -0
  322. package/src/pr-forge.ts +332 -0
  323. package/src/pr-loop-bundle.test.ts +46 -0
  324. package/src/pr-loop.test.ts +171 -0
  325. package/src/pr-loop.ts +324 -0
  326. package/src/telemetry-attribution.test.ts +23 -1
  327. package/src/telemetry-attribution.ts +51 -0
  328. package/src/terraform/hcl2json.test.ts +21 -0
  329. package/src/terraform/hcl2json.ts +81 -0
  330. package/src/terraform/parse.ts +11 -74
  331. package/src/vendor/js-yaml-load.test.ts +115 -0
  332. package/src/vendor/js-yaml-load.ts +1939 -0
  333. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  334. package/src/workspace/app-kind.test.ts +196 -0
  335. package/src/workspace/box-cli.ts +113 -0
  336. package/src/workspace/box-factory.test.ts +4 -1
  337. package/src/workspace/box-factory.ts +37 -4
  338. package/src/workspace/box-listing-write.schema.json +204 -0
  339. package/src/workspace/box-listing.test.ts +234 -0
  340. package/src/workspace/box-listing.ts +324 -0
  341. package/src/workspace/box-publish.schema.json +498 -0
  342. package/src/workspace/box-publish.test.ts +204 -0
  343. package/src/workspace/box-publish.ts +420 -0
  344. package/src/workspace/box-ship.test.ts +96 -0
  345. package/src/workspace/broker-protocol.schema.json +186 -0
  346. package/src/workspace/broker-protocol.test.ts +122 -0
  347. package/src/workspace/broker-protocol.ts +317 -0
  348. package/src/workspace/builder-tiers.test.ts +146 -0
  349. package/src/workspace/change-set.schema.json +460 -0
  350. package/src/workspace/chant-migrations.e2e.test.ts +17 -5
  351. package/src/workspace/checks/boxes.test.ts +3 -0
  352. package/src/workspace/checks.ts +10 -3
  353. package/src/workspace/component-environments.test.ts +105 -0
  354. package/src/workspace/composites.schema.json +23 -4
  355. package/src/workspace/composites.test.ts +8 -8
  356. package/src/workspace/composites.ts +13 -2
  357. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
  358. package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
  359. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
  360. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
  361. package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
  362. package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
  363. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
  364. package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
  365. package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
  366. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
  367. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
  368. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
  369. package/src/workspace/conformance/broker.test.ts +122 -0
  370. package/src/workspace/conformance/broker.ts +792 -0
  371. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  372. package/src/workspace/conformance/index.mjs +22 -1
  373. package/src/workspace/conformance/index.ts +5 -1
  374. package/src/workspace/conformance/vitest.ts +33 -1
  375. package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
  376. package/src/workspace/conformance/writer.test.ts +32 -2
  377. package/src/workspace/conformance/writer.ts +339 -28
  378. package/src/workspace/decide.test.ts +208 -4
  379. package/src/workspace/decide.ts +427 -73
  380. package/src/workspace/decision-points.schema.json +11 -4
  381. package/src/workspace/declaration.schema.json +153 -4
  382. package/src/workspace/declaration.ts +141 -0
  383. package/src/workspace/environments.ts +66 -14
  384. package/src/workspace/factory-rules.test.ts +62 -0
  385. package/src/workspace/factory-rules.ts +218 -0
  386. package/src/workspace/graph-cli.ts +2 -2
  387. package/src/workspace/graph.schema.json +67 -0
  388. package/src/workspace/identity.test.ts +23 -0
  389. package/src/workspace/init.ts +82 -2
  390. package/src/workspace/intent-cli.ts +26 -9
  391. package/src/workspace/intent-patch-id.test.ts +162 -0
  392. package/src/workspace/intent-record.schema.json +130 -3
  393. package/src/workspace/intent-record.ts +74 -28
  394. package/src/workspace/intent-reviews.test.ts +76 -0
  395. package/src/workspace/intent-squash.test.ts +213 -0
  396. package/src/workspace/intent-why.test.ts +5 -5
  397. package/src/workspace/intent.schema.json +234 -4
  398. package/src/workspace/intent.ts +260 -66
  399. package/src/workspace/json-edit.test.ts +121 -0
  400. package/src/workspace/json-edit.ts +142 -0
  401. package/src/workspace/jsonc.ts +20 -0
  402. package/src/workspace/kinds.ts +157 -2
  403. package/src/workspace/lexicon-symbols.test.ts +148 -0
  404. package/src/workspace/lexicon-symbols.ts +125 -0
  405. package/src/workspace/lineage-upgrade.test.ts +63 -0
  406. package/src/workspace/lineage-upgrade.ts +146 -5
  407. package/src/workspace/plan-summary.schema.json +155 -0
  408. package/src/workspace/point-answer.schema.json +42 -2
  409. package/src/workspace/points-cli.ts +42 -16
  410. package/src/workspace/points-write.schema.json +162 -4
  411. package/src/workspace/points.schema.json +157 -2
  412. package/src/workspace/points.test.ts +13 -1
  413. package/src/workspace/points.ts +118 -12
  414. package/src/workspace/pr-report.schema.json +95 -0
  415. package/src/workspace/profiles.test.ts +159 -0
  416. package/src/workspace/read-contract.test.ts +2 -2
  417. package/src/workspace/reason-codes.test.ts +13 -1
  418. package/src/workspace/reason-codes.ts +38 -4
  419. package/src/workspace/record-sessions.ts +9 -8
  420. package/src/workspace/records-amend.schema.json +78 -0
  421. package/src/workspace/records-cli.ts +20 -6
  422. package/src/workspace/records-close.schema.json +78 -0
  423. package/src/workspace/records-close.ts +31 -7
  424. package/src/workspace/records-concurrent.e2e.test.ts +135 -0
  425. package/src/workspace/records-concurrent.test.ts +145 -0
  426. package/src/workspace/records-formats.test.ts +46 -10
  427. package/src/workspace/records-new.schema.json +7 -0
  428. package/src/workspace/records-review.schema.json +78 -0
  429. package/src/workspace/records-sessions-write.test.ts +69 -0
  430. package/src/workspace/records-sessions.test.ts +22 -0
  431. package/src/workspace/records-write.ts +192 -25
  432. package/src/workspace/records.schema.json +16 -0
  433. package/src/workspace/records.ts +50 -9
  434. package/src/workspace/reference-kinds/app/package.json +10 -0
  435. package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
  436. package/src/workspace/run-statement.schema.json +622 -0
  437. package/src/workspace/runs-cli.ts +343 -14
  438. package/src/workspace/runs-statement.test.ts +239 -0
  439. package/src/workspace/runs-write.schema.json +416 -5
  440. package/src/workspace/runs.schema.json +451 -4
  441. package/src/workspace/runs.test.ts +39 -0
  442. package/src/workspace/runs.ts +296 -35
  443. package/src/workspace/session-kinds.ts +3 -3
  444. package/src/workspace/squash.ts +198 -0
  445. package/src/workspace/status-contract.test.ts +5 -2
  446. package/src/workspace/status-gates.ts +3 -1
  447. package/src/workspace/status.schema.json +126 -3
  448. package/src/workspace/status.ts +90 -1
  449. package/src/workspace/symbols.ts +20 -9
  450. package/src/workspace/trust/run-commits.ts +115 -0
  451. package/src/workspace/trust/run-statement.ts +226 -0
  452. package/src/workspace/trust/seal.ts +8 -2
  453. package/src/workspace/trust/verify-cli.ts +6 -5
  454. package/src/workspace/trust/verify.ts +39 -6
  455. package/src/workspace/wip-cli.ts +97 -0
  456. package/src/workspace/wip-write.schema.json +504 -0
  457. package/src/workspace/wip.schema.json +334 -0
  458. package/src/workspace/wip.test.ts +294 -0
  459. package/src/workspace/wip.ts +753 -0
  460. package/src/workspace/work-cli.ts +3 -0
  461. package/src/workspace/work-evidence.schema.json +3 -0
  462. package/src/workspace/work-size.test.ts +77 -0
  463. package/src/workspace/work-size.ts +93 -0
  464. package/src/workspace/workspace-kinds.schema.json +122 -0
  465. package/src/workspace/write-lock-cli.ts +131 -0
  466. package/src/workspace/write-lock.schema.json +172 -0
  467. package/src/workspace/write-lock.test.ts +189 -0
  468. package/src/workspace/write-lock.ts +474 -0
  469. package/src/workspace/write-scope.test.ts +16 -1
  470. package/src/workspace/write-scope.ts +33 -11
  471. package/src/yaml.ts +31 -21
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Gated waves on a fan-out (#3049): one gate per wave, each bound to the
3
+ * wave's set digest, each wave planned once the waves before it applied.
4
+ *
5
+ * The capability here stands in for `terraform-apply`: `plan` returns the
6
+ * root and a digest over what it would write (its own `version` and every
7
+ * input), `run` applies, records what it applied and exposes an output.
8
+ */
9
+
10
+ import { describe, expect, test } from "vitest";
11
+ import { CapabilityRegistry, type DeployContext } from "./capability";
12
+ import { memoryGateLedgerPort } from "../op/gate";
13
+ import { planFanOut } from "./fan-out";
14
+ import { EarlierWaveNotAppliedError, runFanOut, type FanOutRunOptions } from "./fan-out-run";
15
+ import { computePlanDigest } from "../lifecycle/plan-digest";
16
+ import type { DriverComponent } from "./driver";
17
+ import type { GateResolutionRecord, PendingGateRecord } from "../lifecycle/gate-ledger";
18
+
19
+ interface RootInput {
20
+ root: string;
21
+ version?: string;
22
+ vars?: Record<string, unknown>;
23
+ }
24
+
25
+ /** A component deploying one root. `vars` may wire another root's output. */
26
+ const root = (name: string, dependsOn?: string[], vars?: Record<string, unknown>): DriverComponent => ({
27
+ name,
28
+ deploy: [{ phase: "Apply", steps: [{ kind: "fake-root", root: name, ...(vars ? { vars } : {}) }] }],
29
+ ...(dependsOn ? { dependsOn } : {}),
30
+ });
31
+
32
+ /** net → a, b; app reads a's output. */
33
+ const ESTATE: DriverComponent[] = [
34
+ root("net"),
35
+ root("a", ["net"]),
36
+ root("b", ["net"]),
37
+ root("app", ["a"], { upstream: { stackOutput: { stack: "a", name: "id" } } }),
38
+ ];
39
+
40
+ interface World {
41
+ registry: CapabilityRegistry;
42
+ /** Roots applied, in order. */
43
+ applied: string[];
44
+ /** Each plan call: the root and the inputs it planned with. */
45
+ planned: Array<{ root: string; vars?: Record<string, unknown> }>;
46
+ /** Each apply: whether it applied the wave's own plan. */
47
+ fromPlan: Record<string, boolean>;
48
+ /** What each root would write: change one to "change a root". */
49
+ versions: Record<string, string>;
50
+ failing: Set<string>;
51
+ }
52
+
53
+ function world(): World {
54
+ const w: World = {
55
+ registry: new CapabilityRegistry(),
56
+ applied: [],
57
+ planned: [],
58
+ fromPlan: {},
59
+ versions: {},
60
+ failing: new Set(),
61
+ };
62
+ const digestOf = (input: RootInput) =>
63
+ computePlanDigest("fake-root", { root: input.root, version: w.versions[input.root] ?? "v1", vars: input.vars ?? null });
64
+ w.registry.register({
65
+ kind: "fake-root",
66
+ async plan(_ctx: DeployContext, input: RootInput) {
67
+ w.planned.push({ root: input.root, ...(input.vars ? { vars: input.vars } : {}) });
68
+ const planDigest = digestOf(input);
69
+ return { member: input.root, planDigest, artifact: { planDigest } };
70
+ },
71
+ async run(ctx: DeployContext, input: RootInput) {
72
+ w.fromPlan[input.root] = ctx.plans?.[input.root] !== undefined;
73
+ if (w.failing.has(input.root)) throw new Error(`${input.root} failed`);
74
+ w.applied.push(input.root);
75
+ return { outputs: { id: `${input.root}-id-${w.versions[input.root] ?? "v1"}` } };
76
+ },
77
+ } as never);
78
+ return w;
79
+ }
80
+
81
+ const NOW = "2026-10-03T00:00:00Z";
82
+ const LATER = "2026-10-03T01:00:00Z";
83
+
84
+ /** A ledger that has seen `pending` and then the approvals in `resolutions`, as `chant approve` writes them. */
85
+ function ledger(pending: PendingGateRecord[] = [], resolutions: Array<Partial<GateResolutionRecord>> = []) {
86
+ return memoryGateLedgerPort({
87
+ pending,
88
+ resolutions: resolutions.map((r) => ({
89
+ version: 1 as const,
90
+ kind: "resolution" as const,
91
+ op: "fan-out",
92
+ gate: "release-wave-1",
93
+ resolvedBy: "ana",
94
+ timestamp: LATER,
95
+ ...r,
96
+ })),
97
+ });
98
+ }
99
+
100
+ const waveOptions = (gates: ReturnType<typeof memoryGateLedgerPort>, extra: Partial<FanOutRunOptions> = {}): FanOutRunOptions => ({
101
+ env: "test",
102
+ gates,
103
+ now: NOW,
104
+ waveGate: { op: "fan-out", gate: "release" },
105
+ ...extra,
106
+ });
107
+
108
+ describe("binding: each wave's gate names that wave's set digest", () => {
109
+ test("an unapproved first wave stops the run with its digest pending, and nothing applies", async () => {
110
+ const w = world();
111
+ const plan = planFanOut({ components: ESTATE, changed: ["net"] });
112
+ const gates = ledger();
113
+
114
+ const result = await runFanOut(plan, ESTATE, w.registry, waveOptions(gates));
115
+
116
+ expect(result.status).toBe("gated");
117
+ expect(w.applied).toEqual([]);
118
+ // Only wave 1 was planned: later waves read outputs it has not written.
119
+ expect(w.planned.map((p) => p.root)).toEqual(["net"]);
120
+ expect(result.gate?.gate).toBe("release-wave-1");
121
+ expect(result.gate?.planDigest).toBe(result.waves?.[0]?.digest);
122
+ expect(result.waves).toMatchObject([{ wave: 1, status: "gated", members: [{ member: "net" }] }]);
123
+ });
124
+
125
+ test("an approval with no digest, or with another wave's, does not let the wave through", async () => {
126
+ const w = world();
127
+ const plan = planFanOut({ components: ESTATE, changed: ["net"] });
128
+ const first = await runFanOut(plan, ESTATE, w.registry, waveOptions(ledger()));
129
+ const pending = [first.gate!];
130
+
131
+ const noDigest = await runFanOut(plan, ESTATE, w.registry, waveOptions(ledger(pending, [{}])));
132
+ expect(noDigest.status).toBe("gated");
133
+
134
+ const otherWave = "jcs1-sha256:" + "9".repeat(64);
135
+ const wrong = await runFanOut(plan, ESTATE, w.registry, waveOptions(ledger(pending, [{ planDigest: otherWave }])));
136
+ expect(wrong.status).toBe("gated");
137
+ expect(wrong.waves?.[0]?.approved).toBe(otherWave);
138
+
139
+ // Wave 1's digest approved under wave 2's gate does not answer wave 1 either.
140
+ const misfiled = await runFanOut(
141
+ plan,
142
+ ESTATE,
143
+ w.registry,
144
+ waveOptions(ledger(pending, [{ gate: "release-wave-2", planDigest: first.gate!.planDigest! }])),
145
+ );
146
+ expect(misfiled.status).toBe("gated");
147
+ expect(w.applied).toEqual([]);
148
+ });
149
+
150
+ test("approving wave 1's digest applies wave 1, then stops at wave 2 with its own digest", async () => {
151
+ const w = world();
152
+ const plan = planFanOut({ components: ESTATE, changed: ["net"] });
153
+ const first = await runFanOut(plan, ESTATE, w.registry, waveOptions(ledger()));
154
+
155
+ const result = await runFanOut(
156
+ plan,
157
+ ESTATE,
158
+ w.registry,
159
+ waveOptions(ledger([first.gate!], [{ planDigest: first.gate!.planDigest! }])),
160
+ );
161
+
162
+ expect(w.applied).toEqual(["net"]);
163
+ expect(w.fromPlan.net).toBe(true);
164
+ expect(result.status).toBe("gated");
165
+ expect(result.completed).toEqual(["net"]);
166
+ expect(result.gate?.gate).toBe("release-wave-2");
167
+ expect(result.waves?.map((r) => [r.wave, r.status])).toEqual([[1, "applied"], [2, "gated"]]);
168
+ expect(result.waves?.[1]?.digest).not.toBe(result.waves?.[0]?.digest);
169
+ });
170
+ });
171
+
172
+ describe("changed set: approve wave 1, change a root, re-run", () => {
173
+ test("the run stops naming the approved digest and the new one, and applies nothing", async () => {
174
+ const w = world();
175
+ const plan = planFanOut({ components: ESTATE, changed: ["net"] });
176
+ const first = await runFanOut(plan, ESTATE, w.registry, waveOptions(ledger()));
177
+ const approved = first.gate!.planDigest!;
178
+
179
+ w.versions.net = "v2";
180
+ const result = await runFanOut(plan, ESTATE, w.registry, waveOptions(ledger([first.gate!], [{ planDigest: approved }])));
181
+
182
+ expect(result.status).toBe("gated");
183
+ expect(w.applied).toEqual([]);
184
+ const wave = result.waves![0]!;
185
+ expect(wave.approved).toBe(approved);
186
+ expect(wave.digest).not.toBe(approved);
187
+ // The new plan is recorded pending, so the next `chant approve` approves what will run.
188
+ expect(result.gate?.planDigest).toBe(wave.digest);
189
+ });
190
+ });
191
+
192
+ /**
193
+ * A ledger where every pending wave is approved for exactly the digest it was
194
+ * recorded with: each wave stops once, the next attempt passes it.
195
+ */
196
+ function approvingLedger() {
197
+ const gates = memoryGateLedgerPort();
198
+ return {
199
+ ...gates,
200
+ async read() {
201
+ const { pending } = await gates.read("fan-out");
202
+ return {
203
+ pending,
204
+ resolutions: pending.map((p) => ({
205
+ version: 1 as const,
206
+ kind: "resolution" as const,
207
+ op: p.op,
208
+ gate: p.gate,
209
+ resolvedBy: "ana",
210
+ timestamp: LATER,
211
+ planDigest: p.planDigest!,
212
+ })),
213
+ };
214
+ },
215
+ };
216
+ }
217
+
218
+ /** Re-run until the fan-out stops gating, carrying progress and outputs the way `--resume` does. */
219
+ async function runToEnd(w: World, plan: ReturnType<typeof planFanOut>, components: DriverComponent[]) {
220
+ const gates = approvingLedger();
221
+ let outputs: Record<string, Record<string, unknown>> = {};
222
+ let result;
223
+ for (let i = 0; i < 8; i++) {
224
+ result = await runFanOut(plan, components, w.registry, {
225
+ ...waveOptions(memoryGateLedgerPort()),
226
+ gates,
227
+ progress: { completed: [...w.applied] },
228
+ componentOutputs: outputs,
229
+ });
230
+ outputs = result.componentOutputs;
231
+ if (result.status !== "gated") break;
232
+ }
233
+ return result!;
234
+ }
235
+
236
+ describe("planned at the wave", () => {
237
+ test("a root that reads an upstream output plans with the value the upstream wave wrote", async () => {
238
+ const w = world();
239
+ w.versions.a = "v7";
240
+ const plan = planFanOut({ components: ESTATE, changed: ["net"] });
241
+
242
+ const result = await runToEnd(w, plan, ESTATE);
243
+
244
+ expect(result.status).toBe("ok");
245
+ const appPlans = w.planned.filter((p) => p.root === "app");
246
+ expect(appPlans.length).toBeGreaterThan(0);
247
+ expect(appPlans.every((p) => p.vars?.upstream === "a-id-v7")).toBe(true);
248
+ });
249
+
250
+ test("each wave is planned after the wave before it applied, never up front", async () => {
251
+ const w = world();
252
+ const plan = planFanOut({ components: ESTATE, changed: ["net"] });
253
+ const events: string[] = [];
254
+ const registry = w.registry;
255
+ const capability = registry.resolve("fake-root") as unknown as {
256
+ plan: (ctx: DeployContext, input: RootInput) => Promise<unknown>;
257
+ run: (ctx: DeployContext, input: RootInput) => Promise<unknown>;
258
+ };
259
+ const plan0 = capability.plan.bind(capability);
260
+ const run0 = capability.run.bind(capability);
261
+ capability.plan = async (ctx, input) => (events.push(`plan ${input.root}`), plan0(ctx, input));
262
+ capability.run = async (ctx, input) => (events.push(`apply ${input.root}`), run0(ctx, input));
263
+
264
+ const result = await runToEnd(w, plan, ESTATE);
265
+
266
+ expect(result.status).toBe("ok");
267
+ expect(w.applied).toEqual(["net", "a", "b", "app"]);
268
+ const first = (e: string) => events.indexOf(e);
269
+ expect(first("plan a")).toBeGreaterThan(first("apply net"));
270
+ expect(first("plan b")).toBeGreaterThan(first("apply net"));
271
+ expect(first("plan app")).toBeGreaterThan(first("apply a"));
272
+ });
273
+ });
274
+
275
+ describe("canary", () => {
276
+ test("a canary list is wave 1 and is gated first; the remaining waves follow the graph", async () => {
277
+ const w = world();
278
+ const plan = planFanOut({ components: ESTATE, changed: ["net"], canary: ["b"] });
279
+ expect(plan.waves).toEqual([["b"], ["net"], ["a"], ["app"]]);
280
+
281
+ const result = await runFanOut(plan, ESTATE, w.registry, waveOptions(ledger()));
282
+ expect(result.gate?.gate).toBe("release-wave-1");
283
+ expect(result.waves?.[0]?.members.map((m) => m.member)).toEqual(["b"]);
284
+ });
285
+ });
286
+
287
+ describe("resume", () => {
288
+ test("a run stopped at wave 2's gate resumes there and never re-applies wave 1", async () => {
289
+ const w = world();
290
+ const plan = planFanOut({ components: ESTATE, changed: ["net"] });
291
+ const first = await runFanOut(plan, ESTATE, w.registry, waveOptions(ledger()));
292
+ const second = await runFanOut(
293
+ plan,
294
+ ESTATE,
295
+ w.registry,
296
+ waveOptions(ledger([first.gate!], [{ planDigest: first.gate!.planDigest! }])),
297
+ );
298
+ expect(second.gate?.gate).toBe("release-wave-2");
299
+ expect(w.applied).toEqual(["net"]);
300
+
301
+ const third = await runFanOut(
302
+ plan,
303
+ ESTATE,
304
+ w.registry,
305
+ waveOptions(
306
+ ledger([first.gate!, second.gate!], [
307
+ { planDigest: first.gate!.planDigest! },
308
+ { gate: "release-wave-2", planDigest: second.gate!.planDigest! },
309
+ ]),
310
+ { progress: { completed: second.completed } },
311
+ ),
312
+ );
313
+ // Wave 2 ran; wave 1 did not run again. Wave 3 is next.
314
+ expect(w.applied).toEqual(["net", "a", "b"]);
315
+ expect(third.gate?.gate).toBe("release-wave-3");
316
+ expect(third.waves?.map((r) => r.wave)).toEqual([2, 3]);
317
+ });
318
+
319
+ test("after a failed root, the retry applies only what is left, and its dependents wait for it", async () => {
320
+ const w = world();
321
+ const plan = planFanOut({ components: ESTATE, changed: ["net"] });
322
+ w.failing.add("a");
323
+
324
+ const failed = await runToEnd(w, plan, ESTATE);
325
+ expect(failed.status).toBe("fail");
326
+ expect(w.applied).toEqual(["net", "b"]);
327
+ expect(failed.blocked).toEqual([{ component: "app", reason: "blocked", blockedBy: "a" }]);
328
+ expect(failed.waves?.find((r) => r.wave === 2)).toMatchObject({ status: "failed", failed: [{ component: "a" }] });
329
+
330
+ w.failing.clear();
331
+ const retried = await runToEnd(w, plan, ESTATE);
332
+ expect(retried.status).toBe("ok");
333
+ // net and b did not apply a second time.
334
+ expect(w.applied).toEqual(["net", "b", "a", "app"]);
335
+ });
336
+ });
337
+
338
+ describe("one wave per CI job", () => {
339
+ test("--wave 2 refuses while wave 1 has work left, and runs only wave 2 once it has applied", async () => {
340
+ const w = world();
341
+ const plan = planFanOut({ components: ESTATE, changed: ["net"] });
342
+ await expect(
343
+ runFanOut(plan, ESTATE, w.registry, { ...waveOptions(ledger()), waveGate: { op: "fan-out", gate: "release", only: 2 } }),
344
+ ).rejects.toThrow(EarlierWaveNotAppliedError);
345
+
346
+ const result = await runFanOut(plan, ESTATE, w.registry, {
347
+ ...waveOptions(ledger()),
348
+ waveGate: { op: "fan-out", gate: "release", only: 2 },
349
+ progress: { completed: ["net"] },
350
+ });
351
+ expect(result.gate?.gate).toBe("release-wave-2");
352
+ expect(result.waves?.map((r) => r.wave)).toEqual([2]);
353
+ expect(w.planned.map((p) => p.root).sort()).toEqual(["a", "b"]);
354
+ });
355
+ });
356
+
357
+ describe("both modes", () => {
358
+ test("a run takes one gate over the set or one per wave, not both", async () => {
359
+ const w = world();
360
+ const plan = planFanOut({ components: ESTATE, changed: ["net"] });
361
+ await expect(
362
+ runFanOut(plan, ESTATE, w.registry, { ...waveOptions(ledger()), gate: { op: "fan-out", gate: "release" } }),
363
+ ).rejects.toThrow(/not both/);
364
+ });
365
+
366
+ test("a component with no plannable step is a member under its composition digest", async () => {
367
+ const w = world();
368
+ w.registry.register({ kind: "plain", async run() { return {}; } } as never);
369
+ const estate: DriverComponent[] = [{ name: "svc", deploy: [{ phase: "Deploy", steps: [{ kind: "plain" }] }] }];
370
+ const plan = planFanOut({ components: estate, changed: ["svc"] });
371
+ const result = await runFanOut(plan, estate, w.registry, waveOptions(ledger()));
372
+ expect(result.waves?.[0]?.members.map((m) => m.member)).toEqual(["svc"]);
373
+ expect(result.waves?.[0]?.members[0]?.planDigest).toMatch(/^jcs1-sha256:/);
374
+ });
375
+ });
@@ -36,6 +36,7 @@
36
36
  import { resolveComponentGraph, type DriverComponent } from "./driver";
37
37
  import { deployUnits } from "./deploy-units";
38
38
  import { computePlanDigest } from "../lifecycle/plan-digest";
39
+ import { layerWaves } from "../gated-waves";
39
40
 
40
41
  /** Thrown when `changed` or `indeterminate` names a component the project does not have. */
41
42
  export class UnknownComponentError extends Error {
@@ -90,6 +91,12 @@ export interface FanOutRequest {
90
91
  * the derivation — the selection, the order and the edges it came from.
91
92
  */
92
93
  inputDigests?: Record<string, string>;
94
+ /**
95
+ * Components that form wave 1 whatever the graph order (#3049), so a change
96
+ * lands on them first. Only selected ones are placed: a canary the change
97
+ * does not reach has nothing to apply. The remaining waves follow the graph.
98
+ */
99
+ canary?: string[];
93
100
  }
94
101
 
95
102
  export interface FanOutPlan {
@@ -137,7 +144,7 @@ function consumersOf(components: DriverComponent[]): Map<string, string[]> {
137
144
  * through a fan-out is worse than finding out before it starts.
138
145
  */
139
146
  export function planFanOut(request: FanOutRequest): FanOutPlan {
140
- const { components, changed, indeterminate = [], inputDigests } = request;
147
+ const { components, changed, indeterminate = [], inputDigests, canary = [] } = request;
141
148
 
142
149
  // Refuses DependencyCycleError / UnknownDependencyError over the whole graph.
143
150
  // Its `order` is deliberately not used: `topoSort` walks in declaration
@@ -169,24 +176,18 @@ export function planFanOut(request: FanOutRequest): FanOutPlan {
169
176
  }
170
177
 
171
178
  // Waves over the selected subgraph only. An unselected dependency is already
172
- // applied, so it is not a reason for its dependents to wait.
173
- const remaining = new Set(selected);
174
- const selectedDeps = new Map(
175
- [...selected].map((name) => [name, new Set((byName.get(name)!.dependsOn ?? []).filter((d) => selected.has(d)))]),
179
+ // applied, so it is not a reason for its dependents to wait. The layering is
180
+ // the one terragucci calls without components (../gated-waves.ts).
181
+ // resolveComponentGraph already refused every cycle in the full graph, and a
182
+ // subgraph of an acyclic graph is acyclic, so it cannot stall.
183
+ const waves = layerWaves(
184
+ [...selected].map((name) => ({ name, dependsOn: byName.get(name)!.dependsOn ?? [] })),
185
+ { canary, known: [...byName.keys()] },
176
186
  );
177
- const waves: string[][] = [];
178
- while (remaining.size > 0) {
179
- const wave = [...remaining]
180
- .filter((n) => [...selectedDeps.get(n)!].every((d) => !remaining.has(d)))
181
- .sort();
182
- // resolveComponentGraph already refused every cycle in the full graph, and
183
- // a subgraph of an acyclic graph is acyclic, so this cannot stall.
184
- for (const n of wave) remaining.delete(n);
185
- waves.push(wave);
186
- }
187
187
 
188
- // Every dependency lands in an earlier wave than its dependents, so the
189
- // flattening is a topological order, and a sorted one.
188
+ // Every dependency lands in an earlier wave than its dependents, except
189
+ // where a canary list put a dependent first on purpose, so the flattening is
190
+ // a topological order, and a sorted one.
190
191
  const order = waves.flat();
191
192
 
192
193
  const seeds = [
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The jobs of a generated gated-wave pipeline (#3049): one CI job per wave.
3
+ *
4
+ * The ruling in #3347 puts gated waves on forge CI by default, a job per
5
+ * wave, with approvals on `chant/lifecycle`. Each forge's generator (the
6
+ * gitlab and github lexicons) takes these jobs and adds its own transport;
7
+ * the commands and the order live here so the two cannot disagree.
8
+ *
9
+ * Job `wave-k` runs `chant components fan-out --wave-gate <gate> --wave k`.
10
+ * It derives the fan-out from the change, plans wave k against the outputs
11
+ * the record holds, decides that wave's gate and applies it. The attempt
12
+ * record (`--resume`) is the artifact handed from job to job: it says which
13
+ * components applied and what they exposed. A gate nobody approved exits 3,
14
+ * which fails the job; after `chant approve fan-out <gate>-wave-<k> --plan
15
+ * <digest>`, re-running that job carries on.
16
+ *
17
+ * How many jobs: the waves of the whole component graph, with the canary list
18
+ * in front. A change selects a subset, and a subset never layers deeper than
19
+ * the graph it came from, so a job past the change's last wave finds nothing
20
+ * to run and exits 0.
21
+ */
22
+
23
+ import type { DriverComponent } from "./driver";
24
+ import { layerWaves } from "../gated-waves";
25
+
26
+ /** Where each wave job keeps the attempt record it hands the next one. */
27
+ export const GATED_WAVE_RECORD = ".chant/fan-out.json";
28
+
29
+ /** The `gatedWaves` generator option (`ComponentPipelineOptions.gatedWaves`). */
30
+ export interface GatedWavePipelineOptions {
31
+ /** The base gate name. Wave k waits on `<gate>-wave-<k>`. */
32
+ gate: string;
33
+ /** Components that form wave 1. */
34
+ canary?: string[];
35
+ /**
36
+ * The ref the change is measured from (`--base`). Default `HEAD~1`: the
37
+ * pipeline runs on the merge commit and fans out what it changed. Every
38
+ * job must derive the same fan-out, so this must not move between jobs.
39
+ */
40
+ base?: string;
41
+ }
42
+
43
+ export interface GatedWaveJob {
44
+ /** 1-based. */
45
+ wave: number;
46
+ /** `wave-<k>`. */
47
+ jobName: string;
48
+ /** The previous wave's job, if any. */
49
+ needs: string[];
50
+ /** The job's command, as argv. */
51
+ command: string[];
52
+ }
53
+
54
+ /** One job per possible wave, in order, each needing the one before. */
55
+ export function gatedWaveJobs(
56
+ components: DriverComponent[],
57
+ env: string,
58
+ options: GatedWavePipelineOptions,
59
+ ): GatedWaveJob[] {
60
+ const count = layerWaves(
61
+ components.map((c) => ({ name: c.name, dependsOn: c.dependsOn ?? [] })),
62
+ { ...(options.canary?.length ? { canary: options.canary } : {}) },
63
+ ).length;
64
+ const jobs: GatedWaveJob[] = [];
65
+ for (let wave = 1; wave <= count; wave++) {
66
+ jobs.push({
67
+ wave,
68
+ jobName: `wave-${wave}`,
69
+ needs: wave > 1 ? [`wave-${wave - 1}`] : [],
70
+ command: [
71
+ "chant", "components", "fan-out",
72
+ "--base", options.base ?? "HEAD~1",
73
+ "--env", env,
74
+ "--wave-gate", options.gate,
75
+ ...(options.canary?.length ? ["--canary", options.canary.join(",")] : []),
76
+ "--wave", String(wave),
77
+ "--resume", GATED_WAVE_RECORD,
78
+ ],
79
+ });
80
+ }
81
+ return jobs;
82
+ }
@@ -14,6 +14,7 @@ export {
14
14
  type Gate,
15
15
  type Phase,
16
16
  type Component,
17
+ type ComponentEnvironmentDeclaration,
17
18
  phase,
18
19
  gate,
19
20
  stackOutput,
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Kahn layering over a plain name graph: the wave computation
3
+ * `resolveComponentGraph` (./driver.ts) runs for `chant components fan-out`.
4
+ *
5
+ * It lives on its own, with no imports, so code that needs the same order
6
+ * without the driver can take it. The terraform lexicon's pin-bump rollout
7
+ * (#3189) orders roots with it, and that code has to bundle without the
8
+ * driver's gate and ledger modules (#3421).
9
+ */
10
+
11
+ /** The layers, or the names left when no name is ready (a cycle). */
12
+ export type KahnLayers = { waves: string[][]; cycle?: undefined } | { waves?: undefined; cycle: string[] };
13
+
14
+ /**
15
+ * Split `deps` (name -> the names it depends on) into waves. A name is ready
16
+ * once every dependency it names has been placed in an earlier wave. Names in
17
+ * one wave are sorted. A dependency that is not a key of `deps` is the caller's
18
+ * to refuse or drop first: here it never becomes ready, so it reads as a cycle.
19
+ */
20
+ export function kahnLayers(deps: ReadonlyMap<string, ReadonlySet<string>>): KahnLayers {
21
+ const remaining = new Set(deps.keys());
22
+ const waves: string[][] = [];
23
+ while (remaining.size > 0) {
24
+ const wave = [...remaining].filter((n) => [...deps.get(n)!].every((d) => !remaining.has(d))).sort();
25
+ if (wave.length === 0) return { cycle: [...remaining].sort() };
26
+ for (const n of wave) remaining.delete(n);
27
+ waves.push(wave);
28
+ }
29
+ return { waves };
30
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The jobs of a generated pull-request pipeline (#3183): a plan job on every
3
+ * pull request and an apply job on every push to the target branch.
4
+ *
5
+ * Each forge's generator (the github, gitlab and forgejo lexicons) adds its
6
+ * own triggers, permissions and artifact handling; the commands and the
7
+ * values they read from the forge live here so the three cannot disagree.
8
+ *
9
+ * - The plan job runs `chant components pr-plan` against the pull request's
10
+ * target commit. It posts the note and the `chant/plan` status and writes
11
+ * nothing to `chant/lifecycle`, since it runs the pull request's code.
12
+ * - The apply job runs `chant components pr-apply` against the commit the
13
+ * push replaced, finds the pull request that merged the pushed commit
14
+ * through the forge, and applies only when an approval stands for the
15
+ * digest it plans. With `requireReview` (the default) the approver must
16
+ * also have approved the pull request on the forge. It needs write access
17
+ * to `chant/lifecycle`, to record the pending fact when the gate stops it.
18
+ * One apply runs at a time per environment.
19
+ *
20
+ * Measuring the push from the commit it replaced works for merge commits,
21
+ * squash merges and rebase merges alike, and every job measures from the
22
+ * merge base of that commit and its own head (`resolveMergeBase`).
23
+ */
24
+
25
+ import type { ForgeKind } from "../pr-forge";
26
+ import { PR_APPLY_GATE } from "../pr-loop";
27
+
28
+ /** Where both jobs write their reports, kept as a CI artifact. */
29
+ export const PR_LOOP_REPORT_DIR = ".chant/pr";
30
+
31
+ /** The `prLoop` generator option (`ComponentPipelineOptions.prLoop`). */
32
+ export interface PrLoopPipelineOptions {
33
+ /** The gate the apply waits on. Default `pr-apply`. */
34
+ gate?: string;
35
+ /**
36
+ * The branch pull requests target and merges land on. Default `main` on
37
+ * GitHub and Forgejo; on GitLab, the project's default branch.
38
+ */
39
+ branch?: string;
40
+ /** Count an approval only from someone who approved the pull request on the forge. Default true. */
41
+ requireReview?: boolean;
42
+ /**
43
+ * Which GitHub-shaped forge the workflow is for. Set by the forgejo
44
+ * generator, which reuses the github one; a caller never needs to.
45
+ */
46
+ forge?: "github" | "forgejo";
47
+ }
48
+
49
+ /** One job's command and the forge values it reads, as environment variables for its step. */
50
+ export interface PrLoopJob {
51
+ /** `plan` or `apply`. */
52
+ jobName: "plan" | "apply";
53
+ /** The command line. Reads `$BASE_SHA` and, for the plan, `$PR_NUMBER`. */
54
+ command: string;
55
+ /**
56
+ * Values the command reads, by variable name, as the forge spells them:
57
+ * `${{ ... }}` expressions on GitHub and Forgejo, GitLab's predefined
58
+ * variables on GitLab.
59
+ */
60
+ env: Record<string, string>;
61
+ }
62
+
63
+ /** The two jobs for `forge`. */
64
+ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelineOptions = {}): PrLoopJob[] {
65
+ const gate = options.gate ?? PR_APPLY_GATE;
66
+ const common = ["--env", env, "--gate", gate, "--output", PR_LOOP_REPORT_DIR, "--forge", forge];
67
+ const github = forge !== "gitlab";
68
+ const token: Record<string, string> = github ? { GITHUB_TOKEN: "${{ github.token }}" } : {};
69
+ return [
70
+ {
71
+ jobName: "plan",
72
+ command: ["chant", "components", "pr-plan", "--base", '"$BASE_SHA"', "--pr", '"$PR_NUMBER"', ...common].join(" "),
73
+ env: github
74
+ ? { BASE_SHA: "${{ github.event.pull_request.base.sha }}", PR_NUMBER: "${{ github.event.pull_request.number }}", ...token }
75
+ : { BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA", PR_NUMBER: "$CI_MERGE_REQUEST_IID" },
76
+ },
77
+ {
78
+ jobName: "apply",
79
+ command: [
80
+ "chant", "components", "pr-apply", "--base", '"$BASE_SHA"', ...common,
81
+ ...(options.requireReview === false ? [] : ["--require-review"]),
82
+ ].join(" "),
83
+ env: github ? { BASE_SHA: "${{ github.event.before }}", ...token } : { BASE_SHA: "$CI_COMMIT_BEFORE_SHA" },
84
+ },
85
+ ];
86
+ }
87
+
88
+ /** The concurrency group (GitHub, Forgejo) or resource group (GitLab) that keeps one apply at a time per environment. */
89
+ export function prApplyGroup(env: string): string {
90
+ return `chant-apply-${env}`;
91
+ }