@intentius/chant 0.124.46 → 0.124.49

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 (726) hide show
  1. package/dist/__golden__/wave-loop/op-by-apply-after-fails.json +10 -0
  2. package/dist/__golden__/wave-loop/op-by-apply-hold.json +10 -0
  3. package/dist/__golden__/wave-loop/op-by-apply-ignored.json +10 -0
  4. package/dist/__golden__/wave-loop/op-by-apply-pass.json +10 -0
  5. package/dist/__golden__/wave-loop/op-by-apply-skip.json +10 -0
  6. package/dist/__golden__/wave-loop/op-cost-member-clash.json +6 -0
  7. package/dist/__golden__/wave-loop/op-cost-member.json +13 -0
  8. package/dist/__golden__/wave-loop/op-denied-source.json +16 -0
  9. package/dist/__golden__/wave-loop/op-denied.json +16 -0
  10. package/dist/__golden__/wave-loop/op-discover-rest-short.json +6 -0
  11. package/dist/__golden__/wave-loop/op-discover-rest-stops.json +13 -0
  12. package/dist/__golden__/wave-loop/op-discover-rest.json +26 -0
  13. package/dist/__golden__/wave-loop/op-discover-wide.json +26 -0
  14. package/dist/__golden__/wave-loop/op-held-on-again.json +16 -0
  15. package/dist/__golden__/wave-loop/op-held-on-before-apply.json +16 -0
  16. package/dist/__golden__/wave-loop/op-held-on.json +16 -0
  17. package/dist/__golden__/wave-loop/op-hooks-apply-fails.json +13 -0
  18. package/dist/__golden__/wave-loop/op-hooks-fail-after-apply.json +13 -0
  19. package/dist/__golden__/wave-loop/op-hooks-fail-before-apply.json +13 -0
  20. package/dist/__golden__/wave-loop/op-hooks-fail-before-plan.json +13 -0
  21. package/dist/__golden__/wave-loop/op-hooks-held.json +13 -0
  22. package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +13 -0
  23. package/dist/__golden__/wave-loop/op-hooks-pass.json +13 -0
  24. package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +13 -0
  25. package/dist/__golden__/wave-loop/op-hooks-share.json +33 -0
  26. package/dist/__golden__/wave-loop/op-outcome-held.json +10 -0
  27. package/dist/__golden__/wave-loop/op-outcome-replan-approved.json +10 -0
  28. package/dist/__golden__/wave-loop/op-outcome-replan-forever.json +10 -0
  29. package/dist/__golden__/wave-loop/op-outcome-replan.json +10 -0
  30. package/dist/__golden__/wave-loop/op-outcome-stood-down.json +10 -0
  31. package/dist/__golden__/wave-loop/op-outcome-unreadable.json +10 -0
  32. package/dist/__golden__/wave-loop/op-overridden.json +16 -0
  33. package/dist/__golden__/wave-loop/op-per-run.json +20 -0
  34. package/dist/__golden__/wave-loop/op-point.json +13 -0
  35. package/dist/__golden__/wave-loop/op-pr-review.json +10 -0
  36. package/dist/__golden__/wave-loop/op-sealed.json +10 -0
  37. package/dist/__golden__/wave-loop/op-spend-change-only.json +10 -0
  38. package/dist/__golden__/wave-loop/op-spend.json +10 -0
  39. package/dist/__golden__/wave-loop/op-wave-commands.json +55 -0
  40. package/dist/__golden__/wave-loop/op-wide-refused.json +26 -0
  41. package/dist/__golden__/wave-loop/op-wide-waiting.json +29 -0
  42. package/dist/__golden__/wave-loop/op-wide.json +39 -0
  43. package/dist/action-pins.d.ts +61 -0
  44. package/dist/action-pins.d.ts.map +1 -0
  45. package/dist/action-pins.js +77 -0
  46. package/dist/cli/commands/carve.d.ts +3 -0
  47. package/dist/cli/commands/carve.d.ts.map +1 -1
  48. package/dist/cli/commands/carve.js +36 -7
  49. package/dist/cli/commands/import-report.d.ts +26 -0
  50. package/dist/cli/commands/import-report.d.ts.map +1 -0
  51. package/dist/cli/commands/import-report.js +65 -0
  52. package/dist/cli/commands/import.d.ts +11 -1
  53. package/dist/cli/commands/import.d.ts.map +1 -1
  54. package/dist/cli/commands/import.js +22 -3
  55. package/dist/cli/commands/migrate.d.ts +13 -1
  56. package/dist/cli/commands/migrate.d.ts.map +1 -1
  57. package/dist/cli/commands/migrate.js +134 -10
  58. package/dist/cli/handlers/approve.d.ts.map +1 -1
  59. package/dist/cli/handlers/approve.js +41 -14
  60. package/dist/cli/handlers/build.d.ts.map +1 -1
  61. package/dist/cli/handlers/build.js +6 -1
  62. package/dist/cli/handlers/graph-reader.d.ts +26 -0
  63. package/dist/cli/handlers/graph-reader.d.ts.map +1 -0
  64. package/dist/cli/handlers/graph-reader.js +41 -0
  65. package/dist/cli/handlers/graph.d.ts.map +1 -1
  66. package/dist/cli/handlers/graph.js +14 -0
  67. package/dist/cli/handlers/ledger.d.ts +3 -0
  68. package/dist/cli/handlers/ledger.d.ts.map +1 -0
  69. package/dist/cli/handlers/ledger.js +55 -0
  70. package/dist/cli/handlers/migrate.d.ts.map +1 -1
  71. package/dist/cli/handlers/migrate.js +18 -11
  72. package/dist/cli/handlers/misc.d.ts.map +1 -1
  73. package/dist/cli/handlers/misc.js +12 -1
  74. package/dist/cli/handlers/pr-env.js +2 -2
  75. package/dist/cli/handlers/pr.d.ts +15 -1
  76. package/dist/cli/handlers/pr.d.ts.map +1 -1
  77. package/dist/cli/handlers/pr.js +183 -40
  78. package/dist/cli/handlers/query.d.ts +7 -2
  79. package/dist/cli/handlers/query.d.ts.map +1 -1
  80. package/dist/cli/handlers/query.js +16 -4
  81. package/dist/cli/handlers/release.d.ts.map +1 -1
  82. package/dist/cli/handlers/release.js +6 -3
  83. package/dist/cli/handlers/search.d.ts +2 -4
  84. package/dist/cli/handlers/search.d.ts.map +1 -1
  85. package/dist/cli/handlers/search.js +13 -39
  86. package/dist/cli/main.d.ts.map +1 -1
  87. package/dist/cli/main.js +87 -14
  88. package/dist/cli/parse-args.d.ts.map +1 -1
  89. package/dist/cli/parse-args.js +47 -2
  90. package/dist/cli/registry.d.ts +22 -4
  91. package/dist/cli/registry.d.ts.map +1 -1
  92. package/dist/components/builders.d.ts +5 -1
  93. package/dist/components/builders.d.ts.map +1 -1
  94. package/dist/components/builders.js +5 -0
  95. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  96. package/dist/components/capability-plugin-loader.js +16 -1
  97. package/dist/components/capability-plugin.d.ts +14 -0
  98. package/dist/components/capability-plugin.d.ts.map +1 -1
  99. package/dist/components/pr-hooks.d.ts +21 -1
  100. package/dist/components/pr-hooks.d.ts.map +1 -1
  101. package/dist/components/pr-hooks.js +72 -4
  102. package/dist/components/pr-pipeline.d.ts +45 -10
  103. package/dist/components/pr-pipeline.d.ts.map +1 -1
  104. package/dist/components/pr-pipeline.js +47 -14
  105. package/dist/components/pr-policy.d.ts +36 -0
  106. package/dist/components/pr-policy.d.ts.map +1 -1
  107. package/dist/components/pr-policy.js +36 -0
  108. package/dist/components/pr-run.d.ts +3 -0
  109. package/dist/components/pr-run.d.ts.map +1 -1
  110. package/dist/components/pr-run.js +1 -0
  111. package/dist/components/starter-plugin.d.ts +1 -0
  112. package/dist/components/starter-plugin.d.ts.map +1 -1
  113. package/dist/components/starter-plugin.js +5 -1
  114. package/dist/components/verbs/index.d.ts +2 -0
  115. package/dist/components/verbs/index.d.ts.map +1 -1
  116. package/dist/components/verbs/index.js +2 -0
  117. package/dist/components/verbs/publish-artifact.d.ts +65 -0
  118. package/dist/components/verbs/publish-artifact.d.ts.map +1 -0
  119. package/dist/components/verbs/publish-artifact.js +74 -0
  120. package/dist/components/verbs/publish-image.d.ts +129 -0
  121. package/dist/components/verbs/publish-image.d.ts.map +1 -0
  122. package/dist/components/verbs/publish-image.js +145 -0
  123. package/dist/config-environments.d.ts +20 -0
  124. package/dist/config-environments.d.ts.map +1 -1
  125. package/dist/config-environments.js +12 -0
  126. package/dist/config.d.ts +25 -1
  127. package/dist/config.d.ts.map +1 -1
  128. package/dist/config.js +10 -2
  129. package/dist/gate-decision.d.ts +20 -2
  130. package/dist/gate-decision.d.ts.map +1 -1
  131. package/dist/gate-decision.js +30 -3
  132. package/dist/graph-ir.d.ts +25 -0
  133. package/dist/graph-ir.d.ts.map +1 -1
  134. package/dist/graph-walk.d.ts +66 -0
  135. package/dist/graph-walk.d.ts.map +1 -0
  136. package/dist/graph-walk.js +70 -0
  137. package/dist/import/parser.d.ts +19 -0
  138. package/dist/import/parser.d.ts.map +1 -1
  139. package/dist/lexicon.d.ts +94 -6
  140. package/dist/lexicon.d.ts.map +1 -1
  141. package/dist/lifecycle/gate-ledger.d.ts +11 -0
  142. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  143. package/dist/lifecycle/git.d.ts +2 -5
  144. package/dist/lifecycle/git.d.ts.map +1 -1
  145. package/dist/lifecycle/ledger-read.d.ts +65 -0
  146. package/dist/lifecycle/ledger-read.d.ts.map +1 -0
  147. package/dist/lifecycle/ledger-read.js +160 -0
  148. package/dist/lifecycle/release-workstation.d.ts +7 -0
  149. package/dist/lifecycle/release-workstation.d.ts.map +1 -1
  150. package/dist/lifecycle/release-workstation.js +25 -0
  151. package/dist/lifecycle/run-ledger.d.ts +4 -3
  152. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  153. package/dist/lifecycle/run-ledger.js +1 -0
  154. package/dist/lint/discover.js +3 -1
  155. package/dist/lint/post-synth.d.ts +127 -1
  156. package/dist/lint/post-synth.d.ts.map +1 -1
  157. package/dist/lint/post-synth.js +84 -2
  158. package/dist/lint/rules/op/index.d.ts +1 -0
  159. package/dist/lint/rules/op/index.d.ts.map +1 -1
  160. package/dist/lint/rules/op/index.js +2 -0
  161. package/dist/lint/rules/op/ops019-workspace-op-target.d.ts +32 -0
  162. package/dist/lint/rules/op/ops019-workspace-op-target.d.ts.map +1 -0
  163. package/dist/lint/rules/op/ops019-workspace-op-target.js +63 -0
  164. package/dist/op/activities/activity-contracts.d.ts +9 -6
  165. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  166. package/dist/op/activities/activity-contracts.js +5 -2
  167. package/dist/op/activities/index.d.ts +2 -2
  168. package/dist/op/activities/index.d.ts.map +1 -1
  169. package/dist/op/activities/index.js +1 -1
  170. package/dist/op/activities/review-agent.d.ts +90 -4
  171. package/dist/op/activities/review-agent.d.ts.map +1 -1
  172. package/dist/op/activities/review-agent.js +217 -19
  173. package/dist/op/activities/work-evidence.d.ts +8 -1
  174. package/dist/op/activities/work-evidence.d.ts.map +1 -1
  175. package/dist/op/activities/work-evidence.js +1 -0
  176. package/dist/op/builders.d.ts +9 -5
  177. package/dist/op/builders.d.ts.map +1 -1
  178. package/dist/op/builders.js +9 -5
  179. package/dist/op/composites/apply-op.d.ts +29 -2
  180. package/dist/op/composites/apply-op.d.ts.map +1 -1
  181. package/dist/op/composites/apply-op.js +20 -1
  182. package/dist/op/gate-approval-base.d.ts +25 -0
  183. package/dist/op/gate-approval-base.d.ts.map +1 -0
  184. package/dist/op/gate-approval-base.js +71 -0
  185. package/dist/op/gate-approval.d.ts +18 -9
  186. package/dist/op/gate-approval.d.ts.map +1 -1
  187. package/dist/op/gate-approval.js +13 -31
  188. package/dist/op/gate-name.d.ts +13 -0
  189. package/dist/op/gate-name.d.ts.map +1 -1
  190. package/dist/op/gate-name.js +14 -1
  191. package/dist/op/gate-quorum.d.ts +100 -3
  192. package/dist/op/gate-quorum.d.ts.map +1 -1
  193. package/dist/op/gate-quorum.js +108 -6
  194. package/dist/op/gate-review.d.ts +9 -0
  195. package/dist/op/gate-review.d.ts.map +1 -1
  196. package/dist/op/gate-review.js +13 -0
  197. package/dist/op/gate.d.ts +47 -5
  198. package/dist/op/gate.d.ts.map +1 -1
  199. package/dist/op/gate.js +25 -4
  200. package/dist/op/index.d.ts +3 -3
  201. package/dist/op/index.d.ts.map +1 -1
  202. package/dist/op/index.js +1 -1
  203. package/dist/op/local-executor.d.ts +45 -3
  204. package/dist/op/local-executor.d.ts.map +1 -1
  205. package/dist/op/local-executor.js +68 -7
  206. package/dist/op/op-waves-run.d.ts +26 -1
  207. package/dist/op/op-waves-run.d.ts.map +1 -1
  208. package/dist/op/op-waves-run.js +259 -13
  209. package/dist/op/op-waves.d.ts +72 -5
  210. package/dist/op/op-waves.d.ts.map +1 -1
  211. package/dist/op/op-waves.js +110 -15
  212. package/dist/op/runtime.d.ts +11 -0
  213. package/dist/op/runtime.d.ts.map +1 -1
  214. package/dist/op/schedule-overdue.d.ts +16 -0
  215. package/dist/op/schedule-overdue.d.ts.map +1 -1
  216. package/dist/op/schedule-overdue.js +36 -0
  217. package/dist/op/stage-hooks.d.ts +9 -2
  218. package/dist/op/stage-hooks.d.ts.map +1 -1
  219. package/dist/op/stage-hooks.js +7 -5
  220. package/dist/op/workspace-run-env.d.ts +22 -0
  221. package/dist/op/workspace-run-env.d.ts.map +1 -0
  222. package/dist/op/workspace-run-env.js +21 -0
  223. package/dist/pr-forge.d.ts +24 -0
  224. package/dist/pr-forge.d.ts.map +1 -1
  225. package/dist/pr-forge.js +47 -0
  226. package/dist/pr-loop.d.ts +10 -0
  227. package/dist/pr-loop.d.ts.map +1 -1
  228. package/dist/pr-loop.js +4 -0
  229. package/dist/pr-migrations.d.ts +62 -0
  230. package/dist/pr-migrations.d.ts.map +1 -0
  231. package/dist/pr-migrations.js +50 -0
  232. package/dist/pr-post.d.ts +42 -0
  233. package/dist/pr-post.d.ts.map +1 -1
  234. package/dist/pr-post.js +68 -2
  235. package/dist/read-filter.d.ts +65 -0
  236. package/dist/read-filter.d.ts.map +1 -0
  237. package/dist/read-filter.js +179 -0
  238. package/dist/run-event.d.ts +1 -1
  239. package/dist/run-event.d.ts.map +1 -1
  240. package/dist/terraform/carve.d.ts +24 -3
  241. package/dist/terraform/carve.d.ts.map +1 -1
  242. package/dist/terraform/carve.js +32 -14
  243. package/dist/terraform/cuts.d.ts +97 -0
  244. package/dist/terraform/cuts.d.ts.map +1 -0
  245. package/dist/terraform/cuts.js +138 -0
  246. package/dist/terraform/folds.d.ts +26 -0
  247. package/dist/terraform/folds.d.ts.map +1 -0
  248. package/dist/terraform/folds.js +38 -0
  249. package/dist/terraform/graph.d.ts.map +1 -1
  250. package/dist/terraform/graph.js +10 -0
  251. package/dist/terraform/reach.d.ts +140 -0
  252. package/dist/terraform/reach.d.ts.map +1 -0
  253. package/dist/terraform/reach.js +170 -0
  254. package/dist/terraform/score.d.ts +14 -0
  255. package/dist/terraform/score.d.ts.map +1 -1
  256. package/dist/terraform/score.js +14 -31
  257. package/dist/terraform/state.d.ts.map +1 -1
  258. package/dist/terraform/state.js +4 -2
  259. package/dist/terraform/types.d.ts +8 -0
  260. package/dist/terraform/types.d.ts.map +1 -1
  261. package/dist/wave-blocked.d.ts +10 -1
  262. package/dist/wave-blocked.d.ts.map +1 -1
  263. package/dist/wave-blocked.js +31 -3
  264. package/dist/wave-runtime.mjs +264 -254
  265. package/dist/workspace/check.schema.json +13 -2
  266. package/dist/workspace/checks/ci-branch.d.ts +44 -0
  267. package/dist/workspace/checks/ci-branch.d.ts.map +1 -0
  268. package/dist/workspace/checks/ci-branch.js +133 -0
  269. package/dist/workspace/checks/gate-approval.d.ts +58 -0
  270. package/dist/workspace/checks/gate-approval.d.ts.map +1 -0
  271. package/dist/workspace/checks/gate-approval.js +103 -0
  272. package/dist/workspace/checks/links.d.ts +3 -0
  273. package/dist/workspace/checks/links.d.ts.map +1 -1
  274. package/dist/workspace/checks/links.js +12 -1
  275. package/dist/workspace/checks/org.d.ts +48 -0
  276. package/dist/workspace/checks/org.d.ts.map +1 -0
  277. package/dist/workspace/checks/org.js +156 -0
  278. package/dist/workspace/checks.d.ts +12 -1
  279. package/dist/workspace/checks.d.ts.map +1 -1
  280. package/dist/workspace/checks.js +63 -7
  281. package/dist/workspace/ci-branches.d.ts +16 -0
  282. package/dist/workspace/ci-branches.d.ts.map +1 -1
  283. package/dist/workspace/ci-branches.js +27 -0
  284. package/dist/workspace/ci-green-cli.js +7 -7
  285. package/dist/workspace/ci-green-workflow.d.ts +1 -1
  286. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  287. package/dist/workspace/ci-green-workflow.js +13 -8
  288. package/dist/workspace/ci-green.d.ts +14 -0
  289. package/dist/workspace/ci-green.d.ts.map +1 -1
  290. package/dist/workspace/ci-green.js +44 -11
  291. package/dist/workspace/ci-run.d.ts +9 -1
  292. package/dist/workspace/ci-run.d.ts.map +1 -1
  293. package/dist/workspace/ci-run.js +67 -6
  294. package/dist/workspace/ci-serve.d.ts +9 -0
  295. package/dist/workspace/ci-serve.d.ts.map +1 -1
  296. package/dist/workspace/ci-serve.js +114 -76
  297. package/dist/workspace/compose-graph.d.ts +14 -0
  298. package/dist/workspace/compose-graph.d.ts.map +1 -1
  299. package/dist/workspace/composites.d.ts +4 -1
  300. package/dist/workspace/composites.d.ts.map +1 -1
  301. package/dist/workspace/composites.js +5 -2
  302. package/dist/workspace/composites.schema.json +22 -0
  303. package/dist/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
  304. package/dist/workspace/cross-root-lint.d.ts +56 -0
  305. package/dist/workspace/cross-root-lint.d.ts.map +1 -0
  306. package/dist/workspace/cross-root-lint.js +111 -0
  307. package/dist/workspace/dbt-models-reader.d.ts +44 -0
  308. package/dist/workspace/dbt-models-reader.d.ts.map +1 -0
  309. package/dist/workspace/dbt-models-reader.js +356 -0
  310. package/dist/workspace/dbt-project-common.d.ts +40 -0
  311. package/dist/workspace/dbt-project-common.d.ts.map +1 -0
  312. package/dist/workspace/dbt-project-common.js +106 -0
  313. package/dist/workspace/dbt-project-reader.d.ts +4 -3
  314. package/dist/workspace/dbt-project-reader.d.ts.map +1 -1
  315. package/dist/workspace/dbt-project-reader.js +14 -108
  316. package/dist/workspace/decision-rank.d.ts +26 -0
  317. package/dist/workspace/decision-rank.d.ts.map +1 -0
  318. package/dist/workspace/decision-rank.js +16 -0
  319. package/dist/workspace/declaration.d.ts +50 -2
  320. package/dist/workspace/declaration.d.ts.map +1 -1
  321. package/dist/workspace/declaration.js +57 -11
  322. package/dist/workspace/declaration.schema.json +166 -24
  323. package/dist/workspace/environments.d.ts +26 -2
  324. package/dist/workspace/environments.d.ts.map +1 -1
  325. package/dist/workspace/environments.js +26 -2
  326. package/dist/workspace/exit-cli.d.ts +44 -0
  327. package/dist/workspace/exit-cli.d.ts.map +1 -0
  328. package/dist/workspace/exit-cli.js +198 -0
  329. package/dist/workspace/exit.d.ts +159 -0
  330. package/dist/workspace/exit.d.ts.map +1 -0
  331. package/dist/workspace/exit.js +480 -0
  332. package/dist/workspace/exit.schema.json +257 -0
  333. package/dist/workspace/flyway-dir.d.ts +39 -0
  334. package/dist/workspace/flyway-dir.d.ts.map +1 -0
  335. package/dist/workspace/flyway-dir.js +104 -0
  336. package/dist/workspace/flyway-layout.d.ts +96 -0
  337. package/dist/workspace/flyway-layout.d.ts.map +1 -0
  338. package/dist/workspace/flyway-layout.js +308 -0
  339. package/dist/workspace/graph-cache.d.ts +3 -1
  340. package/dist/workspace/graph-cache.d.ts.map +1 -1
  341. package/dist/workspace/graph-cache.js +12 -1
  342. package/dist/workspace/graph-cli.d.ts.map +1 -1
  343. package/dist/workspace/graph-cli.js +26 -8
  344. package/dist/workspace/graph.schema.json +34 -0
  345. package/dist/workspace/import-fetch.d.ts +38 -0
  346. package/dist/workspace/import-fetch.d.ts.map +1 -0
  347. package/dist/workspace/import-fetch.js +99 -0
  348. package/dist/workspace/import-init.d.ts +26 -0
  349. package/dist/workspace/import-init.d.ts.map +1 -0
  350. package/dist/workspace/import-init.js +130 -0
  351. package/dist/workspace/import-source.d.ts +147 -0
  352. package/dist/workspace/import-source.d.ts.map +1 -0
  353. package/dist/workspace/import-source.js +370 -0
  354. package/dist/workspace/init.d.ts +4 -0
  355. package/dist/workspace/init.d.ts.map +1 -1
  356. package/dist/workspace/init.js +70 -3
  357. package/dist/workspace/intent.d.ts +2 -3
  358. package/dist/workspace/intent.d.ts.map +1 -1
  359. package/dist/workspace/intent.js +5 -9
  360. package/dist/workspace/kind-link-readers.d.ts +59 -2
  361. package/dist/workspace/kind-link-readers.d.ts.map +1 -1
  362. package/dist/workspace/kind-link-readers.js +17 -1
  363. package/dist/workspace/kind-readers.d.ts +6 -0
  364. package/dist/workspace/kind-readers.d.ts.map +1 -1
  365. package/dist/workspace/kind-readers.js +18 -0
  366. package/dist/workspace/kinds.d.ts +90 -3
  367. package/dist/workspace/kinds.d.ts.map +1 -1
  368. package/dist/workspace/kinds.js +161 -7
  369. package/dist/workspace/ledger.schema.json +158 -0
  370. package/dist/workspace/lexicon-outputs.d.ts +20 -1
  371. package/dist/workspace/lexicon-outputs.d.ts.map +1 -1
  372. package/dist/workspace/lexicon-outputs.js +102 -10
  373. package/dist/workspace/lineage-check.d.ts +3 -0
  374. package/dist/workspace/lineage-check.d.ts.map +1 -1
  375. package/dist/workspace/lineage-check.js +23 -4
  376. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  377. package/dist/workspace/lineage-upgrade.js +13 -4
  378. package/dist/workspace/links.d.ts +8 -1
  379. package/dist/workspace/links.d.ts.map +1 -1
  380. package/dist/workspace/links.js +25 -1
  381. package/dist/workspace/ls.d.ts +8 -0
  382. package/dist/workspace/ls.d.ts.map +1 -1
  383. package/dist/workspace/ls.js +7 -1
  384. package/dist/workspace/ls.schema.json +7 -0
  385. package/dist/workspace/member-commands.d.ts.map +1 -1
  386. package/dist/workspace/member-commands.js +54 -5
  387. package/dist/workspace/migrate-links.d.ts +45 -0
  388. package/dist/workspace/migrate-links.d.ts.map +1 -0
  389. package/dist/workspace/migrate-links.js +100 -0
  390. package/dist/workspace/migration-dirs.d.ts +74 -0
  391. package/dist/workspace/migration-dirs.d.ts.map +1 -0
  392. package/dist/workspace/migration-dirs.js +254 -0
  393. package/dist/workspace/new-migrations.d.ts +47 -0
  394. package/dist/workspace/new-migrations.d.ts.map +1 -0
  395. package/dist/workspace/new-migrations.js +91 -0
  396. package/dist/workspace/pr-report.schema.json +46 -0
  397. package/dist/workspace/promote.d.ts +96 -0
  398. package/dist/workspace/promote.d.ts.map +1 -0
  399. package/dist/workspace/promote.js +263 -0
  400. package/dist/workspace/reason-codes.d.ts +21 -0
  401. package/dist/workspace/reason-codes.d.ts.map +1 -1
  402. package/dist/workspace/reason-codes.js +25 -0
  403. package/dist/workspace/records-cli.d.ts +11 -0
  404. package/dist/workspace/records-cli.d.ts.map +1 -1
  405. package/dist/workspace/records-cli.js +57 -5
  406. package/dist/workspace/records.schema.json +5 -0
  407. package/dist/workspace/release-candidates.d.ts +2 -0
  408. package/dist/workspace/release-candidates.d.ts.map +1 -1
  409. package/dist/workspace/release-candidates.js +5 -2
  410. package/dist/workspace/release-train.d.ts +53 -0
  411. package/dist/workspace/release-train.d.ts.map +1 -0
  412. package/dist/workspace/release-train.js +134 -0
  413. package/dist/workspace/release-workflow.d.ts.map +1 -1
  414. package/dist/workspace/release-workflow.js +4 -3
  415. package/dist/workspace/repo-ref.d.ts +45 -0
  416. package/dist/workspace/repo-ref.d.ts.map +1 -0
  417. package/dist/workspace/repo-ref.js +114 -0
  418. package/dist/workspace/run-event.schema.json +2 -2
  419. package/dist/workspace/runs-cli.d.ts +9 -0
  420. package/dist/workspace/runs-cli.d.ts.map +1 -1
  421. package/dist/workspace/runs-cli.js +48 -3
  422. package/dist/workspace/runs.schema.json +7 -0
  423. package/dist/workspace/search-contract.d.ts +1 -1
  424. package/dist/workspace/search-contract.d.ts.map +1 -1
  425. package/dist/workspace/status-decisions.d.ts +37 -0
  426. package/dist/workspace/status-decisions.d.ts.map +1 -0
  427. package/dist/workspace/status-decisions.js +147 -0
  428. package/dist/workspace/status-gates.d.ts +9 -0
  429. package/dist/workspace/status-gates.d.ts.map +1 -1
  430. package/dist/workspace/status.d.ts +4 -0
  431. package/dist/workspace/status.d.ts.map +1 -1
  432. package/dist/workspace/status.js +14 -2
  433. package/dist/workspace/status.schema.json +6 -0
  434. package/dist/workspace/work-cli.d.ts.map +1 -1
  435. package/dist/workspace/work-cli.js +5 -3
  436. package/dist/workspace/work-evidence-ops.d.ts +83 -0
  437. package/dist/workspace/work-evidence-ops.d.ts.map +1 -0
  438. package/dist/workspace/work-evidence-ops.js +169 -0
  439. package/dist/workspace/work-evidence.d.ts +12 -2
  440. package/dist/workspace/work-evidence.d.ts.map +1 -1
  441. package/dist/workspace/work-evidence.js +21 -3
  442. package/dist/workspace/work-evidence.schema.json +49 -2
  443. package/dist/workspace/workspace-gates.d.ts +110 -0
  444. package/dist/workspace/workspace-gates.d.ts.map +1 -0
  445. package/dist/workspace/workspace-gates.js +140 -0
  446. package/dist/workspace/workspace-kinds.schema.json +108 -10
  447. package/dist/workspace/workspace-ledger.d.ts +76 -0
  448. package/dist/workspace/workspace-ledger.d.ts.map +1 -0
  449. package/dist/workspace/workspace-ledger.js +91 -0
  450. package/dist/workspace/workspace-run.d.ts +112 -0
  451. package/dist/workspace/workspace-run.d.ts.map +1 -0
  452. package/dist/workspace/workspace-run.js +432 -0
  453. package/package.json +1 -1
  454. package/src/__golden__/wave-loop/op-by-apply-after-fails.json +10 -0
  455. package/src/__golden__/wave-loop/op-by-apply-hold.json +10 -0
  456. package/src/__golden__/wave-loop/op-by-apply-ignored.json +10 -0
  457. package/src/__golden__/wave-loop/op-by-apply-pass.json +10 -0
  458. package/src/__golden__/wave-loop/op-by-apply-skip.json +10 -0
  459. package/src/__golden__/wave-loop/op-cost-member-clash.json +6 -0
  460. package/src/__golden__/wave-loop/op-cost-member.json +13 -0
  461. package/src/__golden__/wave-loop/op-denied-source.json +16 -0
  462. package/src/__golden__/wave-loop/op-denied.json +16 -0
  463. package/src/__golden__/wave-loop/op-discover-rest-short.json +6 -0
  464. package/src/__golden__/wave-loop/op-discover-rest-stops.json +13 -0
  465. package/src/__golden__/wave-loop/op-discover-rest.json +26 -0
  466. package/src/__golden__/wave-loop/op-discover-wide.json +26 -0
  467. package/src/__golden__/wave-loop/op-held-on-again.json +16 -0
  468. package/src/__golden__/wave-loop/op-held-on-before-apply.json +16 -0
  469. package/src/__golden__/wave-loop/op-held-on.json +16 -0
  470. package/src/__golden__/wave-loop/op-hooks-apply-fails.json +13 -0
  471. package/src/__golden__/wave-loop/op-hooks-fail-after-apply.json +13 -0
  472. package/src/__golden__/wave-loop/op-hooks-fail-before-apply.json +13 -0
  473. package/src/__golden__/wave-loop/op-hooks-fail-before-plan.json +13 -0
  474. package/src/__golden__/wave-loop/op-hooks-held.json +13 -0
  475. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +13 -0
  476. package/src/__golden__/wave-loop/op-hooks-pass.json +13 -0
  477. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +13 -0
  478. package/src/__golden__/wave-loop/op-hooks-share.json +33 -0
  479. package/src/__golden__/wave-loop/op-outcome-held.json +10 -0
  480. package/src/__golden__/wave-loop/op-outcome-replan-approved.json +10 -0
  481. package/src/__golden__/wave-loop/op-outcome-replan-forever.json +10 -0
  482. package/src/__golden__/wave-loop/op-outcome-replan.json +10 -0
  483. package/src/__golden__/wave-loop/op-outcome-stood-down.json +10 -0
  484. package/src/__golden__/wave-loop/op-outcome-unreadable.json +10 -0
  485. package/src/__golden__/wave-loop/op-overridden.json +16 -0
  486. package/src/__golden__/wave-loop/op-per-run.json +20 -0
  487. package/src/__golden__/wave-loop/op-point.json +13 -0
  488. package/src/__golden__/wave-loop/op-pr-review.json +10 -0
  489. package/src/__golden__/wave-loop/op-sealed.json +10 -0
  490. package/src/__golden__/wave-loop/op-spend-change-only.json +10 -0
  491. package/src/__golden__/wave-loop/op-spend.json +10 -0
  492. package/src/__golden__/wave-loop/op-wave-commands.json +55 -0
  493. package/src/__golden__/wave-loop/op-wide-refused.json +26 -0
  494. package/src/__golden__/wave-loop/op-wide-waiting.json +29 -0
  495. package/src/__golden__/wave-loop/op-wide.json +39 -0
  496. package/src/action-pins.test.ts +78 -0
  497. package/src/action-pins.ts +92 -0
  498. package/src/cli/commands/carve-score-once.test.ts +99 -0
  499. package/src/cli/commands/carve.ts +37 -6
  500. package/src/cli/commands/check-lexicon.test.ts +2 -2
  501. package/src/cli/commands/import-report.ts +71 -0
  502. package/src/cli/commands/import.ts +33 -3
  503. package/src/cli/commands/migrate-dialect.test.ts +145 -0
  504. package/src/cli/commands/migrate.ts +132 -11
  505. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +4 -4
  506. package/src/cli/handlers/approve.ts +33 -4
  507. package/src/cli/handlers/build.ts +7 -1
  508. package/src/cli/handlers/graph-reader.test.ts +31 -0
  509. package/src/cli/handlers/graph-reader.ts +51 -0
  510. package/src/cli/handlers/graph.ts +14 -0
  511. package/src/cli/handlers/ledger.ts +54 -0
  512. package/src/cli/handlers/migrate.ts +21 -12
  513. package/src/cli/handlers/misc.ts +10 -1
  514. package/src/cli/handlers/pr-env.ts +2 -2
  515. package/src/cli/handlers/pr.ts +176 -37
  516. package/src/cli/handlers/query.ts +16 -4
  517. package/src/cli/handlers/release.ts +6 -3
  518. package/src/cli/handlers/search.ts +13 -34
  519. package/src/cli/main.test.ts +17 -0
  520. package/src/cli/main.ts +87 -14
  521. package/src/cli/parse-args.ts +35 -2
  522. package/src/cli/registry.ts +22 -4
  523. package/src/components/builders.ts +8 -0
  524. package/src/components/capability-plugin-loader.ts +13 -1
  525. package/src/components/capability-plugin.ts +16 -0
  526. package/src/components/pr-env-run-bundle.test.ts +1 -0
  527. package/src/components/pr-hooks.test.ts +73 -5
  528. package/src/components/pr-hooks.ts +95 -5
  529. package/src/components/pr-pipeline.ts +64 -16
  530. package/src/components/pr-policy.test.ts +64 -1
  531. package/src/components/pr-policy.ts +53 -0
  532. package/src/components/pr-run.ts +4 -0
  533. package/src/components/registry.test.ts +1 -1
  534. package/src/components/starter-plugin.ts +6 -0
  535. package/src/components/verbs/index.ts +2 -0
  536. package/src/components/verbs/publish-artifact.test.ts +60 -0
  537. package/src/components/verbs/publish-artifact.ts +122 -0
  538. package/src/components/verbs/publish-image.test.ts +101 -0
  539. package/src/components/verbs/publish-image.ts +265 -0
  540. package/src/config-environments.ts +28 -1
  541. package/src/config.ts +24 -1
  542. package/src/gate-decision-bundle.test.ts +5 -2
  543. package/src/gate-decision-identity.test.ts +20 -0
  544. package/src/gate-decision.ts +46 -3
  545. package/src/graph-ir.ts +27 -0
  546. package/src/graph-walk.test.ts +44 -0
  547. package/src/graph-walk.ts +102 -0
  548. package/src/import/parser.ts +20 -0
  549. package/src/lexicon.ts +98 -6
  550. package/src/lifecycle/gate-ledger.ts +8 -0
  551. package/src/lifecycle/git.ts +2 -2
  552. package/src/lifecycle/ledger-read.test.ts +107 -0
  553. package/src/lifecycle/ledger-read.ts +195 -0
  554. package/src/lifecycle/release-workstation.ts +24 -0
  555. package/src/lifecycle/run-ledger.ts +5 -2
  556. package/src/lint/cross-root.test.ts +122 -0
  557. package/src/lint/discover.ts +3 -1
  558. package/src/lint/post-synth.ts +212 -2
  559. package/src/lint/rules/op/index.ts +2 -0
  560. package/src/lint/rules/op/ops019-workspace-op-target.ts +89 -0
  561. package/src/op/activities/activity-contracts.ts +5 -2
  562. package/src/op/activities/index.ts +2 -2
  563. package/src/op/activities/review-agent.test.ts +221 -4
  564. package/src/op/activities/review-agent.ts +265 -22
  565. package/src/op/activities/work-evidence.ts +9 -1
  566. package/src/op/builders.ts +9 -5
  567. package/src/op/composites/apply-op.test.ts +108 -0
  568. package/src/op/composites/apply-op.ts +37 -2
  569. package/src/op/gate-approval-at-base.test.ts +97 -0
  570. package/src/op/gate-approval-base.ts +72 -0
  571. package/src/op/gate-approval.test.ts +6 -1
  572. package/src/op/gate-approval.ts +22 -27
  573. package/src/op/gate-name.ts +16 -1
  574. package/src/op/gate-quorum-at-base.test.ts +211 -0
  575. package/src/op/gate-quorum.ts +156 -8
  576. package/src/op/gate-review.ts +14 -0
  577. package/src/op/gate.ts +69 -9
  578. package/src/op/index.ts +3 -2
  579. package/src/op/local-executor.ts +91 -11
  580. package/src/op/op-waves-depends-on.test.ts +81 -0
  581. package/src/op/op-waves-hook-targets.test.ts +4 -2
  582. package/src/op/op-waves-quorum-product.test.ts +158 -0
  583. package/src/op/op-waves-run.ts +288 -14
  584. package/src/op/op-waves.ts +143 -14
  585. package/src/op/runtime.ts +8 -0
  586. package/src/op/schedule-overdue.ts +33 -0
  587. package/src/op/stage-hooks.ts +13 -5
  588. package/src/op/workspace-run-env.ts +29 -0
  589. package/src/pr-forge-bundle.test.ts +2 -2
  590. package/src/pr-forge.test.ts +47 -0
  591. package/src/pr-forge.ts +68 -0
  592. package/src/pr-loop-bundle.test.ts +3 -2
  593. package/src/pr-loop.test.ts +52 -0
  594. package/src/pr-loop.ts +12 -0
  595. package/src/pr-migrations.ts +97 -0
  596. package/src/pr-post-open.e2e.test.ts +36 -0
  597. package/src/pr-post.test.ts +102 -3
  598. package/src/pr-post.ts +90 -2
  599. package/src/read-filter.test.ts +54 -0
  600. package/src/read-filter.ts +193 -0
  601. package/src/run-event.ts +1 -1
  602. package/src/stage-hooks-bundle.test.ts +1 -0
  603. package/src/terraform/carve.ts +52 -15
  604. package/src/terraform/cuts.test.ts +173 -0
  605. package/src/terraform/cuts.ts +188 -0
  606. package/src/terraform/folds.ts +44 -0
  607. package/src/terraform/graph.ts +9 -0
  608. package/src/terraform/radius.test.ts +90 -0
  609. package/src/terraform/reach.test.ts +146 -0
  610. package/src/terraform/reach.ts +247 -0
  611. package/src/terraform/score.ts +27 -29
  612. package/src/terraform/state.ts +3 -1
  613. package/src/terraform/types.ts +8 -0
  614. package/src/wave-blocked.ts +28 -3
  615. package/src/wave-loop-golden.test.ts +4 -3
  616. package/src/workspace/__fixtures__/import/spacelift.json +9 -0
  617. package/src/workspace/__fixtures__/import/tfc.json +13 -0
  618. package/src/workspace/behold-kinds.test.ts +2 -1
  619. package/src/workspace/check-contract.test.ts +8 -6
  620. package/src/workspace/check.schema.json +13 -2
  621. package/src/workspace/checks/ci-branch.test.ts +128 -0
  622. package/src/workspace/checks/ci-branch.ts +141 -0
  623. package/src/workspace/checks/gate-approval.test.ts +77 -0
  624. package/src/workspace/checks/gate-approval.ts +121 -0
  625. package/src/workspace/checks/links.ts +12 -1
  626. package/src/workspace/checks/org.ts +169 -0
  627. package/src/workspace/checks.ts +68 -7
  628. package/src/workspace/ci-branches.ts +31 -0
  629. package/src/workspace/ci-green-branches.test.ts +153 -0
  630. package/src/workspace/ci-green-cli.ts +7 -7
  631. package/src/workspace/ci-green-workflow.test.ts +4 -3
  632. package/src/workspace/ci-green-workflow.ts +13 -9
  633. package/src/workspace/ci-green.ts +49 -12
  634. package/src/workspace/ci-run.test.ts +72 -1
  635. package/src/workspace/ci-run.ts +68 -8
  636. package/src/workspace/ci-serve.test.ts +95 -0
  637. package/src/workspace/ci-serve.ts +106 -65
  638. package/src/workspace/component-environments.test.ts +25 -2
  639. package/src/workspace/compose-graph.ts +9 -2
  640. package/src/workspace/composites.schema.json +22 -0
  641. package/src/workspace/composites.test.ts +39 -8
  642. package/src/workspace/composites.ts +6 -2
  643. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
  644. package/src/workspace/cross-root-lint.test.ts +90 -0
  645. package/src/workspace/cross-root-lint.ts +126 -0
  646. package/src/workspace/dbt-kind.test.ts +3 -2
  647. package/src/workspace/dbt-models-reader.ts +362 -0
  648. package/src/workspace/dbt-models.test.ts +248 -0
  649. package/src/workspace/dbt-project-common.ts +113 -0
  650. package/src/workspace/dbt-project-reader.ts +29 -112
  651. package/src/workspace/decision-rank.ts +40 -0
  652. package/src/workspace/declaration.schema.json +166 -24
  653. package/src/workspace/declaration.ts +101 -15
  654. package/src/workspace/environments.ts +40 -4
  655. package/src/workspace/exit-cli.ts +219 -0
  656. package/src/workspace/exit.schema.json +257 -0
  657. package/src/workspace/exit.test.ts +185 -0
  658. package/src/workspace/exit.ts +560 -0
  659. package/src/workspace/flyway-dir.ts +113 -0
  660. package/src/workspace/flyway-layout.ts +337 -0
  661. package/src/workspace/graph-cache.ts +12 -2
  662. package/src/workspace/graph-cli.ts +25 -10
  663. package/src/workspace/graph-contract.test.ts +4 -2
  664. package/src/workspace/graph.schema.json +34 -0
  665. package/src/workspace/identity.test.ts +29 -2
  666. package/src/workspace/import-fetch.ts +151 -0
  667. package/src/workspace/import-init.ts +147 -0
  668. package/src/workspace/import-source.test.ts +183 -0
  669. package/src/workspace/import-source.ts +462 -0
  670. package/src/workspace/init.test.ts +55 -1
  671. package/src/workspace/init.ts +66 -4
  672. package/src/workspace/intent.ts +6 -14
  673. package/src/workspace/kind-link-readers.ts +68 -3
  674. package/src/workspace/kind-readers.test.ts +36 -0
  675. package/src/workspace/kind-readers.ts +18 -1
  676. package/src/workspace/kinds.test.ts +35 -7
  677. package/src/workspace/kinds.ts +212 -11
  678. package/src/workspace/ledger.schema.json +158 -0
  679. package/src/workspace/lexicon-outputs.ts +132 -13
  680. package/src/workspace/lineage-check.ts +30 -7
  681. package/src/workspace/lineage-upgrade.test.ts +20 -0
  682. package/src/workspace/lineage-upgrade.ts +16 -7
  683. package/src/workspace/links.ts +24 -1
  684. package/src/workspace/ls-contract.test.ts +1 -1
  685. package/src/workspace/ls.schema.json +7 -0
  686. package/src/workspace/ls.ts +10 -1
  687. package/src/workspace/member-commands.ts +60 -2
  688. package/src/workspace/member-file-schema.test.ts +124 -0
  689. package/src/workspace/migrate-links.test.ts +99 -0
  690. package/src/workspace/migrate-links.ts +108 -0
  691. package/src/workspace/migration-dirs.test.ts +272 -0
  692. package/src/workspace/migration-dirs.ts +267 -0
  693. package/src/workspace/new-migrations.test.ts +98 -0
  694. package/src/workspace/new-migrations.ts +109 -0
  695. package/src/workspace/org-scope.test.ts +238 -0
  696. package/src/workspace/pr-report.schema.json +46 -0
  697. package/src/workspace/promote.test.ts +114 -0
  698. package/src/workspace/promote.ts +302 -0
  699. package/src/workspace/reason-codes.test.ts +10 -1
  700. package/src/workspace/reason-codes.ts +25 -0
  701. package/src/workspace/records-cli.ts +58 -5
  702. package/src/workspace/records.schema.json +5 -0
  703. package/src/workspace/release-candidates.ts +7 -2
  704. package/src/workspace/release-train.ts +157 -0
  705. package/src/workspace/release-workflow.ts +4 -3
  706. package/src/workspace/repo-ref.ts +113 -0
  707. package/src/workspace/run-event.schema.json +2 -2
  708. package/src/workspace/runs-cli.ts +48 -4
  709. package/src/workspace/runs.schema.json +7 -0
  710. package/src/workspace/status-contract.test.ts +46 -1
  711. package/src/workspace/status-decisions.test.ts +100 -0
  712. package/src/workspace/status-decisions.ts +151 -0
  713. package/src/workspace/status-gates.ts +9 -0
  714. package/src/workspace/status.schema.json +6 -0
  715. package/src/workspace/status.ts +17 -3
  716. package/src/workspace/where-filters.test.ts +39 -0
  717. package/src/workspace/work-cli.ts +5 -3
  718. package/src/workspace/work-evidence-ops.test.ts +176 -0
  719. package/src/workspace/work-evidence-ops.ts +189 -0
  720. package/src/workspace/work-evidence.schema.json +49 -2
  721. package/src/workspace/work-evidence.ts +22 -3
  722. package/src/workspace/workspace-gates.ts +200 -0
  723. package/src/workspace/workspace-kinds.schema.json +108 -10
  724. package/src/workspace/workspace-ledger.ts +130 -0
  725. package/src/workspace/workspace-run.test.ts +322 -0
  726. package/src/workspace/workspace-run.ts +461 -0
@@ -31,6 +31,7 @@ describe("the stage-hooks bundle", () => {
31
31
  "packages/core/src/op/gate-quorum.ts",
32
32
  "packages/core/src/op/op-waves.ts",
33
33
  "packages/core/src/op/stage-hooks.ts",
34
+ "packages/core/src/wave-blocked.ts",
34
35
  "packages/core/src/wave-layout.ts",
35
36
  ]);
36
37
  expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(48 * 1024);
@@ -13,8 +13,9 @@
13
13
 
14
14
  import { inboundEdges, outboundEdges } from "./graph";
15
15
  import { foldParentOf, resolveTier } from "./tier-map";
16
- import { scoreEstate, type ScoreOptions } from "./score";
17
- import type { TfGraph } from "./types";
16
+ import { scoreEstate, type Peelability, type ScoreOptions } from "./score";
17
+ import { adjacencyOf, type Adjacency } from "../graph-walk";
18
+ import type { TfEdge, TfGraph, TfNode } from "./types";
18
19
 
19
20
  /** One resource in the carve set (the selected node plus its folded sub-resources). */
20
21
  export interface CarveMember {
@@ -85,24 +86,53 @@ export interface CarveReport {
85
86
  * that fold into it (same name, a `foldParentOf` child type whose parent is the
86
87
  * selection). Those are carried along and inlined, not left behind.
87
88
  */
88
- export function resolveCarveSet(graph: TfGraph, target: string): CarveMember[] {
89
- const byAddress = new Map(graph.nodes.map((n) => [n.address, n]));
89
+ export function resolveCarveSet(graph: TfGraph, target: string, index?: BoundaryIndex): CarveMember[] {
90
+ const { byAddress, foldChildren } = index ?? boundaryIndex(graph);
90
91
  const selected = byAddress.get(target);
91
92
  if (!selected) return [];
92
93
 
93
94
  const members: CarveMember[] = [{ address: selected.address, type: selected.type }];
94
95
  if (selected.kind === "resource" && selected.type) {
95
- for (const node of graph.nodes) {
96
- if (node.kind !== "resource" || !node.type) continue;
97
- const parentType = foldParentOf(node.type);
98
- if (parentType === selected.type && node.name === selected.name) {
99
- members.push({ address: node.address, type: node.type, foldedInto: selected.address });
100
- }
96
+ for (const node of foldChildren.get(foldKey(selected.type, selected.name)) ?? []) {
97
+ members.push({ address: node.address, type: node.type, foldedInto: selected.address });
101
98
  }
102
99
  }
103
100
  return members;
104
101
  }
105
102
 
103
+ const foldKey = (parentType: string, name: string): string => `${parentType}\u001f${name}`;
104
+
105
+ /**
106
+ * What `boundaryReport` looks up per target, built once per graph: nodes by
107
+ * address, the edges by both ends, and each fold parent's children in graph
108
+ * order. Without it each report scans the node and edge lists itself, which
109
+ * over a whole estate is O(V·E) (#4391).
110
+ */
111
+ export interface BoundaryIndex {
112
+ byAddress: Map<string, TfNode>;
113
+ adjacency: Adjacency<TfEdge>;
114
+ /** `foldKey(parent type, name)` → resource nodes that fold into that parent. */
115
+ foldChildren: Map<string, TfNode[]>;
116
+ }
117
+
118
+ export function boundaryIndex(graph: TfGraph): BoundaryIndex {
119
+ const foldChildren = new Map<string, TfNode[]>();
120
+ for (const node of graph.nodes) {
121
+ if (node.kind !== "resource" || !node.type) continue;
122
+ const parentType = foldParentOf(node.type);
123
+ if (!parentType) continue;
124
+ const key = foldKey(parentType, node.name);
125
+ const list = foldChildren.get(key);
126
+ if (list) list.push(node);
127
+ else foldChildren.set(key, [node]);
128
+ }
129
+ return {
130
+ byAddress: new Map(graph.nodes.map((n) => [n.address, n])),
131
+ adjacency: adjacencyOf(graph.edges),
132
+ foldChildren,
133
+ };
134
+ }
135
+
106
136
  /**
107
137
  * Which source the graph came from, and so which vocabulary its bridge patches
108
138
  * are named in. Terraform is the default; `cdk` is the read-only cloud-assembly
@@ -115,6 +145,13 @@ export interface BoundaryOptions {
115
145
  dialect?: CarveDialect;
116
146
  /** Scoring hooks, so `peelability` is the same number the ranking reported. */
117
147
  score?: ScoreOptions;
148
+ /**
149
+ * The target's score, when the caller already holds it (`carve advise`).
150
+ * Without it the estate is scored to find it (#4391).
151
+ */
152
+ peelability?: Pick<Peelability, "score">;
153
+ /** `boundaryIndex(graph)`, shared across calls on one graph. */
154
+ index?: BoundaryIndex;
118
155
  }
119
156
 
120
157
  /** The bridge patch an inbound cut needs, per source. */
@@ -131,7 +168,7 @@ function inboundBridge(dialect: CarveDialect, fromOutput: boolean): BoundaryEdge
131
168
  */
132
169
  export function boundaryReport(graph: TfGraph, target: string, opts: BoundaryOptions = {}): CarveReport | null {
133
170
  const dialect = opts.dialect ?? "terraform";
134
- const carveSet = resolveCarveSet(graph, target);
171
+ const carveSet = resolveCarveSet(graph, target, opts.index);
135
172
  if (carveSet.length === 0) return null;
136
173
  const inSet = new Set(carveSet.map((m) => m.address));
137
174
 
@@ -139,7 +176,7 @@ export function boundaryReport(graph: TfGraph, target: string, opts: BoundaryOpt
139
176
  const outbound: BoundaryEdge[] = [];
140
177
 
141
178
  for (const member of carveSet) {
142
- for (const e of inboundEdges(graph, member.address)) {
179
+ for (const e of (opts.index ? (opts.index.adjacency.in.get(member.address) ?? []) : inboundEdges(graph, member.address))) {
143
180
  if (inSet.has(e.from)) continue; // internal edge
144
181
  inbound.push({
145
182
  direction: "inbound",
@@ -152,7 +189,7 @@ export function boundaryReport(graph: TfGraph, target: string, opts: BoundaryOpt
152
189
  ...(e.crossStack ? { crossStack: true } : {}),
153
190
  });
154
191
  }
155
- for (const e of outboundEdges(graph, member.address)) {
192
+ for (const e of (opts.index ? (opts.index.adjacency.out.get(member.address) ?? []) : outboundEdges(graph, member.address))) {
156
193
  if (inSet.has(e.to)) continue; // internal edge
157
194
  outbound.push({
158
195
  direction: "outbound",
@@ -167,13 +204,13 @@ export function boundaryReport(graph: TfGraph, target: string, opts: BoundaryOpt
167
204
  }
168
205
  }
169
206
 
170
- const score = scoreEstate(graph, opts.score).find((r) => r.address === target);
207
+ const score = opts.peelability ?? scoreEstate(graph, { ...opts.score, radius: false }).find((r) => r.address === target);
171
208
  const diagnostics: string[] = [];
172
209
  const tierOf = opts.score?.tierOf ?? resolveTier;
173
210
  if (
174
211
  carveSet[0].type &&
175
212
  tierOf(carveSet[0].type) === null &&
176
- graph.nodes.find((n) => n.address === target)?.kind === "resource"
213
+ (opts.index?.byAddress ?? new Map(graph.nodes.map((n) => [n.address, n]))).get(target)?.kind === "resource"
177
214
  ) {
178
215
  diagnostics.push(`No known native mapping for ${carveSet[0].type} — emit will have no target type.`);
179
216
  }
@@ -0,0 +1,173 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { join } from "path";
3
+ import { loadHcl2json, parseTerraformDir } from "./parse";
4
+ import { closure, components, cuts } from "./cuts";
5
+ import type { TfEdge, TfGraph, TfNode } from "./types";
6
+
7
+ /**
8
+ * #3909: cuts and reference-closed sets. The proof runs against depth-estate
9
+ * (#2323) as `parseTerraformDir` builds it; several components, a cycle, a
10
+ * straddling output and cross-stack edges are on hand-built graphs.
11
+ */
12
+ let parserAvailable = false;
13
+ try {
14
+ await loadHcl2json();
15
+ parserAvailable = true;
16
+ } catch {
17
+ parserAvailable = false;
18
+ }
19
+
20
+ const ESTATE = join(__dirname, "__fixtures__", "depth-estate");
21
+ const ends = (edges: TfEdge[]): string[] => edges.map((e) => `${e.from} -> ${e.to}`);
22
+
23
+ const node = (address: string, extra: Partial<TfNode> = {}): TfNode => {
24
+ const [type, name] = address.split(".");
25
+ return { address, kind: "resource", type, name, instances: 1, hasDynamic: false, ...extra };
26
+ };
27
+ const edge = (from: string, to: string, extra: Partial<TfEdge> = {}): TfEdge => ({ from, to, attrs: [], via: [], ...extra });
28
+
29
+ const DEPTH_ESTATE_BLOCKS = [
30
+ "aws_cloudwatch_log_group.api",
31
+ "aws_db_subnet_group.main",
32
+ "aws_kms_key.data",
33
+ "aws_lambda_function.api",
34
+ "aws_lb.public",
35
+ "aws_lb_listener.https",
36
+ "aws_lb_target_group.app",
37
+ "aws_rds_cluster.main",
38
+ "aws_rds_cluster_instance.one",
39
+ "aws_s3_bucket.assets",
40
+ "aws_s3_bucket_versioning.assets",
41
+ "aws_security_group.app",
42
+ "aws_security_group.db",
43
+ "aws_subnet.a",
44
+ "aws_subnet.b",
45
+ "aws_subnet.private",
46
+ "aws_vpc.main",
47
+ "module.platform",
48
+ ];
49
+
50
+ describe("against depth-estate", () => {
51
+ test("components match the pinned list: one, with the module and the count hiding what it holds", async () => {
52
+ if (!parserAvailable) return;
53
+ const graph = await parseTerraformDir(ESTATE);
54
+ expect(components(graph)).toEqual([
55
+ {
56
+ resources: DEPTH_ESTATE_BLOCKS,
57
+ outputs: ["output.db_endpoint", "output.lb_dns"],
58
+ instances: "unknown",
59
+ hiddenBy: ["aws_subnet.private", "module.platform"],
60
+ },
61
+ ]);
62
+ });
63
+
64
+ test("cuts of a hand-picked set name each crossing edge with its direction", async () => {
65
+ if (!parserAvailable) return;
66
+ const graph = await parseTerraformDir(ESTATE);
67
+ const c = cuts(graph, ["aws_rds_cluster.main", "aws_rds_cluster_instance.one"]);
68
+ expect(ends(c.inbound)).toEqual([
69
+ "aws_lambda_function.api -> aws_rds_cluster.main",
70
+ "output.db_endpoint -> aws_rds_cluster.main",
71
+ ]);
72
+ expect(ends(c.outbound)).toEqual([
73
+ "aws_rds_cluster.main -> aws_db_subnet_group.main",
74
+ "aws_rds_cluster.main -> aws_kms_key.data",
75
+ "aws_rds_cluster.main -> aws_security_group.db",
76
+ ]);
77
+ expect(c.counts).toEqual({
78
+ inbound: { resources: 1, outputs: 1, crossStack: 0 },
79
+ outbound: { resources: 3, outputs: 0, crossStack: 0 },
80
+ });
81
+ expect(c.bothWays).toBe(true);
82
+ });
83
+
84
+ test("closure from a leaf with outbound allowed leaves only outbound cuts", async () => {
85
+ if (!parserAvailable) return;
86
+ const graph = await parseTerraformDir(ESTATE);
87
+ const leaf = closure(graph, ["aws_rds_cluster_instance.one"], "outbound");
88
+ expect(leaf.resources).toEqual(["aws_rds_cluster_instance.one"]);
89
+ expect(leaf.inbound).toEqual([]);
90
+ expect(ends(leaf.outbound)).toEqual(["aws_rds_cluster_instance.one -> aws_rds_cluster.main"]);
91
+
92
+ // A seed with dependents takes them all, outputs included.
93
+ const key = closure(graph, ["aws_kms_key.data"], "outbound");
94
+ expect(key.resources).toEqual(["aws_kms_key.data", "aws_lambda_function.api", "aws_rds_cluster.main", "aws_rds_cluster_instance.one"]);
95
+ expect(key.outputs).toEqual(["output.db_endpoint"]);
96
+ expect(key.inbound).toEqual([]);
97
+ expect(key.outbound.length).toBeGreaterThan(0);
98
+ expect(key.instances).toBe(4);
99
+ });
100
+
101
+ test("closure with inbound allowed takes what the seed depends on, and leaves only inbound cuts", async () => {
102
+ if (!parserAvailable) return;
103
+ const graph = await parseTerraformDir(ESTATE);
104
+ const c = closure(graph, ["aws_rds_cluster_instance.one"], "inbound");
105
+ expect(c.outbound).toEqual([]);
106
+ expect(c.resources).toContain("aws_vpc.main");
107
+ expect(c.inbound.every((e) => !c.resources.includes(e.from))).toBe(true);
108
+ });
109
+
110
+ test("closure with none allowed returns the leaf's whole component", async () => {
111
+ if (!parserAvailable) return;
112
+ const graph = await parseTerraformDir(ESTATE);
113
+ const c = closure(graph, ["aws_rds_cluster_instance.one"], "none");
114
+ expect(c.resources).toEqual(DEPTH_ESTATE_BLOCKS);
115
+ expect(c.outputs).toEqual(["output.db_endpoint", "output.lb_dns"]);
116
+ expect([...c.inbound, ...c.outbound]).toEqual([]);
117
+ expect(c.instances).toBe("unknown");
118
+ });
119
+
120
+ test("a set that splits a fold is contracted before its cuts are counted", async () => {
121
+ if (!parserAvailable) return;
122
+ const graph = await parseTerraformDir(ESTATE);
123
+ const child = cuts(graph, ["aws_s3_bucket_versioning.assets"]);
124
+ const parent = cuts(graph, ["aws_s3_bucket.assets"]);
125
+ expect(child).toEqual(parent);
126
+ expect(parent.resources).toEqual(["aws_s3_bucket.assets", "aws_s3_bucket_versioning.assets"]);
127
+ expect(ends(parent.inbound)).toEqual(["aws_lambda_function.api -> aws_s3_bucket.assets"]);
128
+ expect(parent.outbound).toEqual([]);
129
+ });
130
+ });
131
+
132
+ describe("on hand-built graphs", () => {
133
+ test("separate components stay separate, and an output never joins them", () => {
134
+ const graph: TfGraph = {
135
+ nodes: [node("aws_sqs_queue.a"), node("aws_sqs_queue.b"), node("aws_sns_topic.c")],
136
+ edges: [
137
+ edge("aws_sqs_queue.b", "aws_sqs_queue.a"),
138
+ edge("output.both", "aws_sqs_queue.a", { fromKind: "output" }),
139
+ edge("output.both", "aws_sns_topic.c", { fromKind: "output" }),
140
+ ],
141
+ };
142
+ expect(components(graph)).toEqual([
143
+ { resources: ["aws_sns_topic.c"], outputs: ["output.both"], instances: 1 },
144
+ { resources: ["aws_sqs_queue.a", "aws_sqs_queue.b"], outputs: ["output.both"], instances: 2 },
145
+ ]);
146
+ // The straddling output stays outside a none-closure, its read left as a cut.
147
+ const c = closure(graph, ["aws_sqs_queue.a"], "none");
148
+ expect(c.outputs).toEqual([]);
149
+ expect(ends(c.inbound)).toEqual(["output.both -> aws_sqs_queue.a"]);
150
+ });
151
+
152
+ test("a depends_on cycle terminates in every closure", () => {
153
+ const graph: TfGraph = {
154
+ nodes: [node("aws_sqs_queue.a"), node("aws_sqs_queue.b"), node("aws_sqs_queue.c")],
155
+ edges: [edge("aws_sqs_queue.a", "aws_sqs_queue.b"), edge("aws_sqs_queue.b", "aws_sqs_queue.c"), edge("aws_sqs_queue.c", "aws_sqs_queue.a")],
156
+ };
157
+ for (const allow of ["inbound", "outbound", "none"] as const) {
158
+ expect(closure(graph, ["aws_sqs_queue.a"], allow).resources).toEqual(["aws_sqs_queue.a", "aws_sqs_queue.b", "aws_sqs_queue.c"]);
159
+ }
160
+ });
161
+
162
+ test("crossStack cuts are counted apart", () => {
163
+ const graph: TfGraph = {
164
+ nodes: [node("Data/Bucket"), node("App/Fn")],
165
+ edges: [edge("App/Fn", "Data/Bucket", { crossStack: true })],
166
+ };
167
+ expect(cuts(graph, ["Data/Bucket"]).counts.inbound).toEqual({ resources: 1, outputs: 0, crossStack: 1 });
168
+ });
169
+
170
+ test("an address the graph does not hold is an error", () => {
171
+ expect(() => cuts({ nodes: [], edges: [] }, ["aws_vpc.nope"])).toThrow(/not a resource, module or output/);
172
+ });
173
+ });
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Cuts and reference-closed sets over the carve graph (#3909).
3
+ *
4
+ * `carve advise` scores one block at a time. A consumer splitting one root into
5
+ * several (terragucci's estate carving) needs the same question for a set:
6
+ * which edges cross its boundary and which way, and the smallest set around a
7
+ * seed whose crossings all run a way the caller allows. These are pure
8
+ * functions over `TfGraph`, on the index and walk in `reach.ts`, so the rules
9
+ * there hold here too: folds contract first, outputs are terminal and counted
10
+ * apart, modules and unresolved `count` are named as hiding their contents, a
11
+ * `depends_on` cycle terminates, and `crossStack` edges are counted apart.
12
+ *
13
+ * Nothing here ranks sets, reads state or plan times, writes HCL, or knows
14
+ * about roots or backends; that stays with the consumer.
15
+ */
16
+
17
+ import { carveIndex, cmp, hidesContents, isOutputAddress, walkFrom, weigh, type CarveIndex } from "./reach";
18
+ import type { TfEdge, TfGraph } from "./types";
19
+
20
+ /** Cut edges of one direction, counted. */
21
+ export interface CutCount {
22
+ /** Edges whose reading side is a resource or module block. */
23
+ resources: number;
24
+ /** Edges whose reading side is an `output` block. */
25
+ outputs: number;
26
+ /** Of all of them, the `crossStack` edges (a CDK `Fn::ImportValue`). */
27
+ crossStack: number;
28
+ }
29
+
30
+ /** A set of blocks, as the walks see it. */
31
+ export interface BlockSet {
32
+ /** Resource and module addresses, each fold parent with the blocks folded into it. Sorted. */
33
+ resources: string[];
34
+ /** `output.<name>` pseudo-addresses in the set. Sorted. */
35
+ outputs: string[];
36
+ }
37
+
38
+ export interface Cuts extends BlockSet {
39
+ /** Edges where something outside reads something inside, as the graph holds them. */
40
+ inbound: TfEdge[];
41
+ /** Edges where something inside reads something outside. */
42
+ outbound: TfEdge[];
43
+ counts: { inbound: CutCount; outbound: CutCount };
44
+ /** Cuts run in both directions: the set can move only with wiring both ways. */
45
+ bothWays: boolean;
46
+ }
47
+
48
+ /** The cut directions a closure may leave: `inbound` (outside may read in), `outbound` (inside may read out), or `none`. */
49
+ export type CutAllowance = "inbound" | "outbound" | "none";
50
+
51
+ export interface Closure extends Cuts {
52
+ /** The seeds as the walk saw them, folds contracted. */
53
+ seeds: string[];
54
+ allow: CutAllowance;
55
+ /**
56
+ * How many resource instances the set stands for: each block once, or its
57
+ * instance count once `--state` resolved it. `"unknown"` where a module or an
58
+ * unresolved `count`/`for_each` hides how much it holds; `hiddenBy` names them.
59
+ */
60
+ instances: number | "unknown";
61
+ hiddenBy?: string[];
62
+ }
63
+
64
+ export interface Component extends BlockSet {
65
+ /** As {@link Closure.instances}. */
66
+ instances: number | "unknown";
67
+ hiddenBy?: string[];
68
+ }
69
+
70
+ /**
71
+ * The graph's weakly connected components (#3909): blocks joined by a
72
+ * reference either way, fold-contracted, each module one node. An output never
73
+ * joins two components, since outputs are terminal; it is listed in every
74
+ * component it reads into. Components are sorted by their first address.
75
+ */
76
+ export function components(graph: TfGraph): Component[] {
77
+ const index = carveIndex(graph);
78
+ const assigned = new Set<string>();
79
+ const found: Component[] = [];
80
+ for (const address of [...index.nodes.keys()].sort(cmp)) {
81
+ const start = index.rep(address);
82
+ if (assigned.has(start)) continue;
83
+ const reached = walkFrom(index, [start], "both");
84
+ const reps = [start, ...[...reached.keys()].filter((a) => !isOutputAddress(a))];
85
+ for (const rep of reps) assigned.add(rep);
86
+ found.push({ ...blockSet(index, reps, [...reached.keys()].filter(isOutputAddress)), ...holdings(index, reps) });
87
+ }
88
+ return found;
89
+ }
90
+
91
+ /**
92
+ * Every edge crossing the boundary of `addresses` (#3909). A folded block in
93
+ * the set brings its parent and siblings, and a fold parent its folded blocks,
94
+ * before anything is counted, so an edge inside one fold is never a cut. An
95
+ * `output.<name>` in the set is inside it like any block.
96
+ *
97
+ * Throws when an address is not a block of the graph.
98
+ */
99
+ export function cuts(graph: TfGraph, addresses: Iterable<string>): Cuts {
100
+ const index = carveIndex(graph);
101
+ return cutsOf(index, contract(index, addresses));
102
+ }
103
+
104
+ /**
105
+ * The smallest superset of `seeds` whose remaining cuts all run in the
106
+ * direction `allow` permits, plus those cuts (#3909).
107
+ *
108
+ * - `outbound`: nothing outside may read in, so the set takes every
109
+ * dependent of the seeds, outputs included.
110
+ * - `inbound`: nothing inside may read out, so the set takes everything the
111
+ * seeds depend on.
112
+ * - `none`: the seeds' whole components, with each output that reads only
113
+ * into them.
114
+ *
115
+ * Outputs are terminal, so none of these walks passes through one. An output
116
+ * that reads both the set and something outside it is therefore a cut no
117
+ * closure removes: under `none` it stays outside and its read into the set is
118
+ * left as an inbound cut, for the caller to wire.
119
+ */
120
+ export function closure(graph: TfGraph, seeds: Iterable<string>, allow: CutAllowance): Closure {
121
+ const index = carveIndex(graph);
122
+ const start = contract(index, seeds);
123
+ const direction = allow === "outbound" ? "in" : allow === "inbound" ? "out" : "both";
124
+ const members = new Set([...start, ...walkFrom(index, start, direction).keys()]);
125
+ if (allow === "none") {
126
+ for (const address of [...members]) {
127
+ if (!isOutputAddress(address) || start.has(address)) continue;
128
+ const reads = (index.adj.out.get(address) ?? []).map((e) => e.to);
129
+ if (!reads.every((to) => members.has(to))) members.delete(address);
130
+ }
131
+ }
132
+ const found = cutsOf(index, members);
133
+ return {
134
+ ...found,
135
+ seeds: [...start].sort(cmp),
136
+ allow,
137
+ ...holdings(index, [...members].filter((a) => !isOutputAddress(a))),
138
+ };
139
+ }
140
+
141
+ /** The addresses as the walks see them: each fold child replaced by its parent. Throws on an unknown address. */
142
+ function contract(index: CarveIndex, addresses: Iterable<string>): Set<string> {
143
+ const out = new Set<string>();
144
+ for (const address of addresses) {
145
+ const known = index.nodes.has(address) || (isOutputAddress(address) && index.adj.out.has(address));
146
+ if (!known) throw new Error(`carve: ${address} is not a resource, module or output in this graph`);
147
+ out.add(index.rep(address));
148
+ }
149
+ return out;
150
+ }
151
+
152
+ function cutsOf(index: CarveIndex, members: ReadonlySet<string>): Cuts {
153
+ const inbound: TfEdge[] = [];
154
+ const outbound: TfEdge[] = [];
155
+ for (const address of members) {
156
+ for (const e of index.adj.in.get(address) ?? []) if (!members.has(e.from)) inbound.push(e.edge);
157
+ for (const e of index.adj.out.get(address) ?? []) if (!members.has(e.to)) outbound.push(e.edge);
158
+ }
159
+ const byEnds = (a: TfEdge, b: TfEdge): number => cmp(a.from, b.from) || cmp(a.to, b.to);
160
+ inbound.sort(byEnds);
161
+ outbound.sort(byEnds);
162
+ const reps = [...members];
163
+ return {
164
+ ...blockSet(index, reps.filter((a) => !isOutputAddress(a)), reps.filter(isOutputAddress)),
165
+ inbound,
166
+ outbound,
167
+ counts: { inbound: countOf(inbound), outbound: countOf(outbound) },
168
+ bothWays: inbound.length > 0 && outbound.length > 0,
169
+ };
170
+ }
171
+
172
+ function countOf(edges: readonly TfEdge[]): CutCount {
173
+ const outputs = edges.filter((e) => e.fromKind === "output").length;
174
+ return { resources: edges.length - outputs, outputs, crossStack: edges.filter((e) => e.crossStack).length };
175
+ }
176
+
177
+ function blockSet(index: CarveIndex, reps: readonly string[], outputs: readonly string[]): BlockSet {
178
+ return {
179
+ resources: reps.flatMap((rep) => index.membersOf(rep)).sort(cmp),
180
+ outputs: [...outputs].sort(cmp),
181
+ };
182
+ }
183
+
184
+ function holdings(index: CarveIndex, reps: readonly string[]): { instances: number | "unknown"; hiddenBy?: string[] } {
185
+ const instances = weigh(index, reps);
186
+ if (instances !== "unknown") return { instances };
187
+ return { instances, hiddenBy: reps.filter((a) => hidesContents(index.nodes.get(a)!)).sort(cmp) };
188
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Fold detection for the carve graph, shared by the scorer and the walks.
3
+ *
4
+ * A sub-resource whose type inlines into a parent (`foldParentOf`) carves with
5
+ * that parent when the parent is in the estate under the same name:
6
+ * `aws_s3_bucket_versioning.assets` goes with `aws_s3_bucket.assets`. The
7
+ * scorer drops it from the ranking and discounts its edge to the parent; the
8
+ * walks in `reach.ts` contract it into the parent before stepping, since an
9
+ * uncontracted walk counts the child as a separate dependent (#2325 measured a
10
+ * 100% overstatement on the sample estate). It lived privately in `score.ts`
11
+ * until the walks needed it.
12
+ */
13
+
14
+ import { foldParentOf } from "./tier-map";
15
+ import type { TfGraph } from "./types";
16
+
17
+ export interface Folds {
18
+ /** Every address that folds into a parent present in the estate. */
19
+ folded: Set<string>;
20
+ /** Parent address → the addresses folded into it. */
21
+ childrenOf: Map<string, Set<string>>;
22
+ }
23
+
24
+ /**
25
+ * Build the set of addresses that fold into a parent present in the estate, plus
26
+ * the reverse lookup (parent → its folded children), so a parent can discount
27
+ * inbound edges coming from its own folded sub-resources.
28
+ */
29
+ export function computeFolds(graph: TfGraph): Folds {
30
+ const byAddress = new Map(graph.nodes.map((n) => [n.address, n]));
31
+ const folded = new Set<string>();
32
+ const childrenOf = new Map<string, Set<string>>();
33
+ for (const node of graph.nodes) {
34
+ if (node.kind !== "resource" || !node.type) continue;
35
+ const parentType = foldParentOf(node.type);
36
+ if (!parentType) continue;
37
+ const parentAddress = `${parentType}.${node.name}`;
38
+ if (!byAddress.has(parentAddress)) continue; // sub-resource without its parent → score it on its own
39
+ folded.add(node.address);
40
+ if (!childrenOf.has(parentAddress)) childrenOf.set(parentAddress, new Set());
41
+ childrenOf.get(parentAddress)!.add(node.address);
42
+ }
43
+ return { folded, childrenOf };
44
+ }
@@ -270,6 +270,13 @@ function blockHasMeta(block: unknown, key: string): boolean {
270
270
  return !!block && typeof block === "object" && key in (block as Record<string, unknown>);
271
271
  }
272
272
 
273
+ /** Which expansion meta-argument a block carries, as a spreadable `TfNode` field (#2325). */
274
+ function expansionOf(block: unknown): { expansion?: "count" | "for_each" } {
275
+ if (blockHasMeta(block, "count")) return { expansion: "count" };
276
+ if (blockHasMeta(block, "for_each")) return { expansion: "for_each" };
277
+ return {};
278
+ }
279
+
273
280
  /**
274
281
  * The literal string at a dotted path into a block, or undefined when the path
275
282
  * misses or lands on an interpolation. A dotted path walks nested blocks —
@@ -366,6 +373,7 @@ export function buildGraph(tree: Hcl2JsonTree, exprRefs: ExpressionRefs): TfGrap
366
373
  name,
367
374
  instances: 1,
368
375
  hasDynamic: dynamic || touchesData,
376
+ ...expansionOf(block),
369
377
  identity: literalIdentity(block, type),
370
378
  dataSourceValues: dataSourceValues(block, type),
371
379
  });
@@ -386,6 +394,7 @@ export function buildGraph(tree: Hcl2JsonTree, exprRefs: ExpressionRefs): TfGrap
386
394
  name,
387
395
  instances: 1,
388
396
  hasDynamic: dynamic || touchesData,
397
+ ...expansionOf(block),
389
398
  });
390
399
  }
391
400
 
@@ -0,0 +1,90 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { join } from "path";
3
+ import { carveAdvise, carveJson, formatCarveReport } from "../cli/commands/carve";
4
+ import { loadHcl2json } from "./parse";
5
+ import { blastRadius } from "./reach";
6
+ import { scoreEstate } from "./score";
7
+ import type { TfEdge, TfGraph, TfNode } from "./types";
8
+
9
+ /** #2326: blast radius on advise, its own axis beside the score. */
10
+ let parserAvailable = false;
11
+ try {
12
+ await loadHcl2json();
13
+ parserAvailable = true;
14
+ } catch {
15
+ parserAvailable = false;
16
+ }
17
+
18
+ const ESTATE = join(__dirname, "__fixtures__", "depth-estate");
19
+ const STATE = join(ESTATE, "terraform.tfstate");
20
+
21
+ const node = (address: string, extra: Partial<TfNode> = {}): TfNode => {
22
+ const [type, name] = address.split(".");
23
+ return { address, kind: "resource", type, name, instances: 1, hasDynamic: false, ...extra };
24
+ };
25
+ const edge = (from: string, to: string, extra: Partial<TfEdge> = {}): TfEdge => ({ from, to, attrs: [], via: [], ...extra });
26
+
27
+ describe("radius on carve advise against depth-estate", () => {
28
+ test("each resource carries its radius in the JSON, and the score still sums from its penalties", async () => {
29
+ if (!parserAvailable) return;
30
+ const result = await carveAdvise({ from: ESTATE, statePath: STATE });
31
+ const byAddress = new Map(carveJson(result).resources.map((r) => [r.address, r]));
32
+ expect(byAddress.get("aws_db_subnet_group.main")?.radius).toEqual({ resources: 3, outputs: 1, depth: 2 });
33
+ expect(byAddress.get("aws_lambda_function.api")?.radius).toEqual({ resources: 0, outputs: 0, depth: 0 });
34
+ for (const r of byAddress.values()) {
35
+ if (r.breakdown.tier === null) continue;
36
+ const sum = 100 + Object.values(r.breakdown.penalties).reduce((a, b) => a + b, 0);
37
+ expect(r.score).toBe(Math.max(0, Math.min(100, sum)));
38
+ }
39
+ });
40
+
41
+ test("the subnet group outranks the cluster on peelability and out-reaches it on radius", async () => {
42
+ if (!parserAvailable) return;
43
+ const result = await carveAdvise({ from: ESTATE });
44
+ const group = result.results!.find((r) => r.address === "aws_db_subnet_group.main")!;
45
+ const cluster = result.results!.find((r) => r.address === "aws_rds_cluster.main")!;
46
+ expect(group.score).toBeGreaterThan(cluster.score);
47
+ expect(group.radius!.resources).toBeGreaterThan(cluster.radius!.resources as number);
48
+ });
49
+
50
+ test("a module or an unresolved count makes the resource count unknown, and says which", async () => {
51
+ if (!parserAvailable) return;
52
+ const bare = await carveAdvise({ from: ESTATE });
53
+ const vpc = bare.results!.find((r) => r.address === "aws_vpc.main")!;
54
+ expect(vpc.radius).toEqual({ resources: "unknown", outputs: 2, depth: 3, hiddenBy: ["aws_subnet.private", "module.platform"] });
55
+ const withState = await carveAdvise({ from: ESTATE, statePath: STATE });
56
+ expect(withState.results!.find((r) => r.address === "aws_vpc.main")!.radius!.hiddenBy).toEqual(["module.platform"]);
57
+ });
58
+
59
+ test("the human report prints the radius as a column", async () => {
60
+ if (!parserAvailable) return;
61
+ const text = formatCarveReport(await carveAdvise({ from: ESTATE }));
62
+ expect(text).toMatch(/ 80 {2}3\+1 d2 {2}aws_db_subnet_group\.main /);
63
+ expect(text).toMatch(/\?\+2 d3 {2}aws_vpc\.main /);
64
+ expect(text).toMatch(/ - {7}aws_lambda_function\.api /);
65
+ expect(text).toContain("Radius: resources+outputs");
66
+ });
67
+ });
68
+
69
+ describe("radius on hand-built graphs", () => {
70
+ test("instances weight the count once state resolved them, and only then", () => {
71
+ const graph = (resolved: boolean): TfGraph => ({
72
+ nodes: [
73
+ node("aws_vpc.main"),
74
+ node("aws_subnet.private", { expansion: "count", hasDynamic: true, instances: resolved ? 3 : 1, ...(resolved ? { instancesFrom: "state" as const } : {}) }),
75
+ ],
76
+ edges: [edge("aws_subnet.private", "aws_vpc.main")],
77
+ });
78
+ expect(blastRadius(graph(true), "aws_vpc.main")).toEqual({ resources: 3, outputs: 0, depth: 1 });
79
+ expect(blastRadius(graph(false), "aws_vpc.main")).toEqual({ resources: "unknown", outputs: 0, depth: 1, hiddenBy: ["aws_subnet.private"] });
80
+ });
81
+
82
+ test("on a graph with crossStack edges the radius splits off what sits across a stack boundary", () => {
83
+ const graph: TfGraph = {
84
+ nodes: [node("Data/Bucket"), node("Data/Reader"), node("App/Fn"), node("App/Alarm")],
85
+ edges: [edge("Data/Reader", "Data/Bucket"), edge("App/Fn", "Data/Bucket", { crossStack: true }), edge("App/Alarm", "App/Fn")],
86
+ };
87
+ const bucket = scoreEstate(graph, { tierOf: () => ({ tier: 1, mapsTo: "AWS::S3::Bucket" }) }).find((r) => r.address === "Data/Bucket")!;
88
+ expect(bucket.radius).toEqual({ resources: 3, outputs: 0, depth: 2, crossStack: 2 });
89
+ });
90
+ });