@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
@@ -23,8 +23,9 @@
23
23
  * component in the project and not the selection.
24
24
  * - the upstream is in the change and its plan cannot say, or says the value
25
25
  * is known only after apply: no value. The dependent's members list the
26
- * reference under `unknownInputs`, and the approval does not cover what
27
- * it will do with it.
26
+ * reference under `unknownInputs`. The apply plans it again once the
27
+ * upstream applied, and the approval covers that round when it matches the
28
+ * preview (see `coveredRound`).
28
29
  *
29
30
  * `--seed-outputs` wins over both. The apply stage's plan reads the same
30
31
  * values, so on the same estate it gets the digest the plan stage showed.
@@ -66,6 +67,14 @@
66
67
  * says whether the approved change set covers what it planned, by the same
67
68
  * test a resume uses ({@link planCoveredBy}). Covered, the wave applies under
68
69
  * the pull request's approval; not covered, it needs its own.
70
+ *
71
+ * ## Groups
72
+ *
73
+ * A fan-out with `groups` (ws-107, `./groups.ts`) plans each group with one
74
+ * `planGroup` call at its wave and applies it with one `runGroup` call
75
+ * there. A member's read of another member of its group is computed by that
76
+ * apply: it is no `unknownInputs` entry and holds nothing back. Each member's
77
+ * digest binds the group's, so the one approval binds the composition.
69
78
  */
70
79
 
71
80
  import { composeChangeSet, verifyChangeSetDigest, type ChangeSetDocument, type ChangeSetEntry, type ChangeSetPart } from "../change-set";
@@ -77,7 +86,9 @@ import {
77
86
  PR_REPORT_CONTRACT,
78
87
  PR_REPORT_SCHEMA_ID,
79
88
  type PrApproval,
89
+ type PrGroup,
80
90
  type PrMember,
91
+ type PrMemberRead,
81
92
  type PrRefusal,
82
93
  type PrRound,
83
94
  type PrReport,
@@ -88,17 +99,20 @@ import {
88
99
  deployContext,
89
100
  movedWiringReferences,
90
101
  unresolvedWiringReferences,
102
+ wiringReads,
91
103
  type DriverComponent,
92
104
  type DriverComponentResult,
93
105
  } from "./driver";
94
106
  import { deployUnits } from "./deploy-units";
95
107
  import { runFanOut, type FanOutRunResult } from "./fan-out-run";
96
- import { remainingFanOut, type ComponentChangeSignal, type FanOutPlan } from "./fan-out";
108
+ import { remainingFanOut, type ComponentChangeSignal, type FanOutGroup, type FanOutGroupFallback, type FanOutPlan } from "./fan-out";
109
+ import { coveredRound, type PlanShape, type RoundCoverage } from "./covered-round";
97
110
  import type { PrApplyRecord } from "./fan-out-record";
98
- import type { CapabilityRegistry, PlannedOutputs } from "./capability";
111
+ import type { CapabilityRegistry, GroupRun, PlannedOutputs } from "./capability";
112
+ import { groupsByComponent, narrowGroups, planComponentGroup, regroupRemaining } from "./groups";
99
113
  import type { RunProgressEvent } from "./run-progress";
100
114
  import type { RunEventSink } from "../run-event-stream";
101
- import { capabilitySteps, planWaveComponent, readDeployedOutputs } from "./wave-plan";
115
+ import { capabilitySteps, planWaveComponent, readDeployedOutputs, type WaveComponentPlan } from "./wave-plan";
102
116
  import type { PrMigrationFile } from "../pr-migrations";
103
117
 
104
118
  export interface PrPlanSetOptions {
@@ -145,10 +159,26 @@ export interface PrPlanSet {
145
159
  * report's `plannedOutputs`.
146
160
  */
147
161
  plannedOutputs?: Record<string, string[]>;
162
+ /** The shapes lexicons supplied with their plans (#4419), by member. */
163
+ shapes?: Record<string, PlanShape>;
164
+ /** The groups planned as one (ws-107), in plan order of their first member. Absent when none. */
165
+ groups?: PrPlanGroup[];
166
+ /** The components that could have grouped and did not, with why (ws-107, ruling 1): the fan-out's `fallbacks`. */
167
+ fallbacks?: FanOutGroupFallback[];
148
168
  /** Whether any component failed to plan. */
149
169
  failed: boolean;
150
170
  }
151
171
 
172
+ /** A group a pull request planned as one (ws-107). */
173
+ export interface PrPlanGroup extends FanOutGroup {
174
+ /** The group digest folded into each member's `planDigest`; `null` when the group failed to plan. */
175
+ digest: string | null;
176
+ /** Why it failed to plan. Every member is then plan-failed with it. */
177
+ error?: string;
178
+ /** What `runGroup` gets back. Like `plans`, it lives in the process that planned. */
179
+ run?: GroupRun;
180
+ }
181
+
152
182
  /** A failed component's members: the units its steps target, else the component itself. */
153
183
  function failedParts(component: DriverComponent, error: string): ChangeSetPart[] {
154
184
  const units = deployUnits(component.deploy).map((u) => u.unit);
@@ -178,18 +208,25 @@ export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
178
208
  const parts: ChangeSetPart[] = [];
179
209
  const componentOf = new Map<string, string>();
180
210
  const unknownOf = new Map<string, { unknown: string[]; build: string[] }>();
211
+ const readsBy = new Map<string, PrMemberRead[]>();
181
212
  const plans: Record<string, Record<string, unknown>> = {};
213
+ const shapes: Record<string, PlanShape> = {};
182
214
  let failed = false;
183
215
  const emit = options.onRunEvent;
184
216
  emit?.({ type: "phase-start", component: null, phase: PR_PLAN_PHASE });
217
+ // ws-107: each group plans once, in its wave, with one call.
218
+ const groupOf = groupsByComponent(options.plan);
219
+ const groups: PrPlanGroup[] = [];
185
220
 
186
221
  for (const wave of options.plan.waves) {
187
222
  // The dependencies' outputs first, each once. One the change plans is
188
223
  // read from its plan, which an earlier wave made (#3999). Any other is
189
224
  // read from the deployed estate (#4104). A dependency that cannot say
190
225
  // leaves its references unresolved, which its dependent's plan reports
191
- // if it needs them.
192
- const deps = [...new Set(wave.flatMap((name) => byName.get(name)?.dependsOn ?? []))].filter((dep) => !read.has(dep));
226
+ // if it needs them. A member of the dependent's own group plans with it.
227
+ const deps = [
228
+ ...new Set(wave.flatMap((name) => (byName.get(name)?.dependsOn ?? []).filter((dep) => !groupOf.get(name)?.components.includes(dep)))),
229
+ ].filter((dep) => !read.has(dep));
193
230
  const fromEstate: string[] = [];
194
231
  for (const dep of deps) {
195
232
  read.add(dep);
@@ -203,26 +240,60 @@ export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
203
240
  planned[dep] = Object.keys(known).sort();
204
241
  }
205
242
  Object.assign(deployed, await readDeployedOutputs(fromEstate, byName, options.registry, (dep) => deployContext(options, dep), outputs));
206
- const results = await Promise.all(
207
- wave.map(async (name) => {
208
- const at = { component: name, phase: PR_PLAN_PHASE, step: "plan" };
209
- emit?.({ type: "step-start", ...at });
210
- const start = Date.now();
211
- const p = await planWaveComponent(byName.get(name)!, options.registry, deployContext(options, name), outputs);
212
- emit?.({
213
- type: "step-settle",
214
- ...at,
215
- status: "error" in p ? "failed" : "ok",
216
- durationMs: Date.now() - start,
217
- ...("error" in p ? { error: p.error } : {}),
218
- });
219
- return p;
220
- }),
221
- );
243
+ const settle = (name: string, start: number, p: WaveComponentPlan): void => {
244
+ emit?.({
245
+ type: "step-settle",
246
+ component: name,
247
+ phase: PR_PLAN_PHASE,
248
+ step: "plan",
249
+ status: "error" in p ? "failed" : "ok",
250
+ durationMs: Date.now() - start,
251
+ ...("error" in p ? { error: p.error } : {}),
252
+ });
253
+ };
254
+ const groupsHere = [...new Set(wave.map((name) => groupOf.get(name)).filter((g): g is FanOutGroup => g !== undefined))];
255
+ const results = (
256
+ await Promise.all([
257
+ ...wave
258
+ .filter((name) => !groupOf.has(name))
259
+ .map(async (name) => {
260
+ emit?.({ type: "step-start", component: name, phase: PR_PLAN_PHASE, step: "plan" });
261
+ const start = Date.now();
262
+ const p = await planWaveComponent(byName.get(name)!, options.registry, deployContext(options, name), outputs);
263
+ settle(name, start, p);
264
+ return [p];
265
+ }),
266
+ ...groupsHere.map(async (group) => {
267
+ for (const name of group.components) emit?.({ type: "step-start", component: name, phase: PR_PLAN_PHASE, step: "plan" });
268
+ const start = Date.now();
269
+ const g = await planComponentGroup({
270
+ group,
271
+ components: byName,
272
+ registry: options.registry,
273
+ env: options.env,
274
+ ...(options.vars ? { vars: options.vars } : {}),
275
+ ctxOf: (name) => deployContext(options, name),
276
+ outputs,
277
+ });
278
+ for (const p of g.plans) settle(p.component, start, p);
279
+ const error = g.plans.find((p): p is Extract<WaveComponentPlan, { error: string }> => "error" in p)?.error;
280
+ groups.push({ ...group, digest: g.digest ?? null, ...(error !== undefined ? { error } : {}), ...(g.run ? { run: g.run } : {}) });
281
+ return g.plans;
282
+ }),
283
+ ])
284
+ ).flat();
285
+ /** What each member of a group planned to expose, for its group-mates' reads. */
286
+ const plannedHere = new Map(results.map((p) => [p.component, "error" in p ? undefined : p.outputs?.known]));
222
287
  for (const p of results) {
223
288
  const component = byName.get(p.component)!;
224
- const unknown = unknownInputsOf(component, outputs, inChange);
289
+ const group = groupOf.get(p.component);
290
+ const mates = group ? new Set(group.components) : undefined;
291
+ const unknown = unknownInputsOf(component, outputs, inChange, mates);
225
292
  if (unknown.unknown.length > 0 || unknown.build.length > 0) unknownOf.set(p.component, unknown);
293
+ // A read of a group-mate is known when that mate's plan knows it.
294
+ const readsView = group ? { ...outputs, ...Object.fromEntries(group.components.filter((c) => c !== p.component).map((c) => [c, { ...(plannedHere.get(c) ?? {}) }])) } : outputs;
295
+ const reads = readsOf(component, readsView);
296
+ if (reads.length > 0) readsBy.set(p.component, reads);
226
297
  if ("error" in p) {
227
298
  failed = true;
228
299
  plannedBy.set(p.component, undefined);
@@ -234,6 +305,7 @@ export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
234
305
  }
235
306
  plannedBy.set(p.component, p.outputs);
236
307
  plans[p.component] = p.plans;
308
+ Object.assign(shapes, p.shapes);
237
309
  for (const part of p.parts) {
238
310
  parts.push(part);
239
311
  componentOf.set(part.member.member, p.component);
@@ -246,6 +318,7 @@ export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
246
318
  const members: PrMember[] = doc.members.map((m) => {
247
319
  const component = componentOf.get(m.member) ?? m.member;
248
320
  const unknown = unknownOf.get(component);
321
+ const reads = readsBy.get(component);
249
322
  return {
250
323
  member: m.member,
251
324
  component,
@@ -255,17 +328,23 @@ export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
255
328
  ...(m.error ? { error: m.error } : {}),
256
329
  ...(unknown?.unknown.length ? { unknownInputs: unknown.unknown } : {}),
257
330
  ...(unknown?.build.length ? { buildInputs: unknown.build } : {}),
331
+ ...(reads ? { reads } : {}),
258
332
  };
259
333
  });
260
334
  const sorted = (r: Record<string, string[]>) => Object.fromEntries(Object.entries(r).sort(([a], [b]) => a.localeCompare(b)));
261
335
  const deployedOutputs = sorted(deployed);
262
336
  const plannedOutputs = sorted(planned);
337
+ const at = new Map(options.plan.order.map((name, i) => [name, i]));
338
+ groups.sort((a, b) => (at.get(a.components[0]!) ?? 0) - (at.get(b.components[0]!) ?? 0));
263
339
  return {
264
340
  doc,
265
341
  members,
266
342
  plans,
267
343
  outputs,
268
344
  failed,
345
+ ...(groups.length > 0 ? { groups } : {}),
346
+ ...(options.plan.fallbacks?.length ? { fallbacks: options.plan.fallbacks } : {}),
347
+ ...(Object.keys(shapes).length > 0 ? { shapes } : {}),
269
348
  ...(Object.keys(deployedOutputs).length > 0 ? { deployedOutputs } : {}),
270
349
  ...(Object.keys(plannedOutputs).length > 0 ? { plannedOutputs } : {}),
271
350
  };
@@ -277,41 +356,72 @@ export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
277
356
  * that upstream applies, each sorted. `build` are the published artifact
278
357
  * identities a build in the change produces (`WiringReference.publish`):
279
358
  * the build's provenance binds those, so they are never approved again.
280
- * `unknown` are the rest.
359
+ * `unknown` are the rest. A reference to a member of `group`, the
360
+ * component's own group (ws-107), is neither: the group's apply computes it.
281
361
  */
282
362
  function unknownInputsOf(
283
363
  component: DriverComponent,
284
364
  outputs: Record<string, Record<string, unknown>>,
285
365
  inChange: ReadonlySet<string>,
366
+ group?: ReadonlySet<string>,
286
367
  ): { unknown: string[]; build: string[] } {
287
368
  const unknown = new Set<string>();
288
369
  const build = new Set<string>();
289
370
  for (const step of capabilitySteps(component.deploy)) {
290
371
  const { kind: _kind, ...rest } = step;
291
372
  for (const r of unresolvedWiringReferences(rest, outputs)) {
373
+ if (group?.has(r.source)) continue;
292
374
  if (inChange.has(r.source)) (r.publish ? build : unknown).add(r.ref);
293
375
  }
294
376
  }
295
377
  return { unknown: [...unknown].sort(), build: [...build].sort() };
296
378
  }
297
379
 
380
+ /**
381
+ * Every other component's output `component`'s steps read, one entry per
382
+ * upstream, sorted: what the report's `reads` says. A read whose value
383
+ * `outputs` holds is listed as well as one it lacks (`unknown`), so the
384
+ * report names each read between components whether or not the plan knew
385
+ * its value. A component's reads of its own outputs are left out.
386
+ */
387
+ function readsOf(component: DriverComponent, outputs: Record<string, Record<string, unknown>>): PrMemberRead[] {
388
+ const by = new Map<string, { outputs: Set<string>; unknown: Set<string> }>();
389
+ for (const step of capabilitySteps(component.deploy)) {
390
+ const { kind: _kind, ...rest } = step;
391
+ for (const r of wiringReads(rest, outputs)) {
392
+ if (r.source === component.name) continue;
393
+ const name = r.ref.slice(r.source.length + 1);
394
+ const at = by.get(r.source) ?? { outputs: new Set<string>(), unknown: new Set<string>() };
395
+ by.set(r.source, at);
396
+ at.outputs.add(name);
397
+ if (!r.known) at.unknown.add(name);
398
+ }
399
+ }
400
+ return [...by.entries()]
401
+ .sort(([a], [b]) => a.localeCompare(b))
402
+ .map(([upstream, r]) => ({ upstream, outputs: [...r.outputs].sort(), ...(r.unknown.size > 0 ? { unknown: [...r.unknown].sort() } : {}) }));
403
+ }
404
+
298
405
  /**
299
406
  * Why `component` cannot apply the plan that read `read` now that `now`
300
407
  * stands (#3999): each reference its steps make that resolves to another
301
408
  * value, said once. `undefined` when every one resolves as it did. A
302
409
  * published artifact's identity from a build in `inChange` never holds a
303
- * member back: the build's provenance binds it, not this approval.
410
+ * member back: the build's provenance binds it, not this approval. Nor
411
+ * does a reference to a member of `group`, the component's own group
412
+ * (ws-107), whose apply computes it.
304
413
  */
305
414
  export function heldReason(
306
415
  component: DriverComponent,
307
416
  read: Record<string, Record<string, unknown>>,
308
417
  now: Record<string, Record<string, unknown>>,
309
418
  inChange: ReadonlySet<string> = new Set(),
419
+ group: ReadonlySet<string> = new Set(),
310
420
  ): string | undefined {
311
421
  const refs = new Set<string>();
312
422
  for (const step of capabilitySteps(component.deploy)) {
313
423
  const { kind: _kind, ...rest } = step;
314
- for (const r of movedWiringReferences(rest, read, now)) if (!(r.publish && inChange.has(r.source))) refs.add(r.ref);
424
+ for (const r of movedWiringReferences(rest, read, now)) if (!(r.publish && inChange.has(r.source)) && !group.has(r.source)) refs.add(r.ref);
315
425
  }
316
426
  if (refs.size === 0) return undefined;
317
427
  return `its plan read ${[...refs].sort().join(", ")} before ${refs.size === 1 ? "that output" : "those outputs"} applied as another value`;
@@ -409,13 +519,23 @@ export interface PrApplyOptions {
409
519
  * The gate is {@link prRoundGate}. Without it, held members are left
410
520
  * `waiting` and nothing is planned again.
411
521
  */
412
- decideRound?: (round: { round: number; gate: string; set: PrPlanSet; plan: FanOutPlan }) => Promise<PrRoundDecision>;
522
+ /**
523
+ * Where the approval of `set` stands (#4419). When it is `approved`, a round
524
+ * whose fresh plan does only what `set` previewed, with only the inputs it
525
+ * marked unknown filled in, applies under that approval without a gate of
526
+ * its own (`coveredRound`), and the round records it (`PrRound.covered`).
527
+ * Without it every round decides its own gate.
528
+ */
529
+ approval?: PrApproval;
530
+ decideRound?: (round: { round: number; gate: string; set: PrPlanSet; plan: FanOutPlan; notCovered?: string }) => Promise<PrRoundDecision>;
413
531
  }
414
532
 
415
533
  /** How a round's gate was decided: whether it applies, and where its approval stands. */
416
534
  export interface PrRoundDecision {
417
535
  satisfied: boolean;
418
536
  approval: PrApproval;
537
+ /** Set when the first approval covered the round (#4419): no gate of its own was decided. */
538
+ covered?: PrRound["covered"];
419
539
  /** Why it does not apply, when an approval stands but does not count (`--require-review`). */
420
540
  message?: string;
421
541
  }
@@ -430,6 +550,98 @@ export interface PrApplyResult {
430
550
  rounds?: PrRound[];
431
551
  }
432
552
 
553
+ /**
554
+ * Whether the first approval covers a round's fresh plan (#4419). The
555
+ * approval binds `options.set.doc.digest`; the round's components and
556
+ * members must be ones that plan previewed, and what differs is only what
557
+ * it marked unknown.
558
+ */
559
+ function roundCover(
560
+ options: PrApplyOptions,
561
+ fresh: PrPlanSet,
562
+ plan: FanOutPlan,
563
+ now: Record<string, Record<string, unknown>>,
564
+ inChange: ReadonlySet<string>,
565
+ byName: ReadonlyMap<string, DriverComponent>,
566
+ ): RoundCoverage {
567
+ if (options.approval?.status !== "approved") return { covered: false, reason: "no approval of the first plan stands" };
568
+ // ws-107: a round's group is covered only when it has the approved group's members.
569
+ const moved = groupsMoved(options.set.groups ?? [], fresh.groups ?? [], plan.order);
570
+ if (moved) return { covered: false, reason: moved };
571
+ // The values the round reads of a component it plans again are the ones
572
+ // that plan says it will expose: it has not applied yet, so the outputs
573
+ // standing say nothing of it. An output the first plan knew and the round's
574
+ // plan knows as the same value is not a changed input, whichever of the
575
+ // two applies first.
576
+ const reads: Record<string, Record<string, unknown>> = { ...now };
577
+ for (const name of plan.order) if (fresh.outputs[name]) reads[name] = fresh.outputs[name]!;
578
+ const changedKnown: Record<string, string[]> = {};
579
+ for (const name of plan.order) {
580
+ const component = byName.get(name);
581
+ if (!component) continue;
582
+ const refs = new Set<string>();
583
+ for (const step of capabilitySteps(component.deploy)) {
584
+ const { kind: _kind, ...rest } = step;
585
+ for (const r of movedWiringReferences(rest, options.set.outputs, reads)) if (!(r.publish && inChange.has(r.source))) refs.add(r.ref);
586
+ }
587
+ changedKnown[name] = [...refs].sort();
588
+ }
589
+ // A member whose round plan still reads only outputs of components this
590
+ // round plans and has not applied (a chain of imports, #4466d) cannot apply
591
+ // in this round: it is held again once those apply, and its own round
592
+ // checks its coverage. Only the members this round can apply are checked.
593
+ const inRound = plan.order;
594
+ const fromRound = (ref: string): boolean => inRound.some((c) => ref === c || ref.startsWith(`${c}.`));
595
+ const waits = (m: { unknownInputs?: string[] }): boolean => (m.unknownInputs?.length ?? 0) > 0 && m.unknownInputs!.every(fromRound);
596
+ const approved = { doc: options.set.doc, members: options.set.members, ...(options.set.shapes ? { shapes: options.set.shapes } : {}) };
597
+ return coveredRound({
598
+ approved,
599
+ approvedDigest: options.set.doc.digest,
600
+ fresh: { doc: fresh.doc, members: fresh.members.filter((m) => !waits(m)), ...(fresh.shapes ? { shapes: fresh.shapes } : {}) },
601
+ changedKnown,
602
+ attributes: (member) => memberAttributes(options.set.doc, fresh.doc, member, fresh.shapes?.[member] !== undefined),
603
+ });
604
+ }
605
+
606
+ /**
607
+ * Why the groups `fresh` planned are not the `approved` ones over the
608
+ * components in `order` (ws-107), or `undefined` when each group the round
609
+ * plans has the members the approved group had.
610
+ */
611
+ function groupsMoved(approved: readonly FanOutGroup[], fresh: readonly FanOutGroup[], order: readonly string[]): string | undefined {
612
+ const same = (a: readonly string[], b: readonly string[]): boolean => a.length === b.length && a.every((c) => b.includes(c));
613
+ const inRound = new Set(order);
614
+ for (const was of approved) {
615
+ const here = was.components.filter((c) => inRound.has(c));
616
+ if (here.length === 0) continue;
617
+ const now = fresh.find((g) => g.components.includes(here[0]!));
618
+ if (!now || !same(now.components, was.components)) {
619
+ return `the approved plan grouped ${was.components.join(", ")}, and the round ${now ? `groups ${now.components.join(", ")}` : `plans ${here.join(", ")} without that group`}`;
620
+ }
621
+ }
622
+ for (const now of fresh) {
623
+ if (!approved.some((was) => same(was.components, now.components))) return `the round groups ${now.components.join(", ")}, which the approved plan did not`;
624
+ }
625
+ return undefined;
626
+ }
627
+
628
+ /**
629
+ * The part of coverage that reads the change set's own attributes, shared by
630
+ * a round, a resume and a wave: a member whose planner describes no changes
631
+ * (and gave no shape) is covered only by the digest it was approved with, and
632
+ * anything else is {@link planCoveredBy}'s.
633
+ */
634
+ function memberAttributes(approved: ChangeSetDocument, fresh: ChangeSetDocument, member: string, hasShape: boolean): string | undefined {
635
+ if (!hasShape) {
636
+ const now = fresh.members.find((m) => m.member === member);
637
+ const was = approved.members.find((m) => m.member === member);
638
+ if (now && was && now.planner === "chant" && now.lexicon === "chant" && now.status === "planned" && now.planDigest !== was.planDigest) {
639
+ return `${member} now plans to ${now.planDigest}, not the approved ${was.planDigest ?? "(none)"}, and its planner describes no changes to compare`;
640
+ }
641
+ }
642
+ return planCoveredBy(approved, fresh, member);
643
+ }
644
+
433
645
  /** Two runs of one apply as one: `b` ran after `a`, against the outputs `a` left. */
434
646
  function joinRuns(a: FanOutRunResult, b: FanOutRunResult): FanOutRunResult {
435
647
  const status: FanOutRunResult["status"] = a.status === "fail" || b.status === "fail" ? "fail" : a.status === "gated" || b.status === "gated" ? "gated" : "ok";
@@ -444,6 +656,8 @@ function joinRuns(a: FanOutRunResult, b: FanOutRunResult): FanOutRunResult {
444
656
  componentOutputs: b.componentOutputs,
445
657
  ...(gate ? { gate } : {}),
446
658
  ...(b.held ? { held: b.held } : {}),
659
+ ...(a.notStarted || b.notStarted ? { notStarted: [...new Set([...(a.notStarted ?? []), ...(b.notStarted ?? [])])].sort() } : {}),
660
+ ...(a.groupOutcomes || b.groupOutcomes ? { groupOutcomes: { ...(a.groupOutcomes ?? {}), ...(b.groupOutcomes ?? {}) } } : {}),
447
661
  };
448
662
  }
449
663
 
@@ -467,20 +681,25 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
467
681
  ...Object.fromEntries(Object.entries(options.set.outputs).filter(([name]) => !toApply.has(name))),
468
682
  ...(options.resumed?.outputs ?? {}),
469
683
  };
470
- const runRound = (plan: FanOutPlan, plans: Record<string, Record<string, unknown>>, read: Record<string, Record<string, unknown>>, outputs: Record<string, Record<string, unknown>>) =>
471
- runFanOut(plan, options.components, options.registry, {
684
+ const runRound = (plan: FanOutPlan, set: Pick<PrPlanSet, "plans" | "groups">, read: Record<string, Record<string, unknown>>, outputs: Record<string, Record<string, unknown>>) => {
685
+ // ws-107: each group runs the plan its group planned, and a read of a group-mate holds nothing back.
686
+ const groupOf = groupsByComponent(plan);
687
+ const groupRuns = Object.fromEntries((set.groups ?? []).filter((g) => g.run).map((g) => [g.components[0]!, g.run!]));
688
+ return runFanOut(plan, options.components, options.registry, {
472
689
  env: options.env,
473
690
  ...(options.vars ? { vars: options.vars } : {}),
474
- plans,
691
+ plans: set.plans,
692
+ ...(Object.keys(groupRuns).length > 0 ? { groupRuns } : {}),
475
693
  ...(options.targets ? { targets: options.targets } : {}),
476
694
  componentOutputs: outputs,
477
- hold: (name, now) => heldReason(byName.get(name)!, read, now, inChange),
695
+ hold: (name, now) => heldReason(byName.get(name)!, read, now, inChange, new Set(groupOf.get(name)?.components ?? [])),
478
696
  ...(options.onProgress ? { onProgress: options.onProgress } : {}),
479
697
  ...(options.onRunEvent ? { onRunEvent: options.onRunEvent } : {}),
480
698
  ...(options.onComponentSettled ? { onComponentSettled: options.onComponentSettled } : {}),
481
699
  });
700
+ };
482
701
 
483
- let run = await runRound(options.plan, options.set.plans, options.set.outputs, { ...standing });
702
+ let run = await runRound(options.plan, options.set, options.set.outputs, { ...standing });
484
703
  const rounds: PrRound[] = [];
485
704
  /** Per component, the round that applied it or holds it, and that round's set, after the first. */
486
705
  const roundOf = new Map<string, { round: number; set: PrPlanSet }>();
@@ -489,7 +708,8 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
489
708
  const round = rounds.length + 2;
490
709
  const gate = prRoundGate(options.gate ?? PR_APPLY_GATE, round);
491
710
  const heldNames = new Set(run.held.map((h) => h.component));
492
- const plan = remainingFanOut(options.plan, options.components, { completed: options.plan.order.filter((name) => !heldNames.has(name)) });
711
+ // A held group is planned again as the same group (ws-107).
712
+ const plan = narrowGroups(remainingFanOut(options.plan, options.components, { completed: options.plan.order.filter((name) => !heldNames.has(name)) }), options.components, options.plan.groups);
493
713
  const set = await planPrSet({
494
714
  components: options.components,
495
715
  plan,
@@ -500,39 +720,62 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
500
720
  ...(options.targets ? { targets: options.targets } : {}),
501
721
  });
502
722
  for (const name of plan.order) roundOf.set(name, { round, set });
503
- const record = (status: PrRound["status"], approval: PrApproval, message?: string): void => {
504
- rounds.push({ round, gate, digest: set.doc.digest, components: plan.order, approval, status, ...(message ? { message } : {}), changeSet: set.doc });
723
+ const cover = roundCover(options, set, plan, run.componentOutputs, inChange, byName);
724
+ const record = (status: PrRound["status"], approval: PrApproval, message?: string, covered?: PrRound["covered"]): void => {
725
+ rounds.push({
726
+ round,
727
+ gate,
728
+ digest: set.doc.digest,
729
+ components: plan.order,
730
+ approval,
731
+ status,
732
+ ...(message ? { message } : {}),
733
+ ...(covered ? { covered } : {}),
734
+ ...(!covered && options.approval?.status === "approved" && cover.covered === false ? { notCovered: cover.reason } : {}),
735
+ changeSet: set.doc,
736
+ });
505
737
  };
506
738
  if (set.failed) {
507
739
  record("plan-failed", { status: "pending" });
508
740
  stopped = "plan-failed";
509
741
  break;
510
742
  }
511
- const decision = await options.decideRound({ round, gate, set, plan });
743
+ const decision: PrRoundDecision =
744
+ cover.covered && options.approval
745
+ ? { satisfied: true, approval: options.approval, covered: { digest: cover.digest, filledInputs: cover.filledInputs } }
746
+ : await options.decideRound({ round, gate, set, plan, ...(options.approval?.status === "approved" && !cover.covered ? { notCovered: cover.reason } : {}) });
512
747
  if (!decision.satisfied) {
513
748
  record("waiting", decision.approval, decision.message);
514
749
  stopped = "waiting";
515
750
  break;
516
751
  }
517
- const next = await runRound(plan, set.plans, set.outputs, run.componentOutputs);
518
- record(next.status === "fail" ? "failed" : "applied", decision.approval);
752
+ const next = await runRound(plan, set, set.outputs, run.componentOutputs);
753
+ record(next.status === "fail" ? "failed" : "applied", decision.approval, undefined, decision.covered);
519
754
  run = joinRuns(run, next);
520
755
  }
521
756
 
522
757
  const completed = new Set(run.completed);
758
+ const notStarted = new Set(run.notStarted ?? []);
523
759
  const held = new Map((run.held ?? []).map((h) => [h.component, h]));
524
760
  const blocked = new Map(run.blocked.map((b) => [b.component, b.blockedBy]));
525
761
  const errorOf = (component: string): string | undefined => {
526
762
  const result = [...run.results].reverse().find((r) => r.component === component);
527
763
  if (result?.status === "gated") return `stopped at its own gate ${result.gate?.gate ?? ""}`.trim();
528
- return result?.records.find((r) => r.status === "fail")?.error;
764
+ return result?.records.find((r) => r.status === "fail" || (notStarted.has(component) && r.status === "skipped"))?.error;
529
765
  };
530
766
  const members = options.set.members.map((m): PrMember => {
531
767
  if (priorCompleted.has(m.component)) return { ...m, status: "applied" };
532
768
  const later = roundOf.get(m.component);
533
769
  const fresh = later?.set.members.find((x) => x.member === m.member);
534
- const asPlanned: PrMember = later && fresh ? { ...m, planDigest: fresh.planDigest, counts: fresh.counts, round: later.round } : m;
770
+ const planned: PrMember = later && fresh ? { ...m, planDigest: fresh.planDigest, counts: fresh.counts, round: later.round } : m;
771
+ // #4446: how its part of a group's apply ended, as the capability said.
772
+ const groupOutcome = run.groupOutcomes?.[m.component];
773
+ const asPlanned: PrMember = groupOutcome ? { ...planned, groupOutcome } : planned;
535
774
  if (completed.has(m.component)) return { ...asPlanned, status: "applied" };
775
+ if (notStarted.has(m.component)) {
776
+ const error = errorOf(m.component);
777
+ return { ...asPlanned, status: "not-started", ...(error ? { error } : {}) };
778
+ }
536
779
  const h = held.get(m.component);
537
780
  if (h) return { ...asPlanned, status: "waiting", held: h.heldBy ? `${h.heldBy} is held: ${h.reason}` : h.reason };
538
781
  if (fresh?.status === "plan-failed") return { ...asPlanned, status: "plan-failed", ...(fresh.error ? { error: fresh.error } : {}) };
@@ -639,6 +882,12 @@ export interface PrResumeOptions {
639
882
  record: PrApplyRecord;
640
883
  /** Components an earlier attempt applied. */
641
884
  completed: string[];
885
+ /**
886
+ * The outputs the components in `completed` left (#4446). A member left
887
+ * of an approved group plans against the outputs its group-mates applied
888
+ * with, which the group's apply computed and the approved plan did not know.
889
+ */
890
+ applied?: Record<string, Record<string, unknown>>;
642
891
  }
643
892
 
644
893
  export type PrResume =
@@ -672,25 +921,48 @@ export type PrResume =
672
921
  * is `record.digest`, and the document gives that digest. The digest binds
673
922
  * the entries, holes and side effects {@link planCoveredBy} reads (#3555),
674
923
  * so a record cannot widen what a re-planned member may do.
924
+ *
925
+ * A group that failed part way (ws-107, "Resume"; #4446) leaves its
926
+ * `partial`, `failed` and `not-started` members; `applied` and `no-changes`
927
+ * ones are in `completed`. What is left of each approved group is grouped
928
+ * again ({@link regroupRemaining}), so it plans as a smaller group, or one
929
+ * by one when the rule or the capability says so. The smaller group's
930
+ * digest differs from the approved one, so coverage rests on entries and
931
+ * shapes, as for any resumed member.
675
932
  */
676
933
  export async function resumePrSet(options: PrResumeOptions): Promise<PrResume> {
677
934
  const { record } = options;
678
935
  if (record.changeSet.digest !== record.digest || !verifyChangeSetDigest(record.changeSet)) {
679
936
  return { ok: false, reasons: [`the record's change set is not the one its digest ${record.digest} names`] };
680
937
  }
681
- const plan = remainingFanOut(options.plan, options.components, { completed: options.completed });
938
+ const plan = await regroupRemaining({
939
+ components: options.components,
940
+ plan: remainingFanOut(options.plan, options.components, { completed: options.completed }),
941
+ registry: options.registry,
942
+ env: options.env,
943
+ ...(options.vars ? { vars: options.vars } : {}),
944
+ groups: options.plan.groups,
945
+ ...(options.plan.fallbacks ? { fallbacks: options.plan.fallbacks } : {}),
946
+ });
947
+ // The outputs of the applied members of a group with members left: the values the group's apply computed.
948
+ const left = new Set(plan.order);
949
+ const fromGroup = Object.fromEntries(
950
+ (options.plan.groups ?? [])
951
+ .filter((g) => g.components.some((c) => left.has(c)))
952
+ .flatMap((g) => g.components.filter((c) => !left.has(c) && options.applied?.[c] !== undefined))
953
+ .map((c) => [c, options.applied![c]!]),
954
+ );
682
955
  const fresh = await planPrSet({
683
956
  components: options.components,
684
957
  plan,
685
958
  registry: options.registry,
686
959
  env: options.env,
687
960
  ...(options.vars ? { vars: options.vars } : {}),
688
- seededOutputs: { ...(options.seededOutputs ?? {}), ...options.record.planOutputs },
961
+ seededOutputs: { ...(options.seededOutputs ?? {}), ...options.record.planOutputs, ...fromGroup },
689
962
  ...(options.targets ? { targets: options.targets } : {}),
690
963
  });
691
- const reasons = fresh.doc.members
692
- .map((m) => planCoveredBy(options.record.changeSet, fresh.doc, m.member))
693
- .filter((r): r is string => r !== undefined);
964
+ // #4422: decided as a round decides (`coveredRound`), by the same function a wave uses.
965
+ const reasons = waveNotCovered(options.record.changeSet, fresh.doc, options.record.members);
694
966
  if (reasons.length > 0) return { ok: false, reasons, fresh };
695
967
  return {
696
968
  ok: true,
@@ -700,7 +972,9 @@ export async function resumePrSet(options: PrResumeOptions): Promise<PrResume> {
700
972
  doc: options.record.changeSet,
701
973
  members: options.record.members.map((m) => ({ ...m, status: "planned" as const })),
702
974
  plans: fresh.plans,
703
- outputs: options.record.planOutputs,
975
+ outputs: { ...options.record.planOutputs, ...fromGroup },
976
+ ...(fresh.groups ? { groups: fresh.groups } : {}),
977
+ ...(fresh.fallbacks ? { fallbacks: fresh.fallbacks } : {}),
704
978
  failed: false,
705
979
  },
706
980
  };
@@ -720,7 +994,17 @@ export function prWavePlan(plan: FanOutPlan, components: DriverComponent[], wave
720
994
  const seeds = [
721
995
  ...new Set([...plan.seeds, ...order.flatMap((name) => (byName.get(name)?.dependsOn ?? []).filter((dep) => !inWave.has(dep)))]),
722
996
  ].sort();
723
- return { order, waves: order.length > 0 ? [order] : [], skipped: [], seeds, indeterminate: plan.indeterminate, digest: plan.digest };
997
+ // ws-107: a group sits in one wave, so what is left of it plans and applies as one there.
998
+ const groups = (plan.groups ?? []).map((g) => ({ ...g, components: g.components.filter((c) => inWave.has(c)) })).filter((g) => g.components.length > 1);
999
+ return {
1000
+ order,
1001
+ waves: order.length > 0 ? [order] : [],
1002
+ skipped: [],
1003
+ seeds,
1004
+ indeterminate: plan.indeterminate,
1005
+ digest: plan.digest,
1006
+ ...(groups.length > 0 ? { groups } : {}),
1007
+ };
724
1008
  }
725
1009
 
726
1010
  /**
@@ -731,20 +1015,47 @@ export function prWavePlan(plan: FanOutPlan, components: DriverComponent[], wave
731
1015
  * returns no change set) is covered only by the digest it was approved
732
1016
  * with, since with no entries there is nothing to compare a moved plan by.
733
1017
  */
734
- export function waveNotCovered(approved: ChangeSetDocument, fresh: ChangeSetDocument, approvedMembers: readonly PrMember[] = []): string[] {
1018
+ export function waveNotCovered(
1019
+ approved: ChangeSetDocument,
1020
+ fresh: ChangeSetDocument,
1021
+ approvedMembers: readonly PrMember[] = [],
1022
+ shapes: { approved?: Record<string, PlanShape>; fresh?: Record<string, PlanShape> } = {},
1023
+ ): string[] {
735
1024
  // #3999: a member whose approved plan waited only on an artifact a build in
736
1025
  // the change publishes is covered by that build's provenance, as it is
737
1026
  // without --wave-gate.
738
1027
  const byBuild = new Set(approvedMembers.filter((m) => m.buildInputs?.length && !m.unknownInputs?.length).map((m) => m.member));
1028
+ const prMember = new Map(approvedMembers.map((m) => [m.member, m]));
1029
+ // #4422: the same decision a round makes (`coveredRound`), so a resume, a
1030
+ // wave and a round agree for equivalent inputs. A member the document names
1031
+ // and the caller does not stands for a component of its own name.
1032
+ const approvedSide = {
1033
+ doc: approved,
1034
+ members: approved.members.map((m) => ({
1035
+ member: m.member,
1036
+ component: prMember.get(m.member)?.component ?? m.member,
1037
+ ...(prMember.get(m.member)?.unknownInputs?.length ? { unknownInputs: prMember.get(m.member)!.unknownInputs } : {}),
1038
+ })),
1039
+ ...(shapes.approved && shapes.fresh ? { shapes: shapes.approved } : {}),
1040
+ };
739
1041
  return fresh.members
740
1042
  .map((m) => {
741
1043
  if (byBuild.has(m.member)) return undefined;
742
1044
  const was = approved.members.find((a) => a.member === m.member);
743
- const opaque = m.planner === "chant" && m.lexicon === "chant" && m.status === "planned";
744
- if (opaque && was && was.planDigest !== m.planDigest) {
745
- return `${m.member} now plans to ${m.planDigest}, not the approved ${was.planDigest ?? "(none)"}, and its planner describes no changes to compare`;
746
- }
747
- return planCoveredBy(approved, fresh, m.member);
1045
+ if (!was) return planCoveredBy(approved, fresh, m.member);
1046
+ if (was.planDigest !== null && m.planDigest === was.planDigest) return undefined;
1047
+ const hasShape = shapes.approved !== undefined && shapes.fresh?.[m.member] !== undefined;
1048
+ const cover = coveredRound({
1049
+ approved: approvedSide,
1050
+ approvedDigest: approved.digest,
1051
+ fresh: {
1052
+ doc: fresh,
1053
+ members: [{ member: m.member, component: prMember.get(m.member)?.component ?? m.member }],
1054
+ ...(hasShape ? { shapes: shapes.fresh! } : {}),
1055
+ },
1056
+ attributes: (member) => memberAttributes(approved, fresh, member, hasShape),
1057
+ });
1058
+ return cover.covered ? undefined : cover.reason;
748
1059
  })
749
1060
  .filter((r): r is string => r !== undefined);
750
1061
  }
@@ -778,9 +1089,36 @@ export interface PrReportInput {
778
1089
  migrations?: PrMigrationFile[];
779
1090
  }
780
1091
 
1092
+ /**
1093
+ * The report's `groups` (#4446): each group the set planned, with the wave
1094
+ * of `waves` its members sit in and the reads of group-mates its members'
1095
+ * change-set parts name, which the group's apply computes.
1096
+ */
1097
+ function reportGroups(set: PrPlanSet, waves: readonly string[][]): PrGroup[] {
1098
+ const componentOf = new Map(set.members.map((m) => [m.member, m.component]));
1099
+ return (set.groups ?? []).map((g) => {
1100
+ const wave = waves.findIndex((w) => g.components.some((c) => w.includes(c))) + 1;
1101
+ const reads = set.doc.members
1102
+ .filter((m) => g.components.includes(componentOf.get(m.member) ?? m.member))
1103
+ .flatMap((m) => (m.group?.reads ?? []).map((ref) => ({ component: componentOf.get(m.member) ?? m.member, ref })))
1104
+ .sort((a, b) => a.component.localeCompare(b.component) || a.ref.localeCompare(b.ref));
1105
+ return {
1106
+ capability: g.capability,
1107
+ components: [...g.components],
1108
+ wave: wave > 0 ? wave : 1,
1109
+ digest: g.digest,
1110
+ ...(g.error !== undefined ? { error: g.error } : {}),
1111
+ ...(reads.length > 0 ? { reads } : {}),
1112
+ };
1113
+ });
1114
+ }
1115
+
781
1116
  /** The report a stage writes. */
782
1117
  export function prReport(input: PrReportInput): PrReport {
783
1118
  const changed = new Set(input.signal.changed);
1119
+ const groups = reportGroups(input.set, input.plan.waves);
1120
+ const groupOf = new Map(groups.flatMap((g, i) => g.components.map((c) => [c, i] as const)));
1121
+ const members = (input.members ?? input.set.members).map((m) => (groupOf.has(m.component) ? { ...m, group: groupOf.get(m.component)! } : m));
784
1122
  return {
785
1123
  $schema: PR_REPORT_SCHEMA_ID,
786
1124
  contract: PR_REPORT_CONTRACT,
@@ -811,7 +1149,9 @@ export function prReport(input: PrReportInput): PrReport {
811
1149
  indeterminate: input.plan.indeterminate,
812
1150
  waves: input.plan.waves,
813
1151
  },
814
- members: input.members ?? input.set.members,
1152
+ ...(groups.length > 0 ? { groups } : {}),
1153
+ ...(input.set.fallbacks?.length ? { fallbacks: input.set.fallbacks.map((f) => ({ ...f })) } : {}),
1154
+ members,
815
1155
  changeSet: input.set.doc,
816
1156
  };
817
1157
  }