@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,14 +67,24 @@
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
  import { composeChangeSet, verifyChangeSetDigest } from "../change-set.js";
71
80
  import { evaluateGate } from "../op/gate.js";
72
81
  import { memberCounts, PR_APPLY_GATE, prRoundGate, PR_REPORT_CONTRACT, PR_REPORT_SCHEMA_ID, } from "../pr-loop.js";
73
- import { deployContext, movedWiringReferences, unresolvedWiringReferences, } from "./driver.js";
82
+ import { deployContext, movedWiringReferences, unresolvedWiringReferences, wiringReads, } from "./driver.js";
74
83
  import { deployUnits } from "./deploy-units.js";
75
84
  import { runFanOut } from "./fan-out-run.js";
76
85
  import { remainingFanOut } from "./fan-out.js";
86
+ import { coveredRound } from "./covered-round.js";
87
+ import { groupsByComponent, narrowGroups, planComponentGroup, regroupRemaining } from "./groups.js";
77
88
  import { capabilitySteps, planWaveComponent, readDeployedOutputs } from "./wave-plan.js";
78
89
  /** The phase a pull request's planning is, in its run's events. */
79
90
  export const PR_PLAN_PHASE = "plan";
@@ -105,17 +116,24 @@ export async function planPrSet(options) {
105
116
  const parts = [];
106
117
  const componentOf = new Map();
107
118
  const unknownOf = new Map();
119
+ const readsBy = new Map();
108
120
  const plans = {};
121
+ const shapes = {};
109
122
  let failed = false;
110
123
  const emit = options.onRunEvent;
111
124
  emit?.({ type: "phase-start", component: null, phase: PR_PLAN_PHASE });
125
+ // ws-107: each group plans once, in its wave, with one call.
126
+ const groupOf = groupsByComponent(options.plan);
127
+ const groups = [];
112
128
  for (const wave of options.plan.waves) {
113
129
  // The dependencies' outputs first, each once. One the change plans is
114
130
  // read from its plan, which an earlier wave made (#3999). Any other is
115
131
  // read from the deployed estate (#4104). A dependency that cannot say
116
132
  // leaves its references unresolved, which its dependent's plan reports
117
- // if it needs them.
118
- const deps = [...new Set(wave.flatMap((name) => byName.get(name)?.dependsOn ?? []))].filter((dep) => !read.has(dep));
133
+ // if it needs them. A member of the dependent's own group plans with it.
134
+ const deps = [
135
+ ...new Set(wave.flatMap((name) => (byName.get(name)?.dependsOn ?? []).filter((dep) => !groupOf.get(name)?.components.includes(dep)))),
136
+ ].filter((dep) => !read.has(dep));
119
137
  const fromEstate = [];
120
138
  for (const dep of deps) {
121
139
  read.add(dep);
@@ -130,25 +148,62 @@ export async function planPrSet(options) {
130
148
  planned[dep] = Object.keys(known).sort();
131
149
  }
132
150
  Object.assign(deployed, await readDeployedOutputs(fromEstate, byName, options.registry, (dep) => deployContext(options, dep), outputs));
133
- const results = await Promise.all(wave.map(async (name) => {
134
- const at = { component: name, phase: PR_PLAN_PHASE, step: "plan" };
135
- emit?.({ type: "step-start", ...at });
136
- const start = Date.now();
137
- const p = await planWaveComponent(byName.get(name), options.registry, deployContext(options, name), outputs);
151
+ const settle = (name, start, p) => {
138
152
  emit?.({
139
153
  type: "step-settle",
140
- ...at,
154
+ component: name,
155
+ phase: PR_PLAN_PHASE,
156
+ step: "plan",
141
157
  status: "error" in p ? "failed" : "ok",
142
158
  durationMs: Date.now() - start,
143
159
  ...("error" in p ? { error: p.error } : {}),
144
160
  });
145
- return p;
146
- }));
161
+ };
162
+ const groupsHere = [...new Set(wave.map((name) => groupOf.get(name)).filter((g) => g !== undefined))];
163
+ const results = (await Promise.all([
164
+ ...wave
165
+ .filter((name) => !groupOf.has(name))
166
+ .map(async (name) => {
167
+ emit?.({ type: "step-start", component: name, phase: PR_PLAN_PHASE, step: "plan" });
168
+ const start = Date.now();
169
+ const p = await planWaveComponent(byName.get(name), options.registry, deployContext(options, name), outputs);
170
+ settle(name, start, p);
171
+ return [p];
172
+ }),
173
+ ...groupsHere.map(async (group) => {
174
+ for (const name of group.components)
175
+ emit?.({ type: "step-start", component: name, phase: PR_PLAN_PHASE, step: "plan" });
176
+ const start = Date.now();
177
+ const g = await planComponentGroup({
178
+ group,
179
+ components: byName,
180
+ registry: options.registry,
181
+ env: options.env,
182
+ ...(options.vars ? { vars: options.vars } : {}),
183
+ ctxOf: (name) => deployContext(options, name),
184
+ outputs,
185
+ });
186
+ for (const p of g.plans)
187
+ settle(p.component, start, p);
188
+ const error = g.plans.find((p) => "error" in p)?.error;
189
+ groups.push({ ...group, digest: g.digest ?? null, ...(error !== undefined ? { error } : {}), ...(g.run ? { run: g.run } : {}) });
190
+ return g.plans;
191
+ }),
192
+ ])).flat();
193
+ /** What each member of a group planned to expose, for its group-mates' reads. */
194
+ const plannedHere = new Map(results.map((p) => [p.component, "error" in p ? undefined : p.outputs?.known]));
147
195
  for (const p of results) {
148
196
  const component = byName.get(p.component);
149
- const unknown = unknownInputsOf(component, outputs, inChange);
197
+ const group = groupOf.get(p.component);
198
+ const mates = group ? new Set(group.components) : undefined;
199
+ const unknown = unknownInputsOf(component, outputs, inChange, mates);
150
200
  if (unknown.unknown.length > 0 || unknown.build.length > 0)
151
201
  unknownOf.set(p.component, unknown);
202
+ // A read of a group-mate is known when that mate's plan knows it.
203
+ const readsView = group ? { ...outputs, ...Object.fromEntries(group.components.filter((c) => c !== p.component).map((c) => [c, { ...(plannedHere.get(c) ?? {}) }])) } : outputs;
204
+ const reads = readsOf(component, readsView);
205
+ if (reads.length > 0)
206
+ readsBy.set(p.component, reads);
152
207
  if ("error" in p) {
153
208
  failed = true;
154
209
  plannedBy.set(p.component, undefined);
@@ -160,6 +215,7 @@ export async function planPrSet(options) {
160
215
  }
161
216
  plannedBy.set(p.component, p.outputs);
162
217
  plans[p.component] = p.plans;
218
+ Object.assign(shapes, p.shapes);
163
219
  for (const part of p.parts) {
164
220
  parts.push(part);
165
221
  componentOf.set(part.member.member, p.component);
@@ -171,6 +227,7 @@ export async function planPrSet(options) {
171
227
  const members = doc.members.map((m) => {
172
228
  const component = componentOf.get(m.member) ?? m.member;
173
229
  const unknown = unknownOf.get(component);
230
+ const reads = readsBy.get(component);
174
231
  return {
175
232
  member: m.member,
176
233
  component,
@@ -180,17 +237,23 @@ export async function planPrSet(options) {
180
237
  ...(m.error ? { error: m.error } : {}),
181
238
  ...(unknown?.unknown.length ? { unknownInputs: unknown.unknown } : {}),
182
239
  ...(unknown?.build.length ? { buildInputs: unknown.build } : {}),
240
+ ...(reads ? { reads } : {}),
183
241
  };
184
242
  });
185
243
  const sorted = (r) => Object.fromEntries(Object.entries(r).sort(([a], [b]) => a.localeCompare(b)));
186
244
  const deployedOutputs = sorted(deployed);
187
245
  const plannedOutputs = sorted(planned);
246
+ const at = new Map(options.plan.order.map((name, i) => [name, i]));
247
+ groups.sort((a, b) => (at.get(a.components[0]) ?? 0) - (at.get(b.components[0]) ?? 0));
188
248
  return {
189
249
  doc,
190
250
  members,
191
251
  plans,
192
252
  outputs,
193
253
  failed,
254
+ ...(groups.length > 0 ? { groups } : {}),
255
+ ...(options.plan.fallbacks?.length ? { fallbacks: options.plan.fallbacks } : {}),
256
+ ...(Object.keys(shapes).length > 0 ? { shapes } : {}),
194
257
  ...(Object.keys(deployedOutputs).length > 0 ? { deployedOutputs } : {}),
195
258
  ...(Object.keys(plannedOutputs).length > 0 ? { plannedOutputs } : {}),
196
259
  };
@@ -201,33 +264,64 @@ export async function planPrSet(options) {
201
264
  * that upstream applies, each sorted. `build` are the published artifact
202
265
  * identities a build in the change produces (`WiringReference.publish`):
203
266
  * the build's provenance binds those, so they are never approved again.
204
- * `unknown` are the rest.
267
+ * `unknown` are the rest. A reference to a member of `group`, the
268
+ * component's own group (ws-107), is neither: the group's apply computes it.
205
269
  */
206
- function unknownInputsOf(component, outputs, inChange) {
270
+ function unknownInputsOf(component, outputs, inChange, group) {
207
271
  const unknown = new Set();
208
272
  const build = new Set();
209
273
  for (const step of capabilitySteps(component.deploy)) {
210
274
  const { kind: _kind, ...rest } = step;
211
275
  for (const r of unresolvedWiringReferences(rest, outputs)) {
276
+ if (group?.has(r.source))
277
+ continue;
212
278
  if (inChange.has(r.source))
213
279
  (r.publish ? build : unknown).add(r.ref);
214
280
  }
215
281
  }
216
282
  return { unknown: [...unknown].sort(), build: [...build].sort() };
217
283
  }
284
+ /**
285
+ * Every other component's output `component`'s steps read, one entry per
286
+ * upstream, sorted: what the report's `reads` says. A read whose value
287
+ * `outputs` holds is listed as well as one it lacks (`unknown`), so the
288
+ * report names each read between components whether or not the plan knew
289
+ * its value. A component's reads of its own outputs are left out.
290
+ */
291
+ function readsOf(component, outputs) {
292
+ const by = new Map();
293
+ for (const step of capabilitySteps(component.deploy)) {
294
+ const { kind: _kind, ...rest } = step;
295
+ for (const r of wiringReads(rest, outputs)) {
296
+ if (r.source === component.name)
297
+ continue;
298
+ const name = r.ref.slice(r.source.length + 1);
299
+ const at = by.get(r.source) ?? { outputs: new Set(), unknown: new Set() };
300
+ by.set(r.source, at);
301
+ at.outputs.add(name);
302
+ if (!r.known)
303
+ at.unknown.add(name);
304
+ }
305
+ }
306
+ return [...by.entries()]
307
+ .sort(([a], [b]) => a.localeCompare(b))
308
+ .map(([upstream, r]) => ({ upstream, outputs: [...r.outputs].sort(), ...(r.unknown.size > 0 ? { unknown: [...r.unknown].sort() } : {}) }));
309
+ }
218
310
  /**
219
311
  * Why `component` cannot apply the plan that read `read` now that `now`
220
312
  * stands (#3999): each reference its steps make that resolves to another
221
313
  * value, said once. `undefined` when every one resolves as it did. A
222
314
  * published artifact's identity from a build in `inChange` never holds a
223
- * member back: the build's provenance binds it, not this approval.
315
+ * member back: the build's provenance binds it, not this approval. Nor
316
+ * does a reference to a member of `group`, the component's own group
317
+ * (ws-107), whose apply computes it.
224
318
  */
225
- export function heldReason(component, read, now, inChange = new Set()) {
319
+ export function heldReason(component, read, now, inChange = new Set(), group = new Set()) {
226
320
  const refs = new Set();
227
321
  for (const step of capabilitySteps(component.deploy)) {
228
322
  const { kind: _kind, ...rest } = step;
229
323
  for (const r of movedWiringReferences(rest, read, now))
230
- if (!(r.publish && inChange.has(r.source)))
324
+ if (!(r.publish && inChange.has(r.source)) && !group.has(r.source))
231
325
  refs.add(r.ref);
232
326
  }
233
327
  if (refs.size === 0)
@@ -289,6 +383,97 @@ export function approversWhoAuthored(approvedBy, authors) {
289
383
  const wrote = new Set(authors.map(norm));
290
384
  return approvedBy.filter((by) => wrote.has(norm(by)));
291
385
  }
386
+ /**
387
+ * Whether the first approval covers a round's fresh plan (#4419). The
388
+ * approval binds `options.set.doc.digest`; the round's components and
389
+ * members must be ones that plan previewed, and what differs is only what
390
+ * it marked unknown.
391
+ */
392
+ function roundCover(options, fresh, plan, now, inChange, byName) {
393
+ if (options.approval?.status !== "approved")
394
+ return { covered: false, reason: "no approval of the first plan stands" };
395
+ // ws-107: a round's group is covered only when it has the approved group's members.
396
+ const moved = groupsMoved(options.set.groups ?? [], fresh.groups ?? [], plan.order);
397
+ if (moved)
398
+ return { covered: false, reason: moved };
399
+ // The values the round reads of a component it plans again are the ones
400
+ // that plan says it will expose: it has not applied yet, so the outputs
401
+ // standing say nothing of it. An output the first plan knew and the round's
402
+ // plan knows as the same value is not a changed input, whichever of the
403
+ // two applies first.
404
+ const reads = { ...now };
405
+ for (const name of plan.order)
406
+ if (fresh.outputs[name])
407
+ reads[name] = fresh.outputs[name];
408
+ const changedKnown = {};
409
+ for (const name of plan.order) {
410
+ const component = byName.get(name);
411
+ if (!component)
412
+ continue;
413
+ const refs = new Set();
414
+ for (const step of capabilitySteps(component.deploy)) {
415
+ const { kind: _kind, ...rest } = step;
416
+ for (const r of movedWiringReferences(rest, options.set.outputs, reads))
417
+ if (!(r.publish && inChange.has(r.source)))
418
+ refs.add(r.ref);
419
+ }
420
+ changedKnown[name] = [...refs].sort();
421
+ }
422
+ // A member whose round plan still reads only outputs of components this
423
+ // round plans and has not applied (a chain of imports, #4466d) cannot apply
424
+ // in this round: it is held again once those apply, and its own round
425
+ // checks its coverage. Only the members this round can apply are checked.
426
+ const inRound = plan.order;
427
+ const fromRound = (ref) => inRound.some((c) => ref === c || ref.startsWith(`${c}.`));
428
+ const waits = (m) => (m.unknownInputs?.length ?? 0) > 0 && m.unknownInputs.every(fromRound);
429
+ const approved = { doc: options.set.doc, members: options.set.members, ...(options.set.shapes ? { shapes: options.set.shapes } : {}) };
430
+ return coveredRound({
431
+ approved,
432
+ approvedDigest: options.set.doc.digest,
433
+ fresh: { doc: fresh.doc, members: fresh.members.filter((m) => !waits(m)), ...(fresh.shapes ? { shapes: fresh.shapes } : {}) },
434
+ changedKnown,
435
+ attributes: (member) => memberAttributes(options.set.doc, fresh.doc, member, fresh.shapes?.[member] !== undefined),
436
+ });
437
+ }
438
+ /**
439
+ * Why the groups `fresh` planned are not the `approved` ones over the
440
+ * components in `order` (ws-107), or `undefined` when each group the round
441
+ * plans has the members the approved group had.
442
+ */
443
+ function groupsMoved(approved, fresh, order) {
444
+ const same = (a, b) => a.length === b.length && a.every((c) => b.includes(c));
445
+ const inRound = new Set(order);
446
+ for (const was of approved) {
447
+ const here = was.components.filter((c) => inRound.has(c));
448
+ if (here.length === 0)
449
+ continue;
450
+ const now = fresh.find((g) => g.components.includes(here[0]));
451
+ if (!now || !same(now.components, was.components)) {
452
+ return `the approved plan grouped ${was.components.join(", ")}, and the round ${now ? `groups ${now.components.join(", ")}` : `plans ${here.join(", ")} without that group`}`;
453
+ }
454
+ }
455
+ for (const now of fresh) {
456
+ if (!approved.some((was) => same(was.components, now.components)))
457
+ return `the round groups ${now.components.join(", ")}, which the approved plan did not`;
458
+ }
459
+ return undefined;
460
+ }
461
+ /**
462
+ * The part of coverage that reads the change set's own attributes, shared by
463
+ * a round, a resume and a wave: a member whose planner describes no changes
464
+ * (and gave no shape) is covered only by the digest it was approved with, and
465
+ * anything else is {@link planCoveredBy}'s.
466
+ */
467
+ function memberAttributes(approved, fresh, member, hasShape) {
468
+ if (!hasShape) {
469
+ const now = fresh.members.find((m) => m.member === member);
470
+ const was = approved.members.find((m) => m.member === member);
471
+ if (now && was && now.planner === "chant" && now.lexicon === "chant" && now.status === "planned" && now.planDigest !== was.planDigest) {
472
+ return `${member} now plans to ${now.planDigest}, not the approved ${was.planDigest ?? "(none)"}, and its planner describes no changes to compare`;
473
+ }
474
+ }
475
+ return planCoveredBy(approved, fresh, member);
476
+ }
292
477
  /** Two runs of one apply as one: `b` ran after `a`, against the outputs `a` left. */
293
478
  function joinRuns(a, b) {
294
479
  const status = a.status === "fail" || b.status === "fail" ? "fail" : a.status === "gated" || b.status === "gated" ? "gated" : "ok";
@@ -303,6 +488,8 @@ function joinRuns(a, b) {
303
488
  componentOutputs: b.componentOutputs,
304
489
  ...(gate ? { gate } : {}),
305
490
  ...(b.held ? { held: b.held } : {}),
491
+ ...(a.notStarted || b.notStarted ? { notStarted: [...new Set([...(a.notStarted ?? []), ...(b.notStarted ?? [])])].sort() } : {}),
492
+ ...(a.groupOutcomes || b.groupOutcomes ? { groupOutcomes: { ...(a.groupOutcomes ?? {}), ...(b.groupOutcomes ?? {}) } } : {}),
306
493
  };
307
494
  }
308
495
  /**
@@ -325,18 +512,24 @@ export async function applyPrSet(options) {
325
512
  ...Object.fromEntries(Object.entries(options.set.outputs).filter(([name]) => !toApply.has(name))),
326
513
  ...(options.resumed?.outputs ?? {}),
327
514
  };
328
- const runRound = (plan, plans, read, outputs) => runFanOut(plan, options.components, options.registry, {
329
- env: options.env,
330
- ...(options.vars ? { vars: options.vars } : {}),
331
- plans,
332
- ...(options.targets ? { targets: options.targets } : {}),
333
- componentOutputs: outputs,
334
- hold: (name, now) => heldReason(byName.get(name), read, now, inChange),
335
- ...(options.onProgress ? { onProgress: options.onProgress } : {}),
336
- ...(options.onRunEvent ? { onRunEvent: options.onRunEvent } : {}),
337
- ...(options.onComponentSettled ? { onComponentSettled: options.onComponentSettled } : {}),
338
- });
339
- let run = await runRound(options.plan, options.set.plans, options.set.outputs, { ...standing });
515
+ const runRound = (plan, set, read, outputs) => {
516
+ // ws-107: each group runs the plan its group planned, and a read of a group-mate holds nothing back.
517
+ const groupOf = groupsByComponent(plan);
518
+ const groupRuns = Object.fromEntries((set.groups ?? []).filter((g) => g.run).map((g) => [g.components[0], g.run]));
519
+ return runFanOut(plan, options.components, options.registry, {
520
+ env: options.env,
521
+ ...(options.vars ? { vars: options.vars } : {}),
522
+ plans: set.plans,
523
+ ...(Object.keys(groupRuns).length > 0 ? { groupRuns } : {}),
524
+ ...(options.targets ? { targets: options.targets } : {}),
525
+ componentOutputs: outputs,
526
+ hold: (name, now) => heldReason(byName.get(name), read, now, inChange, new Set(groupOf.get(name)?.components ?? [])),
527
+ ...(options.onProgress ? { onProgress: options.onProgress } : {}),
528
+ ...(options.onRunEvent ? { onRunEvent: options.onRunEvent } : {}),
529
+ ...(options.onComponentSettled ? { onComponentSettled: options.onComponentSettled } : {}),
530
+ });
531
+ };
532
+ let run = await runRound(options.plan, options.set, options.set.outputs, { ...standing });
340
533
  const rounds = [];
341
534
  /** Per component, the round that applied it or holds it, and that round's set, after the first. */
342
535
  const roundOf = new Map();
@@ -345,7 +538,8 @@ export async function applyPrSet(options) {
345
538
  const round = rounds.length + 2;
346
539
  const gate = prRoundGate(options.gate ?? PR_APPLY_GATE, round);
347
540
  const heldNames = new Set(run.held.map((h) => h.component));
348
- const plan = remainingFanOut(options.plan, options.components, { completed: options.plan.order.filter((name) => !heldNames.has(name)) });
541
+ // A held group is planned again as the same group (ws-107).
542
+ const plan = narrowGroups(remainingFanOut(options.plan, options.components, { completed: options.plan.order.filter((name) => !heldNames.has(name)) }), options.components, options.plan.groups);
349
543
  const set = await planPrSet({
350
544
  components: options.components,
351
545
  plan,
@@ -357,41 +551,63 @@ export async function applyPrSet(options) {
357
551
  });
358
552
  for (const name of plan.order)
359
553
  roundOf.set(name, { round, set });
360
- const record = (status, approval, message) => {
361
- rounds.push({ round, gate, digest: set.doc.digest, components: plan.order, approval, status, ...(message ? { message } : {}), changeSet: set.doc });
554
+ const cover = roundCover(options, set, plan, run.componentOutputs, inChange, byName);
555
+ const record = (status, approval, message, covered) => {
556
+ rounds.push({
557
+ round,
558
+ gate,
559
+ digest: set.doc.digest,
560
+ components: plan.order,
561
+ approval,
562
+ status,
563
+ ...(message ? { message } : {}),
564
+ ...(covered ? { covered } : {}),
565
+ ...(!covered && options.approval?.status === "approved" && cover.covered === false ? { notCovered: cover.reason } : {}),
566
+ changeSet: set.doc,
567
+ });
362
568
  };
363
569
  if (set.failed) {
364
570
  record("plan-failed", { status: "pending" });
365
571
  stopped = "plan-failed";
366
572
  break;
367
573
  }
368
- const decision = await options.decideRound({ round, gate, set, plan });
574
+ const decision = cover.covered && options.approval
575
+ ? { satisfied: true, approval: options.approval, covered: { digest: cover.digest, filledInputs: cover.filledInputs } }
576
+ : await options.decideRound({ round, gate, set, plan, ...(options.approval?.status === "approved" && !cover.covered ? { notCovered: cover.reason } : {}) });
369
577
  if (!decision.satisfied) {
370
578
  record("waiting", decision.approval, decision.message);
371
579
  stopped = "waiting";
372
580
  break;
373
581
  }
374
- const next = await runRound(plan, set.plans, set.outputs, run.componentOutputs);
375
- record(next.status === "fail" ? "failed" : "applied", decision.approval);
582
+ const next = await runRound(plan, set, set.outputs, run.componentOutputs);
583
+ record(next.status === "fail" ? "failed" : "applied", decision.approval, undefined, decision.covered);
376
584
  run = joinRuns(run, next);
377
585
  }
378
586
  const completed = new Set(run.completed);
587
+ const notStarted = new Set(run.notStarted ?? []);
379
588
  const held = new Map((run.held ?? []).map((h) => [h.component, h]));
380
589
  const blocked = new Map(run.blocked.map((b) => [b.component, b.blockedBy]));
381
590
  const errorOf = (component) => {
382
591
  const result = [...run.results].reverse().find((r) => r.component === component);
383
592
  if (result?.status === "gated")
384
593
  return `stopped at its own gate ${result.gate?.gate ?? ""}`.trim();
385
- return result?.records.find((r) => r.status === "fail")?.error;
594
+ return result?.records.find((r) => r.status === "fail" || (notStarted.has(component) && r.status === "skipped"))?.error;
386
595
  };
387
596
  const members = options.set.members.map((m) => {
388
597
  if (priorCompleted.has(m.component))
389
598
  return { ...m, status: "applied" };
390
599
  const later = roundOf.get(m.component);
391
600
  const fresh = later?.set.members.find((x) => x.member === m.member);
392
- const asPlanned = later && fresh ? { ...m, planDigest: fresh.planDigest, counts: fresh.counts, round: later.round } : m;
601
+ const planned = later && fresh ? { ...m, planDigest: fresh.planDigest, counts: fresh.counts, round: later.round } : m;
602
+ // #4446: how its part of a group's apply ended, as the capability said.
603
+ const groupOutcome = run.groupOutcomes?.[m.component];
604
+ const asPlanned = groupOutcome ? { ...planned, groupOutcome } : planned;
393
605
  if (completed.has(m.component))
394
606
  return { ...asPlanned, status: "applied" };
607
+ if (notStarted.has(m.component)) {
608
+ const error = errorOf(m.component);
609
+ return { ...asPlanned, status: "not-started", ...(error ? { error } : {}) };
610
+ }
395
611
  const h = held.get(m.component);
396
612
  if (h)
397
613
  return { ...asPlanned, status: "waiting", held: h.heldBy ? `${h.heldBy} is held: ${h.reason}` : h.reason };
@@ -506,25 +722,46 @@ export function planCoveredBy(approved, fresh, member) {
506
722
  * is `record.digest`, and the document gives that digest. The digest binds
507
723
  * the entries, holes and side effects {@link planCoveredBy} reads (#3555),
508
724
  * so a record cannot widen what a re-planned member may do.
725
+ *
726
+ * A group that failed part way (ws-107, "Resume"; #4446) leaves its
727
+ * `partial`, `failed` and `not-started` members; `applied` and `no-changes`
728
+ * ones are in `completed`. What is left of each approved group is grouped
729
+ * again ({@link regroupRemaining}), so it plans as a smaller group, or one
730
+ * by one when the rule or the capability says so. The smaller group's
731
+ * digest differs from the approved one, so coverage rests on entries and
732
+ * shapes, as for any resumed member.
509
733
  */
510
734
  export async function resumePrSet(options) {
511
735
  const { record } = options;
512
736
  if (record.changeSet.digest !== record.digest || !verifyChangeSetDigest(record.changeSet)) {
513
737
  return { ok: false, reasons: [`the record's change set is not the one its digest ${record.digest} names`] };
514
738
  }
515
- const plan = remainingFanOut(options.plan, options.components, { completed: options.completed });
739
+ const plan = await regroupRemaining({
740
+ components: options.components,
741
+ plan: remainingFanOut(options.plan, options.components, { completed: options.completed }),
742
+ registry: options.registry,
743
+ env: options.env,
744
+ ...(options.vars ? { vars: options.vars } : {}),
745
+ groups: options.plan.groups,
746
+ ...(options.plan.fallbacks ? { fallbacks: options.plan.fallbacks } : {}),
747
+ });
748
+ // The outputs of the applied members of a group with members left: the values the group's apply computed.
749
+ const left = new Set(plan.order);
750
+ const fromGroup = Object.fromEntries((options.plan.groups ?? [])
751
+ .filter((g) => g.components.some((c) => left.has(c)))
752
+ .flatMap((g) => g.components.filter((c) => !left.has(c) && options.applied?.[c] !== undefined))
753
+ .map((c) => [c, options.applied[c]]));
516
754
  const fresh = await planPrSet({
517
755
  components: options.components,
518
756
  plan,
519
757
  registry: options.registry,
520
758
  env: options.env,
521
759
  ...(options.vars ? { vars: options.vars } : {}),
522
- seededOutputs: { ...(options.seededOutputs ?? {}), ...options.record.planOutputs },
760
+ seededOutputs: { ...(options.seededOutputs ?? {}), ...options.record.planOutputs, ...fromGroup },
523
761
  ...(options.targets ? { targets: options.targets } : {}),
524
762
  });
525
- const reasons = fresh.doc.members
526
- .map((m) => planCoveredBy(options.record.changeSet, fresh.doc, m.member))
527
- .filter((r) => r !== undefined);
763
+ // #4422: decided as a round decides (`coveredRound`), by the same function a wave uses.
764
+ const reasons = waveNotCovered(options.record.changeSet, fresh.doc, options.record.members);
528
765
  if (reasons.length > 0)
529
766
  return { ok: false, reasons, fresh };
530
767
  return {
@@ -535,7 +772,9 @@ export async function resumePrSet(options) {
535
772
  doc: options.record.changeSet,
536
773
  members: options.record.members.map((m) => ({ ...m, status: "planned" })),
537
774
  plans: fresh.plans,
538
- outputs: options.record.planOutputs,
775
+ outputs: { ...options.record.planOutputs, ...fromGroup },
776
+ ...(fresh.groups ? { groups: fresh.groups } : {}),
777
+ ...(fresh.fallbacks ? { fallbacks: fresh.fallbacks } : {}),
539
778
  failed: false,
540
779
  },
541
780
  };
@@ -554,7 +793,17 @@ export function prWavePlan(plan, components, wave, completed = []) {
554
793
  const seeds = [
555
794
  ...new Set([...plan.seeds, ...order.flatMap((name) => (byName.get(name)?.dependsOn ?? []).filter((dep) => !inWave.has(dep)))]),
556
795
  ].sort();
557
- return { order, waves: order.length > 0 ? [order] : [], skipped: [], seeds, indeterminate: plan.indeterminate, digest: plan.digest };
796
+ // ws-107: a group sits in one wave, so what is left of it plans and applies as one there.
797
+ const groups = (plan.groups ?? []).map((g) => ({ ...g, components: g.components.filter((c) => inWave.has(c)) })).filter((g) => g.components.length > 1);
798
+ return {
799
+ order,
800
+ waves: order.length > 0 ? [order] : [],
801
+ skipped: [],
802
+ seeds,
803
+ indeterminate: plan.indeterminate,
804
+ digest: plan.digest,
805
+ ...(groups.length > 0 ? { groups } : {}),
806
+ };
558
807
  }
559
808
  /**
560
809
  * Why the approved change set does not cover a wave's new plan, one line per
@@ -564,27 +813,77 @@ export function prWavePlan(plan, components, wave, completed = []) {
564
813
  * returns no change set) is covered only by the digest it was approved
565
814
  * with, since with no entries there is nothing to compare a moved plan by.
566
815
  */
567
- export function waveNotCovered(approved, fresh, approvedMembers = []) {
816
+ export function waveNotCovered(approved, fresh, approvedMembers = [], shapes = {}) {
568
817
  // #3999: a member whose approved plan waited only on an artifact a build in
569
818
  // the change publishes is covered by that build's provenance, as it is
570
819
  // without --wave-gate.
571
820
  const byBuild = new Set(approvedMembers.filter((m) => m.buildInputs?.length && !m.unknownInputs?.length).map((m) => m.member));
821
+ const prMember = new Map(approvedMembers.map((m) => [m.member, m]));
822
+ // #4422: the same decision a round makes (`coveredRound`), so a resume, a
823
+ // wave and a round agree for equivalent inputs. A member the document names
824
+ // and the caller does not stands for a component of its own name.
825
+ const approvedSide = {
826
+ doc: approved,
827
+ members: approved.members.map((m) => ({
828
+ member: m.member,
829
+ component: prMember.get(m.member)?.component ?? m.member,
830
+ ...(prMember.get(m.member)?.unknownInputs?.length ? { unknownInputs: prMember.get(m.member).unknownInputs } : {}),
831
+ })),
832
+ ...(shapes.approved && shapes.fresh ? { shapes: shapes.approved } : {}),
833
+ };
572
834
  return fresh.members
573
835
  .map((m) => {
574
836
  if (byBuild.has(m.member))
575
837
  return undefined;
576
838
  const was = approved.members.find((a) => a.member === m.member);
577
- const opaque = m.planner === "chant" && m.lexicon === "chant" && m.status === "planned";
578
- if (opaque && was && was.planDigest !== m.planDigest) {
579
- return `${m.member} now plans to ${m.planDigest}, not the approved ${was.planDigest ?? "(none)"}, and its planner describes no changes to compare`;
580
- }
581
- return planCoveredBy(approved, fresh, m.member);
839
+ if (!was)
840
+ return planCoveredBy(approved, fresh, m.member);
841
+ if (was.planDigest !== null && m.planDigest === was.planDigest)
842
+ return undefined;
843
+ const hasShape = shapes.approved !== undefined && shapes.fresh?.[m.member] !== undefined;
844
+ const cover = coveredRound({
845
+ approved: approvedSide,
846
+ approvedDigest: approved.digest,
847
+ fresh: {
848
+ doc: fresh,
849
+ members: [{ member: m.member, component: prMember.get(m.member)?.component ?? m.member }],
850
+ ...(hasShape ? { shapes: shapes.fresh } : {}),
851
+ },
852
+ attributes: (member) => memberAttributes(approved, fresh, member, hasShape),
853
+ });
854
+ return cover.covered ? undefined : cover.reason;
582
855
  })
583
856
  .filter((r) => r !== undefined);
584
857
  }
858
+ /**
859
+ * The report's `groups` (#4446): each group the set planned, with the wave
860
+ * of `waves` its members sit in and the reads of group-mates its members'
861
+ * change-set parts name, which the group's apply computes.
862
+ */
863
+ function reportGroups(set, waves) {
864
+ const componentOf = new Map(set.members.map((m) => [m.member, m.component]));
865
+ return (set.groups ?? []).map((g) => {
866
+ const wave = waves.findIndex((w) => g.components.some((c) => w.includes(c))) + 1;
867
+ const reads = set.doc.members
868
+ .filter((m) => g.components.includes(componentOf.get(m.member) ?? m.member))
869
+ .flatMap((m) => (m.group?.reads ?? []).map((ref) => ({ component: componentOf.get(m.member) ?? m.member, ref })))
870
+ .sort((a, b) => a.component.localeCompare(b.component) || a.ref.localeCompare(b.ref));
871
+ return {
872
+ capability: g.capability,
873
+ components: [...g.components],
874
+ wave: wave > 0 ? wave : 1,
875
+ digest: g.digest,
876
+ ...(g.error !== undefined ? { error: g.error } : {}),
877
+ ...(reads.length > 0 ? { reads } : {}),
878
+ };
879
+ });
880
+ }
585
881
  /** The report a stage writes. */
586
882
  export function prReport(input) {
587
883
  const changed = new Set(input.signal.changed);
884
+ const groups = reportGroups(input.set, input.plan.waves);
885
+ const groupOf = new Map(groups.flatMap((g, i) => g.components.map((c) => [c, i])));
886
+ const members = (input.members ?? input.set.members).map((m) => (groupOf.has(m.component) ? { ...m, group: groupOf.get(m.component) } : m));
588
887
  return {
589
888
  $schema: PR_REPORT_SCHEMA_ID,
590
889
  contract: PR_REPORT_CONTRACT,
@@ -615,7 +914,9 @@ export function prReport(input) {
615
914
  indeterminate: input.plan.indeterminate,
616
915
  waves: input.plan.waves,
617
916
  },
618
- members: input.members ?? input.set.members,
917
+ ...(groups.length > 0 ? { groups } : {}),
918
+ ...(input.set.fallbacks?.length ? { fallbacks: input.set.fallbacks.map((f) => ({ ...f })) } : {}),
919
+ members,
619
920
  changeSet: input.set.doc,
620
921
  };
621
922
  }