@intentius/chant 0.124.52 → 0.124.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (584) hide show
  1. package/dist/audit/discover.d.ts.map +1 -1
  2. package/dist/audit/discover.js +3 -1
  3. package/dist/behaviour.d.ts +1 -1
  4. package/dist/behaviour.d.ts.map +1 -1
  5. package/dist/capabilities/templates.d.ts +65 -0
  6. package/dist/capabilities/templates.d.ts.map +1 -0
  7. package/dist/capabilities/templates.js +193 -0
  8. package/dist/capabilities/templates.json +145 -0
  9. package/dist/change-set-graph.d.ts +35 -3
  10. package/dist/change-set-graph.d.ts.map +1 -1
  11. package/dist/change-set-graph.js +60 -9
  12. package/dist/change-set.d.ts +65 -7
  13. package/dist/change-set.d.ts.map +1 -1
  14. package/dist/change-set.js +23 -12
  15. package/dist/ci-identity.d.ts +7 -2
  16. package/dist/ci-identity.d.ts.map +1 -1
  17. package/dist/ci-identity.js +4 -1
  18. package/dist/cli/commands/import-index.d.ts +52 -0
  19. package/dist/cli/commands/import-index.d.ts.map +1 -0
  20. package/dist/cli/commands/import-index.js +226 -0
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/lint.js +4 -2
  23. package/dist/cli/handlers/components.d.ts.map +1 -1
  24. package/dist/cli/handlers/components.js +11 -2
  25. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  26. package/dist/cli/handlers/fan-out.js +16 -3
  27. package/dist/cli/handlers/graph.d.ts.map +1 -1
  28. package/dist/cli/handlers/graph.js +22 -4
  29. package/dist/cli/handlers/index-cloud.d.ts +14 -0
  30. package/dist/cli/handlers/index-cloud.d.ts.map +1 -0
  31. package/dist/cli/handlers/index-cloud.js +22 -0
  32. package/dist/cli/handlers/index-diff.d.ts +18 -0
  33. package/dist/cli/handlers/index-diff.d.ts.map +1 -0
  34. package/dist/cli/handlers/index-diff.js +142 -0
  35. package/dist/cli/handlers/lifecycle-drift.d.ts +25 -0
  36. package/dist/cli/handlers/lifecycle-drift.d.ts.map +1 -0
  37. package/dist/cli/handlers/lifecycle-drift.js +64 -0
  38. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  39. package/dist/cli/handlers/lifecycle.js +188 -28
  40. package/dist/cli/handlers/misc.d.ts +9 -0
  41. package/dist/cli/handlers/misc.d.ts.map +1 -1
  42. package/dist/cli/handlers/misc.js +77 -0
  43. package/dist/cli/handlers/pr.d.ts.map +1 -1
  44. package/dist/cli/handlers/pr.js +15 -2
  45. package/dist/cli/handlers/promote.d.ts.map +1 -1
  46. package/dist/cli/handlers/promote.js +89 -15
  47. package/dist/cli/handlers/query.d.ts.map +1 -1
  48. package/dist/cli/handlers/query.js +28 -4
  49. package/dist/cli/handlers/scenario.d.ts.map +1 -1
  50. package/dist/cli/handlers/scenario.js +8 -7
  51. package/dist/cli/handlers/search.d.ts +34 -4
  52. package/dist/cli/handlers/search.d.ts.map +1 -1
  53. package/dist/cli/handlers/search.js +216 -36
  54. package/dist/cli/main.d.ts.map +1 -1
  55. package/dist/cli/main.js +55 -1
  56. package/dist/cli/mcp/lifecycle-tools.d.ts.map +1 -1
  57. package/dist/cli/mcp/lifecycle-tools.js +3 -2
  58. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  59. package/dist/cli/mcp/resource-handlers.js +4 -3
  60. package/dist/cli/mcp/workspace-tools.js +1 -1
  61. package/dist/cli/parse-args.d.ts.map +1 -1
  62. package/dist/cli/parse-args.js +55 -2
  63. package/dist/cli/registry.d.ts +19 -1
  64. package/dist/cli/registry.d.ts.map +1 -1
  65. package/dist/cloud-lister/aws.d.ts +35 -0
  66. package/dist/cloud-lister/aws.d.ts.map +1 -0
  67. package/dist/cloud-lister/aws.js +203 -0
  68. package/dist/cloud-lister/azure.d.ts +34 -0
  69. package/dist/cloud-lister/azure.d.ts.map +1 -0
  70. package/dist/cloud-lister/azure.js +129 -0
  71. package/dist/cloud-lister/common.d.ts +40 -0
  72. package/dist/cloud-lister/common.d.ts.map +1 -0
  73. package/dist/cloud-lister/common.js +83 -0
  74. package/dist/cloud-lister/credentials.d.ts +50 -0
  75. package/dist/cloud-lister/credentials.d.ts.map +1 -0
  76. package/dist/cloud-lister/credentials.js +86 -0
  77. package/dist/cloud-lister/fakes.d.ts +27 -0
  78. package/dist/cloud-lister/fakes.d.ts.map +1 -0
  79. package/dist/cloud-lister/fakes.js +50 -0
  80. package/dist/cloud-lister/gcp.d.ts +42 -0
  81. package/dist/cloud-lister/gcp.d.ts.map +1 -0
  82. package/dist/cloud-lister/gcp.js +143 -0
  83. package/dist/cloud-lister/guard.d.ts +49 -0
  84. package/dist/cloud-lister/guard.d.ts.map +1 -0
  85. package/dist/cloud-lister/guard.js +103 -0
  86. package/dist/cloud-lister/ids.d.ts +23 -0
  87. package/dist/cloud-lister/ids.d.ts.map +1 -0
  88. package/dist/cloud-lister/ids.js +42 -0
  89. package/dist/cloud-lister/index.d.ts +67 -0
  90. package/dist/cloud-lister/index.d.ts.map +1 -0
  91. package/dist/cloud-lister/index.js +102 -0
  92. package/dist/cloud-lister/kubernetes.d.ts +64 -0
  93. package/dist/cloud-lister/kubernetes.d.ts.map +1 -0
  94. package/dist/cloud-lister/kubernetes.js +276 -0
  95. package/dist/cloud-lister/markers.d.ts +31 -0
  96. package/dist/cloud-lister/markers.d.ts.map +1 -0
  97. package/dist/cloud-lister/markers.js +73 -0
  98. package/dist/cloud-lister/mask.d.ts +31 -0
  99. package/dist/cloud-lister/mask.d.ts.map +1 -0
  100. package/dist/cloud-lister/mask.js +74 -0
  101. package/dist/cloud-lister/sigv4.d.ts +29 -0
  102. package/dist/cloud-lister/sigv4.d.ts.map +1 -0
  103. package/dist/cloud-lister/sigv4.js +54 -0
  104. package/dist/cloud-lister/transport.d.ts +55 -0
  105. package/dist/cloud-lister/transport.d.ts.map +1 -0
  106. package/dist/cloud-lister/transport.js +86 -0
  107. package/dist/cloud-lister/types.d.ts +121 -0
  108. package/dist/cloud-lister/types.d.ts.map +1 -0
  109. package/dist/cloud-lister/types.js +24 -0
  110. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  111. package/dist/components/capability-plugin-loader.js +6 -0
  112. package/dist/components/capability-plugin.d.ts +9 -0
  113. package/dist/components/capability-plugin.d.ts.map +1 -1
  114. package/dist/components/capability.d.ts +215 -0
  115. package/dist/components/capability.d.ts.map +1 -1
  116. package/dist/components/cli-support.d.ts +21 -0
  117. package/dist/components/cli-support.d.ts.map +1 -1
  118. package/dist/components/cli-support.js +58 -14
  119. package/dist/components/declared-reads.d.ts +38 -0
  120. package/dist/components/declared-reads.d.ts.map +1 -0
  121. package/dist/components/declared-reads.js +131 -0
  122. package/dist/components/deploy-units.d.ts.map +1 -1
  123. package/dist/components/deploy-units.js +4 -0
  124. package/dist/components/driver.d.ts +34 -1
  125. package/dist/components/driver.d.ts.map +1 -1
  126. package/dist/components/driver.js +66 -3
  127. package/dist/components/fan-out-run.d.ts +23 -1
  128. package/dist/components/fan-out-run.d.ts.map +1 -1
  129. package/dist/components/fan-out-run.js +82 -14
  130. package/dist/components/fan-out-support.d.ts +6 -0
  131. package/dist/components/fan-out-support.d.ts.map +1 -1
  132. package/dist/components/fan-out-support.js +4 -1
  133. package/dist/components/fan-out.d.ts +28 -0
  134. package/dist/components/fan-out.d.ts.map +1 -1
  135. package/dist/components/groups.d.ts +229 -0
  136. package/dist/components/groups.d.ts.map +1 -0
  137. package/dist/components/groups.js +760 -0
  138. package/dist/components/index.d.ts +6 -2
  139. package/dist/components/index.d.ts.map +1 -1
  140. package/dist/components/index.js +4 -0
  141. package/dist/components/plan-shape-conformance.d.ts +48 -0
  142. package/dist/components/plan-shape-conformance.d.ts.map +1 -0
  143. package/dist/components/plan-shape-conformance.js +122 -0
  144. package/dist/components/pr-run.d.ts +41 -4
  145. package/dist/components/pr-run.d.ts.map +1 -1
  146. package/dist/components/pr-run.js +209 -39
  147. package/dist/components/promote-rounds.d.ts +9 -0
  148. package/dist/components/promote-rounds.d.ts.map +1 -1
  149. package/dist/components/promote-rounds.js +23 -2
  150. package/dist/components/promote.d.ts +17 -0
  151. package/dist/components/promote.d.ts.map +1 -1
  152. package/dist/components/promote.js +36 -6
  153. package/dist/components/release-gate.d.ts +23 -1
  154. package/dist/components/release-gate.d.ts.map +1 -1
  155. package/dist/components/release-gate.js +97 -33
  156. package/dist/components/run-groups.d.ts +58 -0
  157. package/dist/components/run-groups.d.ts.map +1 -0
  158. package/dist/components/run-groups.js +78 -0
  159. package/dist/components/starter-plugin.d.ts +1 -0
  160. package/dist/components/starter-plugin.d.ts.map +1 -1
  161. package/dist/components/starter-plugin.js +6 -1
  162. package/dist/components/verbs/index.d.ts +1 -0
  163. package/dist/components/verbs/index.d.ts.map +1 -1
  164. package/dist/components/verbs/index.js +1 -0
  165. package/dist/components/verbs/member-outputs.d.ts +187 -0
  166. package/dist/components/verbs/member-outputs.d.ts.map +1 -0
  167. package/dist/components/verbs/member-outputs.js +218 -0
  168. package/dist/config.d.ts +60 -0
  169. package/dist/config.d.ts.map +1 -1
  170. package/dist/config.js +27 -0
  171. package/dist/deep-observation.d.ts +1 -13
  172. package/dist/deep-observation.d.ts.map +1 -1
  173. package/dist/deep-observation.js +5 -22
  174. package/dist/drift-record.d.ts +542 -0
  175. package/dist/drift-record.d.ts.map +1 -0
  176. package/dist/drift-record.js +886 -0
  177. package/dist/engine-facts.d.ts +144 -0
  178. package/dist/engine-facts.d.ts.map +1 -0
  179. package/dist/engine-facts.js +14 -0
  180. package/dist/graph-ir.d.ts +153 -2
  181. package/dist/graph-ir.d.ts.map +1 -1
  182. package/dist/graph-ir.js +224 -6
  183. package/dist/graph-refs.d.ts +145 -0
  184. package/dist/graph-refs.d.ts.map +1 -1
  185. package/dist/graph-refs.js +305 -0
  186. package/dist/graph-schema/fixtures/adopt.json +15 -0
  187. package/dist/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  188. package/dist/graph-schema/fixtures/index.json +68 -0
  189. package/dist/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  190. package/dist/graph-schema/fixtures.d.ts +23 -0
  191. package/dist/graph-schema/fixtures.d.ts.map +1 -0
  192. package/dist/graph-schema/fixtures.js +246 -0
  193. package/dist/graph-schema/generate.d.ts +15 -0
  194. package/dist/graph-schema/generate.d.ts.map +1 -0
  195. package/dist/graph-schema/generate.js +267 -0
  196. package/dist/graph-schema/graph.schema.json +1387 -0
  197. package/dist/health.schema.json +2 -1
  198. package/dist/identity.d.ts.map +1 -1
  199. package/dist/identity.js +2 -0
  200. package/dist/index.d.ts +1 -0
  201. package/dist/index.d.ts.map +1 -1
  202. package/dist/index.js +2 -0
  203. package/dist/instance-nodes.d.ts +159 -0
  204. package/dist/instance-nodes.d.ts.map +1 -0
  205. package/dist/instance-nodes.js +231 -0
  206. package/dist/lexicon.d.ts +72 -0
  207. package/dist/lexicon.d.ts.map +1 -1
  208. package/dist/lifecycle/adoption-file.d.ts +56 -0
  209. package/dist/lifecycle/adoption-file.d.ts.map +1 -0
  210. package/dist/lifecycle/adoption-file.js +123 -0
  211. package/dist/lifecycle/cloud-index.d.ts +186 -0
  212. package/dist/lifecycle/cloud-index.d.ts.map +1 -0
  213. package/dist/lifecycle/cloud-index.js +618 -0
  214. package/dist/lifecycle/derive-edges.d.ts +59 -0
  215. package/dist/lifecycle/derive-edges.d.ts.map +1 -0
  216. package/dist/lifecycle/derive-edges.js +126 -0
  217. package/dist/lifecycle/git.d.ts +10 -9
  218. package/dist/lifecycle/git.d.ts.map +1 -1
  219. package/dist/lifecycle/git.js +7 -1
  220. package/dist/lifecycle/identity-match.d.ts +133 -0
  221. package/dist/lifecycle/identity-match.d.ts.map +1 -0
  222. package/dist/lifecycle/identity-match.js +147 -0
  223. package/dist/lifecycle/index-diff.d.ts +224 -0
  224. package/dist/lifecycle/index-diff.d.ts.map +1 -0
  225. package/dist/lifecycle/index-diff.js +327 -0
  226. package/dist/lifecycle/index-document.d.ts +130 -0
  227. package/dist/lifecycle/index-document.d.ts.map +1 -0
  228. package/dist/lifecycle/index-document.js +189 -0
  229. package/dist/lifecycle/index-drift.d.ts +121 -0
  230. package/dist/lifecycle/index-drift.d.ts.map +1 -0
  231. package/dist/lifecycle/index-drift.js +178 -0
  232. package/dist/lifecycle/index-import.d.ts +168 -0
  233. package/dist/lifecycle/index-import.d.ts.map +1 -0
  234. package/dist/lifecycle/index-import.js +515 -0
  235. package/dist/lifecycle/index-join.d.ts +129 -0
  236. package/dist/lifecycle/index-join.d.ts.map +1 -0
  237. package/dist/lifecycle/index-join.js +248 -0
  238. package/dist/lifecycle/index.d.ts +4 -0
  239. package/dist/lifecycle/index.d.ts.map +1 -1
  240. package/dist/lifecycle/index.js +4 -0
  241. package/dist/lifecycle/observation-baseline.d.ts +61 -21
  242. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  243. package/dist/lifecycle/observation-baseline.js +144 -19
  244. package/dist/lifecycle/observe.d.ts.map +1 -1
  245. package/dist/lifecycle/observe.js +6 -1
  246. package/dist/lifecycle/query.d.ts +45 -4
  247. package/dist/lifecycle/query.d.ts.map +1 -1
  248. package/dist/lifecycle/query.js +89 -7
  249. package/dist/lifecycle/replay.d.ts +10 -3
  250. package/dist/lifecycle/replay.d.ts.map +1 -1
  251. package/dist/lifecycle/replay.js +10 -6
  252. package/dist/lifecycle/scenario.d.ts +4 -3
  253. package/dist/lifecycle/scenario.d.ts.map +1 -1
  254. package/dist/lifecycle/snapshot-store-fake.d.ts +9 -0
  255. package/dist/lifecycle/snapshot-store-fake.d.ts.map +1 -0
  256. package/dist/lifecycle/snapshot-store-fake.js +21 -0
  257. package/dist/lifecycle/snapshot-store-s3.d.ts +102 -0
  258. package/dist/lifecycle/snapshot-store-s3.d.ts.map +1 -0
  259. package/dist/lifecycle/snapshot-store-s3.js +254 -0
  260. package/dist/lifecycle/snapshot-store.d.ts +170 -0
  261. package/dist/lifecycle/snapshot-store.d.ts.map +1 -0
  262. package/dist/lifecycle/snapshot-store.js +832 -0
  263. package/dist/lifecycle/snapshot.d.ts +11 -0
  264. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  265. package/dist/lifecycle/snapshot.js +18 -5
  266. package/dist/lifecycle/type-listing.d.ts +148 -0
  267. package/dist/lifecycle/type-listing.d.ts.map +1 -0
  268. package/dist/lifecycle/type-listing.js +166 -0
  269. package/dist/lint/component-checks.d.ts +15 -1
  270. package/dist/lint/component-checks.d.ts.map +1 -1
  271. package/dist/lint/component-checks.js +1 -0
  272. package/dist/lint/presets/relaxed.json +2 -1
  273. package/dist/lint/presets/strict.json +2 -1
  274. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts +4 -0
  275. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts.map +1 -0
  276. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.js +13 -0
  277. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts +4 -0
  278. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts.map +1 -0
  279. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.js +8 -0
  280. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts +33 -0
  281. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts.map +1 -0
  282. package/dist/lint/rules/comp/comp008-applies-signal-only-root.js +123 -0
  283. package/dist/lint/rules/comp/index.d.ts +2 -1
  284. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  285. package/dist/lint/rules/comp/index.js +4 -1
  286. package/dist/observation.d.ts +19 -2
  287. package/dist/observation.d.ts.map +1 -1
  288. package/dist/observation.js +18 -1
  289. package/dist/op/activities/activity-contracts.d.ts +1 -1
  290. package/dist/op/activities/activity-contracts.js +1 -1
  291. package/dist/op/activities/change-set.d.ts +6 -4
  292. package/dist/op/activities/change-set.d.ts.map +1 -1
  293. package/dist/op/activities/change-set.js +4 -2
  294. package/dist/op/activity-failure.d.ts +27 -0
  295. package/dist/op/activity-failure.d.ts.map +1 -1
  296. package/dist/op/activity-failure.js +31 -0
  297. package/dist/op/index.d.ts +2 -1
  298. package/dist/op/index.d.ts.map +1 -1
  299. package/dist/op/index.js +1 -1
  300. package/dist/op/local-executor.d.ts +5 -1
  301. package/dist/op/local-executor.d.ts.map +1 -1
  302. package/dist/op/local-executor.js +22 -8
  303. package/dist/pr-loop.d.ts +56 -1
  304. package/dist/pr-loop.d.ts.map +1 -1
  305. package/dist/pr-loop.js +51 -1
  306. package/dist/pulumi-urn.d.ts +101 -0
  307. package/dist/pulumi-urn.d.ts.map +1 -0
  308. package/dist/pulumi-urn.js +131 -0
  309. package/dist/release-floors.d.ts +23 -0
  310. package/dist/release-floors.d.ts.map +1 -0
  311. package/dist/release-floors.js +22 -0
  312. package/dist/sensitive-key.d.ts +23 -0
  313. package/dist/sensitive-key.d.ts.map +1 -0
  314. package/dist/sensitive-key.js +31 -0
  315. package/dist/wave-runtime.mjs +234 -234
  316. package/dist/workspace/adoption-report.d.ts +53 -0
  317. package/dist/workspace/adoption-report.d.ts.map +1 -0
  318. package/dist/workspace/adoption-report.js +76 -0
  319. package/dist/workspace/capabilities-contract.d.ts +7 -0
  320. package/dist/workspace/capabilities-contract.d.ts.map +1 -1
  321. package/dist/workspace/capabilities-contract.js +2 -1
  322. package/dist/workspace/capabilities.schema.json +98 -0
  323. package/dist/workspace/change-set.schema.json +42 -3
  324. package/dist/workspace/ci-cloud-identity.d.ts +13 -1
  325. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  326. package/dist/workspace/ci-cloud-identity.js +30 -1
  327. package/dist/workspace/compose-graph.d.ts +10 -1
  328. package/dist/workspace/compose-graph.d.ts.map +1 -1
  329. package/dist/workspace/compose-graph.js +6 -1
  330. package/dist/workspace/conformance/index.d.ts +1 -1
  331. package/dist/workspace/conformance/index.d.ts.map +1 -1
  332. package/dist/workspace/conformance/index.js +6 -1
  333. package/dist/workspace/drift-cli.d.ts +81 -0
  334. package/dist/workspace/drift-cli.d.ts.map +1 -0
  335. package/dist/workspace/drift-cli.js +317 -0
  336. package/dist/workspace/drift.schema.json +672 -0
  337. package/dist/workspace/graph-cli.d.ts +2 -0
  338. package/dist/workspace/graph-cli.d.ts.map +1 -1
  339. package/dist/workspace/graph-cli.js +3 -0
  340. package/dist/workspace/graph.schema.json +72 -0
  341. package/dist/workspace/init.d.ts +5 -1
  342. package/dist/workspace/init.d.ts.map +1 -1
  343. package/dist/workspace/init.js +55 -9
  344. package/dist/workspace/ledger.schema.json +1 -1
  345. package/dist/workspace/nested-graph.d.ts.map +1 -1
  346. package/dist/workspace/nested-graph.js +2 -0
  347. package/dist/workspace/pr-report.schema.json +51 -2
  348. package/dist/workspace/promote-waves.d.ts +112 -0
  349. package/dist/workspace/promote-waves.d.ts.map +1 -0
  350. package/dist/workspace/promote-waves.js +290 -0
  351. package/dist/workspace/promote.d.ts +84 -2
  352. package/dist/workspace/promote.d.ts.map +1 -1
  353. package/dist/workspace/promote.js +347 -61
  354. package/dist/workspace/reason-codes.d.ts +12 -0
  355. package/dist/workspace/reason-codes.d.ts.map +1 -1
  356. package/dist/workspace/reason-codes.js +14 -0
  357. package/dist/workspace/search-contract.d.ts +3 -3
  358. package/dist/workspace/search-contract.d.ts.map +1 -1
  359. package/dist/workspace/search-contract.js +2 -0
  360. package/dist/workspace/search.schema.json +103 -5
  361. package/package.json +14 -1
  362. package/src/__fixtures__/drift-record/README.md +58 -0
  363. package/src/__fixtures__/drift-record/choudoufu-cone.ndjson +4 -0
  364. package/src/__fixtures__/drift-record/choudoufu-resource.ndjson +3 -0
  365. package/src/__fixtures__/drift-record/component-status-ledger.json +65 -0
  366. package/src/__fixtures__/drift-record/component-status.json +91 -0
  367. package/src/__fixtures__/drift-record/deep-diff.json +259 -0
  368. package/src/__fixtures__/drift-record/lifecycle-diff-live.json +825 -0
  369. package/src/__fixtures__/drift-record/live-diff.json +125 -0
  370. package/src/audit/discover.e2e.test.ts +14 -0
  371. package/src/audit/discover.ts +3 -1
  372. package/src/behaviour.ts +1 -1
  373. package/src/capabilities/templates.json +145 -0
  374. package/src/capabilities/templates.test.ts +212 -0
  375. package/src/capabilities/templates.ts +204 -0
  376. package/src/change-set-bundle.test.ts +83 -0
  377. package/src/change-set-graph.ts +80 -9
  378. package/src/change-set.test.ts +24 -0
  379. package/src/change-set.ts +77 -14
  380. package/src/ci-identity-targets.test.ts +23 -0
  381. package/src/ci-identity.ts +10 -4
  382. package/src/cli/commands/import-index.test.ts +117 -0
  383. package/src/cli/commands/import-index.ts +265 -0
  384. package/src/cli/commands/lint.ts +4 -2
  385. package/src/cli/handlers/components.test.ts +9 -0
  386. package/src/cli/handlers/components.ts +12 -2
  387. package/src/cli/handlers/fan-out.ts +17 -3
  388. package/src/cli/handlers/graph.test.ts +42 -0
  389. package/src/cli/handlers/graph.ts +26 -4
  390. package/src/cli/handlers/index-cloud.ts +32 -0
  391. package/src/cli/handlers/index-diff.test.ts +68 -0
  392. package/src/cli/handlers/index-diff.ts +146 -0
  393. package/src/cli/handlers/ledger-root-member.test.ts +161 -0
  394. package/src/cli/handlers/lifecycle-drift.ts +68 -0
  395. package/src/cli/handlers/lifecycle.test.ts +33 -11
  396. package/src/cli/handlers/lifecycle.ts +205 -30
  397. package/src/cli/handlers/misc.ts +73 -0
  398. package/src/cli/handlers/pr-waves-blocked.test.ts +37 -2
  399. package/src/cli/handlers/pr.ts +14 -2
  400. package/src/cli/handlers/promote.test.ts +64 -0
  401. package/src/cli/handlers/promote.ts +81 -6
  402. package/src/cli/handlers/query.ts +27 -4
  403. package/src/cli/handlers/run-declared-reads-cli.test.ts +187 -0
  404. package/src/cli/handlers/run-groups-cli.test.ts +229 -0
  405. package/src/cli/handlers/scenario.test.ts +13 -0
  406. package/src/cli/handlers/scenario.ts +8 -7
  407. package/src/cli/handlers/search-index.test.ts +255 -0
  408. package/src/cli/handlers/search-json.test.ts +1 -1
  409. package/src/cli/handlers/search-ownership.test.ts +194 -0
  410. package/src/cli/handlers/search.ts +271 -38
  411. package/src/cli/main.test.ts +7 -0
  412. package/src/cli/main.ts +56 -1
  413. package/src/cli/mcp/lifecycle-tools.ts +3 -2
  414. package/src/cli/mcp/resource-handlers.ts +4 -3
  415. package/src/cli/mcp/workspace-tools.ts +1 -1
  416. package/src/cli/parse-args.ts +40 -2
  417. package/src/cli/registry.ts +19 -1
  418. package/src/cloud-lister/aws.test.ts +202 -0
  419. package/src/cloud-lister/aws.ts +226 -0
  420. package/src/cloud-lister/azure.test.ts +132 -0
  421. package/src/cloud-lister/azure.ts +142 -0
  422. package/src/cloud-lister/common.ts +115 -0
  423. package/src/cloud-lister/credentials.ts +110 -0
  424. package/src/cloud-lister/fakes.ts +66 -0
  425. package/src/cloud-lister/gcp.test.ts +121 -0
  426. package/src/cloud-lister/gcp.ts +159 -0
  427. package/src/cloud-lister/guard.test.ts +198 -0
  428. package/src/cloud-lister/guard.ts +111 -0
  429. package/src/cloud-lister/ids.ts +54 -0
  430. package/src/cloud-lister/index.ts +134 -0
  431. package/src/cloud-lister/kubernetes.test.ts +160 -0
  432. package/src/cloud-lister/kubernetes.ts +284 -0
  433. package/src/cloud-lister/markers.ts +74 -0
  434. package/src/cloud-lister/mask.ts +77 -0
  435. package/src/cloud-lister/shape.test.ts +81 -0
  436. package/src/cloud-lister/sigv4.ts +70 -0
  437. package/src/cloud-lister/transport.ts +118 -0
  438. package/src/cloud-lister/types.ts +135 -0
  439. package/src/components/capability-plugin-loader.ts +4 -0
  440. package/src/components/capability-plugin.ts +10 -0
  441. package/src/components/capability.ts +229 -0
  442. package/src/components/cli-support.ts +70 -14
  443. package/src/components/declared-reads.test.ts +103 -0
  444. package/src/components/declared-reads.ts +134 -0
  445. package/src/components/deploy-units.ts +4 -0
  446. package/src/components/driver.ts +89 -4
  447. package/src/components/fan-out-run.ts +95 -15
  448. package/src/components/fan-out-support.ts +10 -1
  449. package/src/components/fan-out.ts +30 -0
  450. package/src/components/groups.test.ts +320 -0
  451. package/src/components/groups.ts +880 -0
  452. package/src/components/index.ts +42 -0
  453. package/src/components/plan-shape-conformance.ts +130 -0
  454. package/src/components/pr-env-run-bundle.test.ts +2 -0
  455. package/src/components/pr-run.test.ts +516 -3
  456. package/src/components/pr-run.ts +231 -41
  457. package/src/components/promote-rounds.ts +25 -2
  458. package/src/components/promote.ts +55 -5
  459. package/src/components/registry.test.ts +1 -1
  460. package/src/components/release-gate.ts +115 -30
  461. package/src/components/release-groups.test.ts +362 -0
  462. package/src/components/run-groups.ts +113 -0
  463. package/src/components/starter-plugin.ts +6 -0
  464. package/src/components/verbs/index.ts +1 -0
  465. package/src/components/verbs/member-outputs.test.ts +205 -0
  466. package/src/components/verbs/member-outputs.ts +308 -0
  467. package/src/config.ts +44 -0
  468. package/src/deep-observation.ts +5 -23
  469. package/src/drift-record.test.ts +596 -0
  470. package/src/drift-record.ts +1206 -0
  471. package/src/engine-facts.guard.test.ts +111 -0
  472. package/src/engine-facts.test.ts +178 -0
  473. package/src/engine-facts.ts +154 -0
  474. package/src/gated-waves-bundle.test.ts +2 -0
  475. package/src/graph-ir-live.test.ts +92 -0
  476. package/src/graph-ir.ts +317 -7
  477. package/src/graph-refs.ts +416 -0
  478. package/src/graph-schema/fixtures/adopt.json +15 -0
  479. package/src/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  480. package/src/graph-schema/fixtures/index.json +68 -0
  481. package/src/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  482. package/src/graph-schema/fixtures.ts +267 -0
  483. package/src/graph-schema/generate.ts +263 -0
  484. package/src/graph-schema/graph-schema.test.ts +153 -0
  485. package/src/graph-schema/graph.schema.json +1387 -0
  486. package/src/health.schema.json +2 -1
  487. package/src/identity.ts +2 -0
  488. package/src/index.ts +2 -0
  489. package/src/instance-nodes.ts +288 -0
  490. package/src/lexicon.ts +76 -0
  491. package/src/lifecycle/__fixtures__/index-diff/20261026T120000Z-aaaaaaaaaaaa.json +149 -0
  492. package/src/lifecycle/__fixtures__/index-diff/20261026T130000Z-bbbbbbbbbbbb.json +127 -0
  493. package/src/lifecycle/adoption-file.test.ts +100 -0
  494. package/src/lifecycle/adoption-file.ts +154 -0
  495. package/src/lifecycle/cloud-index.test.ts +517 -0
  496. package/src/lifecycle/cloud-index.ts +736 -0
  497. package/src/lifecycle/derive-edges.test.ts +193 -0
  498. package/src/lifecycle/derive-edges.ts +182 -0
  499. package/src/lifecycle/git.ts +10 -4
  500. package/src/lifecycle/identity-match.test.ts +308 -0
  501. package/src/lifecycle/identity-match.ts +257 -0
  502. package/src/lifecycle/index-diff.test.ts +198 -0
  503. package/src/lifecycle/index-diff.ts +476 -0
  504. package/src/lifecycle/index-document.test.ts +64 -0
  505. package/src/lifecycle/index-document.ts +299 -0
  506. package/src/lifecycle/index-drift.test.ts +191 -0
  507. package/src/lifecycle/index-drift.ts +304 -0
  508. package/src/lifecycle/index-import.test.ts +255 -0
  509. package/src/lifecycle/index-import.ts +634 -0
  510. package/src/lifecycle/index-join.test.ts +250 -0
  511. package/src/lifecycle/index-join.ts +304 -0
  512. package/src/lifecycle/index-store-load-bearing.test.ts +187 -0
  513. package/src/lifecycle/index.ts +4 -0
  514. package/src/lifecycle/member-ledger.test.ts +25 -7
  515. package/src/lifecycle/observation-baseline.test.ts +72 -1
  516. package/src/lifecycle/observation-baseline.ts +190 -27
  517. package/src/lifecycle/observe.ts +6 -1
  518. package/src/lifecycle/query.test.ts +69 -1
  519. package/src/lifecycle/query.ts +103 -10
  520. package/src/lifecycle/replay.test.ts +9 -5
  521. package/src/lifecycle/replay.ts +9 -5
  522. package/src/lifecycle/scenario.ts +4 -3
  523. package/src/lifecycle/snapshot-store-fake.ts +29 -0
  524. package/src/lifecycle/snapshot-store-s3.ts +316 -0
  525. package/src/lifecycle/snapshot-store.test.ts +385 -0
  526. package/src/lifecycle/snapshot-store.ts +981 -0
  527. package/src/lifecycle/snapshot.test.ts +13 -0
  528. package/src/lifecycle/snapshot.ts +23 -6
  529. package/src/lifecycle/type-listing.test.ts +166 -0
  530. package/src/lifecycle/type-listing.ts +309 -0
  531. package/src/lint/component-checks.ts +17 -1
  532. package/src/lint/config.test.ts +3 -0
  533. package/src/lint/presets/relaxed.json +2 -1
  534. package/src/lint/presets/strict.json +2 -1
  535. package/src/lint/rules/__fixtures__/comp/comp008/fail/dns.component.ts +15 -0
  536. package/src/lint/rules/__fixtures__/comp/comp008/pass/dns.component.ts +10 -0
  537. package/src/lint/rules/comp/comp.test.ts +2 -1
  538. package/src/lint/rules/comp/comp008-applies-signal-only-root.ts +121 -0
  539. package/src/lint/rules/comp/comp008.test.ts +77 -0
  540. package/src/lint/rules/comp/index.ts +4 -1
  541. package/src/observation-edges.test.ts +33 -0
  542. package/src/observation.ts +36 -1
  543. package/src/op/activities/activity-contracts.ts +1 -1
  544. package/src/op/activities/change-set.ts +8 -5
  545. package/src/op/activity-failure.ts +36 -0
  546. package/src/op/index.ts +2 -1
  547. package/src/op/local-executor.test.ts +80 -0
  548. package/src/op/local-executor.ts +27 -9
  549. package/src/pr-loop-bundle.test.ts +5 -2
  550. package/src/pr-loop.test.ts +43 -0
  551. package/src/pr-loop.ts +111 -2
  552. package/src/pulumi-urn.ts +151 -0
  553. package/src/release-floors.ts +22 -0
  554. package/src/sensitive-key.ts +34 -0
  555. package/src/workspace/adoption-report.test.ts +61 -0
  556. package/src/workspace/adoption-report.ts +103 -0
  557. package/src/workspace/capabilities-contract.test.ts +4 -0
  558. package/src/workspace/capabilities-contract.ts +9 -1
  559. package/src/workspace/capabilities.schema.json +98 -0
  560. package/src/workspace/change-set.schema.json +42 -3
  561. package/src/workspace/ci-cloud-identity.ts +31 -1
  562. package/src/workspace/compose-graph.test.ts +12 -0
  563. package/src/workspace/compose-graph.ts +12 -1
  564. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  565. package/src/workspace/conformance/index.ts +6 -1
  566. package/src/workspace/drift-cli.test.ts +65 -0
  567. package/src/workspace/drift-cli.ts +365 -0
  568. package/src/workspace/drift.schema.json +672 -0
  569. package/src/workspace/graph-cli.ts +5 -0
  570. package/src/workspace/graph.schema.json +72 -0
  571. package/src/workspace/init.test.ts +38 -1
  572. package/src/workspace/init.ts +50 -8
  573. package/src/workspace/intent-gaps.test.ts +18 -2
  574. package/src/workspace/ledger.schema.json +1 -1
  575. package/src/workspace/nested-graph.ts +1 -0
  576. package/src/workspace/pr-report.schema.json +51 -2
  577. package/src/workspace/promote-waves.test.ts +296 -0
  578. package/src/workspace/promote-waves.ts +340 -0
  579. package/src/workspace/promote.test.ts +158 -4
  580. package/src/workspace/promote.ts +398 -57
  581. package/src/workspace/reason-codes.test.ts +4 -1
  582. package/src/workspace/reason-codes.ts +14 -0
  583. package/src/workspace/search-contract.ts +2 -0
  584. package/src/workspace/search.schema.json +103 -5
@@ -10,7 +10,18 @@
10
10
  */
11
11
 
12
12
  import { describe, expect, test } from "vitest";
13
- import { CapabilityRegistry, type DeployContext, type PlannedOutputs } from "./capability";
13
+ import {
14
+ CapabilityRegistry,
15
+ type CapabilityGroupOutcome,
16
+ type DeployContext,
17
+ type GroupContext,
18
+ type GroupFallback,
19
+ type GroupMember,
20
+ type GroupRun,
21
+ type PlannedOutputs,
22
+ } from "./capability";
23
+ import { findComponentGroups, groupFanOut } from "./groups";
24
+ import { failedRunExitCode } from "./driver";
14
25
  import { memoryGateLedgerPort } from "../op/gate";
15
26
  import { planFanOut } from "./fan-out";
16
27
  import {
@@ -28,7 +39,7 @@ import {
28
39
  waveNotCovered,
29
40
  type PrPlanSet,
30
41
  } from "./pr-run";
31
- import { changeSetDigest, changeSetDocumentDigest, composeChangeSet, type ChangeSetEntry, type ChangeSetPart } from "../change-set";
42
+ import { changeSetDigest, changeSetDocumentDigest, composeChangeSet, isEmptyChangeSet, type ChangeSetEntry, type ChangeSetPart } from "../change-set";
32
43
  import type { PrApplyRecord } from "./fan-out-record";
33
44
  import { renderPrNote, type PrApproval } from "../pr-loop";
34
45
  import type { FanOutPlan } from "./fan-out";
@@ -314,7 +325,7 @@ describe("a dependency outside the selection (#4104)", () => {
314
325
  if (!resumed.ok) throw new Error(resumed.reasons.join("; "));
315
326
  expect(w.outputsRead).toEqual([]);
316
327
  expect(resumed.set.doc.digest).toBe(set.doc.digest);
317
- expect(resumed.set.outputs).toBe(record.planOutputs);
328
+ expect(resumed.set.outputs).toEqual(record.planOutputs);
318
329
  });
319
330
  });
320
331
 
@@ -1153,3 +1164,505 @@ describe("a gate per wave on merge (#4003)", () => {
1153
1164
  expect(moved[0]).toMatch(/^job now plans to jcs1-sha256:2+, not the approved jcs1-sha256:1+, and its planner describes no changes to compare$/);
1154
1165
  });
1155
1166
  });
1167
+
1168
+ describe("a group planned and applied as one (ws-107, #4440)", () => {
1169
+ interface StackInput {
1170
+ root: string;
1171
+ vars?: Record<string, unknown>;
1172
+ }
1173
+ const so = (stack: string, name: string) => ({ stackOutput: { stack, name } });
1174
+ const stack = (name: string, dependsOn: string[] = [], vars?: Record<string, unknown>): DriverComponent => ({
1175
+ name,
1176
+ dependsOn,
1177
+ deploy: [{ phase: "Apply", steps: [{ kind: "fake-stack", root: name, ...(vars ? { vars } : {}) }] }],
1178
+ });
1179
+ const plainStack = (name: string, dependsOn: string[] = [], vars?: Record<string, unknown>): DriverComponent => ({
1180
+ name,
1181
+ dependsOn,
1182
+ deploy: [{ phase: "Apply", steps: [{ kind: "fake-plain", root: name, ...(vars ? { vars } : {}) }] }],
1183
+ });
1184
+ /** a; b reads a's new id; c reads b's. */
1185
+ const ABC = [stack("a"), stack("b", ["a"], { upstream: so("a", "id") }), stack("c", ["b"], { upstream: so("b", "id") })];
1186
+
1187
+ /**
1188
+ * `fake-stack` creates one resource and exposes an `id` known only after
1189
+ * it applies. With `group`, it declares `planGroup` and `runGroup`;
1190
+ * `fallback` adds a `groupFallback`. `fake-plain` never groups, and its
1191
+ * plan knows the `id` it will expose.
1192
+ */
1193
+ function groupWorld(opts: { group?: boolean; fallback?: GroupFallback } = {}) {
1194
+ const w = {
1195
+ registry: new CapabilityRegistry(),
1196
+ planned: [] as string[],
1197
+ applied: [] as string[],
1198
+ planGroupCalls: [] as string[][],
1199
+ runGroupCalls: [] as Array<{ components: string[]; run: GroupRun; members: GroupMember[] }>,
1200
+ planThrows: undefined as unknown,
1201
+ runThrows: undefined as unknown,
1202
+ outcome: undefined as ((members: readonly GroupMember[]) => CapabilityGroupOutcome) | undefined,
1203
+ /** Roots whose plan also creates `res.extra`, a change no earlier plan showed. */
1204
+ extra: new Set<string>(),
1205
+ };
1206
+ const planOf = (input: StackInput, known: boolean) => {
1207
+ const planDigest = computePlanDigest("fake-stack", { root: input.root, vars: input.vars ?? null });
1208
+ const outputs: PlannedOutputs = known ? { known: { id: `${input.root}-id` }, unknown: [] } : { known: {}, unknown: ["id"] };
1209
+ return {
1210
+ member: input.root,
1211
+ planDigest,
1212
+ artifact: { planDigest },
1213
+ outputs,
1214
+ changeSet: {
1215
+ member: { member: input.root, lexicon: "test", planner: "tofu" as const, status: "planned" as const, planDigest, holes: [] },
1216
+ entries: [
1217
+ { member: input.root, lexicon: "test", planner: "tofu" as const, address: "res.x", type: "res", action: "create" as const, attributes: [] },
1218
+ ...(w.extra.has(input.root)
1219
+ ? [{ member: input.root, lexicon: "test", planner: "tofu" as const, address: "res.extra", type: "res", action: "create" as const, attributes: [] }]
1220
+ : []),
1221
+ ],
1222
+ },
1223
+ };
1224
+ };
1225
+ w.registry.register({
1226
+ kind: "fake-stack",
1227
+ async plan(_ctx: DeployContext, input: StackInput) {
1228
+ w.planned.push(input.root);
1229
+ return planOf(input, false);
1230
+ },
1231
+ async run(_ctx: DeployContext, input: StackInput) {
1232
+ w.applied.push(input.root);
1233
+ return { outputs: { id: `${input.root}-id` } };
1234
+ },
1235
+ ...(opts.group
1236
+ ? {
1237
+ async planGroup(_ctx: GroupContext, members: readonly GroupMember<StackInput>[]) {
1238
+ w.planGroupCalls.push(members.map((m) => m.component));
1239
+ if (w.planThrows) throw w.planThrows;
1240
+ return {
1241
+ members: Object.fromEntries(members.map((m) => [m.component, planOf(m.input!, false)])),
1242
+ digest: computePlanDigest("fake-composition", members.map((m) => m.component)),
1243
+ artifact: { composed: members.map((m) => m.component) },
1244
+ };
1245
+ },
1246
+ async runGroup(_ctx: GroupContext, run: GroupRun, members: readonly GroupMember<StackInput>[]) {
1247
+ w.runGroupCalls.push({ components: members.map((m) => m.component), run, members: [...members] });
1248
+ if (w.runThrows) throw w.runThrows;
1249
+ if (w.outcome) return w.outcome(members);
1250
+ return { members: Object.fromEntries(members.map((m) => [m.component, { status: "applied" as const, outputs: { id: `${m.component}-id` } }])) };
1251
+ },
1252
+ }
1253
+ : {}),
1254
+ ...(opts.fallback ? { groupFallback: async () => opts.fallback } : {}),
1255
+ } as never);
1256
+ w.registry.register({
1257
+ kind: "fake-plain",
1258
+ async plan(_ctx: DeployContext, input: StackInput) {
1259
+ w.planned.push(input.root);
1260
+ const p = planOf(input, true);
1261
+ return { ...p, changeSet: { member: p.changeSet.member, entries: [] } };
1262
+ },
1263
+ async run(_ctx: DeployContext, input: StackInput) {
1264
+ w.applied.push(input.root);
1265
+ return { outputs: { id: `${input.root}-id` } };
1266
+ },
1267
+ } as never);
1268
+ return w;
1269
+ }
1270
+
1271
+ /** The fan-out `pr.ts`'s prepare step derives: planFanOut, then the groups placed. */
1272
+ async function prepared(components: DriverComponent[], changed: string[], registry: CapabilityRegistry): Promise<FanOutPlan> {
1273
+ const plan = planFanOut({ components, changed });
1274
+ return groupFanOut(plan, components, await findComponentGroups({ components, plan, registry, env: "prod" }));
1275
+ }
1276
+
1277
+ const approvedBy = async (digest: string) =>
1278
+ approvalOf(await decidePrGate(memoryGateLedgerPort({ resolutions: [approval(digest)] }), { op: "pr-12", gate: "pr-apply", digest, now: NOW }));
1279
+
1280
+ test("A, B reading A's new output and C reading B's: one planGroup call, one digest, no unknownInputs, one runGroup call under one approval", async () => {
1281
+ const w = groupWorld({ group: true });
1282
+ const plan = await prepared(ABC, ["a"], w.registry);
1283
+ expect(plan.groups).toEqual([{ capability: "fake-stack", components: ["a", "b", "c"] }]);
1284
+ expect(plan.waves).toEqual([["a", "b", "c"], [], []]);
1285
+
1286
+ const set = await planPrSet({ components: ABC, plan, registry: w.registry, env: "prod" });
1287
+ expect(w.planGroupCalls).toEqual([["a", "b", "c"]]);
1288
+ expect(w.planned).toEqual([]);
1289
+ expect(set.failed).toBe(false);
1290
+ expect(set.members.map((m) => [m.member, m.status, m.unknownInputs])).toEqual([
1291
+ ["a", "planned", undefined],
1292
+ ["b", "planned", undefined],
1293
+ ["c", "planned", undefined],
1294
+ ]);
1295
+ // The report still draws the line from b to a, unknown at plan.
1296
+ expect(set.members.find((m) => m.member === "b")?.reads).toEqual([{ upstream: "a", outputs: ["id"], unknown: ["id"] }]);
1297
+ const groupDigest = set.groups?.[0]?.digest;
1298
+ expect(groupDigest).toEqual(expect.stringMatching(/sha256:/));
1299
+ expect(set.doc.members.map((m) => [m.member, m.group])).toEqual([
1300
+ ["a", { digest: groupDigest, reads: [] }],
1301
+ ["b", { digest: groupDigest, reads: ["a.id"] }],
1302
+ ["c", { digest: groupDigest, reads: ["b.id"] }],
1303
+ ]);
1304
+ // The member's digest binds the group's; the document's binds every member's.
1305
+ expect(set.doc.members.find((m) => m.member === "a")?.planDigest).toBe(
1306
+ computePlanDigest("capability-group-member", { plan: computePlanDigest("fake-stack", { root: "a", vars: null }), group: groupDigest }),
1307
+ );
1308
+ expect(set.doc.digest).toBe(changeSetDocumentDigest(set.doc));
1309
+ // Ruling 4: unknown reads inside the group keep the change set from passing as empty.
1310
+ expect(isEmptyChangeSet(set.doc)).toBe(false);
1311
+
1312
+ const firstApproval = await approvedBy(set.doc.digest);
1313
+ expect(firstApproval.status).toBe("approved");
1314
+ let asked = 0;
1315
+ const applied = await applyPrSet({
1316
+ components: ABC,
1317
+ plan,
1318
+ registry: w.registry,
1319
+ env: "prod",
1320
+ set,
1321
+ approval: firstApproval,
1322
+ decideRound: async () => {
1323
+ asked++;
1324
+ return { satisfied: false, approval: { status: "pending" } };
1325
+ },
1326
+ });
1327
+ expect(w.runGroupCalls.map((c) => c.components)).toEqual([["a", "b", "c"]]);
1328
+ expect(w.applied).toEqual([]);
1329
+ expect(asked).toBe(0);
1330
+ expect(applied.rounds).toBeUndefined();
1331
+ expect(applied.status).toBe("applied");
1332
+ expect(applied.members.map((m) => [m.member, m.status])).toEqual([
1333
+ ["a", "applied"],
1334
+ ["b", "applied"],
1335
+ ["c", "applied"],
1336
+ ]);
1337
+ // runGroup got the bound group digest, the artifact planGroup gave, and b's read of a left out of its input.
1338
+ const call = w.runGroupCalls[0]!;
1339
+ expect(call.run.digest).toBe(groupDigest);
1340
+ expect(call.run.artifact).toEqual({ composed: ["a", "b", "c"] });
1341
+ expect(call.members[1]).toMatchObject({ component: "b", input: { root: "b", vars: {} }, reads: [{ at: "/vars/upstream", ref: "a.id", component: "a", output: "id" }] });
1342
+ expect(applied.run.componentOutputs.c).toEqual({ id: "c-id" });
1343
+ });
1344
+
1345
+ test("the same three without planGroup plan and apply as on main: one by one, B and C wait on unknown inputs for a round", async () => {
1346
+ const w = groupWorld();
1347
+ const derived = planFanOut({ components: ABC, changed: ["a"] });
1348
+ const plan = await prepared(ABC, ["a"], w.registry);
1349
+ expect(plan).toEqual(derived);
1350
+
1351
+ const set = await planPrSet({ components: ABC, plan, registry: w.registry, env: "prod" });
1352
+ expect(w.planGroupCalls).toEqual([]);
1353
+ expect(w.planned).toEqual(["a", "b", "c"]);
1354
+ expect(set.groups).toBeUndefined();
1355
+ expect(set.doc.members.every((m) => m.group === undefined)).toBe(true);
1356
+ expect(set.members.map((m) => [m.member, m.unknownInputs])).toEqual([
1357
+ ["a", undefined],
1358
+ ["b", ["a.id"]],
1359
+ ["c", ["b.id"]],
1360
+ ]);
1361
+ const applied = await applyPrSet({
1362
+ components: ABC,
1363
+ plan,
1364
+ registry: w.registry,
1365
+ env: "prod",
1366
+ set,
1367
+ approval: await approvedBy(set.doc.digest),
1368
+ decideRound: async () => ({ satisfied: false, approval: { status: "pending" } }),
1369
+ });
1370
+ expect(w.runGroupCalls).toEqual([]);
1371
+ expect(w.applied[0]).toBe("a");
1372
+ const round = applied.rounds?.[0];
1373
+ expect(round?.round).toBe(2);
1374
+ expect(round?.covered !== undefined || round?.status === "waiting").toBe(true);
1375
+ });
1376
+
1377
+ test("a capability that refuses the set says why; the members plan one by one and the reason is in the plan set", async () => {
1378
+ const w = groupWorld({ group: true, fallback: { kind: "too-many", reason: "fake-stack composes two at most" } });
1379
+ const plan = await prepared(ABC, ["a"], w.registry);
1380
+ expect(plan.groups).toBeUndefined();
1381
+ const set = await planPrSet({ components: ABC, plan, registry: w.registry, env: "prod" });
1382
+ expect(w.planGroupCalls).toEqual([]);
1383
+ expect(w.planned).toEqual(["a", "b", "c"]);
1384
+ expect(set.fallbacks).toEqual(
1385
+ ["a", "b", "c"].map((component) => ({ component, capability: "fake-stack", kind: "too-many", reason: "fake-stack composes two at most" })),
1386
+ );
1387
+ expect(set.members.find((m) => m.member === "b")?.unknownInputs).toEqual(["a.id"]);
1388
+ });
1389
+
1390
+ test("two plans of one group on one estate give one digest; a group with one more member gives another", async () => {
1391
+ const w = groupWorld({ group: true });
1392
+ const plan = await prepared(ABC, ["a"], w.registry);
1393
+ const first = await planPrSet({ components: ABC, plan, registry: w.registry, env: "prod" });
1394
+ const again = await planPrSet({ components: ABC, plan, registry: w.registry, env: "prod" });
1395
+ expect(again.doc.digest).toBe(first.doc.digest);
1396
+ expect(again.groups?.[0]?.digest).toBe(first.groups?.[0]?.digest);
1397
+
1398
+ const ABCE = [...ABC, stack("e", ["c"], { upstream: so("c", "id") })];
1399
+ const wider = await prepared(ABCE, ["a"], w.registry);
1400
+ expect(wider.groups).toEqual([{ capability: "fake-stack", components: ["a", "b", "c", "e"] }]);
1401
+ const more = await planPrSet({ components: ABCE, plan: wider, registry: w.registry, env: "prod" });
1402
+ expect(more.groups?.[0]?.digest).not.toBe(first.groups?.[0]?.digest);
1403
+ // a plans the same on its own, and still its digest moves with the group's.
1404
+ const a = (s: PrPlanSet) => s.doc.members.find((m) => m.member === "a")?.planDigest;
1405
+ expect(a(more)).not.toBe(a(first));
1406
+ expect(more.doc.digest).not.toBe(first.doc.digest);
1407
+ });
1408
+
1409
+ test("a runGroup that applies A, fails on B and never starts C: A applied, B failed, C not-started, and what depends on C outside the group is blocked by it", async () => {
1410
+ const w = groupWorld({ group: true });
1411
+ w.outcome = () => ({
1412
+ members: {
1413
+ a: { status: "applied", outputs: { id: "a-id" } },
1414
+ b: { status: "failed", error: "b: quota exceeded" },
1415
+ c: { status: "not-started" },
1416
+ },
1417
+ });
1418
+ const components = [...ABC, plainStack("d", ["c"], { upstream: so("c", "id") })];
1419
+ const plan = await prepared(components, ["a"], w.registry);
1420
+ expect(plan.waves).toEqual([["a", "b", "c"], [], [], ["d"]]);
1421
+ const set = await planPrSet({ components, plan, registry: w.registry, env: "prod" });
1422
+ const applied = await applyPrSet({ components, plan, registry: w.registry, env: "prod", set, approval: await approvedBy(set.doc.digest) });
1423
+ expect(applied.status).toBe("failed");
1424
+ expect(Object.fromEntries(applied.members.map((m) => [m.member, m.status]))).toEqual({ a: "applied", b: "failed", c: "not-started", d: "blocked" });
1425
+ expect(applied.members.find((m) => m.member === "b")?.error).toBe("b: quota exceeded");
1426
+ expect(applied.members.find((m) => m.member === "d")?.blockedBy).toBe("c");
1427
+ expect(applied.run.notStarted).toEqual(["c"]);
1428
+ expect(applied.run.componentOutputs.a).toEqual({ id: "a-id" });
1429
+ expect(w.applied).toEqual([]);
1430
+ // pr-apply exits with the failed run's code.
1431
+ expect(failedRunExitCode(applied.run.results)).toBe(1);
1432
+ });
1433
+
1434
+ test("a runGroup that throws a moved plan leaves every member not-started and exits 3; any other throw fails every member", async () => {
1435
+ const moved = groupWorld({ group: true });
1436
+ moved.runThrows = Object.assign(new Error("the target moved since it was planned"), { planMoved: true });
1437
+ const plan = await prepared(ABC, ["a"], moved.registry);
1438
+ const set = await planPrSet({ components: ABC, plan, registry: moved.registry, env: "prod" });
1439
+ const stopped = await applyPrSet({ components: ABC, plan, registry: moved.registry, env: "prod", set });
1440
+ expect(stopped.members.map((m) => m.status)).toEqual(["not-started", "not-started", "not-started"]);
1441
+ expect(failedRunExitCode(stopped.run.results)).toBe(3);
1442
+
1443
+ const broken = groupWorld({ group: true });
1444
+ broken.runThrows = new Error("connection reset");
1445
+ const set2 = await planPrSet({ components: ABC, plan, registry: broken.registry, env: "prod" });
1446
+ const failed = await applyPrSet({ components: ABC, plan, registry: broken.registry, env: "prod", set: set2 });
1447
+ expect(failed.members.map((m) => [m.status, m.error])).toEqual([
1448
+ ["failed", "connection reset"],
1449
+ ["failed", "connection reset"],
1450
+ ["failed", "connection reset"],
1451
+ ]);
1452
+ expect(failedRunExitCode(failed.run.results)).toBe(1);
1453
+ });
1454
+
1455
+ test("a planGroup that throws makes every member plan-failed, and nothing falls back", async () => {
1456
+ const w = groupWorld({ group: true });
1457
+ w.planThrows = new Error("cannot compose: backends differ");
1458
+ const plan = await prepared(ABC, ["a"], w.registry);
1459
+ const set = await planPrSet({ components: ABC, plan, registry: w.registry, env: "prod" });
1460
+ expect(set.failed).toBe(true);
1461
+ expect(w.planned).toEqual([]);
1462
+ expect(set.members.map((m) => [m.member, m.status, m.error])).toEqual([
1463
+ ["a", "plan-failed", "cannot compose: backends differ"],
1464
+ ["b", "plan-failed", "cannot compose: backends differ"],
1465
+ ["c", "plan-failed", "cannot compose: backends differ"],
1466
+ ]);
1467
+ expect(set.groups).toEqual([{ capability: "fake-stack", components: ["a", "b", "c"], digest: null, error: "cannot compose: backends differ" }]);
1468
+ });
1469
+
1470
+ test("under --wave-gate the group is part of the wave it was placed in, and the wave's digest covers it", async () => {
1471
+ const w = groupWorld({ group: true });
1472
+ const components = [plainStack("x"), stack("a", ["x"], { upstream: so("x", "id") }), stack("b", ["a"], { upstream: so("a", "id") }), stack("c", ["b"], { upstream: so("b", "id") })];
1473
+ const plan = await prepared(components, ["x"], w.registry);
1474
+ expect(plan.waves).toEqual([["x"], ["a", "b", "c"], [], []]);
1475
+ const approved = await planPrSet({ components, plan, registry: w.registry, env: "prod" });
1476
+
1477
+ const wave = prWavePlan(plan, components, 2, ["x"]);
1478
+ expect(wave.groups).toEqual(plan.groups);
1479
+ const fresh = await planPrSet({ components, plan: wave, registry: w.registry, env: "prod", seededOutputs: approved.outputs });
1480
+ expect(w.planGroupCalls).toEqual([
1481
+ ["a", "b", "c"],
1482
+ ["a", "b", "c"],
1483
+ ]);
1484
+ expect(fresh.doc.members.map((m) => m.member)).toEqual(["a", "b", "c"]);
1485
+ expect(fresh.doc.members.map((m) => m.group?.digest)).toEqual(approved.doc.members.filter((m) => m.member !== "x").map((m) => m.group?.digest));
1486
+ expect(waveNotCovered(approved.doc, fresh.doc, approved.members, { approved: approved.shapes, fresh: fresh.shapes })).toEqual([]);
1487
+ // A wave whose group composed differently is not covered by the digest.
1488
+ const other = groupWorld({ group: true });
1489
+ const wider = [...components, stack("e", ["c"], { upstream: so("c", "id") })];
1490
+ const widerPlan = await prepared(wider, ["x"], other.registry);
1491
+ const widerWave = await planPrSet({ components: wider, plan: prWavePlan(widerPlan, wider, 2, ["x"]), registry: other.registry, env: "prod", seededOutputs: approved.outputs });
1492
+ const a = (s: PrPlanSet) => s.doc.members.find((m) => m.member === "a")?.planDigest;
1493
+ expect(a(widerWave)).not.toBe(a(approved));
1494
+ });
1495
+
1496
+ describe("the report names each group and member outcome, and a resume plans what is left (#4446)", () => {
1497
+ const at = { pr: 12, env: "prod", base: "1".repeat(40), head: "2".repeat(40), op: "pr-12", gate: "pr-apply" };
1498
+ const signal = { changed: ["a"], indeterminate: [], unclaimed: [] };
1499
+ /** A, B and C, where B fails and C never starts. */
1500
+ const failOnB = (w: ReturnType<typeof groupWorld>) => {
1501
+ w.outcome = () => ({
1502
+ members: {
1503
+ a: { status: "applied", outputs: { id: "a-id" } },
1504
+ b: { status: "failed", error: "b: quota exceeded" },
1505
+ c: { status: "not-started" },
1506
+ },
1507
+ });
1508
+ };
1509
+ /** The first attempt: plan, approve, apply with B failing. What a `--resume` record keeps. */
1510
+ async function failedAttempt(w: ReturnType<typeof groupWorld>) {
1511
+ failOnB(w);
1512
+ const plan = await prepared(ABC, ["a"], w.registry);
1513
+ const set = await planPrSet({ components: ABC, plan, registry: w.registry, env: "prod" });
1514
+ const firstApproval = await approvedBy(set.doc.digest);
1515
+ const first = await applyPrSet({ components: ABC, plan, registry: w.registry, env: "prod", set, approval: firstApproval });
1516
+ w.outcome = undefined;
1517
+ return { plan, set, first, firstApproval, record: recordOf(set) };
1518
+ }
1519
+
1520
+ test("a plan report for the three-member group lists one group with three members, its wave and the group digest", async () => {
1521
+ const w = groupWorld({ group: true });
1522
+ const plan = await prepared(ABC, ["a"], w.registry);
1523
+ const set = await planPrSet({ components: ABC, plan, registry: w.registry, env: "prod" });
1524
+ const report = prReport({ stage: "plan", ...at, plan, signal, set, approval: { status: "pending" }, status: "planned" });
1525
+ expect(report.groups).toEqual([
1526
+ {
1527
+ capability: "fake-stack",
1528
+ components: ["a", "b", "c"],
1529
+ wave: 1,
1530
+ digest: set.groups![0]!.digest,
1531
+ reads: [
1532
+ { component: "b", ref: "a.id" },
1533
+ { component: "c", ref: "b.id" },
1534
+ ],
1535
+ },
1536
+ ]);
1537
+ expect(report.groups![0]!.digest).toEqual(expect.stringMatching(/sha256:/));
1538
+ expect(report.fallbacks).toBeUndefined();
1539
+ expect(report.members.map((m) => [m.member, m.group, m.groupOutcome])).toEqual([
1540
+ ["a", 0, undefined],
1541
+ ["b", 0, undefined],
1542
+ ["c", 0, undefined],
1543
+ ]);
1544
+ // The run's artifact stays in the process that planned.
1545
+ expect(JSON.stringify(report.groups)).not.toContain("composed");
1546
+ const note = renderPrNote(report);
1547
+ expect(note).toContain("`a`, `b` and `c` plan and apply as one (`fake-stack`, wave 1); `b` reads `a.id` and `c` reads `b.id`, known when the group applies.");
1548
+ });
1549
+
1550
+ test("an apply report after a failure on B lists A applied, B failed, C not-started, and the note says plainly that A stays applied", async () => {
1551
+ const w = groupWorld({ group: true });
1552
+ const { plan, set, first, firstApproval } = await failedAttempt(w);
1553
+ const report = prReport({ stage: "apply", ...at, plan, signal, set, approval: firstApproval, status: first.status, members: first.members });
1554
+ expect(report.status).toBe("failed");
1555
+ expect(report.members.map((m) => [m.member, m.status, m.groupOutcome, m.group])).toEqual([
1556
+ ["a", "applied", "applied", 0],
1557
+ ["b", "failed", "failed", 0],
1558
+ ["c", "not-started", "not-started", 0],
1559
+ ]);
1560
+ const note = renderPrNote(report);
1561
+ expect(note).toContain("The apply failed: 1 failed, 1 not started, 1 applied.");
1562
+ expect(note).toContain("The group's apply stopped part way: `a` applied and stays applied; `b` failed; `c` did not start. Nothing that applied is undone.");
1563
+ expect(note).toContain("`pr-apply --resume` plans `b` and `c` again");
1564
+ });
1565
+
1566
+ test("pr-apply --resume on that record plans B and C only, as a smaller group, and applies them under the first approval", async () => {
1567
+ const w = groupWorld({ group: true });
1568
+ const { plan, set, first, firstApproval, record } = await failedAttempt(w);
1569
+ expect(first.run.completed).toEqual(["a"]);
1570
+ w.planGroupCalls.length = 0;
1571
+ w.runGroupCalls.length = 0;
1572
+
1573
+ const resumed = await resumePrSet({ components: ABC, plan, registry: w.registry, env: "prod", record, completed: first.run.completed, applied: { a: first.run.componentOutputs.a! } });
1574
+ if (!resumed.ok) throw new Error(resumed.reasons.join("; "));
1575
+ expect(resumed.plan.order).toEqual(["b", "c"]);
1576
+ expect(resumed.plan.groups).toEqual([{ capability: "fake-stack", components: ["b", "c"] }]);
1577
+ expect(w.planGroupCalls).toEqual([["b", "c"]]);
1578
+ expect(w.planned).toEqual([]);
1579
+ // The smaller group's digest is not the approved one, so coverage rested on entries.
1580
+ expect(resumed.fresh.groups?.[0]?.digest).not.toBe(set.groups?.[0]?.digest);
1581
+ expect(resumed.set.doc.digest).toBe(set.doc.digest);
1582
+ expect(resumed.set.groups?.map((g) => g.components)).toEqual([["b", "c"]]);
1583
+
1584
+ let asked = 0;
1585
+ const applied = await applyPrSet({
1586
+ components: ABC,
1587
+ plan: resumed.plan,
1588
+ registry: w.registry,
1589
+ env: "prod",
1590
+ set: resumed.set,
1591
+ approval: firstApproval,
1592
+ resumed: { completed: first.run.completed, outputs: { a: first.run.componentOutputs.a! } },
1593
+ decideRound: async () => {
1594
+ asked++;
1595
+ return { satisfied: false, approval: { status: "pending" } };
1596
+ },
1597
+ });
1598
+ expect(asked).toBe(0);
1599
+ expect(applied.rounds).toBeUndefined();
1600
+ expect(applied.status).toBe("applied");
1601
+ // B planned and applied against the id A applied with.
1602
+ expect(w.runGroupCalls.map((c) => c.components)).toEqual([["b", "c"]]);
1603
+ expect(w.runGroupCalls[0]!.members[0]).toMatchObject({ component: "b", input: { root: "b", vars: { upstream: "a-id" } } });
1604
+ expect(applied.members.map((m) => [m.member, m.status, m.groupOutcome])).toEqual([
1605
+ ["a", "applied", undefined],
1606
+ ["b", "applied", "applied"],
1607
+ ["c", "applied", "applied"],
1608
+ ]);
1609
+ const report = prReport({ stage: "apply", ...at, plan, signal, set: resumed.set, approval: firstApproval, status: applied.status, members: applied.members, resumed: first.run.completed });
1610
+ expect(report.groups?.map((g) => g.components)).toEqual([["b", "c"]]);
1611
+ expect(report.members.map((m) => [m.member, m.group])).toEqual([
1612
+ ["a", undefined],
1613
+ ["b", 0],
1614
+ ["c", 0],
1615
+ ]);
1616
+ });
1617
+
1618
+ test("a resume whose new plans the approved set does not cover carries nothing, and the new plan waits at its gate", async () => {
1619
+ const w = groupWorld({ group: true });
1620
+ const { plan, set, first, record } = await failedAttempt(w);
1621
+ w.extra.add("c");
1622
+ const resumed = await resumePrSet({ components: ABC, plan, registry: w.registry, env: "prod", record, completed: first.run.completed, applied: { a: first.run.componentOutputs.a! } });
1623
+ expect(resumed.ok).toBe(false);
1624
+ if (resumed.ok) return;
1625
+ expect(resumed.reasons.join("; ")).toMatch(/res\.extra/);
1626
+ // pr-apply then plans every member again, and that plan's digest has no approval of its own.
1627
+ const again = await planPrSet({ components: ABC, plan, registry: w.registry, env: "prod" });
1628
+ expect(again.doc.digest).not.toBe(set.doc.digest);
1629
+ const ledger = memoryGateLedgerPort({ resolutions: [approval(set.doc.digest)] });
1630
+ const check = await decidePrGate(ledger, { op: "pr-12", gate: "pr-apply", digest: again.doc.digest, now: NOW });
1631
+ expect(check.satisfied).toBe(false);
1632
+ expect(approvalOf(check).status).toBe("changed");
1633
+ });
1634
+
1635
+ test("a resume whose capability refuses the smaller group plans what is left one by one, and says why", async () => {
1636
+ const w = groupWorld({ group: true });
1637
+ const { plan, first, record } = await failedAttempt(w);
1638
+ // From here on the capability takes no group.
1639
+ const refusing = groupWorld({ group: true, fallback: { kind: "too-many", reason: "fake-stack composes three or none" } });
1640
+ const resumed = await resumePrSet({ components: ABC, plan, registry: refusing.registry, env: "prod", record, completed: first.run.completed, applied: { a: first.run.componentOutputs.a! } });
1641
+ expect(refusing.planGroupCalls).toEqual([]);
1642
+ expect(refusing.planned).toEqual(["b", "c"]);
1643
+ const fresh = resumed.fresh!;
1644
+ expect(fresh.groups).toBeUndefined();
1645
+ expect(fresh.fallbacks).toEqual(["b", "c"].map((component) => ({ component, capability: "fake-stack", kind: "too-many", reason: "fake-stack composes three or none" })));
1646
+ });
1647
+
1648
+ test("a set the capability refused is under fallbacks with its reason, and the note prints it; kind off is reported and not printed", async () => {
1649
+ const w = groupWorld({ group: true, fallback: { kind: "too-many", reason: "fake-stack composes two at most" } });
1650
+ const plan = await prepared(ABC, ["a"], w.registry);
1651
+ const set = await planPrSet({ components: ABC, plan, registry: w.registry, env: "prod" });
1652
+ const report = prReport({ stage: "plan", ...at, plan, signal, set, approval: { status: "pending" }, status: "planned" });
1653
+ expect(report.groups).toBeUndefined();
1654
+ expect(report.fallbacks).toEqual(
1655
+ ["a", "b", "c"].map((component) => ({ component, capability: "fake-stack", kind: "too-many", reason: "fake-stack composes two at most" })),
1656
+ );
1657
+ expect(report.members.every((m) => m.group === undefined)).toBe(true);
1658
+ expect(renderPrNote(report)).toContain("Planned one by one, not as a group: `a`, `b`, `c` (`too-many`): fake-stack composes two at most.");
1659
+
1660
+ const off = groupWorld({ group: true, fallback: { kind: "off", reason: "terraform.compose is not set" } });
1661
+ const offPlan = await prepared(ABC, ["a"], off.registry);
1662
+ const offSet = await planPrSet({ components: ABC, plan: offPlan, registry: off.registry, env: "prod" });
1663
+ const offReport = prReport({ stage: "plan", ...at, plan: offPlan, signal, set: offSet, approval: { status: "pending" }, status: "planned" });
1664
+ expect(offReport.fallbacks?.map((f) => f.kind)).toEqual(["off", "off", "off"]);
1665
+ expect(renderPrNote(offReport)).not.toContain("not as a group");
1666
+ });
1667
+ });
1668
+ });