@intentius/chant 0.124.52 → 0.124.54

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 (584) hide show
  1. package/dist/audit/discover.d.ts.map +1 -1
  2. package/dist/audit/discover.js +3 -1
  3. package/dist/behaviour.d.ts +1 -1
  4. package/dist/behaviour.d.ts.map +1 -1
  5. package/dist/capabilities/templates.d.ts +65 -0
  6. package/dist/capabilities/templates.d.ts.map +1 -0
  7. package/dist/capabilities/templates.js +193 -0
  8. package/dist/capabilities/templates.json +145 -0
  9. package/dist/change-set-graph.d.ts +35 -3
  10. package/dist/change-set-graph.d.ts.map +1 -1
  11. package/dist/change-set-graph.js +60 -9
  12. package/dist/change-set.d.ts +65 -7
  13. package/dist/change-set.d.ts.map +1 -1
  14. package/dist/change-set.js +23 -12
  15. package/dist/ci-identity.d.ts +7 -2
  16. package/dist/ci-identity.d.ts.map +1 -1
  17. package/dist/ci-identity.js +4 -1
  18. package/dist/cli/commands/import-index.d.ts +52 -0
  19. package/dist/cli/commands/import-index.d.ts.map +1 -0
  20. package/dist/cli/commands/import-index.js +226 -0
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/lint.js +4 -2
  23. package/dist/cli/handlers/components.d.ts.map +1 -1
  24. package/dist/cli/handlers/components.js +11 -2
  25. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  26. package/dist/cli/handlers/fan-out.js +16 -3
  27. package/dist/cli/handlers/graph.d.ts.map +1 -1
  28. package/dist/cli/handlers/graph.js +22 -4
  29. package/dist/cli/handlers/index-cloud.d.ts +14 -0
  30. package/dist/cli/handlers/index-cloud.d.ts.map +1 -0
  31. package/dist/cli/handlers/index-cloud.js +22 -0
  32. package/dist/cli/handlers/index-diff.d.ts +18 -0
  33. package/dist/cli/handlers/index-diff.d.ts.map +1 -0
  34. package/dist/cli/handlers/index-diff.js +142 -0
  35. package/dist/cli/handlers/lifecycle-drift.d.ts +25 -0
  36. package/dist/cli/handlers/lifecycle-drift.d.ts.map +1 -0
  37. package/dist/cli/handlers/lifecycle-drift.js +64 -0
  38. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  39. package/dist/cli/handlers/lifecycle.js +188 -28
  40. package/dist/cli/handlers/misc.d.ts +9 -0
  41. package/dist/cli/handlers/misc.d.ts.map +1 -1
  42. package/dist/cli/handlers/misc.js +77 -0
  43. package/dist/cli/handlers/pr.d.ts.map +1 -1
  44. package/dist/cli/handlers/pr.js +15 -2
  45. package/dist/cli/handlers/promote.d.ts.map +1 -1
  46. package/dist/cli/handlers/promote.js +89 -15
  47. package/dist/cli/handlers/query.d.ts.map +1 -1
  48. package/dist/cli/handlers/query.js +28 -4
  49. package/dist/cli/handlers/scenario.d.ts.map +1 -1
  50. package/dist/cli/handlers/scenario.js +8 -7
  51. package/dist/cli/handlers/search.d.ts +34 -4
  52. package/dist/cli/handlers/search.d.ts.map +1 -1
  53. package/dist/cli/handlers/search.js +216 -36
  54. package/dist/cli/main.d.ts.map +1 -1
  55. package/dist/cli/main.js +55 -1
  56. package/dist/cli/mcp/lifecycle-tools.d.ts.map +1 -1
  57. package/dist/cli/mcp/lifecycle-tools.js +3 -2
  58. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  59. package/dist/cli/mcp/resource-handlers.js +4 -3
  60. package/dist/cli/mcp/workspace-tools.js +1 -1
  61. package/dist/cli/parse-args.d.ts.map +1 -1
  62. package/dist/cli/parse-args.js +55 -2
  63. package/dist/cli/registry.d.ts +19 -1
  64. package/dist/cli/registry.d.ts.map +1 -1
  65. package/dist/cloud-lister/aws.d.ts +35 -0
  66. package/dist/cloud-lister/aws.d.ts.map +1 -0
  67. package/dist/cloud-lister/aws.js +203 -0
  68. package/dist/cloud-lister/azure.d.ts +34 -0
  69. package/dist/cloud-lister/azure.d.ts.map +1 -0
  70. package/dist/cloud-lister/azure.js +129 -0
  71. package/dist/cloud-lister/common.d.ts +40 -0
  72. package/dist/cloud-lister/common.d.ts.map +1 -0
  73. package/dist/cloud-lister/common.js +83 -0
  74. package/dist/cloud-lister/credentials.d.ts +50 -0
  75. package/dist/cloud-lister/credentials.d.ts.map +1 -0
  76. package/dist/cloud-lister/credentials.js +86 -0
  77. package/dist/cloud-lister/fakes.d.ts +27 -0
  78. package/dist/cloud-lister/fakes.d.ts.map +1 -0
  79. package/dist/cloud-lister/fakes.js +50 -0
  80. package/dist/cloud-lister/gcp.d.ts +42 -0
  81. package/dist/cloud-lister/gcp.d.ts.map +1 -0
  82. package/dist/cloud-lister/gcp.js +143 -0
  83. package/dist/cloud-lister/guard.d.ts +49 -0
  84. package/dist/cloud-lister/guard.d.ts.map +1 -0
  85. package/dist/cloud-lister/guard.js +103 -0
  86. package/dist/cloud-lister/ids.d.ts +23 -0
  87. package/dist/cloud-lister/ids.d.ts.map +1 -0
  88. package/dist/cloud-lister/ids.js +42 -0
  89. package/dist/cloud-lister/index.d.ts +67 -0
  90. package/dist/cloud-lister/index.d.ts.map +1 -0
  91. package/dist/cloud-lister/index.js +102 -0
  92. package/dist/cloud-lister/kubernetes.d.ts +64 -0
  93. package/dist/cloud-lister/kubernetes.d.ts.map +1 -0
  94. package/dist/cloud-lister/kubernetes.js +276 -0
  95. package/dist/cloud-lister/markers.d.ts +31 -0
  96. package/dist/cloud-lister/markers.d.ts.map +1 -0
  97. package/dist/cloud-lister/markers.js +73 -0
  98. package/dist/cloud-lister/mask.d.ts +31 -0
  99. package/dist/cloud-lister/mask.d.ts.map +1 -0
  100. package/dist/cloud-lister/mask.js +74 -0
  101. package/dist/cloud-lister/sigv4.d.ts +29 -0
  102. package/dist/cloud-lister/sigv4.d.ts.map +1 -0
  103. package/dist/cloud-lister/sigv4.js +54 -0
  104. package/dist/cloud-lister/transport.d.ts +55 -0
  105. package/dist/cloud-lister/transport.d.ts.map +1 -0
  106. package/dist/cloud-lister/transport.js +86 -0
  107. package/dist/cloud-lister/types.d.ts +121 -0
  108. package/dist/cloud-lister/types.d.ts.map +1 -0
  109. package/dist/cloud-lister/types.js +24 -0
  110. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  111. package/dist/components/capability-plugin-loader.js +6 -0
  112. package/dist/components/capability-plugin.d.ts +9 -0
  113. package/dist/components/capability-plugin.d.ts.map +1 -1
  114. package/dist/components/capability.d.ts +215 -0
  115. package/dist/components/capability.d.ts.map +1 -1
  116. package/dist/components/cli-support.d.ts +21 -0
  117. package/dist/components/cli-support.d.ts.map +1 -1
  118. package/dist/components/cli-support.js +58 -14
  119. package/dist/components/declared-reads.d.ts +38 -0
  120. package/dist/components/declared-reads.d.ts.map +1 -0
  121. package/dist/components/declared-reads.js +131 -0
  122. package/dist/components/deploy-units.d.ts.map +1 -1
  123. package/dist/components/deploy-units.js +4 -0
  124. package/dist/components/driver.d.ts +34 -1
  125. package/dist/components/driver.d.ts.map +1 -1
  126. package/dist/components/driver.js +66 -3
  127. package/dist/components/fan-out-run.d.ts +23 -1
  128. package/dist/components/fan-out-run.d.ts.map +1 -1
  129. package/dist/components/fan-out-run.js +82 -14
  130. package/dist/components/fan-out-support.d.ts +6 -0
  131. package/dist/components/fan-out-support.d.ts.map +1 -1
  132. package/dist/components/fan-out-support.js +4 -1
  133. package/dist/components/fan-out.d.ts +28 -0
  134. package/dist/components/fan-out.d.ts.map +1 -1
  135. package/dist/components/groups.d.ts +229 -0
  136. package/dist/components/groups.d.ts.map +1 -0
  137. package/dist/components/groups.js +760 -0
  138. package/dist/components/index.d.ts +6 -2
  139. package/dist/components/index.d.ts.map +1 -1
  140. package/dist/components/index.js +4 -0
  141. package/dist/components/plan-shape-conformance.d.ts +48 -0
  142. package/dist/components/plan-shape-conformance.d.ts.map +1 -0
  143. package/dist/components/plan-shape-conformance.js +122 -0
  144. package/dist/components/pr-run.d.ts +41 -4
  145. package/dist/components/pr-run.d.ts.map +1 -1
  146. package/dist/components/pr-run.js +209 -39
  147. package/dist/components/promote-rounds.d.ts +9 -0
  148. package/dist/components/promote-rounds.d.ts.map +1 -1
  149. package/dist/components/promote-rounds.js +23 -2
  150. package/dist/components/promote.d.ts +17 -0
  151. package/dist/components/promote.d.ts.map +1 -1
  152. package/dist/components/promote.js +36 -6
  153. package/dist/components/release-gate.d.ts +23 -1
  154. package/dist/components/release-gate.d.ts.map +1 -1
  155. package/dist/components/release-gate.js +97 -33
  156. package/dist/components/run-groups.d.ts +58 -0
  157. package/dist/components/run-groups.d.ts.map +1 -0
  158. package/dist/components/run-groups.js +78 -0
  159. package/dist/components/starter-plugin.d.ts +1 -0
  160. package/dist/components/starter-plugin.d.ts.map +1 -1
  161. package/dist/components/starter-plugin.js +6 -1
  162. package/dist/components/verbs/index.d.ts +1 -0
  163. package/dist/components/verbs/index.d.ts.map +1 -1
  164. package/dist/components/verbs/index.js +1 -0
  165. package/dist/components/verbs/member-outputs.d.ts +187 -0
  166. package/dist/components/verbs/member-outputs.d.ts.map +1 -0
  167. package/dist/components/verbs/member-outputs.js +218 -0
  168. package/dist/config.d.ts +60 -0
  169. package/dist/config.d.ts.map +1 -1
  170. package/dist/config.js +27 -0
  171. package/dist/deep-observation.d.ts +1 -13
  172. package/dist/deep-observation.d.ts.map +1 -1
  173. package/dist/deep-observation.js +5 -22
  174. package/dist/drift-record.d.ts +542 -0
  175. package/dist/drift-record.d.ts.map +1 -0
  176. package/dist/drift-record.js +886 -0
  177. package/dist/engine-facts.d.ts +144 -0
  178. package/dist/engine-facts.d.ts.map +1 -0
  179. package/dist/engine-facts.js +14 -0
  180. package/dist/graph-ir.d.ts +153 -2
  181. package/dist/graph-ir.d.ts.map +1 -1
  182. package/dist/graph-ir.js +224 -6
  183. package/dist/graph-refs.d.ts +145 -0
  184. package/dist/graph-refs.d.ts.map +1 -1
  185. package/dist/graph-refs.js +305 -0
  186. package/dist/graph-schema/fixtures/adopt.json +15 -0
  187. package/dist/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  188. package/dist/graph-schema/fixtures/index.json +68 -0
  189. package/dist/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  190. package/dist/graph-schema/fixtures.d.ts +23 -0
  191. package/dist/graph-schema/fixtures.d.ts.map +1 -0
  192. package/dist/graph-schema/fixtures.js +246 -0
  193. package/dist/graph-schema/generate.d.ts +15 -0
  194. package/dist/graph-schema/generate.d.ts.map +1 -0
  195. package/dist/graph-schema/generate.js +267 -0
  196. package/dist/graph-schema/graph.schema.json +1387 -0
  197. package/dist/health.schema.json +2 -1
  198. package/dist/identity.d.ts.map +1 -1
  199. package/dist/identity.js +2 -0
  200. package/dist/index.d.ts +1 -0
  201. package/dist/index.d.ts.map +1 -1
  202. package/dist/index.js +2 -0
  203. package/dist/instance-nodes.d.ts +159 -0
  204. package/dist/instance-nodes.d.ts.map +1 -0
  205. package/dist/instance-nodes.js +231 -0
  206. package/dist/lexicon.d.ts +72 -0
  207. package/dist/lexicon.d.ts.map +1 -1
  208. package/dist/lifecycle/adoption-file.d.ts +56 -0
  209. package/dist/lifecycle/adoption-file.d.ts.map +1 -0
  210. package/dist/lifecycle/adoption-file.js +123 -0
  211. package/dist/lifecycle/cloud-index.d.ts +186 -0
  212. package/dist/lifecycle/cloud-index.d.ts.map +1 -0
  213. package/dist/lifecycle/cloud-index.js +618 -0
  214. package/dist/lifecycle/derive-edges.d.ts +59 -0
  215. package/dist/lifecycle/derive-edges.d.ts.map +1 -0
  216. package/dist/lifecycle/derive-edges.js +126 -0
  217. package/dist/lifecycle/git.d.ts +10 -9
  218. package/dist/lifecycle/git.d.ts.map +1 -1
  219. package/dist/lifecycle/git.js +7 -1
  220. package/dist/lifecycle/identity-match.d.ts +133 -0
  221. package/dist/lifecycle/identity-match.d.ts.map +1 -0
  222. package/dist/lifecycle/identity-match.js +147 -0
  223. package/dist/lifecycle/index-diff.d.ts +224 -0
  224. package/dist/lifecycle/index-diff.d.ts.map +1 -0
  225. package/dist/lifecycle/index-diff.js +327 -0
  226. package/dist/lifecycle/index-document.d.ts +130 -0
  227. package/dist/lifecycle/index-document.d.ts.map +1 -0
  228. package/dist/lifecycle/index-document.js +189 -0
  229. package/dist/lifecycle/index-drift.d.ts +121 -0
  230. package/dist/lifecycle/index-drift.d.ts.map +1 -0
  231. package/dist/lifecycle/index-drift.js +178 -0
  232. package/dist/lifecycle/index-import.d.ts +168 -0
  233. package/dist/lifecycle/index-import.d.ts.map +1 -0
  234. package/dist/lifecycle/index-import.js +515 -0
  235. package/dist/lifecycle/index-join.d.ts +129 -0
  236. package/dist/lifecycle/index-join.d.ts.map +1 -0
  237. package/dist/lifecycle/index-join.js +248 -0
  238. package/dist/lifecycle/index.d.ts +4 -0
  239. package/dist/lifecycle/index.d.ts.map +1 -1
  240. package/dist/lifecycle/index.js +4 -0
  241. package/dist/lifecycle/observation-baseline.d.ts +61 -21
  242. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  243. package/dist/lifecycle/observation-baseline.js +144 -19
  244. package/dist/lifecycle/observe.d.ts.map +1 -1
  245. package/dist/lifecycle/observe.js +6 -1
  246. package/dist/lifecycle/query.d.ts +45 -4
  247. package/dist/lifecycle/query.d.ts.map +1 -1
  248. package/dist/lifecycle/query.js +89 -7
  249. package/dist/lifecycle/replay.d.ts +10 -3
  250. package/dist/lifecycle/replay.d.ts.map +1 -1
  251. package/dist/lifecycle/replay.js +10 -6
  252. package/dist/lifecycle/scenario.d.ts +4 -3
  253. package/dist/lifecycle/scenario.d.ts.map +1 -1
  254. package/dist/lifecycle/snapshot-store-fake.d.ts +9 -0
  255. package/dist/lifecycle/snapshot-store-fake.d.ts.map +1 -0
  256. package/dist/lifecycle/snapshot-store-fake.js +21 -0
  257. package/dist/lifecycle/snapshot-store-s3.d.ts +102 -0
  258. package/dist/lifecycle/snapshot-store-s3.d.ts.map +1 -0
  259. package/dist/lifecycle/snapshot-store-s3.js +254 -0
  260. package/dist/lifecycle/snapshot-store.d.ts +170 -0
  261. package/dist/lifecycle/snapshot-store.d.ts.map +1 -0
  262. package/dist/lifecycle/snapshot-store.js +832 -0
  263. package/dist/lifecycle/snapshot.d.ts +11 -0
  264. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  265. package/dist/lifecycle/snapshot.js +18 -5
  266. package/dist/lifecycle/type-listing.d.ts +148 -0
  267. package/dist/lifecycle/type-listing.d.ts.map +1 -0
  268. package/dist/lifecycle/type-listing.js +166 -0
  269. package/dist/lint/component-checks.d.ts +15 -1
  270. package/dist/lint/component-checks.d.ts.map +1 -1
  271. package/dist/lint/component-checks.js +1 -0
  272. package/dist/lint/presets/relaxed.json +2 -1
  273. package/dist/lint/presets/strict.json +2 -1
  274. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts +4 -0
  275. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts.map +1 -0
  276. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.js +13 -0
  277. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts +4 -0
  278. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts.map +1 -0
  279. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.js +8 -0
  280. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts +33 -0
  281. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts.map +1 -0
  282. package/dist/lint/rules/comp/comp008-applies-signal-only-root.js +123 -0
  283. package/dist/lint/rules/comp/index.d.ts +2 -1
  284. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  285. package/dist/lint/rules/comp/index.js +4 -1
  286. package/dist/observation.d.ts +19 -2
  287. package/dist/observation.d.ts.map +1 -1
  288. package/dist/observation.js +18 -1
  289. package/dist/op/activities/activity-contracts.d.ts +1 -1
  290. package/dist/op/activities/activity-contracts.js +1 -1
  291. package/dist/op/activities/change-set.d.ts +6 -4
  292. package/dist/op/activities/change-set.d.ts.map +1 -1
  293. package/dist/op/activities/change-set.js +4 -2
  294. package/dist/op/activity-failure.d.ts +27 -0
  295. package/dist/op/activity-failure.d.ts.map +1 -1
  296. package/dist/op/activity-failure.js +31 -0
  297. package/dist/op/index.d.ts +2 -1
  298. package/dist/op/index.d.ts.map +1 -1
  299. package/dist/op/index.js +1 -1
  300. package/dist/op/local-executor.d.ts +5 -1
  301. package/dist/op/local-executor.d.ts.map +1 -1
  302. package/dist/op/local-executor.js +22 -8
  303. package/dist/pr-loop.d.ts +56 -1
  304. package/dist/pr-loop.d.ts.map +1 -1
  305. package/dist/pr-loop.js +51 -1
  306. package/dist/pulumi-urn.d.ts +101 -0
  307. package/dist/pulumi-urn.d.ts.map +1 -0
  308. package/dist/pulumi-urn.js +131 -0
  309. package/dist/release-floors.d.ts +23 -0
  310. package/dist/release-floors.d.ts.map +1 -0
  311. package/dist/release-floors.js +22 -0
  312. package/dist/sensitive-key.d.ts +23 -0
  313. package/dist/sensitive-key.d.ts.map +1 -0
  314. package/dist/sensitive-key.js +31 -0
  315. package/dist/wave-runtime.mjs +234 -234
  316. package/dist/workspace/adoption-report.d.ts +53 -0
  317. package/dist/workspace/adoption-report.d.ts.map +1 -0
  318. package/dist/workspace/adoption-report.js +76 -0
  319. package/dist/workspace/capabilities-contract.d.ts +7 -0
  320. package/dist/workspace/capabilities-contract.d.ts.map +1 -1
  321. package/dist/workspace/capabilities-contract.js +2 -1
  322. package/dist/workspace/capabilities.schema.json +98 -0
  323. package/dist/workspace/change-set.schema.json +42 -3
  324. package/dist/workspace/ci-cloud-identity.d.ts +13 -1
  325. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  326. package/dist/workspace/ci-cloud-identity.js +30 -1
  327. package/dist/workspace/compose-graph.d.ts +10 -1
  328. package/dist/workspace/compose-graph.d.ts.map +1 -1
  329. package/dist/workspace/compose-graph.js +6 -1
  330. package/dist/workspace/conformance/index.d.ts +1 -1
  331. package/dist/workspace/conformance/index.d.ts.map +1 -1
  332. package/dist/workspace/conformance/index.js +6 -1
  333. package/dist/workspace/drift-cli.d.ts +81 -0
  334. package/dist/workspace/drift-cli.d.ts.map +1 -0
  335. package/dist/workspace/drift-cli.js +317 -0
  336. package/dist/workspace/drift.schema.json +672 -0
  337. package/dist/workspace/graph-cli.d.ts +2 -0
  338. package/dist/workspace/graph-cli.d.ts.map +1 -1
  339. package/dist/workspace/graph-cli.js +3 -0
  340. package/dist/workspace/graph.schema.json +72 -0
  341. package/dist/workspace/init.d.ts +5 -1
  342. package/dist/workspace/init.d.ts.map +1 -1
  343. package/dist/workspace/init.js +55 -9
  344. package/dist/workspace/ledger.schema.json +1 -1
  345. package/dist/workspace/nested-graph.d.ts.map +1 -1
  346. package/dist/workspace/nested-graph.js +2 -0
  347. package/dist/workspace/pr-report.schema.json +51 -2
  348. package/dist/workspace/promote-waves.d.ts +112 -0
  349. package/dist/workspace/promote-waves.d.ts.map +1 -0
  350. package/dist/workspace/promote-waves.js +290 -0
  351. package/dist/workspace/promote.d.ts +84 -2
  352. package/dist/workspace/promote.d.ts.map +1 -1
  353. package/dist/workspace/promote.js +347 -61
  354. package/dist/workspace/reason-codes.d.ts +12 -0
  355. package/dist/workspace/reason-codes.d.ts.map +1 -1
  356. package/dist/workspace/reason-codes.js +14 -0
  357. package/dist/workspace/search-contract.d.ts +3 -3
  358. package/dist/workspace/search-contract.d.ts.map +1 -1
  359. package/dist/workspace/search-contract.js +2 -0
  360. package/dist/workspace/search.schema.json +103 -5
  361. package/package.json +14 -1
  362. package/src/__fixtures__/drift-record/README.md +58 -0
  363. package/src/__fixtures__/drift-record/choudoufu-cone.ndjson +4 -0
  364. package/src/__fixtures__/drift-record/choudoufu-resource.ndjson +3 -0
  365. package/src/__fixtures__/drift-record/component-status-ledger.json +65 -0
  366. package/src/__fixtures__/drift-record/component-status.json +91 -0
  367. package/src/__fixtures__/drift-record/deep-diff.json +259 -0
  368. package/src/__fixtures__/drift-record/lifecycle-diff-live.json +825 -0
  369. package/src/__fixtures__/drift-record/live-diff.json +125 -0
  370. package/src/audit/discover.e2e.test.ts +14 -0
  371. package/src/audit/discover.ts +3 -1
  372. package/src/behaviour.ts +1 -1
  373. package/src/capabilities/templates.json +145 -0
  374. package/src/capabilities/templates.test.ts +212 -0
  375. package/src/capabilities/templates.ts +204 -0
  376. package/src/change-set-bundle.test.ts +83 -0
  377. package/src/change-set-graph.ts +80 -9
  378. package/src/change-set.test.ts +24 -0
  379. package/src/change-set.ts +77 -14
  380. package/src/ci-identity-targets.test.ts +23 -0
  381. package/src/ci-identity.ts +10 -4
  382. package/src/cli/commands/import-index.test.ts +117 -0
  383. package/src/cli/commands/import-index.ts +265 -0
  384. package/src/cli/commands/lint.ts +4 -2
  385. package/src/cli/handlers/components.test.ts +9 -0
  386. package/src/cli/handlers/components.ts +12 -2
  387. package/src/cli/handlers/fan-out.ts +17 -3
  388. package/src/cli/handlers/graph.test.ts +42 -0
  389. package/src/cli/handlers/graph.ts +26 -4
  390. package/src/cli/handlers/index-cloud.ts +32 -0
  391. package/src/cli/handlers/index-diff.test.ts +68 -0
  392. package/src/cli/handlers/index-diff.ts +146 -0
  393. package/src/cli/handlers/ledger-root-member.test.ts +161 -0
  394. package/src/cli/handlers/lifecycle-drift.ts +68 -0
  395. package/src/cli/handlers/lifecycle.test.ts +33 -11
  396. package/src/cli/handlers/lifecycle.ts +205 -30
  397. package/src/cli/handlers/misc.ts +73 -0
  398. package/src/cli/handlers/pr-waves-blocked.test.ts +37 -2
  399. package/src/cli/handlers/pr.ts +14 -2
  400. package/src/cli/handlers/promote.test.ts +64 -0
  401. package/src/cli/handlers/promote.ts +81 -6
  402. package/src/cli/handlers/query.ts +27 -4
  403. package/src/cli/handlers/run-declared-reads-cli.test.ts +187 -0
  404. package/src/cli/handlers/run-groups-cli.test.ts +229 -0
  405. package/src/cli/handlers/scenario.test.ts +13 -0
  406. package/src/cli/handlers/scenario.ts +8 -7
  407. package/src/cli/handlers/search-index.test.ts +255 -0
  408. package/src/cli/handlers/search-json.test.ts +1 -1
  409. package/src/cli/handlers/search-ownership.test.ts +194 -0
  410. package/src/cli/handlers/search.ts +271 -38
  411. package/src/cli/main.test.ts +7 -0
  412. package/src/cli/main.ts +56 -1
  413. package/src/cli/mcp/lifecycle-tools.ts +3 -2
  414. package/src/cli/mcp/resource-handlers.ts +4 -3
  415. package/src/cli/mcp/workspace-tools.ts +1 -1
  416. package/src/cli/parse-args.ts +40 -2
  417. package/src/cli/registry.ts +19 -1
  418. package/src/cloud-lister/aws.test.ts +202 -0
  419. package/src/cloud-lister/aws.ts +226 -0
  420. package/src/cloud-lister/azure.test.ts +132 -0
  421. package/src/cloud-lister/azure.ts +142 -0
  422. package/src/cloud-lister/common.ts +115 -0
  423. package/src/cloud-lister/credentials.ts +110 -0
  424. package/src/cloud-lister/fakes.ts +66 -0
  425. package/src/cloud-lister/gcp.test.ts +121 -0
  426. package/src/cloud-lister/gcp.ts +159 -0
  427. package/src/cloud-lister/guard.test.ts +198 -0
  428. package/src/cloud-lister/guard.ts +111 -0
  429. package/src/cloud-lister/ids.ts +54 -0
  430. package/src/cloud-lister/index.ts +134 -0
  431. package/src/cloud-lister/kubernetes.test.ts +160 -0
  432. package/src/cloud-lister/kubernetes.ts +284 -0
  433. package/src/cloud-lister/markers.ts +74 -0
  434. package/src/cloud-lister/mask.ts +77 -0
  435. package/src/cloud-lister/shape.test.ts +81 -0
  436. package/src/cloud-lister/sigv4.ts +70 -0
  437. package/src/cloud-lister/transport.ts +118 -0
  438. package/src/cloud-lister/types.ts +135 -0
  439. package/src/components/capability-plugin-loader.ts +4 -0
  440. package/src/components/capability-plugin.ts +10 -0
  441. package/src/components/capability.ts +229 -0
  442. package/src/components/cli-support.ts +70 -14
  443. package/src/components/declared-reads.test.ts +103 -0
  444. package/src/components/declared-reads.ts +134 -0
  445. package/src/components/deploy-units.ts +4 -0
  446. package/src/components/driver.ts +89 -4
  447. package/src/components/fan-out-run.ts +95 -15
  448. package/src/components/fan-out-support.ts +10 -1
  449. package/src/components/fan-out.ts +30 -0
  450. package/src/components/groups.test.ts +320 -0
  451. package/src/components/groups.ts +880 -0
  452. package/src/components/index.ts +42 -0
  453. package/src/components/plan-shape-conformance.ts +130 -0
  454. package/src/components/pr-env-run-bundle.test.ts +2 -0
  455. package/src/components/pr-run.test.ts +516 -3
  456. package/src/components/pr-run.ts +231 -41
  457. package/src/components/promote-rounds.ts +25 -2
  458. package/src/components/promote.ts +55 -5
  459. package/src/components/registry.test.ts +1 -1
  460. package/src/components/release-gate.ts +115 -30
  461. package/src/components/release-groups.test.ts +362 -0
  462. package/src/components/run-groups.ts +113 -0
  463. package/src/components/starter-plugin.ts +6 -0
  464. package/src/components/verbs/index.ts +1 -0
  465. package/src/components/verbs/member-outputs.test.ts +205 -0
  466. package/src/components/verbs/member-outputs.ts +308 -0
  467. package/src/config.ts +44 -0
  468. package/src/deep-observation.ts +5 -23
  469. package/src/drift-record.test.ts +596 -0
  470. package/src/drift-record.ts +1206 -0
  471. package/src/engine-facts.guard.test.ts +111 -0
  472. package/src/engine-facts.test.ts +178 -0
  473. package/src/engine-facts.ts +154 -0
  474. package/src/gated-waves-bundle.test.ts +2 -0
  475. package/src/graph-ir-live.test.ts +92 -0
  476. package/src/graph-ir.ts +317 -7
  477. package/src/graph-refs.ts +416 -0
  478. package/src/graph-schema/fixtures/adopt.json +15 -0
  479. package/src/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  480. package/src/graph-schema/fixtures/index.json +68 -0
  481. package/src/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  482. package/src/graph-schema/fixtures.ts +267 -0
  483. package/src/graph-schema/generate.ts +263 -0
  484. package/src/graph-schema/graph-schema.test.ts +153 -0
  485. package/src/graph-schema/graph.schema.json +1387 -0
  486. package/src/health.schema.json +2 -1
  487. package/src/identity.ts +2 -0
  488. package/src/index.ts +2 -0
  489. package/src/instance-nodes.ts +288 -0
  490. package/src/lexicon.ts +76 -0
  491. package/src/lifecycle/__fixtures__/index-diff/20261026T120000Z-aaaaaaaaaaaa.json +149 -0
  492. package/src/lifecycle/__fixtures__/index-diff/20261026T130000Z-bbbbbbbbbbbb.json +127 -0
  493. package/src/lifecycle/adoption-file.test.ts +100 -0
  494. package/src/lifecycle/adoption-file.ts +154 -0
  495. package/src/lifecycle/cloud-index.test.ts +517 -0
  496. package/src/lifecycle/cloud-index.ts +736 -0
  497. package/src/lifecycle/derive-edges.test.ts +193 -0
  498. package/src/lifecycle/derive-edges.ts +182 -0
  499. package/src/lifecycle/git.ts +10 -4
  500. package/src/lifecycle/identity-match.test.ts +308 -0
  501. package/src/lifecycle/identity-match.ts +257 -0
  502. package/src/lifecycle/index-diff.test.ts +198 -0
  503. package/src/lifecycle/index-diff.ts +476 -0
  504. package/src/lifecycle/index-document.test.ts +64 -0
  505. package/src/lifecycle/index-document.ts +299 -0
  506. package/src/lifecycle/index-drift.test.ts +191 -0
  507. package/src/lifecycle/index-drift.ts +304 -0
  508. package/src/lifecycle/index-import.test.ts +255 -0
  509. package/src/lifecycle/index-import.ts +634 -0
  510. package/src/lifecycle/index-join.test.ts +250 -0
  511. package/src/lifecycle/index-join.ts +304 -0
  512. package/src/lifecycle/index-store-load-bearing.test.ts +187 -0
  513. package/src/lifecycle/index.ts +4 -0
  514. package/src/lifecycle/member-ledger.test.ts +25 -7
  515. package/src/lifecycle/observation-baseline.test.ts +72 -1
  516. package/src/lifecycle/observation-baseline.ts +190 -27
  517. package/src/lifecycle/observe.ts +6 -1
  518. package/src/lifecycle/query.test.ts +69 -1
  519. package/src/lifecycle/query.ts +103 -10
  520. package/src/lifecycle/replay.test.ts +9 -5
  521. package/src/lifecycle/replay.ts +9 -5
  522. package/src/lifecycle/scenario.ts +4 -3
  523. package/src/lifecycle/snapshot-store-fake.ts +29 -0
  524. package/src/lifecycle/snapshot-store-s3.ts +316 -0
  525. package/src/lifecycle/snapshot-store.test.ts +385 -0
  526. package/src/lifecycle/snapshot-store.ts +981 -0
  527. package/src/lifecycle/snapshot.test.ts +13 -0
  528. package/src/lifecycle/snapshot.ts +23 -6
  529. package/src/lifecycle/type-listing.test.ts +166 -0
  530. package/src/lifecycle/type-listing.ts +309 -0
  531. package/src/lint/component-checks.ts +17 -1
  532. package/src/lint/config.test.ts +3 -0
  533. package/src/lint/presets/relaxed.json +2 -1
  534. package/src/lint/presets/strict.json +2 -1
  535. package/src/lint/rules/__fixtures__/comp/comp008/fail/dns.component.ts +15 -0
  536. package/src/lint/rules/__fixtures__/comp/comp008/pass/dns.component.ts +10 -0
  537. package/src/lint/rules/comp/comp.test.ts +2 -1
  538. package/src/lint/rules/comp/comp008-applies-signal-only-root.ts +121 -0
  539. package/src/lint/rules/comp/comp008.test.ts +77 -0
  540. package/src/lint/rules/comp/index.ts +4 -1
  541. package/src/observation-edges.test.ts +33 -0
  542. package/src/observation.ts +36 -1
  543. package/src/op/activities/activity-contracts.ts +1 -1
  544. package/src/op/activities/change-set.ts +8 -5
  545. package/src/op/activity-failure.ts +36 -0
  546. package/src/op/index.ts +2 -1
  547. package/src/op/local-executor.test.ts +80 -0
  548. package/src/op/local-executor.ts +27 -9
  549. package/src/pr-loop-bundle.test.ts +5 -2
  550. package/src/pr-loop.test.ts +43 -0
  551. package/src/pr-loop.ts +111 -2
  552. package/src/pulumi-urn.ts +151 -0
  553. package/src/release-floors.ts +22 -0
  554. package/src/sensitive-key.ts +34 -0
  555. package/src/workspace/adoption-report.test.ts +61 -0
  556. package/src/workspace/adoption-report.ts +103 -0
  557. package/src/workspace/capabilities-contract.test.ts +4 -0
  558. package/src/workspace/capabilities-contract.ts +9 -1
  559. package/src/workspace/capabilities.schema.json +98 -0
  560. package/src/workspace/change-set.schema.json +42 -3
  561. package/src/workspace/ci-cloud-identity.ts +31 -1
  562. package/src/workspace/compose-graph.test.ts +12 -0
  563. package/src/workspace/compose-graph.ts +12 -1
  564. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  565. package/src/workspace/conformance/index.ts +6 -1
  566. package/src/workspace/drift-cli.test.ts +65 -0
  567. package/src/workspace/drift-cli.ts +365 -0
  568. package/src/workspace/drift.schema.json +672 -0
  569. package/src/workspace/graph-cli.ts +5 -0
  570. package/src/workspace/graph.schema.json +72 -0
  571. package/src/workspace/init.test.ts +38 -1
  572. package/src/workspace/init.ts +50 -8
  573. package/src/workspace/intent-gaps.test.ts +18 -2
  574. package/src/workspace/ledger.schema.json +1 -1
  575. package/src/workspace/nested-graph.ts +1 -0
  576. package/src/workspace/pr-report.schema.json +51 -2
  577. package/src/workspace/promote-waves.test.ts +296 -0
  578. package/src/workspace/promote-waves.ts +340 -0
  579. package/src/workspace/promote.test.ts +158 -4
  580. package/src/workspace/promote.ts +398 -57
  581. package/src/workspace/reason-codes.test.ts +4 -1
  582. package/src/workspace/reason-codes.ts +14 -0
  583. package/src/workspace/search-contract.ts +2 -0
  584. package/src/workspace/search.schema.json +103 -5
@@ -44,6 +44,8 @@ import { CapabilityRegistry, type Capability } from "./capability";
44
44
  import {
45
45
  runInterpretDriver,
46
46
  DriverRunFailure,
47
+ deployContext,
48
+ type InterpretGroups,
47
49
  type DriverComponent,
48
50
  type DriverComponentResult,
49
51
  type DriverGate,
@@ -53,6 +55,9 @@ import {
53
55
  type DriverStep,
54
56
  } from "./driver";
55
57
  import type { GateLedgerPort } from "../op/gate";
58
+ import type { GroupRun } from "./capability";
59
+ import type { FanOutGroup, FanOutPlan } from "./fan-out";
60
+ import { planAndRunComponentGroup, runComponentGroup } from "./groups";
56
61
  import type { Release } from "../lifecycle/release-store";
57
62
  import type { RunProgressEvent } from "./run-progress";
58
63
  import type { RunEventSink } from "../run-event-stream";
@@ -631,6 +636,27 @@ export interface RunPromotionOptions {
631
636
  onProgress?: (event: RunProgressEvent) => void;
632
637
  /** Where the run's events go (#4041, `--events-json`). */
633
638
  onRunEvent?: RunEventSink;
639
+ /**
640
+ * The grouped fan-out (ws-107, #4465, `formRunGroups`): each of its groups
641
+ * applies as one, in the wave it places it in. Absent, or without
642
+ * `groups`: every component runs its own deploy, as before.
643
+ */
644
+ grouped?: FanOutPlan;
645
+ /**
646
+ * The approved plan of each group, as the release gate's plan made it
647
+ * (`ReleasePlan.groups`). A group runs the one whose members are its own;
648
+ * the newest first. A group with none plans in its wave and applies only
649
+ * under `planGroupsAtWave`, and fails otherwise.
650
+ */
651
+ groupRuns?: readonly GroupRun[];
652
+ /** A promote with no release gate to bind plans each group in its wave (#4465). */
653
+ planGroupsAtWave?: boolean;
654
+ }
655
+
656
+ /** The run in `runs` that plans exactly the members of `group`. */
657
+ function approvedRunOf(runs: readonly GroupRun[] | undefined, group: FanOutGroup): GroupRun | undefined {
658
+ const want = [...group.components].sort().join("\n");
659
+ return runs?.find((r) => Object.keys(r.plans).sort().join("\n") === want);
634
660
  }
635
661
 
636
662
  /**
@@ -647,18 +673,42 @@ export async function runPromotion(options: RunPromotionOptions): Promise<Driver
647
673
  .filter((c) => names.has(c.name))
648
674
  .map((c) => ({ ...c, dependsOn: (c.dependsOn ?? []).filter((d) => names.has(d)) }));
649
675
  const expected = new Map(plan.items.map((i) => [i.component, i.digest]));
676
+ const registry = checkPublishedDigest(options.registry, expected);
677
+ const releaseIdentity = (component: string) => {
678
+ const item = plan.items.find((i) => i.component === component);
679
+ return item ? { version: item.digest, revision: item.source.gitSha } : undefined;
680
+ };
681
+ // ws-107 (#4465): a group applies as one, through its approved plan.
682
+ const grouped = options.grouped?.groups?.length ? options.grouped : undefined;
683
+ const byName = new Map(targets.map((c) => [c.name, c]));
684
+ const groups: InterpretGroups | undefined = grouped
685
+ ? {
686
+ waves: grouped.waves,
687
+ groups: grouped.groups!,
688
+ run: (group, componentOutputs) => {
689
+ const each = {
690
+ group,
691
+ components: byName,
692
+ registry,
693
+ env: plan.to,
694
+ ctxOf: (name: string) => deployContext({ env: plan.to, releaseIdentity }, name),
695
+ componentOutputs,
696
+ };
697
+ const approved = approvedRunOf(options.groupRuns, group);
698
+ return approved === undefined && options.planGroupsAtWave ? planAndRunComponentGroup(each) : runComponentGroup({ ...each, run: approved });
699
+ },
700
+ }
701
+ : undefined;
650
702
  try {
651
- return await runInterpretDriver(targets, checkPublishedDigest(options.registry, expected), {
703
+ return await runInterpretDriver(targets, registry, {
652
704
  env: plan.to,
653
705
  componentOutputs: options.componentOutputs ?? {},
654
706
  // #2574: a gate approval for this promote or rollback is bound to the
655
707
  // release it deploys, as well as the environment and composition.
656
708
  releases: expected,
657
709
  // #3061: the workload's service.version and vcs.ref.head.revision are the earlier release's own.
658
- releaseIdentity: (component) => {
659
- const item = plan.items.find((i) => i.component === component);
660
- return item ? { version: item.digest, revision: item.source.gitSha } : undefined;
661
- },
710
+ releaseIdentity,
711
+ ...(groups ? { groups } : {}),
662
712
  ...(options.onProgress ? { onProgress: options.onProgress } : {}),
663
713
  ...(options.onRunEvent ? { onRunEvent: options.onRunEvent } : {}),
664
714
  ...(options.gates ? { gates: options.gates } : {}),
@@ -99,7 +99,7 @@ describe("createCapabilityRegistry", () => {
99
99
  // for them to live in instead.
100
100
  test("family table lists exactly the agnostic starter families plus the Cloudflare apply leaves (#1293)", () => {
101
101
  expect(Object.keys(STARTER_VERB_FAMILIES).sort()).toEqual(
102
- ["build", "cloudflare", "escapeHatch", "publish", "sbom", "secrets", "supplyChainPolicy", "supplyChainSecurity", "waitVerify"].sort(),
102
+ ["build", "cloudflare", "engineOutputs", "escapeHatch", "publish", "sbom", "secrets", "supplyChainPolicy", "supplyChainSecurity", "waitVerify"].sort(),
103
103
  );
104
104
  });
105
105
 
@@ -60,7 +60,9 @@
60
60
  * recorded since (#4035): its record names what ran before that run.
61
61
  */
62
62
 
63
- import type { CapabilityRegistry, PlannedOutputs } from "./capability";
63
+ import type { CapabilityRegistry, GroupRun, PlannedOutputs } from "./capability";
64
+ import type { FanOutGroup, FanOutGroupFallback, FanOutPlan } from "./fan-out";
65
+ import { groupsByComponent, planComponentGroup } from "./groups";
64
66
  import { deployContext, resolveComponentGraph, resolveStepInput, unresolvedWiringReferences, type DriverComponent, type DriverGate, type DriverPhase, type DriverStep } from "./driver";
65
67
  import { componentPlanDigest } from "./gate-plan";
66
68
  import { gateName, RELEASE_GATE_OP } from "../op/gate-name";
@@ -137,6 +139,24 @@ export interface ReleaseChangeSetInput {
137
139
  * `seededOutputs`, which then holds what the run left. Default: every one.
138
140
  */
139
141
  only?: ReadonlySet<string>;
142
+ /**
143
+ * The promote's grouped fan-out (ws-107, #4465, `formRunGroups`). Each of
144
+ * its groups plans with one `planGroup` call in the wave it places it in,
145
+ * and the group digest is folded into each member's part, so the release
146
+ * gate's digest binds the composition. Absent, or without `groups`: every
147
+ * component plans by itself, wave by wave.
148
+ */
149
+ grouped?: FanOutPlan;
150
+ }
151
+
152
+ /** A group the release gate's plan planned as one (ws-107, #4465). */
153
+ export interface ReleasePlanGroup extends FanOutGroup {
154
+ /** The group digest folded into each member's `planDigest`; `null` when the group failed to plan. */
155
+ digest: string | null;
156
+ /** Why the group failed to plan. Every member is then a failed member. */
157
+ error?: string;
158
+ /** What `runGroup` gets back. It lives in the process that planned, as the promote that applies it is. */
159
+ run?: GroupRun;
140
160
  }
141
161
 
142
162
  /** A release's change set for an environment, and what each component was planned against. */
@@ -148,6 +168,10 @@ export interface ReleasePlan {
148
168
  plannedOutputs?: Record<string, string[]>;
149
169
  /** Per component, the references to a promoted upstream that no value stands for until it applies, sorted. Absent when none. */
150
170
  unknownInputs?: Record<string, string[]>;
171
+ /** The groups planned as one (#4465), in the order they planned. Absent when none. */
172
+ groups?: ReleasePlanGroup[];
173
+ /** The components that could have grouped and did not, with why: `grouped.fallbacks`. Absent when none. */
174
+ fallbacks?: FanOutGroupFallback[];
151
175
  }
152
176
 
153
177
  const byString = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
@@ -287,10 +311,94 @@ export async function planReleaseSet(input: ReleaseChangeSetInput): Promise<Rele
287
311
  const inPlan = (name: string): boolean => input.only === undefined || input.only.has(name);
288
312
  const isPlannable = (c: DriverComponent): boolean =>
289
313
  capabilitySteps(c.deploy).some((s) => registry.has(s.kind) && registry.resolve(s.kind).plan !== undefined);
290
- const { waves } = resolveComponentGraph(components.map((c) => ({ ...c, dependsOn: (c.dependsOn ?? []).filter((d) => byName.has(d)) })));
314
+ const failedPart = (member: string, err: unknown): ChangeSetPart => ({
315
+ member: {
316
+ member,
317
+ lexicon: "chant",
318
+ planner: "chant",
319
+ status: "failed",
320
+ error: err instanceof Error ? err.message : String(err),
321
+ planDigest: null,
322
+ holes: [],
323
+ },
324
+ entries: [],
325
+ });
326
+ /** Read each dependency of `component` once, leaving out `mates`, its own group (#4465), and keep what it read. */
327
+ const readDependencies = async (component: DriverComponent, mates: ReadonlySet<string>): Promise<void> => {
328
+ for (const dep of component.dependsOn ?? []) {
329
+ if (mates.has(dep) || read.has(dep) || !byName.has(dep)) continue;
330
+ read.add(dep);
331
+ // A dependency this promote plans exposes what its plan says (#3999).
332
+ if (plannedBy.has(dep) && isPlannable(byName.get(dep)!)) {
333
+ const known = plannedBy.get(dep)?.known ?? {};
334
+ outputs[dep] = { ...known };
335
+ planned[dep] = Object.keys(known).sort();
336
+ continue;
337
+ }
338
+ const found = await readComponentOutputs(byName.get(dep)!, registry, deployContext({ env }, dep), outputs);
339
+ if (found) outputs[dep] = found;
340
+ }
341
+ reads[component.name] = Object.fromEntries(
342
+ (component.dependsOn ?? []).filter((d) => !mates.has(d) && d in outputs).map((d) => [d, structuredClone(outputs[d])]),
343
+ );
344
+ };
345
+ /** The references of `component`'s plannable steps to a promoted upstream outside `mates` that its plan can't say yet. */
346
+ const unknownOf = (component: DriverComponent, mates: ReadonlySet<string>): Set<string> => {
347
+ const unknown = new Set<string>();
348
+ for (const step of capabilitySteps(component.deploy)) {
349
+ if (!registry.has(step.kind) || registry.resolve(step.kind).plan === undefined) continue;
350
+ const { kind: _kind, ...rest } = step;
351
+ for (const r of unresolvedWiringReferences(rest, outputs)) if (plannedBy.has(r.source) && !r.publish && !mates.has(r.source)) unknown.add(r.ref);
352
+ }
353
+ return unknown;
354
+ };
355
+
356
+ // ws-107 (#4465): a group plans once, in the wave the grouped fan-out places it in.
357
+ const grouped = input.grouped?.groups?.length ? input.grouped : undefined;
358
+ const groupOf = grouped ? groupsByComponent(grouped) : new Map<string, FanOutGroup>();
359
+ const groups: ReleasePlanGroup[] = [];
360
+ const planGroupAt = async (group: FanOutGroup): Promise<void> => {
361
+ const mates = new Set(group.components);
362
+ try {
363
+ for (const name of group.components) await readDependencies(byName.get(name)!, mates);
364
+ } catch (err) {
365
+ for (const name of group.components) {
366
+ plannedBy.set(name, undefined);
367
+ parts.push(failedPart(name, err));
368
+ }
369
+ groups.push({ ...group, digest: null, error: err instanceof Error ? err.message : String(err) });
370
+ return;
371
+ }
372
+ const g = await planComponentGroup({ group, components: byName, registry, env, ctxOf: (name) => deployContext({ env }, name), outputs });
373
+ const error = g.plans.find((p): p is Extract<typeof p, { error: string }> => "error" in p)?.error;
374
+ groups.push({ ...group, digest: g.digest ?? null, ...(error !== undefined ? { error } : {}), ...(g.run ? { run: g.run } : {}) });
375
+ for (const p of g.plans) {
376
+ if ("error" in p) {
377
+ plannedBy.set(p.component, undefined);
378
+ parts.push(failedPart(p.component, p.error));
379
+ continue;
380
+ }
381
+ plannedBy.set(p.component, p.outputs);
382
+ parts.push(...p.parts);
383
+ const unknown = unknownOf(byName.get(p.component)!, mates);
384
+ if (unknown.size > 0) unknownInputs[p.component] = [...unknown].sort();
385
+ }
386
+ };
387
+
388
+ const waves = grouped
389
+ ? grouped.waves.map((w) => w.filter((name) => byName.has(name)))
390
+ : resolveComponentGraph(components.map((c) => ({ ...c, dependsOn: (c.dependsOn ?? []).filter((d) => byName.has(d)) }))).waves;
291
391
  for (const wave of waves) {
292
392
  const planning = wave.filter(inPlan);
393
+ const inWave = new Set(planning);
394
+ // What is left of a group in this plan; one member alone plans by itself.
395
+ const groupsHere = [...new Set(planning.map((name) => groupOf.get(name)).filter((g): g is FanOutGroup => g !== undefined))]
396
+ .map((g) => ({ ...g, components: g.components.filter((c) => inWave.has(c)) }))
397
+ .filter((g) => g.components.length > 1);
398
+ const grouping = new Set(groupsHere.flatMap((g) => g.components));
399
+ for (const group of groupsHere) await planGroupAt(group);
293
400
  for (const name of planning) {
401
+ if (grouping.has(name)) continue;
294
402
  const component = byName.get(name)!;
295
403
  const digest = input.digests.get(name) ?? "";
296
404
  const plannable = capabilitySteps(component.deploy).filter((s) => registry.has(s.kind) && registry.resolve(s.kind).plan !== undefined);
@@ -300,25 +408,11 @@ export async function planReleaseSet(input: ReleaseChangeSetInput): Promise<Rele
300
408
  }
301
409
  const ctx = deployContext({ env }, name);
302
410
  try {
303
- for (const dep of component.dependsOn ?? []) {
304
- if (read.has(dep) || !byName.has(dep)) continue;
305
- read.add(dep);
306
- // A dependency this promote plans exposes what its plan says (#3999).
307
- if (plannedBy.has(dep) && isPlannable(byName.get(dep)!)) {
308
- const known = plannedBy.get(dep)?.known ?? {};
309
- outputs[dep] = { ...known };
310
- planned[dep] = Object.keys(known).sort();
311
- continue;
312
- }
313
- const found = await readComponentOutputs(byName.get(dep)!, registry, deployContext({ env }, dep), outputs);
314
- if (found) outputs[dep] = found;
315
- }
316
- reads[name] = Object.fromEntries((component.dependsOn ?? []).filter((d) => d in outputs).map((d) => [d, structuredClone(outputs[d])]));
317
- const unknown = new Set<string>();
411
+ await readDependencies(component, new Set());
412
+ const unknown = unknownOf(component, new Set());
318
413
  let exposes: PlannedOutputs | undefined;
319
414
  for (const step of plannable) {
320
415
  const { kind, ...rest } = step;
321
- for (const r of unresolvedWiringReferences(rest, outputs)) if (plannedBy.has(r.source) && !r.publish) unknown.add(r.ref);
322
416
  const result = await registry.resolve(kind).plan!(planContext(ctx, rest, outputs), resolveStepInput(rest, {}, outputs) as never);
323
417
  if (result.outputs) exposes = { known: { ...(exposes?.known ?? {}), ...result.outputs.known }, unknown: [...new Set([...(exposes?.unknown ?? []), ...result.outputs.unknown])].sort() };
324
418
  parts.push(
@@ -339,18 +433,7 @@ export async function planReleaseSet(input: ReleaseChangeSetInput): Promise<Rele
339
433
  if (unknown.size > 0) unknownInputs[name] = [...unknown].sort();
340
434
  } catch (err) {
341
435
  plannedBy.set(name, undefined);
342
- parts.push({
343
- member: {
344
- member: name,
345
- lexicon: "chant",
346
- planner: "chant",
347
- status: "failed",
348
- error: err instanceof Error ? err.message : String(err),
349
- planDigest: null,
350
- holes: [],
351
- },
352
- entries: [],
353
- });
436
+ parts.push(failedPart(name, err));
354
437
  }
355
438
  }
356
439
  }
@@ -363,6 +446,8 @@ export async function planReleaseSet(input: ReleaseChangeSetInput): Promise<Rele
363
446
  reads,
364
447
  ...(Object.keys(planned).length > 0 ? { plannedOutputs: planned } : {}),
365
448
  ...(Object.keys(unknownInputs).length > 0 ? { unknownInputs } : {}),
449
+ ...(groups.length > 0 ? { groups } : {}),
450
+ ...(input.grouped?.fallbacks?.length ? { fallbacks: input.grouped.fallbacks } : {}),
366
451
  };
367
452
  }
368
453
 
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Composed runs at promote, fan-out and `run --components` (ws-107, #4465),
3
+ * against a fake capability: `fake-stack` creates one resource and exposes an
4
+ * `id` known only after it applies. With `group` it declares `planGroup` and
5
+ * `runGroup`; without, it plans and applies component by component, as
6
+ * before. The pull-request run's groups are in `pr-run.test.ts`.
7
+ */
8
+
9
+ import { describe, expect, test } from "vitest";
10
+ import {
11
+ CapabilityRegistry,
12
+ type CapabilityGroupOutcome,
13
+ type DeployContext,
14
+ type GroupContext,
15
+ type GroupMember,
16
+ type GroupRun,
17
+ type PlannedOutputs,
18
+ } from "./capability";
19
+ import { computePlanDigest } from "../lifecycle/plan-digest";
20
+ import { isEmptyChangeSet } from "../change-set";
21
+ import { makeRelease, type Release } from "../lifecycle/release-store";
22
+ import type { ReleaseRecord } from "../lifecycle/release-ledger";
23
+ import { memoryGateLedgerPort } from "../op/gate";
24
+ import { runInterpretDriver, type DriverComponent, type DriverRunResult, type ReleaseGateBinding } from "./driver";
25
+ import { remainingFanOut } from "./fan-out";
26
+ import { runFanOut } from "./fan-out-run";
27
+ import { componentSetFanOut, formRunGroups, planAndRunComponentGroup, regroupRemaining, WAVE_GATE_FALLBACK } from "./groups";
28
+ import { runPromotion, type PromotionPlan } from "./promote";
29
+ import { runPromotionInRounds } from "./promote-rounds";
30
+ import { planReleaseSet } from "./release-gate";
31
+ import { renderRunGroups, runGroupsReport } from "./run-groups";
32
+
33
+ interface StackInput {
34
+ root: string;
35
+ vars?: Record<string, unknown>;
36
+ }
37
+
38
+ const so = (stack: string, name: string) => ({ stackOutput: { stack, name } });
39
+ const sha = (c: string) => `sha256:${c.repeat(64)}`;
40
+
41
+ const stack = (name: string, dependsOn: string[] = [], vars?: Record<string, unknown>): DriverComponent => ({
42
+ name,
43
+ dependsOn,
44
+ deploy: [{ phase: "Apply", steps: [{ kind: "fake-stack", root: name, ...(vars ? { vars } : {}) }] }],
45
+ });
46
+ /** a; b reads a's new id; c reads b's. */
47
+ const ABC = [stack("a"), stack("b", ["a"], { upstream: so("a", "id") }), stack("c", ["b"], { upstream: so("b", "id") })];
48
+
49
+ function world(opts: { group?: boolean } = {}) {
50
+ const w = {
51
+ registry: new CapabilityRegistry(),
52
+ planned: [] as string[],
53
+ applied: [] as string[],
54
+ planGroupCalls: [] as string[][],
55
+ runGroupCalls: [] as Array<{ components: string[]; run: GroupRun }>,
56
+ outcome: undefined as ((members: readonly GroupMember[]) => CapabilityGroupOutcome) | undefined,
57
+ };
58
+ const planOf = (input: StackInput) => {
59
+ const planDigest = computePlanDigest("fake-stack", { root: input.root, vars: input.vars ?? null });
60
+ const outputs: PlannedOutputs = { known: {}, unknown: ["id"] };
61
+ return {
62
+ member: input.root,
63
+ planDigest,
64
+ artifact: { planDigest },
65
+ outputs,
66
+ changeSet: {
67
+ member: { member: input.root, lexicon: "test", planner: "tofu" as const, status: "planned" as const, planDigest, holes: [] },
68
+ entries: [{ member: input.root, lexicon: "test", planner: "tofu" as const, address: "res.x", type: "res", action: "create" as const, attributes: [] }],
69
+ },
70
+ };
71
+ };
72
+ w.registry.register({
73
+ kind: "fake-stack",
74
+ async plan(_ctx: DeployContext, input: StackInput) {
75
+ w.planned.push(input.root);
76
+ return planOf(input);
77
+ },
78
+ async run(_ctx: DeployContext, input: StackInput) {
79
+ w.applied.push(input.root);
80
+ return { outputs: { id: `${input.root}-id` } };
81
+ },
82
+ ...(opts.group
83
+ ? {
84
+ async planGroup(_ctx: GroupContext, members: readonly GroupMember<StackInput>[]) {
85
+ w.planGroupCalls.push(members.map((m) => m.component));
86
+ return {
87
+ members: Object.fromEntries(members.map((m) => [m.component, planOf(m.input!)])),
88
+ digest: computePlanDigest("fake-composition", members.map((m) => m.component)),
89
+ artifact: { composed: members.map((m) => m.component) },
90
+ };
91
+ },
92
+ async runGroup(_ctx: GroupContext, run: GroupRun, members: readonly GroupMember<StackInput>[]) {
93
+ w.runGroupCalls.push({ components: members.map((m) => m.component), run });
94
+ if (w.outcome) return w.outcome(members);
95
+ return { members: Object.fromEntries(members.map((m) => [m.component, { status: "applied" as const, outputs: { id: `${m.component}-id` } }])) };
96
+ },
97
+ }
98
+ : {}),
99
+ } as never);
100
+ return w;
101
+ }
102
+
103
+ const release = (): Release =>
104
+ makeRelease({
105
+ member: null,
106
+ commit: "a".repeat(40),
107
+ artifactDigests: [],
108
+ targets: [{ env: "prod", templates: [], params: [] }],
109
+ });
110
+
111
+ const source = (component: string): ReleaseRecord => ({
112
+ version: 1,
113
+ component,
114
+ env: "staging",
115
+ digest: sha("d"),
116
+ gitSha: "a".repeat(40),
117
+ runId: "run-1",
118
+ timestamp: "2026-10-01T00:00:00.000Z",
119
+ actor: "ci",
120
+ });
121
+
122
+ const promotion = (components: readonly DriverComponent[]): PromotionPlan => ({
123
+ from: "staging",
124
+ to: "prod",
125
+ items: components.map((c) => ({ component: c.name, digest: sha("d"), source: source(c.name) })),
126
+ notPromoted: [],
127
+ });
128
+
129
+ async function grouped(components: DriverComponent[], registry: CapabilityRegistry, waveGate = false) {
130
+ return formRunGroups({ components, plan: componentSetFanOut(components), registry, env: "prod", ...(waveGate ? { waveGate: true } : {}) });
131
+ }
132
+
133
+ async function planned(components: DriverComponent[], registry: CapabilityRegistry, waveGate = false) {
134
+ const plan = await grouped(components, registry, waveGate);
135
+ const set = await planReleaseSet({ release: release(), env: "prod", components, digests: new Map(), registry, current: new Map(), grouped: plan });
136
+ return { plan, set };
137
+ }
138
+
139
+ const binding = (digest: string): ReleaseGateBinding => ({ op: "release", planDigest: digest, releaseDigest: sha("r") });
140
+
141
+ describe("promote --release plans a group as one (#4465)", () => {
142
+ test("A, B reading A and C reading B: one planGroup call, the group digest in every member's part, no planned outputs between members", async () => {
143
+ const w = world({ group: true });
144
+ const { plan, set } = await planned(ABC, w.registry);
145
+ expect(plan.groups).toEqual([{ capability: "fake-stack", components: ["a", "b", "c"] }]);
146
+ expect(w.planGroupCalls).toEqual([["a", "b", "c"]]);
147
+ expect(w.planned).toEqual([]);
148
+ const digest = set.groups?.[0]?.digest;
149
+ expect(digest).toEqual(expect.stringMatching(/^jcs1-sha256:[0-9a-f]{64}$/));
150
+ expect(set.groups?.[0]?.run?.digest).toBe(digest);
151
+ // The release gate's change set names the group (its digest) on each of its members, and what each reads inside it.
152
+ expect(set.doc.members.map((m) => [m.member, m.status, m.group])).toEqual([
153
+ ["a", "planned", { digest, reads: [] }],
154
+ ["b", "planned", { digest, reads: ["a.id"] }],
155
+ ["c", "planned", { digest, reads: ["b.id"] }],
156
+ ]);
157
+ expect(isEmptyChangeSet(set.doc)).toBe(false);
158
+ // A read of a group-mate is no planned output and no unknown input, so the promote runs no round for it.
159
+ expect(set.plannedOutputs).toBeUndefined();
160
+ expect(set.unknownInputs).toBeUndefined();
161
+ });
162
+
163
+ test("without the group pair it plans component by component, as before", async () => {
164
+ const w = world();
165
+ const { plan, set } = await planned(ABC, w.registry);
166
+ expect(plan.groups).toBeUndefined();
167
+ expect(w.planGroupCalls).toEqual([]);
168
+ expect(w.planned).toEqual(["a", "b", "c"]);
169
+ expect(set.groups).toBeUndefined();
170
+ expect(set.doc.members.every((m) => m.group === undefined)).toBe(true);
171
+ expect(set.unknownInputs).toEqual({ b: ["a.id"], c: ["b.id"] });
172
+ });
173
+
174
+ test("with --wave-gate no group forms: each member falls back as wave-gate and plans by itself", async () => {
175
+ const w = world({ group: true });
176
+ const { plan, set } = await planned(ABC, w.registry, true);
177
+ expect(plan.groups).toBeUndefined();
178
+ expect(plan.waves).toEqual([["a"], ["b"], ["c"]]);
179
+ expect(plan.fallbacks?.map((f) => [f.component, f.kind])).toEqual([
180
+ ["a", WAVE_GATE_FALLBACK],
181
+ ["b", WAVE_GATE_FALLBACK],
182
+ ["c", WAVE_GATE_FALLBACK],
183
+ ]);
184
+ expect(w.planGroupCalls).toEqual([]);
185
+ expect(w.planned).toEqual(["a", "b", "c"]);
186
+ expect(set.fallbacks).toEqual(plan.fallbacks);
187
+ });
188
+ });
189
+
190
+ describe("promote --release applies a group as one (#4465)", () => {
191
+ test("one runGroup call with the plan the release gate bound, and no round", async () => {
192
+ const w = world({ group: true });
193
+ const { plan, set } = await planned(ABC, w.registry);
194
+ const groupRuns = (set.groups ?? []).map((g) => g.run!);
195
+ const runs: string[][] = [];
196
+ const result = await runPromotionInRounds({
197
+ components: ABC,
198
+ outputs: {},
199
+ binding: binding(set.doc.digest),
200
+ approved: set.doc,
201
+ reads: set.reads,
202
+ grouped: plan,
203
+ replan: async () => {
204
+ throw new Error("no round: nothing is planned again");
205
+ },
206
+ bind: async () => {
207
+ throw new Error("no round: nothing is bound again");
208
+ },
209
+ run: (components, outputs, b) => {
210
+ runs.push(components.map((c) => c.name));
211
+ return runPromotion({
212
+ plan: promotion(components),
213
+ components,
214
+ registry: w.registry,
215
+ componentOutputs: outputs,
216
+ gates: memoryGateLedgerPort(),
217
+ releaseGate: b,
218
+ grouped: plan,
219
+ groupRuns,
220
+ });
221
+ },
222
+ });
223
+ expect(result.stages).toEqual([]);
224
+ expect(runs).toEqual([["a", "b", "c"]]);
225
+ expect(w.runGroupCalls.map((c) => c.components)).toEqual([["a", "b", "c"]]);
226
+ expect(w.runGroupCalls[0]!.run.digest).toBe(set.groups![0]!.digest);
227
+ expect(w.applied).toEqual([]);
228
+ expect(result.run.status).toBe("ok");
229
+ expect(result.completed).toEqual(["a", "b", "c"]);
230
+ expect(result.outputs).toMatchObject({ a: { id: "a-id" }, b: { id: "b-id" }, c: { id: "c-id" } });
231
+ });
232
+
233
+ test("a group with no approved plan fails every member unless the promote plans it in its wave", async () => {
234
+ const w = world({ group: true });
235
+ const plan = await grouped(ABC, w.registry);
236
+ const refused = await runPromotion({ plan: promotion(ABC), components: ABC, registry: w.registry, gates: memoryGateLedgerPort(), grouped: plan, groupRuns: [] });
237
+ expect(refused.status).toBe("fail");
238
+ expect(w.runGroupCalls).toEqual([]);
239
+ const ran = await runPromotion({ plan: promotion(ABC), components: ABC, registry: w.registry, gates: memoryGateLedgerPort(), grouped: plan, planGroupsAtWave: true });
240
+ expect(ran.status).toBe("ok");
241
+ expect(w.planGroupCalls).toEqual([["a", "b", "c"]]);
242
+ expect(w.runGroupCalls.map((c) => c.components)).toEqual([["a", "b", "c"]]);
243
+ expect(ran.groupOutcomes).toEqual({ a: "applied", b: "applied", c: "applied" });
244
+ });
245
+
246
+ test("without the group pair each component runs its own deploy, as before", async () => {
247
+ const w = world();
248
+ const plan = await grouped(ABC, w.registry);
249
+ const ran = await runPromotion({ plan: promotion(ABC), components: ABC, registry: w.registry, gates: memoryGateLedgerPort(), grouped: plan });
250
+ expect(ran.status).toBe("ok");
251
+ expect(w.applied).toEqual(["a", "b", "c"]);
252
+ expect(ran.groupOutcomes).toBeUndefined();
253
+ });
254
+
255
+ test("a resume after the group failed part way plans only the members left, as a smaller group", async () => {
256
+ const w = world({ group: true });
257
+ const plan = await grouped(ABC, w.registry);
258
+ // The first attempt: a applied, b failed, c did not start.
259
+ w.outcome = () => ({
260
+ members: { a: { status: "applied", outputs: { id: "a-id" } }, b: { status: "failed", error: "boom" }, c: { status: "not-started" } },
261
+ });
262
+ const first = await runPromotion({ plan: promotion(ABC), components: ABC, registry: w.registry, gates: memoryGateLedgerPort(), grouped: plan, planGroupsAtWave: true });
263
+ expect(first.status).toBe("fail");
264
+ expect(first.groupOutcomes).toEqual({ a: "applied", b: "failed", c: "not-started" });
265
+ const completed = first.results.filter((r) => r.status === "ok").map((r) => r.component);
266
+ expect(completed).toEqual(["a"]);
267
+
268
+ // The resume groups what is left, as the promote handler does, and plans only that.
269
+ w.planGroupCalls.length = 0;
270
+ w.outcome = undefined;
271
+ const left = await regroupRemaining({
272
+ components: ABC,
273
+ plan: remainingFanOut(plan, ABC, { completed }),
274
+ registry: w.registry,
275
+ env: "prod",
276
+ groups: plan.groups,
277
+ });
278
+ expect(left.groups).toEqual([{ capability: "fake-stack", components: ["b", "c"] }]);
279
+ const set = await planReleaseSet({
280
+ release: release(),
281
+ env: "prod",
282
+ components: ABC,
283
+ digests: new Map(),
284
+ registry: w.registry,
285
+ current: new Map(),
286
+ seededOutputs: { a: { id: "a-id" } },
287
+ only: new Set(["b", "c"]),
288
+ grouped: left,
289
+ });
290
+ expect(w.planGroupCalls).toEqual([["b", "c"]]);
291
+ expect(set.doc.members.map((m) => m.member)).toEqual(["b", "c"]);
292
+ });
293
+ });
294
+
295
+ describe("components fan-out and run --components apply a group as one (#4465)", () => {
296
+ test("runFanOut plans a group in its wave and applies it with one runGroup call", async () => {
297
+ const w = world({ group: true });
298
+ const plan = await grouped(ABC, w.registry);
299
+ const result = await runFanOut(plan, ABC, w.registry, { env: "prod", gates: memoryGateLedgerPort(), planGroupsAtWave: true });
300
+ expect(result.status).toBe("ok");
301
+ expect(w.planGroupCalls).toEqual([["a", "b", "c"]]);
302
+ expect(w.runGroupCalls.map((c) => c.components)).toEqual([["a", "b", "c"]]);
303
+ expect(result.groupOutcomes).toEqual({ a: "applied", b: "applied", c: "applied" });
304
+ });
305
+
306
+ test("runInterpretDriver runs a group once, in place of its members' own deploys", async () => {
307
+ const w = world({ group: true });
308
+ const plan = await grouped(ABC, w.registry);
309
+ const byName = new Map(ABC.map((c) => [c.name, c]));
310
+ const run: DriverRunResult = await runInterpretDriver(ABC, w.registry, {
311
+ env: "prod",
312
+ gates: memoryGateLedgerPort(),
313
+ groups: {
314
+ waves: plan.waves,
315
+ groups: plan.groups!,
316
+ run: (group, componentOutputs) =>
317
+ planAndRunComponentGroup({ group, components: byName, registry: w.registry, env: "prod", ctxOf: (name) => ({ env: "prod", component: name }), componentOutputs }),
318
+ },
319
+ });
320
+ expect(run.status).toBe("ok");
321
+ expect(run.waves).toEqual([["a", "b", "c"]]);
322
+ expect(w.applied).toEqual([]);
323
+ expect(w.runGroupCalls.map((c) => c.components)).toEqual([["a", "b", "c"]]);
324
+ expect(run.componentOutputs).toMatchObject({ c: { id: "c-id" } });
325
+ });
326
+ });
327
+
328
+ describe("the promote report names the group and each member's outcome (#4465)", () => {
329
+ test("a group that stopped part way: its members, wave, digest and reads, each member's outcome, and what the same promote plans next", async () => {
330
+ const w = world({ group: true });
331
+ const { plan, set } = await planned(ABC, w.registry);
332
+ const digest = set.groups![0]!.digest!;
333
+ const report = runGroupsReport({ grouped: plan, planned: set.groups!, doc: set.doc, outcomes: { a: "applied", b: "partial", c: "not-started" } });
334
+ expect(report.groups).toEqual([
335
+ {
336
+ capability: "fake-stack",
337
+ components: ["a", "b", "c"],
338
+ wave: 1,
339
+ digest,
340
+ reads: [
341
+ { member: "b", ref: "a.id" },
342
+ { member: "c", ref: "b.id" },
343
+ ],
344
+ outcomes: { a: "applied", b: "partial", c: "not-started" },
345
+ },
346
+ ]);
347
+ expect(renderRunGroups(report, "the same promote")).toEqual([
348
+ `a, b and c plan and apply as one (fake-stack, wave 1, group ${digest}); b reads a.id and c reads b.id, known when the group applies`,
349
+ " a applied; b applied in part, and what applied stays; c did not start",
350
+ " the group's apply stopped part way and nothing that applied is undone; the same promote plans b and c again, as one group where they still join",
351
+ ]);
352
+ });
353
+
354
+ test("under --wave-gate the report says why each member planned by itself", async () => {
355
+ const w = world({ group: true });
356
+ const plan = await grouped(ABC, w.registry, true);
357
+ const lines = renderRunGroups(runGroupsReport({ grouped: plan }), "the same promote");
358
+ expect(lines).toEqual([
359
+ "planned one by one, not as a group: a, b, c (wave-gate): a, b and c would plan and apply as one, but under --wave-gate each wave plans component by component, so no group forms",
360
+ ]);
361
+ });
362
+ });