@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
@@ -39,6 +39,7 @@ import {
39
39
  type DriverGate,
40
40
  type DriverPhase,
41
41
  type DriverRunResult,
42
+ type InterpretGroups,
42
43
  } from "./driver";
43
44
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
44
45
  import type { GateDigestMismatch, GateLedgerPort } from "../op/gate";
@@ -51,7 +52,10 @@ import { existsSync, statSync } from "node:fs";
51
52
  import { getHeadCommit } from "../lifecycle/git";
52
53
  import { buildCapabilityRegistry } from "./capability-plugin-loader";
53
54
  import type { CapabilityRegistry } from "./capability";
55
+ import type { FanOutPlan } from "./fan-out";
56
+ import { componentSetFanOut, formRunGroups, planAndRunComponentGroup } from "./groups";
54
57
  import { applyConfigDefaults } from "./config-defaults";
58
+ import { withDeclaredReads } from "./declared-reads";
55
59
  import { loadChantConfig, type ChantConfig } from "../config";
56
60
 
57
61
  /** One component in `chant list --components` output. */
@@ -510,6 +514,8 @@ export interface RunComponentsResult {
510
514
  };
511
515
  /** This run's resolved build-time parameters (chant #1108) — the component-driver counterpart of `../cli/commands/build.ts`'s `BuildResult.buildParams`. Present only once the run actually reached dispatch (mirrors `BuildResult.buildParams`, which is likewise absent on an early-error return). */
512
516
  buildParams?: BuildParamProvenance[];
517
+ /** The grouped fan-out the run applied (ws-107, #4465), when a group formed or a component fell back from one. */
518
+ grouped?: FanOutPlan;
513
519
  }
514
520
 
515
521
  /**
@@ -545,6 +551,13 @@ export interface ResolvedComponentTargets {
545
551
  targets: DriverComponent[];
546
552
  /** For a list selector (#3950): the dependencies the closure added that the list did not name, in discovery order. */
547
553
  added?: string[];
554
+ /**
555
+ * For a list selector: the names the list gave, and every enabled component
556
+ * the closure was taken over (#4516). `runComponents` takes the closure again
557
+ * once the declared reads are written in, so a listed importer pulls in the
558
+ * component whose export it reads.
559
+ */
560
+ list?: { named: string[]; enabled: DriverComponent[] };
548
561
  /** Component name to the environments it declares (#3153), for the components that declare any (#4330). */
549
562
  sites?: Record<string, ComponentEnvironmentDeclaration[]>;
550
563
  error?: string;
@@ -650,6 +663,14 @@ function resolveComponentList(
650
663
  }
651
664
 
652
665
  const enabled = withoutDisabled(discovered, all);
666
+ return { success: true, ...dependencyClosure(names, enabled), list: { named: names, enabled } };
667
+ }
668
+
669
+ /**
670
+ * `names` and every component of `enabled` they depend on, transitively, in
671
+ * `enabled`'s order, and which of those the names did not give (#3950).
672
+ */
673
+ export function dependencyClosure(names: readonly string[], enabled: readonly DriverComponent[]): { targets: DriverComponent[]; added: string[] } {
653
674
  const byName = new Map(enabled.map((c) => [c.name, c]));
654
675
  const included = new Set<string>();
655
676
  const visit = (name: string): void => {
@@ -662,7 +683,7 @@ function resolveComponentList(
662
683
 
663
684
  const targets = enabled.filter((c) => included.has(c.name));
664
685
  const named = new Set(names);
665
- return { success: true, targets, added: targets.map((c) => c.name).filter((n) => !named.has(n)) };
686
+ return { targets, added: targets.map((c) => c.name).filter((n) => !named.has(n)) };
666
687
  }
667
688
 
668
689
  export async function runComponents(
@@ -674,13 +695,6 @@ export async function runComponents(
674
695
  if (!resolved.success) {
675
696
  return { success: false, selected: [], error: resolved.error };
676
697
  }
677
- const targets = resolved.targets;
678
- const narrowed = options.targets ?? {};
679
- const outside = Object.keys(narrowed).filter((name) => !targets.some((c) => c.name === name));
680
- if (outside.length > 0) {
681
- return { success: false, selected: [], error: `--target names ${outside.join(", ")}, which this run does not run` };
682
- }
683
- const targeted = Object.keys(narrowed).length > 0 ? { targets: narrowed } : {};
684
698
 
685
699
  // (#629) Resolve `chant.config.ts`'s `sbom`/`signing`/`vulnPolicy` sections
686
700
  // and fill their defaults into every recognized step (`generate-sbom`,
@@ -700,8 +714,27 @@ export async function runComponents(
700
714
  lexicons: lexiconNames(config.lexicons),
701
715
  }));
702
716
  const env = options.env ?? "local";
703
- const selected = targets.map((c) => c.name);
704
- const resolvedTargets = targets.map((component) => applyConfigDefaults(component, config));
717
+ // #4516: the reads a capability finds in its steps' files (a template's
718
+ // Fn::ImportValue) order a set run as `deriveFanOut` orders a fan-out, so an
719
+ // importer runs after the component whose export it reads. A list takes its
720
+ // closure again with them written in, so a listed importer pulls in its
721
+ // exporter. A single name runs on its own, against seeded outputs.
722
+ let resolvedTargets: DriverComponent[];
723
+ let added = resolved.added ?? [];
724
+ if (resolved.list) {
725
+ const enabled = await withDeclaredReads(resolved.list.enabled.map((component) => applyConfigDefaults(component, config)), registry, path);
726
+ ({ targets: resolvedTargets, added } = dependencyClosure(resolved.list.named, enabled));
727
+ } else {
728
+ const defaulted = resolved.targets.map((component) => applyConfigDefaults(component, config));
729
+ resolvedTargets = selector === "all" ? await withDeclaredReads(defaulted, registry, path) : defaulted;
730
+ }
731
+ const selected = resolvedTargets.map((c) => c.name);
732
+ const narrowed = options.targets ?? {};
733
+ const outside = Object.keys(narrowed).filter((name) => !selected.includes(name));
734
+ if (outside.length > 0) {
735
+ return { success: false, selected: [], error: `--target names ${outside.join(", ")}, which this run does not run` };
736
+ }
737
+ const targeted = Object.keys(narrowed).length > 0 ? { targets: narrowed } : {};
705
738
  // Cross-component/cross-stack outputs to resolve references against. Seeded
706
739
  // from a caller (a downstream CI job passing an upstream job's dumped
707
740
  // outputs via `--seed-outputs`); grows as this run's components complete.
@@ -719,16 +752,38 @@ export async function runComponents(
719
752
 
720
753
  try {
721
754
  if (selector === "all" || isComponentList(selector)) {
755
+ // ws-107 (#4465): the components a capability takes as one plan in their
756
+ // wave and apply as one group, by the rule the pull-request run uses.
757
+ // A cycle or unknown dependency throws here the error the driver would:
758
+ // `componentSetFanOut` drops a dependency outside the set, so the full
759
+ // graph is resolved first.
760
+ resolveComponentGraph(resolvedTargets);
761
+ const grouped = await formRunGroups({ components: resolvedTargets, plan: componentSetFanOut(resolvedTargets), registry, env });
722
762
  if (isComponentList(selector) && options.onPlan) {
723
763
  // Resolved here, before dispatch, so the order is printed before any
724
- // component runs. A cycle or unknown dependency throws the same error
725
- // `runInterpretDriver` would, caught below.
726
- const graph = resolveComponentGraph(resolvedTargets);
727
- options.onPlan({ order: graph.order, waves: graph.waves, added: resolved.added ?? [] });
764
+ // component runs.
765
+ options.onPlan({ order: grouped.order, waves: grouped.waves.filter((w) => w.length > 0), added });
728
766
  }
767
+ const byName = new Map(resolvedTargets.map((c) => [c.name, c]));
768
+ const groups: InterpretGroups | undefined = grouped.groups?.length
769
+ ? {
770
+ waves: grouped.waves,
771
+ groups: grouped.groups,
772
+ run: (group, componentOutputs) =>
773
+ planAndRunComponentGroup({
774
+ group,
775
+ components: byName,
776
+ registry,
777
+ env,
778
+ ctxOf: (name) => deployContext({ env, releaseIdentity, ...targeted }, name),
779
+ componentOutputs,
780
+ }),
781
+ }
782
+ : undefined;
729
783
  const run = await runInterpretDriver(resolvedTargets, registry, {
730
784
  env,
731
785
  releaseIdentity,
786
+ ...(groups ? { groups } : {}),
732
787
  ...targeted,
733
788
  componentOutputs: seedOutputs,
734
789
  ...(emit ? { onRunEvent: emit } : {}),
@@ -740,6 +795,7 @@ export async function runComponents(
740
795
  selected,
741
796
  components: resolvedTargets,
742
797
  buildParams: options.buildParams,
798
+ ...(grouped.groups?.length || grouped.fallbacks?.length ? { grouped } : {}),
743
799
  ...(run.status === "gated" && run.gate
744
800
  ? {
745
801
  gated: {
@@ -0,0 +1,166 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ coveredRound,
4
+ normalizePlanShape,
5
+ PlanShapeMovedError,
6
+ planShapeCovers,
7
+ planShapeDifferences,
8
+ planShapeDigest,
9
+ type PlanShape,
10
+ type PlanShapeEntry,
11
+ } from "./covered-round";
12
+ import type { ChangeSetDocument } from "../change-set";
13
+
14
+ const shape = (over: Partial<PlanShape> = {}): PlanShape => ({
15
+ member: "svc",
16
+ entries: [
17
+ { logicalId: "Fn", action: "create" },
18
+ { logicalId: "Role", action: "update", replacement: "conditional" },
19
+ ],
20
+ unknownInputs: ["vpc.id"],
21
+ ...over,
22
+ });
23
+
24
+ describe("planShapeCovers", () => {
25
+ test("the same ids and actions with the unknown input filled in is covered", () => {
26
+ expect(planShapeCovers(shape(), shape({ unknownInputs: [], replacement: undefined } as never))).toBeUndefined();
27
+ });
28
+
29
+ test("a resource the preview did not show is not covered", () => {
30
+ const fresh = shape({ unknownInputs: [], entries: [...shape().entries, { logicalId: "Bucket", action: "create" }] });
31
+ expect(planShapeCovers(shape(), fresh)).toBe("svc now plans to create Bucket, which the first plan did not");
32
+ });
33
+
34
+ test("a different action is not covered", () => {
35
+ const fresh = shape({ unknownInputs: [], entries: [{ logicalId: "Fn", action: "delete" }] });
36
+ expect(planShapeCovers(shape(), fresh)).toBe("svc now plans to delete Fn, where the first plan showed create");
37
+ });
38
+
39
+ test("a replacement the preview did not show is not covered; a conditional one resolving either way is", () => {
40
+ const previewed = shape({ entries: [{ logicalId: "Role", action: "update", replacement: "false" }] });
41
+ const replaced = shape({ unknownInputs: [], entries: [{ logicalId: "Role", action: "update", replacement: "true" }] });
42
+ expect(planShapeCovers(previewed, replaced)).toBe("svc now replaces Role, which the first plan did not show");
43
+ for (const r of ["true", "false"] as const) {
44
+ const fresh = shape({ unknownInputs: [], entries: [{ logicalId: "Role", action: "update", replacement: r }] });
45
+ expect(planShapeCovers(shape(), fresh)).toBeUndefined();
46
+ }
47
+ });
48
+
49
+ test("a changed known input is not covered, an unknown one filled is", () => {
50
+ const was = shape({ knownInputs: { "net.cidr": "10.0.0.0/16" } });
51
+ expect(planShapeCovers(was, shape({ unknownInputs: [], knownInputs: { "net.cidr": "10.1.0.0/16" } }))).toBe(
52
+ "svc now reads net.cidr as a different value than the first plan",
53
+ );
54
+ expect(planShapeCovers(was, shape({ unknownInputs: [], knownInputs: { "net.cidr": "10.0.0.0/16", "vpc.id": "vpc-1" } }))).toBeUndefined();
55
+ });
56
+
57
+ test("an input still unknown is not covered", () => {
58
+ expect(planShapeCovers(shape(), shape())).toBe("svc still reads vpc.id, which is not known yet");
59
+ });
60
+ });
61
+
62
+ describe("coveredRound", () => {
63
+ const doc = { members: [], entries: [] } as unknown as ChangeSetDocument;
64
+ const side = (component: string, unknownInputs?: string[], shapes?: Record<string, PlanShape>) => ({
65
+ doc,
66
+ members: [{ member: "svc", component, ...(unknownInputs ? { unknownInputs } : {}) }],
67
+ ...(shapes ? { shapes } : {}),
68
+ });
69
+
70
+ test("covered: binds the first approval's digest and names the inputs filled in", () => {
71
+ const r = coveredRound({
72
+ approved: side("svc", ["vpc.id"], { svc: shape() }),
73
+ approvedDigest: "sha256:aaa",
74
+ fresh: side("svc", undefined, { svc: shape({ unknownInputs: [] }) }),
75
+ });
76
+ expect(r).toEqual({ covered: true, digest: "sha256:aaa", filledInputs: ["vpc.id"] });
77
+ });
78
+
79
+ test("a component the first plan did not preview is not covered", () => {
80
+ const r = coveredRound({ approved: side("svc", ["vpc.id"]), approvedDigest: "sha256:aaa", fresh: side("other") });
81
+ expect(r).toEqual({ covered: false, reason: "other was not in the first plan" });
82
+ });
83
+
84
+ test("a member that was a hole with no preview shape is not covered, even with its unknown input filled in (#4422)", () => {
85
+ // The approved side has no shape for svc and no entries: the reviewer never saw what it would do.
86
+ const fresh = { svc: shape({ unknownInputs: [], entries: [{ logicalId: "Fn", action: "create" }] }) };
87
+ const r = coveredRound({ approved: side("svc", ["vpc.id"]), approvedDigest: "sha256:aaa", fresh: side("svc", undefined, fresh) });
88
+ expect(r).toEqual({ covered: false, reason: "svc now plans to create Fn, which the first plan did not" });
89
+ });
90
+
91
+ test("a known input that moved is not covered", () => {
92
+ const r = coveredRound({
93
+ approved: side("svc", ["vpc.id"]),
94
+ approvedDigest: "sha256:aaa",
95
+ fresh: side("svc"),
96
+ changedKnown: { svc: ["net.cidr", "vpc.id"] },
97
+ });
98
+ expect(r).toEqual({ covered: false, reason: "svc reads net.cidr, which the first plan read as another value" });
99
+ });
100
+ });
101
+
102
+ describe("property-level shapes (ported from plan-shape, #4419)", () => {
103
+ const entry = (over: Partial<PlanShapeEntry> = {}): PlanShapeEntry => ({
104
+ logicalId: "Eni",
105
+ action: "update",
106
+ replacement: "false",
107
+ type: "T",
108
+ properties: ["A", "B"],
109
+ unknown: ["B"],
110
+ ...over,
111
+ });
112
+ const sh = (...entries: PlanShapeEntry[]): PlanShape => ({ member: "m", unknownInputs: ["p"], entries });
113
+ const fresh = (...entries: PlanShapeEntry[]): PlanShape => ({ member: "m", unknownInputs: [], entries });
114
+
115
+ test("digests the same for the same shape in another order", () => {
116
+ const a = sh(entry({ logicalId: "a" }), entry({ logicalId: "b", properties: ["Z", "Y"] }));
117
+ const b = sh(entry({ logicalId: "b", properties: ["Y", "Z"] }), entry({ logicalId: "a" }));
118
+ expect(planShapeDigest(a)).toBe(planShapeDigest(b));
119
+ expect(normalizePlanShape(b).entries.map((e) => e.logicalId)).toEqual(["a", "b"]);
120
+ expect(planShapeDigest(a)).not.toBe(planShapeDigest(sh(entry({ logicalId: "a" }))));
121
+ });
122
+
123
+ test("accepts an identical change and one narrowed where an unknown input drives it", () => {
124
+ expect(planShapeDifferences(sh(entry()), fresh(entry()))).toEqual([]);
125
+ expect(planShapeDifferences(sh(entry()), fresh(entry({ properties: ["A"] })))).toEqual([]);
126
+ expect(planShapeDifferences(sh(entry({ replacement: "true" })), fresh(entry({ replacement: "false" })))).toEqual([]);
127
+ expect(planShapeDifferences(sh(entry({ properties: ["B"] })), fresh())).toEqual([]);
128
+ });
129
+
130
+ test("refuses an added resource, an added property, another action, more replacement", () => {
131
+ expect(planShapeDifferences(sh(entry()), fresh(entry(), entry({ logicalId: "n" })))).toHaveLength(1);
132
+ expect(planShapeCovers(sh(entry()), fresh(entry({ properties: ["A", "B", "C"] })))).toContain("also changes C");
133
+ expect(planShapeCovers(sh(entry()), fresh(entry({ action: "delete" })))).toContain("where the first plan showed update");
134
+ expect(planShapeCovers(sh(entry()), fresh(entry({ replacement: "true" })))).toContain("now replaces Eni");
135
+ expect(planShapeCovers(sh(entry()), fresh(entry({ type: "U" })))).toContain("as U, where the first plan showed T");
136
+ });
137
+
138
+ test("refuses a change that vanishes or weakens with no unknown input behind it", () => {
139
+ expect(planShapeDifferences(sh(entry({ unknown: [] })), fresh())).toHaveLength(1);
140
+ expect(planShapeDifferences(sh(entry({ unknown: [], replacement: "true" })), fresh(entry({ unknown: [], replacement: "false" })))).toHaveLength(1);
141
+ expect(planShapeCovers(sh(entry()), fresh(entry({ properties: ["B"] })))).toContain("no longer changes A");
142
+ });
143
+
144
+ test("a previewed entry without property detail may drop out, as before", () => {
145
+ expect(planShapeDifferences({ member: "m", unknownInputs: [], entries: [{ logicalId: "x", action: "create" }] }, fresh())).toEqual([]);
146
+ });
147
+
148
+ test("an entry with property detail is compared even when its action is no-op", () => {
149
+ const imp = entry({ logicalId: "I", action: "no-op" });
150
+ expect(planShapeCovers(sh(), fresh(imp))).toContain("now plans to no-op I");
151
+ expect(planShapeCovers(sh(imp), fresh(imp))).toBeUndefined();
152
+ });
153
+
154
+ test("the refusal names the differences and marks the plan as moved", () => {
155
+ const err = new PlanShapeMovedError("m", ["m now plans to create Eni, which the first plan did not"]);
156
+ expect(err.planMoved).toBe(true);
157
+ expect(err.message).toContain("Eni, which the first plan did not");
158
+ });
159
+
160
+ test("a round is not covered when a property level difference shows in the shapes a lexicon supplied", () => {
161
+ const doc = { members: [], entries: [] } as unknown as ChangeSetDocument;
162
+ const side = (shape: PlanShape, unknownInputs?: string[]) => ({ doc, members: [{ member: "m", component: "c", ...(unknownInputs ? { unknownInputs } : {}) }], shapes: { m: shape } });
163
+ const r = coveredRound({ approved: side(sh(entry()), ["up.id"]), approvedDigest: "d", fresh: side(fresh(entry({ properties: ["A", "C"] }))) });
164
+ expect(r).toEqual({ covered: false, reason: "m now also changes C on Eni" });
165
+ });
166
+ });
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Covered rounds (#4419): a round planned after an upstream applied is
3
+ * covered by the pull request's first approval when it does only what that
4
+ * approval previewed.
5
+ *
6
+ * A member that reads an output an upstream in the same change has not
7
+ * created yet is previewed with a hole: the input is marked unknown
8
+ * (`PrMember.unknownInputs`). Once the upstream applied, the apply plans the
9
+ * member again with the real value. That fresh plan is covered when
10
+ *
11
+ * - it touches only components the first plan previewed,
12
+ * - within each, only the logical ids and actions previewed (and no
13
+ * replacement the preview did not show), and
14
+ * - the only inputs that differ are the ones the preview marked unknown.
15
+ *
16
+ * A lexicon that can say more than the change set's entries (CloudFormation
17
+ * previews the member with placeholder parameters and reads the real change
18
+ * set afterwards) supplies a {@link PlanShape} on its `CapabilityPlan`.
19
+ * Core decides coverage; the lexicon only reports what it planned.
20
+ *
21
+ * A shape may go below the entry, to the properties a change touches and the
22
+ * ones an unknown input drives. Where it does, a property may drop out when
23
+ * an unknown input drives it (the real value turned out equal to the current
24
+ * one), and never otherwise, and a property may never be added.
25
+ *
26
+ * Coverage binds to the digest of the first approval. {@link coveredRound}
27
+ * takes that digest and records it, so a round planned for a different
28
+ * composition never counts as covered by an approval of another one.
29
+ */
30
+
31
+ import type { ChangeSetAction, ChangeSetDocument } from "../change-set";
32
+ import { computePlanDigest } from "../lifecycle/plan-digest";
33
+
34
+ /** CloudFormation's `Replacement`, in lower case. */
35
+ export type ShapeReplacement = "true" | "false" | "conditional";
36
+
37
+ /** One resource a plan touches. */
38
+ export interface PlanShapeEntry {
39
+ /** The resource's id in the lexicon's own terms: a CloudFormation logical id, a Terraform address. */
40
+ logicalId: string;
41
+ action: ChangeSetAction;
42
+ /**
43
+ * Whether the change replaces the resource, for an `update` or `replace`.
44
+ * `conditional` is the preview not knowing, because a parameter it
45
+ * depends on was a placeholder. Absent means the lexicon does not say.
46
+ */
47
+ replacement?: ShapeReplacement;
48
+ /** The resource's type, when the lexicon says. Compared for equality when both sides carry it. */
49
+ type?: string;
50
+ /**
51
+ * The properties the change touches, when the lexicon lists them. Present
52
+ * on both sides, the fresh plan may touch fewer where an unknown input
53
+ * drives them, and never more.
54
+ */
55
+ properties?: string[];
56
+ /**
57
+ * The properties an unknown input drives. On an update a subset of
58
+ * `properties`; on a create, where a planner cannot list properties, they
59
+ * stand alone. Present (even empty) on an entry that carries detail.
60
+ */
61
+ unknown?: string[];
62
+ }
63
+
64
+ /** What one member's plan does, without the values: what a lexicon supplies and core compares. */
65
+ export interface PlanShape {
66
+ /** The member the shape is of, as in the change set. */
67
+ member: string;
68
+ /** Every resource the plan touches. A `no-op` or `read` entry may be left out. */
69
+ entries: PlanShapeEntry[];
70
+ /**
71
+ * The inputs the plan was made without, as `<component>.<output>`: the
72
+ * ones the preview marked unknown. A fresh plan lists none of them; it
73
+ * is the inputs that were filled in that are recorded.
74
+ */
75
+ unknownInputs: string[];
76
+ /**
77
+ * The inputs the plan read with a value, by reference. When both shapes
78
+ * carry a reference, the values must be equal, or an input that was
79
+ * known has changed. Optional: a shape without it is checked on its
80
+ * entries alone, plus the change set's own attributes in core.
81
+ */
82
+ knownInputs?: Record<string, unknown>;
83
+ }
84
+
85
+ const SKIPPED: ReadonlySet<ChangeSetAction> = new Set(["no-op", "read"]);
86
+
87
+ const REPLACEMENT_RANK: Record<ShapeReplacement, number> = { false: 0, conditional: 1, true: 2 };
88
+
89
+ /** An entry with property detail is compared whatever its action: a lexicon that lists properties means it. */
90
+ const skipped = (e: PlanShapeEntry): boolean => SKIPPED.has(e.action) && e.properties === undefined;
91
+
92
+ /**
93
+ * Every way `fresh` goes beyond `previewed`, as reasons; none means covered.
94
+ * Both are shapes of the same member.
95
+ *
96
+ * The fresh plan may match the preview exactly, or be narrower where an
97
+ * unknown input drives it: a property whose real value equals the current one
98
+ * drops out, a replacement that depended on a stand-in may not happen. It
99
+ * may never add a resource, add a property, change an action or a type,
100
+ * replace more than previewed, read an input that is still unknown, or read a
101
+ * known input as another value.
102
+ */
103
+ export function planShapeDifferences(previewed: PlanShape, fresh: PlanShape): string[] {
104
+ const member = fresh.member;
105
+ const out: string[] = [];
106
+ const was = new Map(previewed.entries.map((e) => [e.logicalId, e]));
107
+ const now = new Set<string>();
108
+ for (const e of fresh.entries) {
109
+ if (skipped(e)) continue;
110
+ now.add(e.logicalId);
111
+ const p = was.get(e.logicalId);
112
+ if (!p) {
113
+ out.push(`${member} now plans to ${e.action} ${e.logicalId}, which the first plan did not`);
114
+ continue;
115
+ }
116
+ if (p.action !== e.action) out.push(`${member} now plans to ${e.action} ${e.logicalId}, where the first plan showed ${p.action}`);
117
+ if (p.type !== undefined && e.type !== undefined && p.type !== e.type) out.push(`${member} now has ${e.logicalId} as ${e.type}, where the first plan showed ${p.type}`);
118
+ if (p.properties && e.properties) {
119
+ const extra = e.properties.filter((x) => !p.properties!.includes(x));
120
+ if (extra.length > 0) out.push(`${member} now also changes ${extra.join(", ")} on ${e.logicalId}`);
121
+ const driven = p.unknown ?? [];
122
+ const lost = p.properties.filter((x) => !driven.includes(x) && !e.properties!.includes(x));
123
+ if (lost.length > 0) out.push(`${member} no longer changes ${lost.join(", ")} on ${e.logicalId}, which no unknown input drives`);
124
+ }
125
+ if (e.replacement === "true" && p.replacement !== "true" && p.replacement !== "conditional") {
126
+ out.push(`${member} now replaces ${e.logicalId}, which the first plan did not show`);
127
+ } else if (e.replacement === "conditional" && p.replacement !== "conditional" && p.replacement !== "true") {
128
+ out.push(`${member} may now replace ${e.logicalId}, which the first plan did not show`);
129
+ } else if (e.replacement && p.replacement && p.unknown?.length === 0 && REPLACEMENT_RANK[e.replacement] < REPLACEMENT_RANK[p.replacement]) {
130
+ out.push(`${member} replacement of ${e.logicalId} was ${p.replacement}, now ${e.replacement}, and no unknown input drives it`);
131
+ }
132
+ }
133
+ // A previewed change that is gone: allowed only where unknown inputs drive all of it.
134
+ for (const p of previewed.entries) {
135
+ if (now.has(p.logicalId) || skipped(p) || !p.properties) continue;
136
+ const driven = p.unknown ?? [];
137
+ if (!(p.properties.length > 0 && p.properties.every((x) => driven.includes(x)))) {
138
+ out.push(`${member} no longer plans to ${p.action} ${p.logicalId}, which the first plan did and no unknown input drives`);
139
+ }
140
+ }
141
+ const unknown = new Set(previewed.unknownInputs);
142
+ const stillUnknown = fresh.unknownInputs[0];
143
+ if (stillUnknown) out.push(`${member} still reads ${stillUnknown}, which is not known yet`);
144
+ for (const [ref, value] of Object.entries(fresh.knownInputs ?? {})) {
145
+ if (unknown.has(ref)) continue;
146
+ if (!previewed.knownInputs) continue;
147
+ if (!(ref in previewed.knownInputs)) out.push(`${member} now reads ${ref}, which the first plan did not`);
148
+ else if (JSON.stringify(previewed.knownInputs[ref] ?? null) !== JSON.stringify(value ?? null)) {
149
+ out.push(`${member} now reads ${ref} as a different value than the first plan`);
150
+ }
151
+ }
152
+ return out;
153
+ }
154
+
155
+ /**
156
+ * Why `fresh` is not covered by `previewed`, or `undefined` when it is: the
157
+ * first of {@link planShapeDifferences}.
158
+ */
159
+ export function planShapeCovers(previewed: PlanShape, fresh: PlanShape): string | undefined {
160
+ return planShapeDifferences(previewed, fresh)[0];
161
+ }
162
+
163
+ /** A shape in the order that makes equal shapes digest equal. */
164
+ export function normalizePlanShape(shape: PlanShape): PlanShape {
165
+ const sorted = (xs: string[]): string[] => [...new Set(xs)].sort();
166
+ return {
167
+ member: shape.member,
168
+ entries: shape.entries
169
+ .map((e) => ({
170
+ logicalId: e.logicalId,
171
+ action: e.action,
172
+ ...(e.replacement ? { replacement: e.replacement } : {}),
173
+ ...(e.type !== undefined ? { type: e.type } : {}),
174
+ ...(e.properties ? { properties: sorted(e.properties) } : {}),
175
+ ...(e.unknown ? { unknown: sorted(e.unknown) } : {}),
176
+ }))
177
+ .sort((a, b) => a.logicalId.localeCompare(b.logicalId) || a.action.localeCompare(b.action)),
178
+ unknownInputs: sorted(shape.unknownInputs),
179
+ ...(shape.knownInputs ? { knownInputs: shape.knownInputs } : {}),
180
+ };
181
+ }
182
+
183
+ export function planShapeDigest(shape: PlanShape): string {
184
+ return computePlanDigest("plan-shape", normalizePlanShape(shape));
185
+ }
186
+
187
+ /**
188
+ * What a lexicon hands the run for a previewed member, through
189
+ * `DeployContext.plans[member]`. `kind` is the lexicon's own so its `run`
190
+ * can tell a shape from its other artifacts.
191
+ */
192
+ export interface PlanShapeArtifact {
193
+ kind: string;
194
+ member: string;
195
+ shape: PlanShape;
196
+ digest: string;
197
+ }
198
+
199
+ /**
200
+ * Thrown by a member's `run` when the change it created is not covered by the
201
+ * shape previewed. The change set is deleted unexecuted. `planMoved` is
202
+ * core's mark for a moved plan, so the command that applied it exits 3.
203
+ */
204
+ export class PlanShapeMovedError extends Error {
205
+ readonly planMoved = true;
206
+ constructor(
207
+ public readonly member: string,
208
+ public readonly differences: string[],
209
+ ) {
210
+ super(`"${member}": refusing to apply. The change differs from the preview that was approved: ${differences.join("; ")}. Plan it again.`);
211
+ this.name = "PlanShapeMovedError";
212
+ }
213
+ }
214
+
215
+ /** The shape a change-set document gives for `member`, when its lexicon supplies none. */
216
+ export function shapeFromChangeSet(doc: ChangeSetDocument, member: string, inputs: { unknownInputs?: string[]; knownInputs?: Record<string, unknown> } = {}): PlanShape {
217
+ return {
218
+ member,
219
+ entries: doc.entries
220
+ .filter((e) => e.member === member)
221
+ .map((e) => ({ logicalId: e.deposed ? `${e.address}#${e.deposed}` : e.address, action: e.action })),
222
+ unknownInputs: [...(inputs.unknownInputs ?? [])].sort(),
223
+ ...(inputs.knownInputs ? { knownInputs: inputs.knownInputs } : {}),
224
+ };
225
+ }
226
+
227
+ /** What a round was covered by, as the report records it (`PrRound.covered`). */
228
+ export interface RoundCover {
229
+ /** The digest of the first approval, which the coverage is bound to. */
230
+ digest: string;
231
+ /** The inputs the first plan marked unknown and this round planned with a value, `<component>.<output>`, sorted. */
232
+ filledInputs: string[];
233
+ }
234
+
235
+ export type RoundCoverage = ({ covered: true } & RoundCover) | { covered: false; reason: string };
236
+
237
+ /** One side of the comparison: a plan set, as far as coverage needs it. */
238
+ export interface CoverageSide {
239
+ doc: ChangeSetDocument;
240
+ /** Per member, its component and (first plan only) the inputs it was previewed without. */
241
+ members: { member: string; component: string; unknownInputs?: string[] }[];
242
+ /** Shapes a lexicon supplied, by member. Where absent, the change set gives the shape. */
243
+ shapes?: Record<string, PlanShape>;
244
+ }
245
+
246
+ export interface CoveredRoundInput {
247
+ /** The first plan, whose approval is being relied on. */
248
+ approved: CoverageSide;
249
+ /** The digest that approval binds: the report's `digest`. */
250
+ approvedDigest: string;
251
+ /** The round's fresh plan. */
252
+ fresh: CoverageSide;
253
+ /**
254
+ * Known inputs that changed between the first plan and the round, as
255
+ * `<component>.<output>`: an input the first plan read with a value that
256
+ * stands as another one now. Core reads them from the outputs.
257
+ */
258
+ changedKnown?: ReadonlyMap<string, string[]> | Record<string, string[]>;
259
+ /** The check on the change set's own attributes, per member (`planCoveredBy`). */
260
+ attributes?: (member: string) => string | undefined;
261
+ }
262
+
263
+ /**
264
+ * Whether the approval of `approvedDigest` covers `fresh`. Covered, the
265
+ * result names the digest and the inputs that were filled in; not covered,
266
+ * the reason, which the apply reports with the approval it asks for.
267
+ */
268
+ export function coveredRound(input: CoveredRoundInput): RoundCoverage {
269
+ const { approved, fresh } = input;
270
+ const previewedComponents = new Set(approved.members.map((m) => m.component));
271
+ const approvedMember = new Map(approved.members.map((m) => [m.member, m]));
272
+ const filled = new Set<string>();
273
+ for (const m of fresh.members) {
274
+ if (!previewedComponents.has(m.component)) return { covered: false, reason: `${m.component} was not in the first plan` };
275
+ const was = approvedMember.get(m.member);
276
+ if (!was) return { covered: false, reason: `${m.member} was not in the first plan` };
277
+ if (was.component !== m.component) return { covered: false, reason: `${m.member} moved from ${was.component} to ${m.component}` };
278
+ const previewed = approved.shapes?.[m.member] ?? shapeFromChangeSet(approved.doc, m.member, { unknownInputs: was.unknownInputs });
279
+ const both = [...new Set([...(previewed.unknownInputs ?? []), ...(was.unknownInputs ?? [])])];
280
+ // A lexicon that only knows the upstream stack lists the bare component; the member names the output. Keep the named one.
281
+ const unknownInputs = both.filter((ref) => !both.some((other) => other.startsWith(`${ref}.`))).sort();
282
+ const now = fresh.shapes?.[m.member] ?? shapeFromChangeSet(fresh.doc, m.member);
283
+ const reason = planShapeCovers({ ...previewed, unknownInputs }, now);
284
+ if (reason) return { covered: false, reason };
285
+ const changed = (input.changedKnown instanceof Map ? input.changedKnown.get(m.component) : (input.changedKnown as Record<string, string[]> | undefined)?.[m.component]) ?? [];
286
+ const changedKnown = changed.filter((ref: string) => !unknownInputs.includes(ref));
287
+ if (changedKnown.length > 0) return { covered: false, reason: `${m.member} reads ${changedKnown.join(", ")}, which the first plan read as another value` };
288
+ const attrs = input.attributes?.(m.member);
289
+ if (attrs) return { covered: false, reason: attrs };
290
+ for (const ref of unknownInputs) filled.add(ref);
291
+ }
292
+ return { covered: true, digest: input.approvedDigest, filledInputs: [...filled].sort() };
293
+ }