@intentius/chant 0.124.51 → 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 (593) 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 +17 -3
  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 +224 -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/covered-round.d.ts +173 -0
  120. package/dist/components/covered-round.d.ts.map +1 -0
  121. package/dist/components/covered-round.js +200 -0
  122. package/dist/components/declared-reads.d.ts +38 -0
  123. package/dist/components/declared-reads.d.ts.map +1 -0
  124. package/dist/components/declared-reads.js +131 -0
  125. package/dist/components/deploy-units.d.ts.map +1 -1
  126. package/dist/components/deploy-units.js +4 -0
  127. package/dist/components/driver.d.ts +42 -1
  128. package/dist/components/driver.d.ts.map +1 -1
  129. package/dist/components/driver.js +74 -3
  130. package/dist/components/fan-out-run.d.ts +23 -1
  131. package/dist/components/fan-out-run.d.ts.map +1 -1
  132. package/dist/components/fan-out-run.js +82 -14
  133. package/dist/components/fan-out-support.d.ts +6 -0
  134. package/dist/components/fan-out-support.d.ts.map +1 -1
  135. package/dist/components/fan-out-support.js +4 -1
  136. package/dist/components/fan-out.d.ts +28 -0
  137. package/dist/components/fan-out.d.ts.map +1 -1
  138. package/dist/components/groups.d.ts +229 -0
  139. package/dist/components/groups.d.ts.map +1 -0
  140. package/dist/components/groups.js +760 -0
  141. package/dist/components/index.d.ts +7 -2
  142. package/dist/components/index.d.ts.map +1 -1
  143. package/dist/components/index.js +5 -0
  144. package/dist/components/plan-shape-conformance.d.ts +48 -0
  145. package/dist/components/plan-shape-conformance.d.ts.map +1 -0
  146. package/dist/components/plan-shape-conformance.js +122 -0
  147. package/dist/components/pr-run.d.ts +62 -7
  148. package/dist/components/pr-run.d.ts.map +1 -1
  149. package/dist/components/pr-run.js +354 -53
  150. package/dist/components/promote-rounds.d.ts +9 -0
  151. package/dist/components/promote-rounds.d.ts.map +1 -1
  152. package/dist/components/promote-rounds.js +23 -2
  153. package/dist/components/promote.d.ts +17 -0
  154. package/dist/components/promote.d.ts.map +1 -1
  155. package/dist/components/promote.js +36 -6
  156. package/dist/components/release-gate.d.ts +23 -1
  157. package/dist/components/release-gate.d.ts.map +1 -1
  158. package/dist/components/release-gate.js +97 -33
  159. package/dist/components/run-groups.d.ts +58 -0
  160. package/dist/components/run-groups.d.ts.map +1 -0
  161. package/dist/components/run-groups.js +78 -0
  162. package/dist/components/starter-plugin.d.ts +1 -0
  163. package/dist/components/starter-plugin.d.ts.map +1 -1
  164. package/dist/components/starter-plugin.js +6 -1
  165. package/dist/components/verbs/index.d.ts +1 -0
  166. package/dist/components/verbs/index.d.ts.map +1 -1
  167. package/dist/components/verbs/index.js +1 -0
  168. package/dist/components/verbs/member-outputs.d.ts +187 -0
  169. package/dist/components/verbs/member-outputs.d.ts.map +1 -0
  170. package/dist/components/verbs/member-outputs.js +218 -0
  171. package/dist/components/wave-plan.d.ts +2 -0
  172. package/dist/components/wave-plan.d.ts.map +1 -1
  173. package/dist/components/wave-plan.js +4 -1
  174. package/dist/config.d.ts +60 -0
  175. package/dist/config.d.ts.map +1 -1
  176. package/dist/config.js +27 -0
  177. package/dist/deep-observation.d.ts +1 -13
  178. package/dist/deep-observation.d.ts.map +1 -1
  179. package/dist/deep-observation.js +5 -22
  180. package/dist/drift-record.d.ts +542 -0
  181. package/dist/drift-record.d.ts.map +1 -0
  182. package/dist/drift-record.js +886 -0
  183. package/dist/engine-facts.d.ts +144 -0
  184. package/dist/engine-facts.d.ts.map +1 -0
  185. package/dist/engine-facts.js +14 -0
  186. package/dist/graph-ir.d.ts +153 -2
  187. package/dist/graph-ir.d.ts.map +1 -1
  188. package/dist/graph-ir.js +224 -6
  189. package/dist/graph-refs.d.ts +145 -0
  190. package/dist/graph-refs.d.ts.map +1 -1
  191. package/dist/graph-refs.js +305 -0
  192. package/dist/graph-schema/fixtures/adopt.json +15 -0
  193. package/dist/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  194. package/dist/graph-schema/fixtures/index.json +68 -0
  195. package/dist/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  196. package/dist/graph-schema/fixtures.d.ts +23 -0
  197. package/dist/graph-schema/fixtures.d.ts.map +1 -0
  198. package/dist/graph-schema/fixtures.js +246 -0
  199. package/dist/graph-schema/generate.d.ts +15 -0
  200. package/dist/graph-schema/generate.d.ts.map +1 -0
  201. package/dist/graph-schema/generate.js +267 -0
  202. package/dist/graph-schema/graph.schema.json +1387 -0
  203. package/dist/health.schema.json +2 -1
  204. package/dist/identity.d.ts.map +1 -1
  205. package/dist/identity.js +2 -0
  206. package/dist/index.d.ts +1 -0
  207. package/dist/index.d.ts.map +1 -1
  208. package/dist/index.js +2 -0
  209. package/dist/instance-nodes.d.ts +159 -0
  210. package/dist/instance-nodes.d.ts.map +1 -0
  211. package/dist/instance-nodes.js +231 -0
  212. package/dist/lexicon.d.ts +72 -0
  213. package/dist/lexicon.d.ts.map +1 -1
  214. package/dist/lifecycle/adoption-file.d.ts +56 -0
  215. package/dist/lifecycle/adoption-file.d.ts.map +1 -0
  216. package/dist/lifecycle/adoption-file.js +123 -0
  217. package/dist/lifecycle/cloud-index.d.ts +186 -0
  218. package/dist/lifecycle/cloud-index.d.ts.map +1 -0
  219. package/dist/lifecycle/cloud-index.js +618 -0
  220. package/dist/lifecycle/derive-edges.d.ts +59 -0
  221. package/dist/lifecycle/derive-edges.d.ts.map +1 -0
  222. package/dist/lifecycle/derive-edges.js +126 -0
  223. package/dist/lifecycle/git.d.ts +10 -9
  224. package/dist/lifecycle/git.d.ts.map +1 -1
  225. package/dist/lifecycle/git.js +7 -1
  226. package/dist/lifecycle/identity-match.d.ts +133 -0
  227. package/dist/lifecycle/identity-match.d.ts.map +1 -0
  228. package/dist/lifecycle/identity-match.js +147 -0
  229. package/dist/lifecycle/index-diff.d.ts +224 -0
  230. package/dist/lifecycle/index-diff.d.ts.map +1 -0
  231. package/dist/lifecycle/index-diff.js +327 -0
  232. package/dist/lifecycle/index-document.d.ts +130 -0
  233. package/dist/lifecycle/index-document.d.ts.map +1 -0
  234. package/dist/lifecycle/index-document.js +189 -0
  235. package/dist/lifecycle/index-drift.d.ts +121 -0
  236. package/dist/lifecycle/index-drift.d.ts.map +1 -0
  237. package/dist/lifecycle/index-drift.js +178 -0
  238. package/dist/lifecycle/index-import.d.ts +168 -0
  239. package/dist/lifecycle/index-import.d.ts.map +1 -0
  240. package/dist/lifecycle/index-import.js +515 -0
  241. package/dist/lifecycle/index-join.d.ts +129 -0
  242. package/dist/lifecycle/index-join.d.ts.map +1 -0
  243. package/dist/lifecycle/index-join.js +248 -0
  244. package/dist/lifecycle/index.d.ts +4 -0
  245. package/dist/lifecycle/index.d.ts.map +1 -1
  246. package/dist/lifecycle/index.js +4 -0
  247. package/dist/lifecycle/observation-baseline.d.ts +61 -21
  248. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  249. package/dist/lifecycle/observation-baseline.js +144 -19
  250. package/dist/lifecycle/observe.d.ts.map +1 -1
  251. package/dist/lifecycle/observe.js +6 -1
  252. package/dist/lifecycle/query.d.ts +45 -4
  253. package/dist/lifecycle/query.d.ts.map +1 -1
  254. package/dist/lifecycle/query.js +89 -7
  255. package/dist/lifecycle/replay.d.ts +10 -3
  256. package/dist/lifecycle/replay.d.ts.map +1 -1
  257. package/dist/lifecycle/replay.js +10 -6
  258. package/dist/lifecycle/scenario.d.ts +4 -3
  259. package/dist/lifecycle/scenario.d.ts.map +1 -1
  260. package/dist/lifecycle/snapshot-store-fake.d.ts +9 -0
  261. package/dist/lifecycle/snapshot-store-fake.d.ts.map +1 -0
  262. package/dist/lifecycle/snapshot-store-fake.js +21 -0
  263. package/dist/lifecycle/snapshot-store-s3.d.ts +102 -0
  264. package/dist/lifecycle/snapshot-store-s3.d.ts.map +1 -0
  265. package/dist/lifecycle/snapshot-store-s3.js +254 -0
  266. package/dist/lifecycle/snapshot-store.d.ts +170 -0
  267. package/dist/lifecycle/snapshot-store.d.ts.map +1 -0
  268. package/dist/lifecycle/snapshot-store.js +832 -0
  269. package/dist/lifecycle/snapshot.d.ts +11 -0
  270. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  271. package/dist/lifecycle/snapshot.js +18 -5
  272. package/dist/lifecycle/type-listing.d.ts +148 -0
  273. package/dist/lifecycle/type-listing.d.ts.map +1 -0
  274. package/dist/lifecycle/type-listing.js +166 -0
  275. package/dist/lint/component-checks.d.ts +15 -1
  276. package/dist/lint/component-checks.d.ts.map +1 -1
  277. package/dist/lint/component-checks.js +1 -0
  278. package/dist/lint/presets/relaxed.json +2 -1
  279. package/dist/lint/presets/strict.json +2 -1
  280. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts +4 -0
  281. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts.map +1 -0
  282. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.js +13 -0
  283. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts +4 -0
  284. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts.map +1 -0
  285. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.js +8 -0
  286. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts +33 -0
  287. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts.map +1 -0
  288. package/dist/lint/rules/comp/comp008-applies-signal-only-root.js +123 -0
  289. package/dist/lint/rules/comp/index.d.ts +2 -1
  290. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  291. package/dist/lint/rules/comp/index.js +4 -1
  292. package/dist/observation.d.ts +19 -2
  293. package/dist/observation.d.ts.map +1 -1
  294. package/dist/observation.js +18 -1
  295. package/dist/op/activities/activity-contracts.d.ts +1 -1
  296. package/dist/op/activities/activity-contracts.js +1 -1
  297. package/dist/op/activities/change-set.d.ts +6 -4
  298. package/dist/op/activities/change-set.d.ts.map +1 -1
  299. package/dist/op/activities/change-set.js +4 -2
  300. package/dist/op/activity-failure.d.ts +27 -0
  301. package/dist/op/activity-failure.d.ts.map +1 -1
  302. package/dist/op/activity-failure.js +31 -0
  303. package/dist/op/index.d.ts +2 -1
  304. package/dist/op/index.d.ts.map +1 -1
  305. package/dist/op/index.js +1 -1
  306. package/dist/op/local-executor.d.ts +5 -1
  307. package/dist/op/local-executor.d.ts.map +1 -1
  308. package/dist/op/local-executor.js +22 -8
  309. package/dist/pr-loop.d.ts +92 -3
  310. package/dist/pr-loop.d.ts.map +1 -1
  311. package/dist/pr-loop.js +62 -3
  312. package/dist/pulumi-urn.d.ts +101 -0
  313. package/dist/pulumi-urn.d.ts.map +1 -0
  314. package/dist/pulumi-urn.js +131 -0
  315. package/dist/release-floors.d.ts +23 -0
  316. package/dist/release-floors.d.ts.map +1 -0
  317. package/dist/release-floors.js +22 -0
  318. package/dist/sensitive-key.d.ts +23 -0
  319. package/dist/sensitive-key.d.ts.map +1 -0
  320. package/dist/sensitive-key.js +31 -0
  321. package/dist/wave-runtime.mjs +234 -234
  322. package/dist/workspace/adoption-report.d.ts +53 -0
  323. package/dist/workspace/adoption-report.d.ts.map +1 -0
  324. package/dist/workspace/adoption-report.js +76 -0
  325. package/dist/workspace/capabilities-contract.d.ts +7 -0
  326. package/dist/workspace/capabilities-contract.d.ts.map +1 -1
  327. package/dist/workspace/capabilities-contract.js +2 -1
  328. package/dist/workspace/capabilities.schema.json +98 -0
  329. package/dist/workspace/change-set.schema.json +42 -3
  330. package/dist/workspace/ci-cloud-identity.d.ts +13 -1
  331. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  332. package/dist/workspace/ci-cloud-identity.js +30 -1
  333. package/dist/workspace/compose-graph.d.ts +10 -1
  334. package/dist/workspace/compose-graph.d.ts.map +1 -1
  335. package/dist/workspace/compose-graph.js +6 -1
  336. package/dist/workspace/conformance/index.d.ts +1 -1
  337. package/dist/workspace/conformance/index.d.ts.map +1 -1
  338. package/dist/workspace/conformance/index.js +6 -1
  339. package/dist/workspace/drift-cli.d.ts +81 -0
  340. package/dist/workspace/drift-cli.d.ts.map +1 -0
  341. package/dist/workspace/drift-cli.js +317 -0
  342. package/dist/workspace/drift.schema.json +672 -0
  343. package/dist/workspace/graph-cli.d.ts +2 -0
  344. package/dist/workspace/graph-cli.d.ts.map +1 -1
  345. package/dist/workspace/graph-cli.js +3 -0
  346. package/dist/workspace/graph.schema.json +72 -0
  347. package/dist/workspace/init.d.ts +5 -1
  348. package/dist/workspace/init.d.ts.map +1 -1
  349. package/dist/workspace/init.js +55 -9
  350. package/dist/workspace/ledger.schema.json +1 -1
  351. package/dist/workspace/nested-graph.d.ts.map +1 -1
  352. package/dist/workspace/nested-graph.js +2 -0
  353. package/dist/workspace/pr-report.schema.json +76 -3
  354. package/dist/workspace/promote-waves.d.ts +112 -0
  355. package/dist/workspace/promote-waves.d.ts.map +1 -0
  356. package/dist/workspace/promote-waves.js +290 -0
  357. package/dist/workspace/promote.d.ts +84 -2
  358. package/dist/workspace/promote.d.ts.map +1 -1
  359. package/dist/workspace/promote.js +347 -61
  360. package/dist/workspace/reason-codes.d.ts +12 -0
  361. package/dist/workspace/reason-codes.d.ts.map +1 -1
  362. package/dist/workspace/reason-codes.js +14 -0
  363. package/dist/workspace/search-contract.d.ts +3 -3
  364. package/dist/workspace/search-contract.d.ts.map +1 -1
  365. package/dist/workspace/search-contract.js +2 -0
  366. package/dist/workspace/search.schema.json +103 -5
  367. package/package.json +14 -1
  368. package/src/__fixtures__/drift-record/README.md +58 -0
  369. package/src/__fixtures__/drift-record/choudoufu-cone.ndjson +4 -0
  370. package/src/__fixtures__/drift-record/choudoufu-resource.ndjson +3 -0
  371. package/src/__fixtures__/drift-record/component-status-ledger.json +65 -0
  372. package/src/__fixtures__/drift-record/component-status.json +91 -0
  373. package/src/__fixtures__/drift-record/deep-diff.json +259 -0
  374. package/src/__fixtures__/drift-record/lifecycle-diff-live.json +825 -0
  375. package/src/__fixtures__/drift-record/live-diff.json +125 -0
  376. package/src/audit/discover.e2e.test.ts +14 -0
  377. package/src/audit/discover.ts +3 -1
  378. package/src/behaviour.ts +1 -1
  379. package/src/capabilities/templates.json +145 -0
  380. package/src/capabilities/templates.test.ts +212 -0
  381. package/src/capabilities/templates.ts +204 -0
  382. package/src/change-set-bundle.test.ts +83 -0
  383. package/src/change-set-graph.ts +80 -9
  384. package/src/change-set.test.ts +24 -0
  385. package/src/change-set.ts +77 -14
  386. package/src/ci-identity-targets.test.ts +23 -0
  387. package/src/ci-identity.ts +10 -4
  388. package/src/cli/commands/import-index.test.ts +117 -0
  389. package/src/cli/commands/import-index.ts +265 -0
  390. package/src/cli/commands/lint.ts +4 -2
  391. package/src/cli/handlers/components.test.ts +9 -0
  392. package/src/cli/handlers/components.ts +12 -2
  393. package/src/cli/handlers/fan-out.ts +17 -3
  394. package/src/cli/handlers/graph.test.ts +42 -0
  395. package/src/cli/handlers/graph.ts +26 -4
  396. package/src/cli/handlers/index-cloud.ts +32 -0
  397. package/src/cli/handlers/index-diff.test.ts +68 -0
  398. package/src/cli/handlers/index-diff.ts +146 -0
  399. package/src/cli/handlers/ledger-root-member.test.ts +161 -0
  400. package/src/cli/handlers/lifecycle-drift.ts +68 -0
  401. package/src/cli/handlers/lifecycle.test.ts +33 -11
  402. package/src/cli/handlers/lifecycle.ts +205 -30
  403. package/src/cli/handlers/misc.ts +73 -0
  404. package/src/cli/handlers/pr-waves-blocked.test.ts +37 -2
  405. package/src/cli/handlers/pr.ts +16 -3
  406. package/src/cli/handlers/promote.test.ts +64 -0
  407. package/src/cli/handlers/promote.ts +81 -6
  408. package/src/cli/handlers/query.ts +27 -4
  409. package/src/cli/handlers/run-declared-reads-cli.test.ts +187 -0
  410. package/src/cli/handlers/run-groups-cli.test.ts +229 -0
  411. package/src/cli/handlers/scenario.test.ts +13 -0
  412. package/src/cli/handlers/scenario.ts +8 -7
  413. package/src/cli/handlers/search-index.test.ts +255 -0
  414. package/src/cli/handlers/search-json.test.ts +1 -1
  415. package/src/cli/handlers/search-ownership.test.ts +194 -0
  416. package/src/cli/handlers/search.ts +271 -38
  417. package/src/cli/main.test.ts +7 -0
  418. package/src/cli/main.ts +56 -1
  419. package/src/cli/mcp/lifecycle-tools.ts +3 -2
  420. package/src/cli/mcp/resource-handlers.ts +4 -3
  421. package/src/cli/mcp/workspace-tools.ts +1 -1
  422. package/src/cli/parse-args.ts +40 -2
  423. package/src/cli/registry.ts +19 -1
  424. package/src/cloud-lister/aws.test.ts +202 -0
  425. package/src/cloud-lister/aws.ts +226 -0
  426. package/src/cloud-lister/azure.test.ts +132 -0
  427. package/src/cloud-lister/azure.ts +142 -0
  428. package/src/cloud-lister/common.ts +115 -0
  429. package/src/cloud-lister/credentials.ts +110 -0
  430. package/src/cloud-lister/fakes.ts +66 -0
  431. package/src/cloud-lister/gcp.test.ts +121 -0
  432. package/src/cloud-lister/gcp.ts +159 -0
  433. package/src/cloud-lister/guard.test.ts +198 -0
  434. package/src/cloud-lister/guard.ts +111 -0
  435. package/src/cloud-lister/ids.ts +54 -0
  436. package/src/cloud-lister/index.ts +134 -0
  437. package/src/cloud-lister/kubernetes.test.ts +160 -0
  438. package/src/cloud-lister/kubernetes.ts +284 -0
  439. package/src/cloud-lister/markers.ts +74 -0
  440. package/src/cloud-lister/mask.ts +77 -0
  441. package/src/cloud-lister/shape.test.ts +81 -0
  442. package/src/cloud-lister/sigv4.ts +70 -0
  443. package/src/cloud-lister/transport.ts +118 -0
  444. package/src/cloud-lister/types.ts +135 -0
  445. package/src/components/capability-plugin-loader.ts +4 -0
  446. package/src/components/capability-plugin.ts +10 -0
  447. package/src/components/capability.ts +238 -0
  448. package/src/components/cli-support.ts +70 -14
  449. package/src/components/covered-round.test.ts +166 -0
  450. package/src/components/covered-round.ts +293 -0
  451. package/src/components/declared-reads.test.ts +103 -0
  452. package/src/components/declared-reads.ts +134 -0
  453. package/src/components/deploy-units.ts +4 -0
  454. package/src/components/driver.ts +101 -4
  455. package/src/components/fan-out-run.ts +95 -15
  456. package/src/components/fan-out-support.ts +10 -1
  457. package/src/components/fan-out.ts +30 -0
  458. package/src/components/groups.test.ts +320 -0
  459. package/src/components/groups.ts +880 -0
  460. package/src/components/index.ts +59 -0
  461. package/src/components/plan-shape-conformance.ts +130 -0
  462. package/src/components/pr-env-run-bundle.test.ts +2 -0
  463. package/src/components/pr-run.test.ts +728 -4
  464. package/src/components/pr-run.ts +395 -55
  465. package/src/components/promote-rounds.ts +25 -2
  466. package/src/components/promote.ts +55 -5
  467. package/src/components/registry.test.ts +1 -1
  468. package/src/components/release-gate.ts +115 -30
  469. package/src/components/release-groups.test.ts +362 -0
  470. package/src/components/run-groups.ts +113 -0
  471. package/src/components/starter-plugin.ts +6 -0
  472. package/src/components/verbs/index.ts +1 -0
  473. package/src/components/verbs/member-outputs.test.ts +205 -0
  474. package/src/components/verbs/member-outputs.ts +308 -0
  475. package/src/components/wave-plan.ts +5 -2
  476. package/src/config.ts +44 -0
  477. package/src/deep-observation.ts +5 -23
  478. package/src/drift-record.test.ts +596 -0
  479. package/src/drift-record.ts +1206 -0
  480. package/src/engine-facts.guard.test.ts +111 -0
  481. package/src/engine-facts.test.ts +178 -0
  482. package/src/engine-facts.ts +154 -0
  483. package/src/gated-waves-bundle.test.ts +2 -0
  484. package/src/graph-ir-live.test.ts +92 -0
  485. package/src/graph-ir.ts +317 -7
  486. package/src/graph-refs.ts +416 -0
  487. package/src/graph-schema/fixtures/adopt.json +15 -0
  488. package/src/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  489. package/src/graph-schema/fixtures/index.json +68 -0
  490. package/src/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  491. package/src/graph-schema/fixtures.ts +267 -0
  492. package/src/graph-schema/generate.ts +263 -0
  493. package/src/graph-schema/graph-schema.test.ts +153 -0
  494. package/src/graph-schema/graph.schema.json +1387 -0
  495. package/src/health.schema.json +2 -1
  496. package/src/identity.ts +2 -0
  497. package/src/index.ts +2 -0
  498. package/src/instance-nodes.ts +288 -0
  499. package/src/lexicon.ts +76 -0
  500. package/src/lifecycle/__fixtures__/index-diff/20261026T120000Z-aaaaaaaaaaaa.json +149 -0
  501. package/src/lifecycle/__fixtures__/index-diff/20261026T130000Z-bbbbbbbbbbbb.json +127 -0
  502. package/src/lifecycle/adoption-file.test.ts +100 -0
  503. package/src/lifecycle/adoption-file.ts +154 -0
  504. package/src/lifecycle/cloud-index.test.ts +517 -0
  505. package/src/lifecycle/cloud-index.ts +736 -0
  506. package/src/lifecycle/derive-edges.test.ts +193 -0
  507. package/src/lifecycle/derive-edges.ts +182 -0
  508. package/src/lifecycle/git.ts +10 -4
  509. package/src/lifecycle/identity-match.test.ts +308 -0
  510. package/src/lifecycle/identity-match.ts +257 -0
  511. package/src/lifecycle/index-diff.test.ts +198 -0
  512. package/src/lifecycle/index-diff.ts +476 -0
  513. package/src/lifecycle/index-document.test.ts +64 -0
  514. package/src/lifecycle/index-document.ts +299 -0
  515. package/src/lifecycle/index-drift.test.ts +191 -0
  516. package/src/lifecycle/index-drift.ts +304 -0
  517. package/src/lifecycle/index-import.test.ts +255 -0
  518. package/src/lifecycle/index-import.ts +634 -0
  519. package/src/lifecycle/index-join.test.ts +250 -0
  520. package/src/lifecycle/index-join.ts +304 -0
  521. package/src/lifecycle/index-store-load-bearing.test.ts +187 -0
  522. package/src/lifecycle/index.ts +4 -0
  523. package/src/lifecycle/member-ledger.test.ts +25 -7
  524. package/src/lifecycle/observation-baseline.test.ts +72 -1
  525. package/src/lifecycle/observation-baseline.ts +190 -27
  526. package/src/lifecycle/observe.ts +6 -1
  527. package/src/lifecycle/query.test.ts +69 -1
  528. package/src/lifecycle/query.ts +103 -10
  529. package/src/lifecycle/replay.test.ts +9 -5
  530. package/src/lifecycle/replay.ts +9 -5
  531. package/src/lifecycle/scenario.ts +4 -3
  532. package/src/lifecycle/snapshot-store-fake.ts +29 -0
  533. package/src/lifecycle/snapshot-store-s3.ts +316 -0
  534. package/src/lifecycle/snapshot-store.test.ts +385 -0
  535. package/src/lifecycle/snapshot-store.ts +981 -0
  536. package/src/lifecycle/snapshot.test.ts +13 -0
  537. package/src/lifecycle/snapshot.ts +23 -6
  538. package/src/lifecycle/type-listing.test.ts +166 -0
  539. package/src/lifecycle/type-listing.ts +309 -0
  540. package/src/lint/component-checks.ts +17 -1
  541. package/src/lint/config.test.ts +3 -0
  542. package/src/lint/presets/relaxed.json +2 -1
  543. package/src/lint/presets/strict.json +2 -1
  544. package/src/lint/rules/__fixtures__/comp/comp008/fail/dns.component.ts +15 -0
  545. package/src/lint/rules/__fixtures__/comp/comp008/pass/dns.component.ts +10 -0
  546. package/src/lint/rules/comp/comp.test.ts +2 -1
  547. package/src/lint/rules/comp/comp008-applies-signal-only-root.ts +121 -0
  548. package/src/lint/rules/comp/comp008.test.ts +77 -0
  549. package/src/lint/rules/comp/index.ts +4 -1
  550. package/src/observation-edges.test.ts +33 -0
  551. package/src/observation.ts +36 -1
  552. package/src/op/activities/activity-contracts.ts +1 -1
  553. package/src/op/activities/change-set.ts +8 -5
  554. package/src/op/activity-failure.ts +36 -0
  555. package/src/op/index.ts +2 -1
  556. package/src/op/local-executor.test.ts +80 -0
  557. package/src/op/local-executor.ts +27 -9
  558. package/src/pr-loop-bundle.test.ts +5 -2
  559. package/src/pr-loop.test.ts +57 -1
  560. package/src/pr-loop.ts +154 -6
  561. package/src/pulumi-urn.ts +151 -0
  562. package/src/release-floors.ts +22 -0
  563. package/src/sensitive-key.ts +34 -0
  564. package/src/workspace/adoption-report.test.ts +61 -0
  565. package/src/workspace/adoption-report.ts +103 -0
  566. package/src/workspace/capabilities-contract.test.ts +4 -0
  567. package/src/workspace/capabilities-contract.ts +9 -1
  568. package/src/workspace/capabilities.schema.json +98 -0
  569. package/src/workspace/change-set.schema.json +42 -3
  570. package/src/workspace/ci-cloud-identity.ts +31 -1
  571. package/src/workspace/compose-graph.test.ts +12 -0
  572. package/src/workspace/compose-graph.ts +12 -1
  573. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  574. package/src/workspace/conformance/index.ts +6 -1
  575. package/src/workspace/drift-cli.test.ts +65 -0
  576. package/src/workspace/drift-cli.ts +365 -0
  577. package/src/workspace/drift.schema.json +672 -0
  578. package/src/workspace/graph-cli.ts +5 -0
  579. package/src/workspace/graph.schema.json +72 -0
  580. package/src/workspace/init.test.ts +38 -1
  581. package/src/workspace/init.ts +50 -8
  582. package/src/workspace/intent-gaps.test.ts +18 -2
  583. package/src/workspace/ledger.schema.json +1 -1
  584. package/src/workspace/nested-graph.ts +1 -0
  585. package/src/workspace/pr-report.schema.json +76 -3
  586. package/src/workspace/promote-waves.test.ts +296 -0
  587. package/src/workspace/promote-waves.ts +340 -0
  588. package/src/workspace/promote.test.ts +158 -4
  589. package/src/workspace/promote.ts +398 -57
  590. package/src/workspace/reason-codes.test.ts +4 -1
  591. package/src/workspace/reason-codes.ts +14 -0
  592. package/src/workspace/search-contract.ts +2 -0
  593. package/src/workspace/search.schema.json +103 -5
@@ -0,0 +1,880 @@
1
+ /**
2
+ * Groups of components a capability plans and applies as one (ws-107, #4440).
3
+ *
4
+ * A pull request's components plan and apply one by one, wave by wave
5
+ * (`planPrSet`, `runFanOut`). A capability that declares `planGroup` and
6
+ * `runGroup` (./capability.ts) can take several of them in one call instead,
7
+ * so a value one member creates reaches the members that read it inside the
8
+ * same apply, with no round in between.
9
+ *
10
+ * Core decides which components form a group, where the group sits among the
11
+ * waves, what its digest binds and what each member's outcome means. The
12
+ * capability decides how. Nothing here names a lexicon.
13
+ *
14
+ * - {@link findComponentGroups}: which components of the change group, and
15
+ * why each other one that could have did not (ws-107, "Which components
16
+ * form a group"). Called once by `pr.ts`'s prepare step.
17
+ * - {@link groupFanOut}: the fan-out with each group in one wave (ws-107,
18
+ * "Where the group sits"). The plan stage, the apply stage and every
19
+ * `--wave-gate` job read these waves.
20
+ * - {@link planComponentGroup}: plan a group in one call, and fold the group
21
+ * digest into each member's plan digest ({@link bindGroup}).
22
+ * - {@link runComponentGroup}: apply it in one call, and turn each member's
23
+ * outcome into a component result.
24
+ * - {@link formRunGroups}, {@link componentSetFanOut} and
25
+ * {@link planAndRunComponentGroup}: the same groups where a promote under a
26
+ * release, `components fan-out` and `run --components` run (#4465).
27
+ */
28
+
29
+ import { computePlanDigest } from "../lifecycle/plan-digest";
30
+ import type { ChangeSetPart } from "../change-set";
31
+ import type { PlanShape } from "./covered-round";
32
+ import {
33
+ accumulateComponentOutputs,
34
+ assertTargetsAccepted,
35
+ resolveComponentGraph,
36
+ resolveStepInput,
37
+ unresolvedWiringSources,
38
+ wiringReads,
39
+ wiringReferencesAt,
40
+ type DriverComponent,
41
+ type DriverComponentResult,
42
+ type DriverPhase,
43
+ type DriverStep,
44
+ } from "./driver";
45
+ import {
46
+ isPlanMovedError,
47
+ type Capability,
48
+ type CapabilityGroupOutcome,
49
+ type CapabilityPlan,
50
+ type CapabilityRegistry,
51
+ type DeployContext,
52
+ type GroupContext,
53
+ type GroupFallback,
54
+ type GroupMember,
55
+ type GroupMemberStatus,
56
+ type GroupRead,
57
+ type GroupRun,
58
+ type PlannedOutputs,
59
+ type ReportedGroupRead,
60
+ } from "./capability";
61
+ import type { FanOutGroup, FanOutGroupFallback, FanOutPlan } from "./fan-out";
62
+ import { bindTargets, type WaveComponentPlan } from "./wave-plan";
63
+
64
+ /** Whether a capability declares the pair; one with only one of the two never gets a group (ws-107). */
65
+ function groupsCapable(capability: Capability<never, unknown>): boolean {
66
+ return typeof capability.planGroup === "function" && typeof capability.runGroup === "function";
67
+ }
68
+
69
+ /** Every step a composition declares, gates included, each with the phase it sits in. */
70
+ function declaredSteps(phases: readonly DriverPhase[]): Array<{ phase: string; step: DriverStep }> {
71
+ const out: Array<{ phase: string; step: DriverStep }> = [];
72
+ const walk = (phase: string, steps: DriverPhase["steps"]): void => {
73
+ for (const step of steps) {
74
+ if (typeof (step as DriverPhase).phase === "string" && Array.isArray((step as DriverPhase).steps)) {
75
+ walk((step as DriverPhase).phase, (step as DriverPhase).steps);
76
+ continue;
77
+ }
78
+ out.push({ phase, step: step as DriverStep });
79
+ }
80
+ };
81
+ for (const phase of phases) walk(phase.phase, phase.steps);
82
+ return out;
83
+ }
84
+
85
+ /**
86
+ * The step of `component` a group capability would take, if any: the first
87
+ * capability step whose capability declares the pair, and whether it is the
88
+ * component's only step, gates counted (ruling 3).
89
+ */
90
+ function groupStepOf(component: DriverComponent, registry: CapabilityRegistry): { kind: string; phase: string; step: DriverStep; only: boolean; steps: number } | undefined {
91
+ const steps = declaredSteps(component.deploy);
92
+ const at = steps.find(({ step }) => step.kind !== "gate" && registry.has(step.kind) && groupsCapable(registry.resolve(step.kind)));
93
+ if (!at) return undefined;
94
+ return { kind: at.step.kind, phase: at.phase, step: at.step, only: steps.length === 1, steps: steps.length };
95
+ }
96
+
97
+ /** The components whose outputs `component`'s steps read, its own left out. */
98
+ function readSources(component: DriverComponent): Set<string> {
99
+ const out = new Set<string>();
100
+ for (const { step } of declaredSteps(component.deploy)) {
101
+ if (step.kind === "gate") continue;
102
+ const { kind: _kind, ...rest } = step;
103
+ for (const r of wiringReads(rest, {})) if (r.source !== component.name) out.add(r.source);
104
+ }
105
+ return out;
106
+ }
107
+
108
+ /** The sets `names` falls into when `joined` links two of them, each in the order of `names`. */
109
+ function connected(names: readonly string[], joined: (a: string, b: string) => boolean): string[][] {
110
+ const seen = new Set<string>();
111
+ const out: string[][] = [];
112
+ for (const start of names) {
113
+ if (seen.has(start)) continue;
114
+ const set = new Set([start]);
115
+ seen.add(start);
116
+ const queue = [start];
117
+ while (queue.length > 0) {
118
+ const at = queue.shift()!;
119
+ for (const other of names) {
120
+ if (seen.has(other) || !joined(at, other)) continue;
121
+ seen.add(other);
122
+ set.add(other);
123
+ queue.push(other);
124
+ }
125
+ }
126
+ out.push(names.filter((n) => set.has(n)));
127
+ }
128
+ return out;
129
+ }
130
+
131
+ /** Each component's transitive `dependsOn`, with the `extra` upstreams a capability reported (#4447), memoised. */
132
+ function upstreamsOf(byName: ReadonlyMap<string, DriverComponent>, extra: ReadonlyMap<string, ReadonlySet<string>> = new Map()): (name: string) => ReadonlySet<string> {
133
+ const memo = new Map<string, Set<string>>();
134
+ const of = (name: string): Set<string> => {
135
+ const known = memo.get(name);
136
+ if (known) return known;
137
+ const out = new Set<string>();
138
+ memo.set(name, out);
139
+ for (const dep of [...(byName.get(name)?.dependsOn ?? []), ...(extra.get(name) ?? [])]) {
140
+ out.add(dep);
141
+ for (const up of of(dep)) out.add(up);
142
+ }
143
+ return out;
144
+ };
145
+ return of;
146
+ }
147
+
148
+ /** The 1-based wave of each component in `waves`. */
149
+ function waveNumbers(waves: readonly string[][]): Map<string, number> {
150
+ const out = new Map<string, number>();
151
+ waves.forEach((wave, i) => wave.forEach((name) => out.set(name, i + 1)));
152
+ return out;
153
+ }
154
+
155
+ /**
156
+ * Each component's wave once every group in `groups` is one unit (ws-107,
157
+ * "Where the group sits"), or `undefined` when the units' dependencies form
158
+ * a cycle. A group's wave is the earliest in which everything its members
159
+ * depend on outside it has applied, and never before the earliest wave any
160
+ * member had. A component outside every group keeps its wave unless a
161
+ * dependency now sits in its wave or after it and got there by a group: a
162
+ * group, or a component a group moved.
163
+ */
164
+ function placeUnits(plan: FanOutPlan, byName: ReadonlyMap<string, DriverComponent>, groups: readonly FanOutGroup[]): Map<string, number> | undefined {
165
+ const inChange = new Set(plan.order);
166
+ const was = waveNumbers(plan.waves);
167
+ const unitOf = new Map<string, string>();
168
+ const members = new Map<string, string[]>();
169
+ for (const g of groups) for (const c of g.components) unitOf.set(c, `group:${g.components[0]}`);
170
+ for (const c of plan.order) {
171
+ if (!unitOf.has(c)) unitOf.set(c, c);
172
+ const u = unitOf.get(c)!;
173
+ members.set(u, [...(members.get(u) ?? []), c]);
174
+ }
175
+ const isGroup = (u: string): boolean => u.startsWith("group:");
176
+ const deps = new Map<string, Set<string>>();
177
+ for (const [u, cs] of members) {
178
+ const out = new Set<string>();
179
+ for (const c of cs) for (const d of byName.get(c)?.dependsOn ?? []) if (inChange.has(d) && unitOf.get(d) !== u) out.add(unitOf.get(d)!);
180
+ deps.set(u, out);
181
+ }
182
+ const wave = new Map<string, number>();
183
+ const moved = new Map<string, boolean>();
184
+ const visiting = new Set<string>();
185
+ const visit = (u: string): number | undefined => {
186
+ const known = wave.get(u);
187
+ if (known !== undefined) return known;
188
+ if (visiting.has(u)) return undefined;
189
+ visiting.add(u);
190
+ const start = Math.min(...members.get(u)!.map((c) => was.get(c) ?? 1));
191
+ let w = start;
192
+ for (const d of deps.get(u)!) {
193
+ const dw = visit(d);
194
+ if (dw === undefined) return undefined;
195
+ if (dw < w) continue;
196
+ if (isGroup(u) || isGroup(d) || moved.get(d)) w = dw + 1;
197
+ }
198
+ visiting.delete(u);
199
+ wave.set(u, w);
200
+ moved.set(u, w !== start || isGroup(u));
201
+ return w;
202
+ };
203
+ for (const u of members.keys()) if (visit(u) === undefined) return undefined;
204
+ return new Map(plan.order.map((c) => [c, wave.get(unitOf.get(c)!)!]));
205
+ }
206
+
207
+ export interface FindComponentGroupsOptions {
208
+ /** Every component in the project. */
209
+ components: DriverComponent[];
210
+ /** The fan-out the change derives (`planFanOut`). Only `order` is grouped. */
211
+ plan: FanOutPlan;
212
+ registry: CapabilityRegistry;
213
+ env: string;
214
+ vars?: Record<string, unknown>;
215
+ /**
216
+ * Only components that share one of these groups may join (#4446): a
217
+ * resume forms groups again over what is left of the approved ones
218
+ * ({@link regroupRemaining}), never new ones.
219
+ */
220
+ within?: readonly FanOutGroup[];
221
+ }
222
+
223
+ /** What {@link findComponentGroups} found, for {@link groupFanOut}. */
224
+ export interface FoundComponentGroups {
225
+ /** In plan order of their first member, each member list upstreams first. */
226
+ groups: FanOutGroup[];
227
+ /** Sorted by component. */
228
+ fallbacks: FanOutGroupFallback[];
229
+ }
230
+
231
+ /** The reads of `self`'s step input of other members of `group`, sorted by `ref` (ws-107). */
232
+ function groupReadsOf(input: Record<string, unknown>, group: ReadonlySet<string>, self: string): GroupRead[] {
233
+ return wiringReferencesAt(input)
234
+ .filter((r) => r.source !== self && group.has(r.source))
235
+ .map((r) => ({ at: r.at, ref: r.ref, component: r.source, output: r.output }));
236
+ }
237
+
238
+ /** The outputs in `outputs` of components outside `group`. */
239
+ function outsideOf(outputs: Record<string, Record<string, unknown>>, group: ReadonlySet<string>): Record<string, Record<string, unknown>> {
240
+ return Object.fromEntries(Object.entries(outputs).filter(([name]) => !group.has(name)));
241
+ }
242
+
243
+ /**
244
+ * The `GroupMember`s of `group` (ws-107). With `outputs`, each input is
245
+ * resolved against the outputs from outside the group and the context names
246
+ * only outside components as unresolved; without, as `groupFallback` asks,
247
+ * there is no input. Throws when a member cannot narrow to its targets.
248
+ */
249
+ function groupMembers(
250
+ group: FanOutGroup,
251
+ byName: ReadonlyMap<string, DriverComponent>,
252
+ registry: CapabilityRegistry,
253
+ ctxOf: (component: string) => DeployContext,
254
+ outputs?: Record<string, Record<string, unknown>>,
255
+ ): GroupMember[] {
256
+ const inGroup = new Set(group.components);
257
+ const outside = outputs ? outsideOf(outputs, inGroup) : undefined;
258
+ return group.components.map((name) => {
259
+ const component = byName.get(name);
260
+ const at = component ? groupStepOf(component, registry) : undefined;
261
+ if (!component || !at) throw new Error(`${name} has no ${group.capability} step to plan in its group`);
262
+ const { kind: _kind, ...step } = at.step;
263
+ const base = ctxOf(name);
264
+ assertTargetsAccepted(component, registry, base);
265
+ const reads = groupReadsOf(step, inGroup, name);
266
+ const dependsOn = component.dependsOn?.length ? { dependsOn: [...component.dependsOn] } : {};
267
+ if (!outside) return { component: name, step, reads, ...dependsOn, ctx: base };
268
+ const unresolved = unresolvedWiringSources(step, outside).filter((source) => !inGroup.has(source));
269
+ return {
270
+ component: name,
271
+ step,
272
+ input: resolveStepInput(step, {}, outside),
273
+ reads,
274
+ ...dependsOn,
275
+ ctx: unresolved.length > 0 ? { ...base, unresolvedInputs: unresolved } : base,
276
+ };
277
+ });
278
+ }
279
+
280
+ function groupContext(group: FanOutGroup, env: string, vars?: Record<string, unknown>): GroupContext {
281
+ return { env, ...(vars ? { vars } : {}), components: [...group.components] };
282
+ }
283
+
284
+ /**
285
+ * The reads each capability with `groupReads` reports among the change's
286
+ * components whose step it takes (#4447): only reads between two such
287
+ * components of one capability, never of a component by itself. A
288
+ * capability that throws reports none.
289
+ */
290
+ async function reportedReads(
291
+ order: readonly string[],
292
+ kindOf: (name: string) => string | undefined,
293
+ byName: ReadonlyMap<string, DriverComponent>,
294
+ options: FindComponentGroupsOptions,
295
+ ctxOf: (component: string) => DeployContext,
296
+ ): Promise<ReportedGroupRead[]> {
297
+ const out: ReportedGroupRead[] = [];
298
+ const kinds = [...new Set(order.map(kindOf).filter((k): k is string => k !== undefined))];
299
+ for (const kind of kinds) {
300
+ const capability = options.registry.resolve(kind);
301
+ if (typeof capability.groupReads !== "function") continue;
302
+ const names = order.filter((name) => kindOf(name) === kind);
303
+ if (names.length < 2) continue;
304
+ const all: FanOutGroup = { capability: kind, components: names };
305
+ let reported: ReportedGroupRead[];
306
+ try {
307
+ reported = await capability.groupReads(groupContext(all, options.env, options.vars), groupMembers(all, byName, options.registry, ctxOf) as never);
308
+ } catch {
309
+ continue;
310
+ }
311
+ const inKind = new Set(names);
312
+ for (const r of reported ?? []) {
313
+ if (r.reader !== r.upstream && inKind.has(r.reader) && inKind.has(r.upstream)) out.push({ ...r });
314
+ }
315
+ }
316
+ return out.sort((a, b) => a.reader.localeCompare(b.reader) || a.ref.localeCompare(b.ref) || a.upstream.localeCompare(b.upstream));
317
+ }
318
+
319
+ /** `set` with each member after every member of it that it reads or depends on, else in the order of `set`. */
320
+ function upstreamsFirst(set: readonly string[], upstreams: (name: string) => ReadonlySet<string>): string[] {
321
+ const out: string[] = [];
322
+ const left = [...set];
323
+ while (left.length > 0) {
324
+ const i = left.findIndex((m) => left.every((o) => o === m || !upstreams(m).has(o)));
325
+ // A cycle among the members keeps the order of `set` for the rest.
326
+ const [next] = left.splice(i < 0 ? 0 : i, 1);
327
+ out.push(next!);
328
+ }
329
+ return out;
330
+ }
331
+
332
+ /** The reads of `reads` between two members of `components`. */
333
+ function readsWithin(reads: readonly ReportedGroupRead[], components: readonly string[]): ReportedGroupRead[] {
334
+ const inGroup = new Set(components);
335
+ return reads.filter((r) => inGroup.has(r.reader) && inGroup.has(r.upstream));
336
+ }
337
+
338
+ /**
339
+ * The groups among the components of the change (ws-107):
340
+ *
341
+ * 1. A component is eligible when its deploy is one step, gates counted
342
+ * (ruling 3), whose capability declares `planGroup` and `runGroup`.
343
+ * 2. Eligible components of one capability joined by reads, in either
344
+ * direction, form a candidate set. A read is a wiring reference in a
345
+ * step, or one the capability reports through `groupReads` (#4447); a
346
+ * reported read also counts as a dependency below, orders the members
347
+ * and is kept on the group as its `reads`.
348
+ * 3. While a component outside a set lies on a `dependsOn` path between two
349
+ * of its members, the members downstream of it leave the set, and both
350
+ * parts are joined by reads again (ruling 2). Outside components are
351
+ * taken in plan order.
352
+ * 4. A set of one is no group. Each other set is offered to its
353
+ * capability's `groupFallback`. A fallback without `drop` dissolves it.
354
+ * One with `drop` leaves those members out (#4419 ruling 1): the rest
355
+ * are joined by reads and split by rule 3 again, a member left on its
356
+ * own falls back as `alone` (or `split`, when a dependency path split
357
+ * it off), and each set that is left is offered again.
358
+ * 5. A group whose placement would put it and another each before the other
359
+ * is dissolved (`cycle`).
360
+ *
361
+ * The fallbacks name every component whose capability declares the pair,
362
+ * that a read joins to another such component of its capability, and that
363
+ * ended in no group: `steps`, `split`, `alone`, `cycle`, or the capability's
364
+ * own kind.
365
+ */
366
+ export async function findComponentGroups(options: FindComponentGroupsOptions): Promise<FoundComponentGroups> {
367
+ const byName = new Map(options.components.map((c) => [c.name, c]));
368
+ const order = options.plan.order.filter((name) => byName.has(name));
369
+ const steps = new Map<string, NonNullable<ReturnType<typeof groupStepOf>>>();
370
+ for (const name of order) {
371
+ const at = groupStepOf(byName.get(name)!, options.registry);
372
+ if (at) steps.set(name, at);
373
+ }
374
+ const ctxOf = (component: string): DeployContext => ({ env: options.env, component, ...(options.vars ? { vars: options.vars } : {}) });
375
+ const reported = await reportedReads(order, (name) => steps.get(name)?.kind, byName, options, ctxOf);
376
+ const sources = new Map(order.map((name) => [name, readSources(byName.get(name)!)]));
377
+ const extra = new Map<string, Set<string>>();
378
+ for (const r of reported) {
379
+ sources.get(r.reader)!.add(r.upstream);
380
+ extra.set(r.reader, (extra.get(r.reader) ?? new Set()).add(r.upstream));
381
+ }
382
+ const reads = (a: string, b: string): boolean => sources.get(a)!.has(b) || sources.get(b)!.has(a);
383
+ const sameKind = (a: string, b: string): boolean => steps.get(a)?.kind === steps.get(b)?.kind;
384
+ const withinOf = options.within ? groupsByComponent({ groups: [...options.within] }) : undefined;
385
+ const together = (a: string, b: string): boolean => !withinOf || (withinOf.get(a) !== undefined && withinOf.get(a) === withinOf.get(b));
386
+ const joined = (a: string, b: string): boolean => sameKind(a, b) && reads(a, b) && together(a, b);
387
+ const fallbacks = new Map<string, FanOutGroupFallback>();
388
+ const fallBack = (component: string, kind: string, reason: string): void => {
389
+ if (!fallbacks.has(component)) fallbacks.set(component, { component, capability: steps.get(component)!.kind, kind, reason });
390
+ };
391
+
392
+ const eligible = order.filter((name) => steps.get(name)?.only === true);
393
+ for (const name of order) {
394
+ const at = steps.get(name);
395
+ if (!at || at.only) continue;
396
+ if (eligible.some((other) => joined(name, other))) {
397
+ fallBack(name, "steps", `${name} deploys in ${at.steps} steps, and only a component whose deploy is one ${at.kind} step joins a group`);
398
+ }
399
+ }
400
+
401
+ const upstreams = upstreamsOf(byName, extra);
402
+ const below = (name: string, of: string): boolean => upstreams(name).has(of);
403
+ /** `start` split while a component outside a set lies on a `dependsOn` path between two of its members (ruling 2), and what split each member. */
404
+ const split = (start: string[][]): { sets: string[][]; by: Map<string, string> } => {
405
+ let sets = start;
406
+ const by = new Map<string, string>();
407
+ for (let changed = true; changed; ) {
408
+ changed = false;
409
+ const next: string[][] = [];
410
+ for (const set of sets) {
411
+ const inSet = new Set(set);
412
+ const between = order.find((x) => !inSet.has(x) && set.some((m) => below(x, m)) && set.some((m) => below(m, x)));
413
+ if (between === undefined) {
414
+ next.push(set);
415
+ continue;
416
+ }
417
+ changed = true;
418
+ for (const m of set) by.set(m, between);
419
+ const down = set.filter((m) => below(m, between));
420
+ const up = set.filter((m) => !below(m, between));
421
+ next.push(...connected(up, joined), ...connected(down, joined));
422
+ }
423
+ sets = next;
424
+ }
425
+ return { sets, by };
426
+ };
427
+ const splitReason = (only: string, by: string | undefined): string =>
428
+ `${by ?? "a component outside the group"} lies on a dependency path between ${only} and the rest of its group, and a group applies in one call`;
429
+
430
+ const joinedSets = connected(eligible, joined);
431
+ const partnered = new Set(joinedSets.filter((s) => s.length > 1).flat());
432
+ const first = split(joinedSets);
433
+ const candidates: string[][] = [];
434
+ for (const set of first.sets) {
435
+ if (set.length > 1) {
436
+ candidates.push(set);
437
+ continue;
438
+ }
439
+ const [only] = set;
440
+ if (only !== undefined && partnered.has(only)) fallBack(only, "split", splitReason(only, first.by.get(only)));
441
+ }
442
+
443
+ const taken: FanOutGroup[] = [];
444
+ // Each set formed again after a drop goes to the front, so groups are still taken in plan order of their first member.
445
+ const queue = [...candidates];
446
+ while (queue.length > 0) {
447
+ const set = queue.shift()!;
448
+ const components = upstreamsFirst(set, upstreams);
449
+ const within = readsWithin(reported, components);
450
+ const group: FanOutGroup = { capability: steps.get(set[0]!)!.kind, components, ...(within.length > 0 ? { reads: within } : {}) };
451
+ const capability = options.registry.resolve(group.capability);
452
+ let refused: GroupFallback | undefined;
453
+ if (capability.groupFallback) {
454
+ try {
455
+ refused = await capability.groupFallback(groupContext(group, options.env, options.vars), groupMembers(group, byName, options.registry, ctxOf) as never);
456
+ } catch (err) {
457
+ refused = { kind: "error", reason: `${group.capability} could not say whether it takes the group: ${err instanceof Error ? err.message : String(err)}` };
458
+ }
459
+ }
460
+ if (refused) {
461
+ const inSet = new Set(set);
462
+ const drops = (refused.drop ?? []).filter((d) => inSet.has(d.component));
463
+ if (drops.length === 0) {
464
+ for (const m of set) fallBack(m, refused.kind, refused.reason);
465
+ continue;
466
+ }
467
+ // Ruling 1: only the dropped members leave; the rest form again by the same rules and are asked again.
468
+ for (const d of drops) fallBack(d.component, d.kind ?? refused.kind, d.reason ?? refused.reason);
469
+ const gone = new Set(drops.map((d) => d.component));
470
+ const again = split(connected(set.filter((m) => !gone.has(m)), joined));
471
+ for (const s of again.sets) {
472
+ const [only] = s;
473
+ if (s.length > 1 || only === undefined) continue;
474
+ const by = again.by.get(only);
475
+ if (by !== undefined) fallBack(only, "split", splitReason(only, by));
476
+ else fallBack(only, "alone", `${only} is the only member of its group left once ${[...gone].join(", ")} fell back, and a group of one is no group`);
477
+ }
478
+ queue.unshift(...again.sets.filter((s) => s.length > 1));
479
+ continue;
480
+ }
481
+ // Placed in plan order of the first member; a group that would close a cycle with those before it is dissolved.
482
+ if (placeUnits(options.plan, byName, [...taken, group]) === undefined) {
483
+ for (const m of set) fallBack(m, "cycle", `placing ${set.join(", ")} as one would put it and another group each before the other`);
484
+ continue;
485
+ }
486
+ taken.push(group);
487
+ }
488
+
489
+ return { groups: taken, fallbacks: [...fallbacks.values()].sort((a, b) => a.component.localeCompare(b.component)) };
490
+ }
491
+
492
+ /**
493
+ * `plan` with each group of `found` in one wave (ws-107). The members leave
494
+ * their waves for the group's, upstreams first; a component outside every
495
+ * group moves to a later wave when a dependency now sits in its own wave or
496
+ * after it. A wave left empty stays, so wave numbers and their gates keep
497
+ * their meaning, and waves may be added at the end. `digest` does not change.
498
+ */
499
+ export function groupFanOut(plan: FanOutPlan, components: DriverComponent[], found: FoundComponentGroups): FanOutPlan {
500
+ const fallbacks = found.fallbacks.length > 0 ? { fallbacks: found.fallbacks } : {};
501
+ const { groups: _groups, fallbacks: _fallbacks, ...bare } = plan;
502
+ if (found.groups.length === 0) return { ...bare, ...fallbacks };
503
+ const byName = new Map(components.map((c) => [c.name, c]));
504
+ const placed = placeUnits(plan, byName, found.groups);
505
+ if (!placed) throw new Error(`groupFanOut: the groups ${found.groups.map((g) => g.components.join(", ")).join("; ")} form a cycle`);
506
+ const total = Math.max(plan.waves.length, ...placed.values());
507
+ const waves: string[][] = Array.from({ length: total }, () => []);
508
+ for (const name of plan.order) waves[placed.get(name)! - 1]!.push(name);
509
+ return { ...bare, order: waves.flat(), waves, groups: found.groups, ...fallbacks };
510
+ }
511
+
512
+ /** `group` with only its members in `inPlan`, and the reported reads between them. */
513
+ function keptOf(group: FanOutGroup, inPlan: ReadonlySet<string>): FanOutGroup {
514
+ const { reads, ...rest } = group;
515
+ const components = group.components.filter((c) => inPlan.has(c));
516
+ const within = readsWithin(reads ?? [], components);
517
+ return { ...rest, components, ...(within.length > 0 ? { reads: within } : {}) };
518
+ }
519
+
520
+ /**
521
+ * `plan`, a narrowing of a grouped fan-out (`remainingFanOut`, a round),
522
+ * with what is left of `groups` placed again: each group keeps its members
523
+ * that are still in `plan`, and one left with fewer than two is no group.
524
+ * Nothing is asked of the capability again.
525
+ */
526
+ export function narrowGroups(plan: FanOutPlan, components: DriverComponent[], groups: readonly FanOutGroup[] | undefined): FanOutPlan {
527
+ if (!groups || groups.length === 0) return plan;
528
+ const inPlan = new Set(plan.order);
529
+ const kept = groups.map((g) => keptOf(g, inPlan)).filter((g) => g.components.length > 1);
530
+ const byName = new Map(components.map((c) => [c.name, c]));
531
+ if (kept.length === 0 || placeUnits(plan, byName, kept) === undefined) return plan;
532
+ return groupFanOut(plan, components, { groups: kept, fallbacks: plan.fallbacks ?? [] });
533
+ }
534
+
535
+ /**
536
+ * `plan`, what a resume left of a grouped fan-out (`remainingFanOut`), with
537
+ * groups formed again over it (ws-107, "Resume"; #4446). Only members left
538
+ * of one of `groups` group again, by the same rule and the same
539
+ * `groupFallback`, so what is left of a group plans as a smaller group, or
540
+ * one by one when fewer than two are left, when they no longer join by
541
+ * reads, or when the capability refuses them. A capability that drops some
542
+ * members leaves the rest to group again as {@link findComponentGroups}
543
+ * does. The fallbacks are the ones this
544
+ * pass found, then those of `fallbacks` for components still in `plan`.
545
+ */
546
+ export async function regroupRemaining(
547
+ options: Omit<FindComponentGroupsOptions, "within"> & { groups: readonly FanOutGroup[] | undefined; fallbacks?: readonly FanOutGroupFallback[] },
548
+ ): Promise<FanOutPlan> {
549
+ const inPlan = new Set(options.plan.order);
550
+ const left = (options.groups ?? []).map((g) => keptOf(g, inPlan)).filter((g) => g.components.length > 1);
551
+ const found: FoundComponentGroups =
552
+ left.length > 0
553
+ ? await findComponentGroups({
554
+ components: options.components,
555
+ plan: options.plan,
556
+ registry: options.registry,
557
+ env: options.env,
558
+ ...(options.vars ? { vars: options.vars } : {}),
559
+ within: left,
560
+ })
561
+ : { groups: [], fallbacks: [] };
562
+ const named = new Set(found.fallbacks.map((f) => f.component));
563
+ const fallbacks = [...found.fallbacks, ...(options.fallbacks ?? []).filter((f) => inPlan.has(f.component) && !named.has(f.component))].sort((a, b) =>
564
+ a.component.localeCompare(b.component),
565
+ );
566
+ return groupFanOut(options.plan, options.components, { groups: found.groups, fallbacks });
567
+ }
568
+
569
+ /** The group each component of `plan` is a member of. */
570
+ export function groupsByComponent(plan: Pick<FanOutPlan, "groups">): Map<string, FanOutGroup> {
571
+ return new Map((plan.groups ?? []).flatMap((g) => g.components.map((c) => [c, g] as const)));
572
+ }
573
+
574
+ /**
575
+ * The group digest (ws-107): over the capability's kind, each member's
576
+ * component, change-set member and own plan digest, sorted, and the
577
+ * capability's digest of the composition. One member more, or the same
578
+ * members composed differently, moves it.
579
+ */
580
+ export function groupDigest(kind: string, members: ReadonlyArray<{ component: string; member: string; planDigest: string }>, composition: string): string {
581
+ const sorted = [...members]
582
+ .map((m) => ({ component: m.component, member: m.member, planDigest: m.planDigest }))
583
+ .sort((a, b) => a.component.localeCompare(b.component) || a.member.localeCompare(b.member));
584
+ return computePlanDigest("capability-group", { kind, members: sorted, composition });
585
+ }
586
+
587
+ /**
588
+ * A member's plan with its group folded in (ws-107), after `bindTargets`:
589
+ * its digest becomes the digest over its own and the group's, written into
590
+ * its change-set part too, and the part carries the group's digest and the
591
+ * `reads` the plan does not know. A plan with no part gets an empty one.
592
+ */
593
+ export function bindGroup(planned: CapabilityPlan, digest: string, reads: readonly string[]): CapabilityPlan {
594
+ const planDigest = computePlanDigest("capability-group-member", { plan: planned.planDigest, group: digest });
595
+ const group = { digest, reads: [...new Set(reads)].sort() };
596
+ const part: ChangeSetPart = planned.changeSet ?? {
597
+ member: { member: planned.member, lexicon: "chant", planner: "chant", status: "planned", planDigest, holes: [] },
598
+ entries: [],
599
+ };
600
+ return { ...planned, planDigest, changeSet: { ...part, member: { ...part.member, planDigest, group } } };
601
+ }
602
+
603
+ /** `value` at the dotted `path`. */
604
+ function atPath(value: unknown, path: string): unknown {
605
+ return path.split(".").reduce<unknown>((acc, key) => (acc == null ? acc : (acc as Record<string, unknown>)[key]), value);
606
+ }
607
+
608
+ /**
609
+ * Whether a reported read (#4447) is known at plan: its `output` in the
610
+ * upstream's planned outputs, or, without one, an upstream plan that names
611
+ * outputs and none of them unknown.
612
+ */
613
+ function knownAtPlan(outputs: PlannedOutputs | undefined, output: string | undefined): boolean {
614
+ if (!outputs) return false;
615
+ if (output !== undefined) return atPath(outputs.known, output) !== undefined;
616
+ return outputs.unknown.length === 0;
617
+ }
618
+
619
+ /** What planning a group gave (ws-107). */
620
+ export interface ComponentGroupPlan {
621
+ /**
622
+ * One per member, in the group's order, as `planWaveComponent` gives a
623
+ * component's: its member, part, planned outputs and shape, its plan under
624
+ * `plans`. When the group failed to plan, each is that error.
625
+ */
626
+ plans: WaveComponentPlan[];
627
+ /** The group digest folded into each member's. Absent when it failed to plan. */
628
+ digest?: string;
629
+ /** What `runGroup` gets back. Absent when it failed to plan. */
630
+ run?: GroupRun;
631
+ }
632
+
633
+ export interface PlanComponentGroupOptions {
634
+ group: FanOutGroup;
635
+ components: ReadonlyMap<string, DriverComponent>;
636
+ registry: CapabilityRegistry;
637
+ env: string;
638
+ vars?: Record<string, unknown>;
639
+ /** Each member's context, as a single step would get it. */
640
+ ctxOf: (component: string) => DeployContext;
641
+ /** The outputs planned and read so far. The group's own members' are left out. */
642
+ outputs: Record<string, Record<string, unknown>>;
643
+ }
644
+
645
+ /**
646
+ * Plan `group` with one `planGroup` call (ws-107). Each member's plan is
647
+ * bound to its targets (`bindTargets`), then to the group ({@link bindGroup}).
648
+ * Never throws: a failure, a throwing `planGroup` included, makes every
649
+ * member plan-failed with its error, and nothing falls back after planning.
650
+ */
651
+ export async function planComponentGroup(options: PlanComponentGroupOptions): Promise<ComponentGroupPlan> {
652
+ const { group } = options;
653
+ const failAll = (err: unknown): ComponentGroupPlan => {
654
+ const error = err instanceof Error ? err.message : String(err);
655
+ return { plans: group.components.map((component) => ({ component, error })) };
656
+ };
657
+ try {
658
+ const capability = options.registry.resolve(group.capability);
659
+ if (!groupsCapable(capability)) throw new Error(`${group.capability} does not plan and apply a group`);
660
+ const members = groupMembers(group, options.components, options.registry, options.ctxOf, options.outputs);
661
+ const planned = await capability.planGroup!(groupContext(group, options.env, options.vars), members as never);
662
+ const bound = new Map<string, CapabilityPlan>();
663
+ for (const m of members) {
664
+ const own = planned.members[m.component];
665
+ if (!own) throw new Error(`${group.capability} planned the group ${group.components.join(", ")} without a plan for ${m.component}`);
666
+ bound.set(m.component, bindTargets(group.capability, m.ctx, own));
667
+ }
668
+ const digest = groupDigest(
669
+ group.capability,
670
+ members.map((m) => ({ component: m.component, member: bound.get(m.component)!.member, planDigest: bound.get(m.component)!.planDigest })),
671
+ planned.digest,
672
+ );
673
+ const plans: WaveComponentPlan[] = [];
674
+ const runPlans: Record<string, CapabilityPlan> = {};
675
+ for (const m of members) {
676
+ const own = bound.get(m.component)!;
677
+ // A read the upstream member's plan does not know is computed by the group's apply.
678
+ const unknown = [
679
+ ...m.reads.filter((r) => atPath(planned.members[r.component]?.outputs?.known, r.output) === undefined).map((r) => r.ref),
680
+ ...(group.reads ?? []).filter((r) => r.reader === m.component && !knownAtPlan(planned.members[r.upstream]?.outputs, r.output)).map((r) => r.ref),
681
+ ];
682
+ const plan = bindGroup(own, digest, unknown);
683
+ runPlans[m.component] = plan;
684
+ const shapes: Record<string, PlanShape> | undefined = own.shape ? { [plan.member]: { ...own.shape, member: plan.member } } : undefined;
685
+ plans.push({
686
+ component: m.component,
687
+ members: [{ member: plan.member, planDigest: plan.planDigest }],
688
+ plans: { [plan.member]: plan },
689
+ parts: [plan.changeSet!],
690
+ ...(own.outputs ? { outputs: own.outputs } : {}),
691
+ ...(shapes ? { shapes } : {}),
692
+ });
693
+ }
694
+ return { plans, digest, run: { ...(planned.artifact !== undefined ? { artifact: planned.artifact } : {}), digest, plans: runPlans } };
695
+ } catch (err) {
696
+ return failAll(err);
697
+ }
698
+ }
699
+
700
+ export interface RunComponentGroupOptions {
701
+ group: FanOutGroup;
702
+ components: ReadonlyMap<string, DriverComponent>;
703
+ registry: CapabilityRegistry;
704
+ env: string;
705
+ vars?: Record<string, unknown>;
706
+ /** Each member's context as a single step would get it, without `plans`. */
707
+ ctxOf: (component: string) => DeployContext;
708
+ /** The approved group plan ({@link ComponentGroupPlan.run}). */
709
+ run: GroupRun | undefined;
710
+ /** The outputs standing. Each member that applied adds its own, as a run does. */
711
+ componentOutputs: Record<string, Record<string, unknown>>;
712
+ }
713
+
714
+ export interface ComponentGroupRunResult {
715
+ /** One per member, in the group's order. A `not-started` member's is `fail` with a `skipped` record. */
716
+ results: DriverComponentResult[];
717
+ /** The members whose part never began, in the group's order. */
718
+ notStarted: string[];
719
+ /**
720
+ * How each member's part ended, as the capability said (#4446): the
721
+ * report's `groupOutcome`. A throw gives every member `failed`, or
722
+ * `not-started` for a moved plan.
723
+ */
724
+ outcomes: Record<string, GroupMemberStatus>;
725
+ }
726
+
727
+ /**
728
+ * Apply `group` with one `runGroup` call (ws-107) and map each member's
729
+ * outcome: `applied` and `no-changes` complete it and record its outputs;
730
+ * `partial` and `failed` fail it; `not-started` leaves it not started. A
731
+ * throw fails every member, since core cannot tell what applied, except a
732
+ * moved plan (`isPlanMovedError`), which leaves every member not started and
733
+ * marks each record `planMoved`, so the command exits 3.
734
+ */
735
+ export async function runComponentGroup(options: RunComponentGroupOptions): Promise<ComponentGroupRunResult> {
736
+ const { group } = options;
737
+ const start = Date.now();
738
+ const phaseOf = (name: string): string => {
739
+ const component = options.components.get(name);
740
+ return (component && groupStepOf(component, options.registry)?.phase) ?? "Apply";
741
+ };
742
+ const record = (name: string, body: Partial<DriverComponentResult["records"][number]> & { status: DriverComponentResult["records"][number]["status"] }) => ({
743
+ component: name,
744
+ phase: phaseOf(name),
745
+ kind: group.capability,
746
+ durationMs: Date.now() - start,
747
+ ...body,
748
+ });
749
+ const everyMember = (status: "failed" | "not-started", error: string, planMoved = false): ComponentGroupRunResult => ({
750
+ results: group.components.map((name) => ({
751
+ component: name,
752
+ ok: false,
753
+ status: "fail" as const,
754
+ records: [record(name, { status: status === "failed" ? "fail" : "skipped", error, ...(planMoved ? { planMoved: true as const } : {}) })],
755
+ })),
756
+ notStarted: status === "not-started" ? [...group.components] : [],
757
+ outcomes: Object.fromEntries(group.components.map((name) => [name, status])),
758
+ });
759
+
760
+ let outcome: CapabilityGroupOutcome;
761
+ try {
762
+ if (!options.run) throw new Error(`no approved plan of the group ${group.components.join(", ")} to apply`);
763
+ const capability = options.registry.resolve(group.capability);
764
+ if (!groupsCapable(capability)) throw new Error(`${group.capability} does not plan and apply a group`);
765
+ const run = options.run;
766
+ const ctxOf = (component: string): DeployContext => {
767
+ const plan = run.plans[component];
768
+ return { ...options.ctxOf(component), ...(plan ? { plans: { [plan.member]: plan } } : {}) };
769
+ };
770
+ const members = groupMembers(group, options.components, options.registry, ctxOf, options.componentOutputs);
771
+ outcome = await capability.runGroup!(groupContext(group, options.env, options.vars), run, members as never);
772
+ } catch (err) {
773
+ const error = err instanceof Error ? err.message : String(err);
774
+ return isPlanMovedError(err) ? everyMember("not-started", `not started: ${error}`, true) : everyMember("failed", error);
775
+ }
776
+
777
+ const results: DriverComponentResult[] = [];
778
+ const notStarted: string[] = [];
779
+ const outcomes: Record<string, GroupMemberStatus> = {};
780
+ for (const name of group.components) {
781
+ const o = outcome.members[name] ?? { status: "failed" as const, error: outcome.error ?? `${group.capability} said nothing of ${name}'s part` };
782
+ outcomes[name] = o.status;
783
+ if (o.status === "applied" || o.status === "no-changes") {
784
+ if (o.outputs) accumulateComponentOutputs(options.componentOutputs, name, { [phaseOf(name)]: { outputs: o.outputs } });
785
+ results.push({ component: name, ok: true, status: "ok", records: [record(name, { status: "ok", output: o })] });
786
+ continue;
787
+ }
788
+ if (o.status === "not-started") {
789
+ notStarted.push(name);
790
+ results.push({ component: name, ok: false, status: "fail", records: [record(name, { status: "skipped", error: o.error ?? "not started: the group's apply stopped before its part began" })] });
791
+ continue;
792
+ }
793
+ const error = o.error ?? outcome.error ?? `${name}'s part of the group ${o.status === "partial" ? "applied in part" : "failed"}`;
794
+ results.push({ component: name, ok: false, status: "fail", records: [record(name, { status: "fail", error })] });
795
+ }
796
+ return { results, notStarted, outcomes };
797
+ }
798
+
799
+ /** The fallback kind of a component that would have joined a group under `--wave-gate` (#4465, ruling 1). */
800
+ export const WAVE_GATE_FALLBACK = "wave-gate";
801
+
802
+ /**
803
+ * The fan-out over `components` and nothing else (#4465), as a promote and
804
+ * `run --components` run them: a dependency outside the set is already
805
+ * satisfied, and nothing is skipped or seeded. `digest` names the set.
806
+ */
807
+ export function componentSetFanOut(components: readonly DriverComponent[]): FanOutPlan {
808
+ const names = new Set(components.map((c) => c.name));
809
+ const { order, waves } = resolveComponentGraph(components.map((c) => ({ ...c, dependsOn: (c.dependsOn ?? []).filter((d) => names.has(d)) })));
810
+ return { order, waves, skipped: [], seeds: [], indeterminate: [], digest: computePlanDigest("component-set", [...names].sort()) };
811
+ }
812
+
813
+ export interface FormRunGroupsOptions extends Omit<FindComponentGroupsOptions, "within"> {
814
+ /** `--wave-gate`: no group forms (#4465, ruling 1). */
815
+ waveGate?: boolean;
816
+ }
817
+
818
+ /**
819
+ * `options.plan` with the groups a promote under a release, `components
820
+ * fan-out` and `run --components` form (#4465), by the rule the
821
+ * pull-request run uses ({@link findComponentGroups}, {@link groupFanOut}).
822
+ *
823
+ * Under a wave gate nothing groups (ruling 1 of #4465): a gated wave plans
824
+ * component by component, as `runFanOut` requires, so each member of a group
825
+ * that would have formed falls back with kind {@link WAVE_GATE_FALLBACK},
826
+ * and the plan keeps its waves.
827
+ */
828
+ export async function formRunGroups(options: FormRunGroupsOptions): Promise<FanOutPlan> {
829
+ const { waveGate, ...find } = options;
830
+ const found = await findComponentGroups(find);
831
+ if (!waveGate || found.groups.length === 0) return groupFanOut(options.plan, options.components, found);
832
+ const gated: FanOutGroupFallback[] = found.groups.flatMap((g) =>
833
+ g.components.map((component) => ({
834
+ component,
835
+ capability: g.capability,
836
+ kind: WAVE_GATE_FALLBACK,
837
+ reason: `${listOf(g.components)} would plan and apply as one, but under --wave-gate each wave plans component by component, so no group forms`,
838
+ })),
839
+ );
840
+ const fallbacks = [...found.fallbacks, ...gated].sort((a, b) => a.component.localeCompare(b.component));
841
+ return groupFanOut(options.plan, options.components, { groups: [], fallbacks });
842
+ }
843
+
844
+ /** `a`, `b` and `c`. */
845
+ function listOf(items: readonly string[]): string {
846
+ return items.length > 1 ? `${items.slice(0, -1).join(", ")} and ${items.at(-1)}` : (items[0] ?? "");
847
+ }
848
+
849
+ /**
850
+ * Plan `group` in its wave against the outputs standing, then apply it
851
+ * (#4465). `components fan-out` and `run --components` run a group this way,
852
+ * since neither plans before it applies, as each of their components plans
853
+ * inside its own step. A group that fails to plan applies nothing: each
854
+ * member fails with the plan's error and is `not-started`.
855
+ */
856
+ export async function planAndRunComponentGroup(options: Omit<RunComponentGroupOptions, "run">): Promise<ComponentGroupRunResult> {
857
+ const { group } = options;
858
+ const planned = await planComponentGroup({
859
+ group,
860
+ components: options.components,
861
+ registry: options.registry,
862
+ env: options.env,
863
+ ...(options.vars ? { vars: options.vars } : {}),
864
+ ctxOf: options.ctxOf,
865
+ outputs: options.componentOutputs,
866
+ });
867
+ if (planned.run) return runComponentGroup({ ...options, run: planned.run });
868
+ const failed = planned.plans.find((p): p is Extract<WaveComponentPlan, { error: string }> => "error" in p);
869
+ const error = `the group ${group.components.join(", ")} failed to plan, so nothing of it applied: ${failed?.error ?? "no plan"}`;
870
+ return {
871
+ results: group.components.map((name) => ({
872
+ component: name,
873
+ ok: false,
874
+ status: "fail" as const,
875
+ records: [{ component: name, phase: "Plan", kind: group.capability, status: "fail" as const, durationMs: 0, error }],
876
+ })),
877
+ notStarted: [...group.components],
878
+ outcomes: Object.fromEntries(group.components.map((name) => [name, "not-started" as const])),
879
+ };
880
+ }