@intentius/chant 0.124.46 → 0.124.48

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 (633) 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 +21 -2
  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/ledger.d.ts +3 -0
  63. package/dist/cli/handlers/ledger.d.ts.map +1 -0
  64. package/dist/cli/handlers/ledger.js +55 -0
  65. package/dist/cli/handlers/migrate.d.ts.map +1 -1
  66. package/dist/cli/handlers/migrate.js +18 -11
  67. package/dist/cli/handlers/misc.d.ts.map +1 -1
  68. package/dist/cli/handlers/misc.js +12 -1
  69. package/dist/cli/handlers/pr-env.js +2 -2
  70. package/dist/cli/handlers/pr.d.ts +15 -1
  71. package/dist/cli/handlers/pr.d.ts.map +1 -1
  72. package/dist/cli/handlers/pr.js +143 -39
  73. package/dist/cli/handlers/query.d.ts +7 -2
  74. package/dist/cli/handlers/query.d.ts.map +1 -1
  75. package/dist/cli/handlers/query.js +16 -4
  76. package/dist/cli/handlers/release.d.ts.map +1 -1
  77. package/dist/cli/handlers/release.js +6 -3
  78. package/dist/cli/handlers/search.d.ts +2 -4
  79. package/dist/cli/handlers/search.d.ts.map +1 -1
  80. package/dist/cli/handlers/search.js +13 -39
  81. package/dist/cli/main.d.ts.map +1 -1
  82. package/dist/cli/main.js +65 -14
  83. package/dist/cli/parse-args.d.ts.map +1 -1
  84. package/dist/cli/parse-args.js +36 -2
  85. package/dist/cli/registry.d.ts +14 -4
  86. package/dist/cli/registry.d.ts.map +1 -1
  87. package/dist/components/builders.d.ts +5 -1
  88. package/dist/components/builders.d.ts.map +1 -1
  89. package/dist/components/builders.js +5 -0
  90. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  91. package/dist/components/capability-plugin-loader.js +16 -1
  92. package/dist/components/capability-plugin.d.ts +14 -0
  93. package/dist/components/capability-plugin.d.ts.map +1 -1
  94. package/dist/components/pr-hooks.d.ts +21 -1
  95. package/dist/components/pr-hooks.d.ts.map +1 -1
  96. package/dist/components/pr-hooks.js +72 -4
  97. package/dist/components/pr-pipeline.d.ts +45 -10
  98. package/dist/components/pr-pipeline.d.ts.map +1 -1
  99. package/dist/components/pr-pipeline.js +47 -14
  100. package/dist/components/pr-policy.d.ts +36 -0
  101. package/dist/components/pr-policy.d.ts.map +1 -1
  102. package/dist/components/pr-policy.js +36 -0
  103. package/dist/components/starter-plugin.d.ts +1 -0
  104. package/dist/components/starter-plugin.d.ts.map +1 -1
  105. package/dist/components/starter-plugin.js +5 -1
  106. package/dist/components/verbs/index.d.ts +2 -0
  107. package/dist/components/verbs/index.d.ts.map +1 -1
  108. package/dist/components/verbs/index.js +2 -0
  109. package/dist/components/verbs/publish-artifact.d.ts +65 -0
  110. package/dist/components/verbs/publish-artifact.d.ts.map +1 -0
  111. package/dist/components/verbs/publish-artifact.js +74 -0
  112. package/dist/components/verbs/publish-image.d.ts +129 -0
  113. package/dist/components/verbs/publish-image.d.ts.map +1 -0
  114. package/dist/components/verbs/publish-image.js +145 -0
  115. package/dist/config-environments.d.ts +20 -0
  116. package/dist/config-environments.d.ts.map +1 -1
  117. package/dist/config-environments.js +12 -0
  118. package/dist/config.d.ts +9 -1
  119. package/dist/config.d.ts.map +1 -1
  120. package/dist/config.js +6 -2
  121. package/dist/gate-decision.d.ts +11 -2
  122. package/dist/gate-decision.d.ts.map +1 -1
  123. package/dist/gate-decision.js +15 -3
  124. package/dist/graph-ir.d.ts +25 -0
  125. package/dist/graph-ir.d.ts.map +1 -1
  126. package/dist/graph-walk.d.ts +66 -0
  127. package/dist/graph-walk.d.ts.map +1 -0
  128. package/dist/graph-walk.js +70 -0
  129. package/dist/import/parser.d.ts +19 -0
  130. package/dist/import/parser.d.ts.map +1 -1
  131. package/dist/lexicon.d.ts +71 -6
  132. package/dist/lexicon.d.ts.map +1 -1
  133. package/dist/lifecycle/gate-ledger.d.ts +11 -0
  134. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  135. package/dist/lifecycle/git.d.ts +2 -5
  136. package/dist/lifecycle/git.d.ts.map +1 -1
  137. package/dist/lifecycle/ledger-read.d.ts +65 -0
  138. package/dist/lifecycle/ledger-read.d.ts.map +1 -0
  139. package/dist/lifecycle/ledger-read.js +160 -0
  140. package/dist/lifecycle/release-workstation.d.ts +7 -0
  141. package/dist/lifecycle/release-workstation.d.ts.map +1 -1
  142. package/dist/lifecycle/release-workstation.js +25 -0
  143. package/dist/lifecycle/run-ledger.d.ts +4 -3
  144. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  145. package/dist/lifecycle/run-ledger.js +1 -0
  146. package/dist/lint/discover.js +3 -1
  147. package/dist/lint/post-synth.d.ts +127 -1
  148. package/dist/lint/post-synth.d.ts.map +1 -1
  149. package/dist/lint/post-synth.js +84 -2
  150. package/dist/lint/rules/op/index.d.ts +1 -0
  151. package/dist/lint/rules/op/index.d.ts.map +1 -1
  152. package/dist/lint/rules/op/index.js +2 -0
  153. package/dist/lint/rules/op/ops019-workspace-op-target.d.ts +32 -0
  154. package/dist/lint/rules/op/ops019-workspace-op-target.d.ts.map +1 -0
  155. package/dist/lint/rules/op/ops019-workspace-op-target.js +63 -0
  156. package/dist/op/activities/activity-contracts.d.ts +5 -2
  157. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  158. package/dist/op/activities/activity-contracts.js +5 -2
  159. package/dist/op/activities/index.d.ts +2 -2
  160. package/dist/op/activities/index.d.ts.map +1 -1
  161. package/dist/op/activities/index.js +1 -1
  162. package/dist/op/activities/review-agent.d.ts +90 -4
  163. package/dist/op/activities/review-agent.d.ts.map +1 -1
  164. package/dist/op/activities/review-agent.js +217 -19
  165. package/dist/op/activities/work-evidence.d.ts +8 -1
  166. package/dist/op/activities/work-evidence.d.ts.map +1 -1
  167. package/dist/op/activities/work-evidence.js +1 -0
  168. package/dist/op/builders.d.ts +9 -5
  169. package/dist/op/builders.d.ts.map +1 -1
  170. package/dist/op/builders.js +9 -5
  171. package/dist/op/composites/apply-op.d.ts +29 -2
  172. package/dist/op/composites/apply-op.d.ts.map +1 -1
  173. package/dist/op/composites/apply-op.js +20 -1
  174. package/dist/op/gate-approval.d.ts +11 -5
  175. package/dist/op/gate-approval.d.ts.map +1 -1
  176. package/dist/op/gate-approval.js +11 -6
  177. package/dist/op/gate-quorum.d.ts +100 -3
  178. package/dist/op/gate-quorum.d.ts.map +1 -1
  179. package/dist/op/gate-quorum.js +108 -6
  180. package/dist/op/gate-review.d.ts +9 -0
  181. package/dist/op/gate-review.d.ts.map +1 -1
  182. package/dist/op/gate-review.js +13 -0
  183. package/dist/op/gate.d.ts +44 -3
  184. package/dist/op/gate.d.ts.map +1 -1
  185. package/dist/op/gate.js +25 -4
  186. package/dist/op/index.d.ts +3 -3
  187. package/dist/op/index.d.ts.map +1 -1
  188. package/dist/op/index.js +1 -1
  189. package/dist/op/local-executor.d.ts +38 -3
  190. package/dist/op/local-executor.d.ts.map +1 -1
  191. package/dist/op/local-executor.js +48 -6
  192. package/dist/op/op-waves-run.d.ts +26 -1
  193. package/dist/op/op-waves-run.d.ts.map +1 -1
  194. package/dist/op/op-waves-run.js +259 -13
  195. package/dist/op/op-waves.d.ts +72 -5
  196. package/dist/op/op-waves.d.ts.map +1 -1
  197. package/dist/op/op-waves.js +110 -15
  198. package/dist/op/runtime.d.ts +11 -0
  199. package/dist/op/runtime.d.ts.map +1 -1
  200. package/dist/op/schedule-overdue.d.ts +16 -0
  201. package/dist/op/schedule-overdue.d.ts.map +1 -1
  202. package/dist/op/schedule-overdue.js +36 -0
  203. package/dist/op/stage-hooks.d.ts +9 -2
  204. package/dist/op/stage-hooks.d.ts.map +1 -1
  205. package/dist/op/stage-hooks.js +7 -5
  206. package/dist/op/workspace-run-env.d.ts +22 -0
  207. package/dist/op/workspace-run-env.d.ts.map +1 -0
  208. package/dist/op/workspace-run-env.js +21 -0
  209. package/dist/pr-forge.d.ts +24 -0
  210. package/dist/pr-forge.d.ts.map +1 -1
  211. package/dist/pr-forge.js +47 -0
  212. package/dist/pr-post.d.ts +42 -0
  213. package/dist/pr-post.d.ts.map +1 -1
  214. package/dist/pr-post.js +67 -1
  215. package/dist/read-filter.d.ts +65 -0
  216. package/dist/read-filter.d.ts.map +1 -0
  217. package/dist/read-filter.js +179 -0
  218. package/dist/run-event.d.ts +1 -1
  219. package/dist/run-event.d.ts.map +1 -1
  220. package/dist/terraform/carve.d.ts +24 -3
  221. package/dist/terraform/carve.d.ts.map +1 -1
  222. package/dist/terraform/carve.js +32 -14
  223. package/dist/terraform/cuts.d.ts +97 -0
  224. package/dist/terraform/cuts.d.ts.map +1 -0
  225. package/dist/terraform/cuts.js +138 -0
  226. package/dist/terraform/folds.d.ts +26 -0
  227. package/dist/terraform/folds.d.ts.map +1 -0
  228. package/dist/terraform/folds.js +38 -0
  229. package/dist/terraform/graph.d.ts.map +1 -1
  230. package/dist/terraform/graph.js +10 -0
  231. package/dist/terraform/reach.d.ts +140 -0
  232. package/dist/terraform/reach.d.ts.map +1 -0
  233. package/dist/terraform/reach.js +170 -0
  234. package/dist/terraform/score.d.ts +14 -0
  235. package/dist/terraform/score.d.ts.map +1 -1
  236. package/dist/terraform/score.js +14 -31
  237. package/dist/terraform/state.d.ts.map +1 -1
  238. package/dist/terraform/state.js +4 -2
  239. package/dist/terraform/types.d.ts +8 -0
  240. package/dist/terraform/types.d.ts.map +1 -1
  241. package/dist/wave-blocked.d.ts +10 -1
  242. package/dist/wave-blocked.d.ts.map +1 -1
  243. package/dist/wave-blocked.js +31 -3
  244. package/dist/wave-runtime.mjs +263 -254
  245. package/dist/workspace/check.schema.json +11 -2
  246. package/dist/workspace/checks/ci-branch.d.ts +44 -0
  247. package/dist/workspace/checks/ci-branch.d.ts.map +1 -0
  248. package/dist/workspace/checks/ci-branch.js +133 -0
  249. package/dist/workspace/checks/links.d.ts +3 -0
  250. package/dist/workspace/checks/links.d.ts.map +1 -1
  251. package/dist/workspace/checks/links.js +12 -1
  252. package/dist/workspace/checks/org.d.ts +48 -0
  253. package/dist/workspace/checks/org.d.ts.map +1 -0
  254. package/dist/workspace/checks/org.js +156 -0
  255. package/dist/workspace/checks.d.ts +11 -1
  256. package/dist/workspace/checks.d.ts.map +1 -1
  257. package/dist/workspace/checks.js +59 -7
  258. package/dist/workspace/ci-branches.d.ts +16 -0
  259. package/dist/workspace/ci-branches.d.ts.map +1 -1
  260. package/dist/workspace/ci-branches.js +27 -0
  261. package/dist/workspace/ci-green-cli.js +7 -7
  262. package/dist/workspace/ci-green-workflow.d.ts +1 -1
  263. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  264. package/dist/workspace/ci-green-workflow.js +13 -8
  265. package/dist/workspace/ci-green.d.ts +14 -0
  266. package/dist/workspace/ci-green.d.ts.map +1 -1
  267. package/dist/workspace/ci-green.js +44 -11
  268. package/dist/workspace/ci-run.d.ts +9 -1
  269. package/dist/workspace/ci-run.d.ts.map +1 -1
  270. package/dist/workspace/ci-run.js +67 -6
  271. package/dist/workspace/ci-serve.d.ts +9 -0
  272. package/dist/workspace/ci-serve.d.ts.map +1 -1
  273. package/dist/workspace/ci-serve.js +114 -76
  274. package/dist/workspace/compose-graph.d.ts +14 -0
  275. package/dist/workspace/compose-graph.d.ts.map +1 -1
  276. package/dist/workspace/composites.d.ts +4 -1
  277. package/dist/workspace/composites.d.ts.map +1 -1
  278. package/dist/workspace/composites.js +5 -2
  279. package/dist/workspace/composites.schema.json +22 -0
  280. package/dist/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
  281. package/dist/workspace/cross-root-lint.d.ts +56 -0
  282. package/dist/workspace/cross-root-lint.d.ts.map +1 -0
  283. package/dist/workspace/cross-root-lint.js +111 -0
  284. package/dist/workspace/dbt-models-reader.d.ts +44 -0
  285. package/dist/workspace/dbt-models-reader.d.ts.map +1 -0
  286. package/dist/workspace/dbt-models-reader.js +356 -0
  287. package/dist/workspace/dbt-project-common.d.ts +40 -0
  288. package/dist/workspace/dbt-project-common.d.ts.map +1 -0
  289. package/dist/workspace/dbt-project-common.js +106 -0
  290. package/dist/workspace/dbt-project-reader.d.ts +4 -3
  291. package/dist/workspace/dbt-project-reader.d.ts.map +1 -1
  292. package/dist/workspace/dbt-project-reader.js +14 -108
  293. package/dist/workspace/decision-rank.d.ts +26 -0
  294. package/dist/workspace/decision-rank.d.ts.map +1 -0
  295. package/dist/workspace/decision-rank.js +16 -0
  296. package/dist/workspace/declaration.d.ts +43 -2
  297. package/dist/workspace/declaration.d.ts.map +1 -1
  298. package/dist/workspace/declaration.js +45 -11
  299. package/dist/workspace/declaration.schema.json +133 -24
  300. package/dist/workspace/environments.d.ts +26 -2
  301. package/dist/workspace/environments.d.ts.map +1 -1
  302. package/dist/workspace/environments.js +26 -2
  303. package/dist/workspace/flyway-dir.d.ts +39 -0
  304. package/dist/workspace/flyway-dir.d.ts.map +1 -0
  305. package/dist/workspace/flyway-dir.js +104 -0
  306. package/dist/workspace/flyway-layout.d.ts +96 -0
  307. package/dist/workspace/flyway-layout.d.ts.map +1 -0
  308. package/dist/workspace/flyway-layout.js +308 -0
  309. package/dist/workspace/graph-cli.d.ts.map +1 -1
  310. package/dist/workspace/graph-cli.js +26 -8
  311. package/dist/workspace/graph.schema.json +34 -0
  312. package/dist/workspace/intent.d.ts +2 -3
  313. package/dist/workspace/intent.d.ts.map +1 -1
  314. package/dist/workspace/intent.js +5 -9
  315. package/dist/workspace/kind-link-readers.d.ts +59 -2
  316. package/dist/workspace/kind-link-readers.d.ts.map +1 -1
  317. package/dist/workspace/kind-link-readers.js +17 -1
  318. package/dist/workspace/kinds.d.ts +66 -2
  319. package/dist/workspace/kinds.d.ts.map +1 -1
  320. package/dist/workspace/kinds.js +98 -4
  321. package/dist/workspace/ledger.schema.json +158 -0
  322. package/dist/workspace/lexicon-outputs.d.ts +20 -1
  323. package/dist/workspace/lexicon-outputs.d.ts.map +1 -1
  324. package/dist/workspace/lexicon-outputs.js +102 -10
  325. package/dist/workspace/lineage-check.d.ts +3 -0
  326. package/dist/workspace/lineage-check.d.ts.map +1 -1
  327. package/dist/workspace/lineage-check.js +23 -4
  328. package/dist/workspace/links.d.ts +8 -1
  329. package/dist/workspace/links.d.ts.map +1 -1
  330. package/dist/workspace/links.js +25 -1
  331. package/dist/workspace/ls.d.ts +8 -0
  332. package/dist/workspace/ls.d.ts.map +1 -1
  333. package/dist/workspace/ls.js +7 -1
  334. package/dist/workspace/ls.schema.json +7 -0
  335. package/dist/workspace/member-commands.d.ts.map +1 -1
  336. package/dist/workspace/member-commands.js +54 -5
  337. package/dist/workspace/migrate-links.d.ts +45 -0
  338. package/dist/workspace/migrate-links.d.ts.map +1 -0
  339. package/dist/workspace/migrate-links.js +100 -0
  340. package/dist/workspace/migration-dirs.d.ts +74 -0
  341. package/dist/workspace/migration-dirs.d.ts.map +1 -0
  342. package/dist/workspace/migration-dirs.js +254 -0
  343. package/dist/workspace/reason-codes.d.ts +8 -0
  344. package/dist/workspace/reason-codes.d.ts.map +1 -1
  345. package/dist/workspace/reason-codes.js +11 -0
  346. package/dist/workspace/records-cli.d.ts +11 -0
  347. package/dist/workspace/records-cli.d.ts.map +1 -1
  348. package/dist/workspace/records-cli.js +57 -5
  349. package/dist/workspace/records.schema.json +5 -0
  350. package/dist/workspace/release-candidates.d.ts +2 -0
  351. package/dist/workspace/release-candidates.d.ts.map +1 -1
  352. package/dist/workspace/release-candidates.js +5 -2
  353. package/dist/workspace/release-train.d.ts +53 -0
  354. package/dist/workspace/release-train.d.ts.map +1 -0
  355. package/dist/workspace/release-train.js +134 -0
  356. package/dist/workspace/release-workflow.d.ts.map +1 -1
  357. package/dist/workspace/release-workflow.js +4 -3
  358. package/dist/workspace/repo-ref.d.ts +45 -0
  359. package/dist/workspace/repo-ref.d.ts.map +1 -0
  360. package/dist/workspace/repo-ref.js +114 -0
  361. package/dist/workspace/run-event.schema.json +2 -2
  362. package/dist/workspace/runs-cli.d.ts +9 -0
  363. package/dist/workspace/runs-cli.d.ts.map +1 -1
  364. package/dist/workspace/runs-cli.js +48 -3
  365. package/dist/workspace/runs.schema.json +7 -0
  366. package/dist/workspace/search-contract.d.ts +1 -1
  367. package/dist/workspace/search-contract.d.ts.map +1 -1
  368. package/dist/workspace/status-decisions.d.ts +37 -0
  369. package/dist/workspace/status-decisions.d.ts.map +1 -0
  370. package/dist/workspace/status-decisions.js +147 -0
  371. package/dist/workspace/status-gates.d.ts +9 -0
  372. package/dist/workspace/status-gates.d.ts.map +1 -1
  373. package/dist/workspace/status.d.ts +4 -0
  374. package/dist/workspace/status.d.ts.map +1 -1
  375. package/dist/workspace/status.js +14 -2
  376. package/dist/workspace/status.schema.json +6 -0
  377. package/dist/workspace/work-cli.d.ts.map +1 -1
  378. package/dist/workspace/work-cli.js +5 -3
  379. package/dist/workspace/work-evidence-ops.d.ts +83 -0
  380. package/dist/workspace/work-evidence-ops.d.ts.map +1 -0
  381. package/dist/workspace/work-evidence-ops.js +169 -0
  382. package/dist/workspace/work-evidence.d.ts +12 -2
  383. package/dist/workspace/work-evidence.d.ts.map +1 -1
  384. package/dist/workspace/work-evidence.js +21 -3
  385. package/dist/workspace/work-evidence.schema.json +49 -2
  386. package/dist/workspace/workspace-gates.d.ts +110 -0
  387. package/dist/workspace/workspace-gates.d.ts.map +1 -0
  388. package/dist/workspace/workspace-gates.js +140 -0
  389. package/dist/workspace/workspace-kinds.schema.json +58 -3
  390. package/dist/workspace/workspace-ledger.d.ts +76 -0
  391. package/dist/workspace/workspace-ledger.d.ts.map +1 -0
  392. package/dist/workspace/workspace-ledger.js +91 -0
  393. package/dist/workspace/workspace-run.d.ts +112 -0
  394. package/dist/workspace/workspace-run.d.ts.map +1 -0
  395. package/dist/workspace/workspace-run.js +432 -0
  396. package/package.json +1 -1
  397. package/src/__golden__/wave-loop/op-by-apply-after-fails.json +10 -0
  398. package/src/__golden__/wave-loop/op-by-apply-hold.json +10 -0
  399. package/src/__golden__/wave-loop/op-by-apply-ignored.json +10 -0
  400. package/src/__golden__/wave-loop/op-by-apply-pass.json +10 -0
  401. package/src/__golden__/wave-loop/op-by-apply-skip.json +10 -0
  402. package/src/__golden__/wave-loop/op-cost-member-clash.json +6 -0
  403. package/src/__golden__/wave-loop/op-cost-member.json +13 -0
  404. package/src/__golden__/wave-loop/op-denied-source.json +16 -0
  405. package/src/__golden__/wave-loop/op-denied.json +16 -0
  406. package/src/__golden__/wave-loop/op-discover-rest-short.json +6 -0
  407. package/src/__golden__/wave-loop/op-discover-rest-stops.json +13 -0
  408. package/src/__golden__/wave-loop/op-discover-rest.json +26 -0
  409. package/src/__golden__/wave-loop/op-discover-wide.json +26 -0
  410. package/src/__golden__/wave-loop/op-held-on-again.json +16 -0
  411. package/src/__golden__/wave-loop/op-held-on-before-apply.json +16 -0
  412. package/src/__golden__/wave-loop/op-held-on.json +16 -0
  413. package/src/__golden__/wave-loop/op-hooks-apply-fails.json +13 -0
  414. package/src/__golden__/wave-loop/op-hooks-fail-after-apply.json +13 -0
  415. package/src/__golden__/wave-loop/op-hooks-fail-before-apply.json +13 -0
  416. package/src/__golden__/wave-loop/op-hooks-fail-before-plan.json +13 -0
  417. package/src/__golden__/wave-loop/op-hooks-held.json +13 -0
  418. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +13 -0
  419. package/src/__golden__/wave-loop/op-hooks-pass.json +13 -0
  420. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +13 -0
  421. package/src/__golden__/wave-loop/op-hooks-share.json +33 -0
  422. package/src/__golden__/wave-loop/op-outcome-held.json +10 -0
  423. package/src/__golden__/wave-loop/op-outcome-replan-approved.json +10 -0
  424. package/src/__golden__/wave-loop/op-outcome-replan-forever.json +10 -0
  425. package/src/__golden__/wave-loop/op-outcome-replan.json +10 -0
  426. package/src/__golden__/wave-loop/op-outcome-stood-down.json +10 -0
  427. package/src/__golden__/wave-loop/op-outcome-unreadable.json +10 -0
  428. package/src/__golden__/wave-loop/op-overridden.json +16 -0
  429. package/src/__golden__/wave-loop/op-per-run.json +20 -0
  430. package/src/__golden__/wave-loop/op-point.json +13 -0
  431. package/src/__golden__/wave-loop/op-pr-review.json +10 -0
  432. package/src/__golden__/wave-loop/op-sealed.json +10 -0
  433. package/src/__golden__/wave-loop/op-spend-change-only.json +10 -0
  434. package/src/__golden__/wave-loop/op-spend.json +10 -0
  435. package/src/__golden__/wave-loop/op-wave-commands.json +55 -0
  436. package/src/__golden__/wave-loop/op-wide-refused.json +26 -0
  437. package/src/__golden__/wave-loop/op-wide-waiting.json +29 -0
  438. package/src/__golden__/wave-loop/op-wide.json +39 -0
  439. package/src/action-pins.test.ts +78 -0
  440. package/src/action-pins.ts +92 -0
  441. package/src/cli/commands/carve-score-once.test.ts +99 -0
  442. package/src/cli/commands/carve.ts +37 -6
  443. package/src/cli/commands/check-lexicon.test.ts +2 -2
  444. package/src/cli/commands/import-report.ts +71 -0
  445. package/src/cli/commands/import.ts +33 -3
  446. package/src/cli/commands/migrate-dialect.test.ts +145 -0
  447. package/src/cli/commands/migrate.ts +132 -11
  448. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +4 -4
  449. package/src/cli/handlers/approve.ts +23 -2
  450. package/src/cli/handlers/build.ts +7 -1
  451. package/src/cli/handlers/ledger.ts +54 -0
  452. package/src/cli/handlers/migrate.ts +21 -12
  453. package/src/cli/handlers/misc.ts +10 -1
  454. package/src/cli/handlers/pr-env.ts +2 -2
  455. package/src/cli/handlers/pr.ts +138 -36
  456. package/src/cli/handlers/query.ts +16 -4
  457. package/src/cli/handlers/release.ts +6 -3
  458. package/src/cli/handlers/search.ts +13 -34
  459. package/src/cli/main.test.ts +15 -0
  460. package/src/cli/main.ts +65 -14
  461. package/src/cli/parse-args.ts +27 -2
  462. package/src/cli/registry.ts +14 -4
  463. package/src/components/builders.ts +8 -0
  464. package/src/components/capability-plugin-loader.ts +13 -1
  465. package/src/components/capability-plugin.ts +16 -0
  466. package/src/components/pr-hooks.test.ts +73 -5
  467. package/src/components/pr-hooks.ts +95 -5
  468. package/src/components/pr-pipeline.ts +64 -16
  469. package/src/components/pr-policy.test.ts +64 -1
  470. package/src/components/pr-policy.ts +53 -0
  471. package/src/components/registry.test.ts +1 -1
  472. package/src/components/starter-plugin.ts +6 -0
  473. package/src/components/verbs/index.ts +2 -0
  474. package/src/components/verbs/publish-artifact.test.ts +60 -0
  475. package/src/components/verbs/publish-artifact.ts +122 -0
  476. package/src/components/verbs/publish-image.test.ts +101 -0
  477. package/src/components/verbs/publish-image.ts +265 -0
  478. package/src/config-environments.ts +28 -1
  479. package/src/config.ts +8 -1
  480. package/src/gate-decision-bundle.test.ts +1 -0
  481. package/src/gate-decision-identity.test.ts +9 -0
  482. package/src/gate-decision.ts +26 -3
  483. package/src/graph-ir.ts +27 -0
  484. package/src/graph-walk.test.ts +44 -0
  485. package/src/graph-walk.ts +102 -0
  486. package/src/import/parser.ts +20 -0
  487. package/src/lexicon.ts +74 -6
  488. package/src/lifecycle/gate-ledger.ts +8 -0
  489. package/src/lifecycle/git.ts +2 -2
  490. package/src/lifecycle/ledger-read.test.ts +107 -0
  491. package/src/lifecycle/ledger-read.ts +195 -0
  492. package/src/lifecycle/release-workstation.ts +24 -0
  493. package/src/lifecycle/run-ledger.ts +5 -2
  494. package/src/lint/cross-root.test.ts +122 -0
  495. package/src/lint/discover.ts +3 -1
  496. package/src/lint/post-synth.ts +212 -2
  497. package/src/lint/rules/op/index.ts +2 -0
  498. package/src/lint/rules/op/ops019-workspace-op-target.ts +89 -0
  499. package/src/op/activities/activity-contracts.ts +5 -2
  500. package/src/op/activities/index.ts +2 -2
  501. package/src/op/activities/review-agent.test.ts +221 -4
  502. package/src/op/activities/review-agent.ts +265 -22
  503. package/src/op/activities/work-evidence.ts +9 -1
  504. package/src/op/builders.ts +9 -5
  505. package/src/op/composites/apply-op.test.ts +108 -0
  506. package/src/op/composites/apply-op.ts +37 -2
  507. package/src/op/gate-approval.test.ts +6 -1
  508. package/src/op/gate-approval.ts +12 -6
  509. package/src/op/gate-quorum-at-base.test.ts +211 -0
  510. package/src/op/gate-quorum.ts +156 -8
  511. package/src/op/gate-review.ts +14 -0
  512. package/src/op/gate.ts +67 -8
  513. package/src/op/index.ts +3 -2
  514. package/src/op/local-executor.ts +69 -9
  515. package/src/op/op-waves-depends-on.test.ts +81 -0
  516. package/src/op/op-waves-hook-targets.test.ts +4 -2
  517. package/src/op/op-waves-quorum-product.test.ts +158 -0
  518. package/src/op/op-waves-run.ts +288 -14
  519. package/src/op/op-waves.ts +143 -14
  520. package/src/op/runtime.ts +8 -0
  521. package/src/op/schedule-overdue.ts +33 -0
  522. package/src/op/stage-hooks.ts +13 -5
  523. package/src/op/workspace-run-env.ts +29 -0
  524. package/src/pr-forge-bundle.test.ts +2 -2
  525. package/src/pr-forge.test.ts +47 -0
  526. package/src/pr-forge.ts +68 -0
  527. package/src/pr-post-open.e2e.test.ts +36 -0
  528. package/src/pr-post.test.ts +102 -3
  529. package/src/pr-post.ts +89 -1
  530. package/src/read-filter.test.ts +54 -0
  531. package/src/read-filter.ts +193 -0
  532. package/src/run-event.ts +1 -1
  533. package/src/stage-hooks-bundle.test.ts +1 -0
  534. package/src/terraform/carve.ts +52 -15
  535. package/src/terraform/cuts.test.ts +173 -0
  536. package/src/terraform/cuts.ts +188 -0
  537. package/src/terraform/folds.ts +44 -0
  538. package/src/terraform/graph.ts +9 -0
  539. package/src/terraform/radius.test.ts +90 -0
  540. package/src/terraform/reach.test.ts +146 -0
  541. package/src/terraform/reach.ts +247 -0
  542. package/src/terraform/score.ts +27 -29
  543. package/src/terraform/state.ts +3 -1
  544. package/src/terraform/types.ts +8 -0
  545. package/src/wave-blocked.ts +28 -3
  546. package/src/wave-loop-golden.test.ts +4 -3
  547. package/src/workspace/behold-kinds.test.ts +1 -1
  548. package/src/workspace/check-contract.test.ts +7 -6
  549. package/src/workspace/check.schema.json +11 -2
  550. package/src/workspace/checks/ci-branch.test.ts +128 -0
  551. package/src/workspace/checks/ci-branch.ts +141 -0
  552. package/src/workspace/checks/links.ts +12 -1
  553. package/src/workspace/checks/org.ts +169 -0
  554. package/src/workspace/checks.ts +64 -7
  555. package/src/workspace/ci-branches.ts +31 -0
  556. package/src/workspace/ci-green-branches.test.ts +153 -0
  557. package/src/workspace/ci-green-cli.ts +7 -7
  558. package/src/workspace/ci-green-workflow.test.ts +4 -3
  559. package/src/workspace/ci-green-workflow.ts +13 -9
  560. package/src/workspace/ci-green.ts +49 -12
  561. package/src/workspace/ci-run.test.ts +72 -1
  562. package/src/workspace/ci-run.ts +68 -8
  563. package/src/workspace/ci-serve.test.ts +95 -0
  564. package/src/workspace/ci-serve.ts +106 -65
  565. package/src/workspace/component-environments.test.ts +25 -2
  566. package/src/workspace/compose-graph.ts +9 -2
  567. package/src/workspace/composites.schema.json +22 -0
  568. package/src/workspace/composites.test.ts +39 -8
  569. package/src/workspace/composites.ts +6 -2
  570. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
  571. package/src/workspace/cross-root-lint.test.ts +90 -0
  572. package/src/workspace/cross-root-lint.ts +126 -0
  573. package/src/workspace/dbt-kind.test.ts +3 -2
  574. package/src/workspace/dbt-models-reader.ts +362 -0
  575. package/src/workspace/dbt-models.test.ts +248 -0
  576. package/src/workspace/dbt-project-common.ts +113 -0
  577. package/src/workspace/dbt-project-reader.ts +29 -112
  578. package/src/workspace/decision-rank.ts +40 -0
  579. package/src/workspace/declaration.schema.json +133 -24
  580. package/src/workspace/declaration.ts +84 -15
  581. package/src/workspace/environments.ts +40 -4
  582. package/src/workspace/flyway-dir.ts +113 -0
  583. package/src/workspace/flyway-layout.ts +337 -0
  584. package/src/workspace/graph-cli.ts +25 -10
  585. package/src/workspace/graph-contract.test.ts +4 -2
  586. package/src/workspace/graph.schema.json +34 -0
  587. package/src/workspace/identity.test.ts +14 -2
  588. package/src/workspace/intent.ts +6 -14
  589. package/src/workspace/kind-link-readers.ts +68 -3
  590. package/src/workspace/kinds.test.ts +12 -7
  591. package/src/workspace/kinds.ts +132 -7
  592. package/src/workspace/ledger.schema.json +158 -0
  593. package/src/workspace/lexicon-outputs.ts +132 -13
  594. package/src/workspace/lineage-check.ts +30 -7
  595. package/src/workspace/links.ts +24 -1
  596. package/src/workspace/ls-contract.test.ts +1 -1
  597. package/src/workspace/ls.schema.json +7 -0
  598. package/src/workspace/ls.ts +10 -1
  599. package/src/workspace/member-commands.ts +60 -2
  600. package/src/workspace/member-file-schema.test.ts +124 -0
  601. package/src/workspace/migrate-links.test.ts +99 -0
  602. package/src/workspace/migrate-links.ts +108 -0
  603. package/src/workspace/migration-dirs.test.ts +272 -0
  604. package/src/workspace/migration-dirs.ts +267 -0
  605. package/src/workspace/org-scope.test.ts +238 -0
  606. package/src/workspace/reason-codes.test.ts +6 -1
  607. package/src/workspace/reason-codes.ts +11 -0
  608. package/src/workspace/records-cli.ts +58 -5
  609. package/src/workspace/records.schema.json +5 -0
  610. package/src/workspace/release-candidates.ts +7 -2
  611. package/src/workspace/release-train.ts +157 -0
  612. package/src/workspace/release-workflow.ts +4 -3
  613. package/src/workspace/repo-ref.ts +113 -0
  614. package/src/workspace/run-event.schema.json +2 -2
  615. package/src/workspace/runs-cli.ts +48 -4
  616. package/src/workspace/runs.schema.json +7 -0
  617. package/src/workspace/status-contract.test.ts +46 -1
  618. package/src/workspace/status-decisions.test.ts +100 -0
  619. package/src/workspace/status-decisions.ts +151 -0
  620. package/src/workspace/status-gates.ts +9 -0
  621. package/src/workspace/status.schema.json +6 -0
  622. package/src/workspace/status.ts +17 -3
  623. package/src/workspace/where-filters.test.ts +39 -0
  624. package/src/workspace/work-cli.ts +5 -3
  625. package/src/workspace/work-evidence-ops.test.ts +176 -0
  626. package/src/workspace/work-evidence-ops.ts +189 -0
  627. package/src/workspace/work-evidence.schema.json +49 -2
  628. package/src/workspace/work-evidence.ts +22 -3
  629. package/src/workspace/workspace-gates.ts +200 -0
  630. package/src/workspace/workspace-kinds.schema.json +58 -3
  631. package/src/workspace/workspace-ledger.ts +130 -0
  632. package/src/workspace/workspace-run.test.ts +322 -0
  633. package/src/workspace/workspace-run.ts +461 -0
@@ -8,9 +8,11 @@
8
8
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
- import { afterAll, describe, expect, test } from "vitest";
12
- import { postPlanReport, readPlanReport } from "./pr-post";
13
- import { prNoteMarker, type PrReport } from "./pr-loop";
11
+ import { afterAll, describe, expect, test, vi } from "vitest";
12
+ import type { CommandContext } from "./cli/registry";
13
+ import { parseArgs } from "./cli/parse-args";
14
+ import { postOpenPlanReports, postPlanReport, readPlanReport } from "./pr-post";
15
+ import { prNoteMarker, prStatusState, type PrReport } from "./pr-loop";
14
16
  import type { ForgeStatus, PrForge } from "./pr-forge";
15
17
 
16
18
  const FIXTURE = join(import.meta.dirname, "__fixtures__", "pr-loop", "pr-plan.golden.json");
@@ -139,3 +141,100 @@ describe("pr-post asks for overdue scheduled Ops itself (#4289, #4295)", () => {
139
141
  expect(quiet.notes[0]!.body).not.toContain("forged warning");
140
142
  });
141
143
  });
144
+
145
+ describe("pr-post --open posts each open merge request's plan from a pipeline of the target branch (#4328)", () => {
146
+ const JOB_URL = (id: number) => `https://gitlab.com/acme/estate/-/jobs/${id}`;
147
+ const OTHER = "c".repeat(40);
148
+
149
+ /** A GitLab client with the merge requests' plan jobs, their artifacts and the statuses already posted. */
150
+ function gitlab(input: { jobs: Array<{ pr: number; head: string; id: number; status: string }>; artifacts: Record<number, string>; posted?: Record<string, string> }) {
151
+ const statuses: Array<{ sha: string; status: ForgeStatus; url?: string }> = [];
152
+ const notes: Array<{ pr: number; body: string }> = [];
153
+ const asked: Array<[string, string]> = [];
154
+ const reads: string[] = [];
155
+ let overdue = 0;
156
+ const forge = {
157
+ kind: "gitlab",
158
+ host: "gitlab.com",
159
+ async mergeRequestJobs(branch: string, job: string) {
160
+ asked.push([branch, job]);
161
+ return input.jobs.map((j) => ({ ...j, url: JOB_URL(j.id) }));
162
+ },
163
+ async jobArtifact(id: number, path: string) {
164
+ reads.push(`${id}:${path}`);
165
+ return input.artifacts[id];
166
+ },
167
+ async statusUrl(sha: string) {
168
+ return input.posted?.[sha];
169
+ },
170
+ async setStatus(this: PrForge, sha: string, status: ForgeStatus) {
171
+ statuses.push({ sha, status, ...(this.runUrl ? { url: this.runUrl } : {}) });
172
+ },
173
+ async upsertNote(pr: number, _marker: string, body: string) {
174
+ notes.push({ pr, body });
175
+ return "1";
176
+ },
177
+ } as unknown as PrForge;
178
+ const count = async () => {
179
+ overdue++;
180
+ return [{ op: "drift", cron: "0 * * * *", since: "2025-10-01T00:00:00Z", message: "Op `drift` is overdue" }];
181
+ };
182
+ return { forge, statuses, notes, asked, reads, overdue: count, overdueCalls: () => overdue };
183
+ }
184
+
185
+ test("a settled plan job's report is read from its artifacts and posted, linking the job; a running one waits", async () => {
186
+ const g = gitlab({
187
+ jobs: [
188
+ { pr: REPORT.pr!, head: HEAD, id: 101, status: "success" },
189
+ { pr: 8, head: OTHER, id: 102, status: "running" },
190
+ ],
191
+ artifacts: { 101: JSON.stringify(REPORT) },
192
+ });
193
+ const { posts, warnings } = await postOpenPlanReports({ forge: g.forge, branch: "main", job: "plan", artifactDir: ".chant/pr/", env: REPORT.env, overdue: g.overdue });
194
+ expect(warnings).toEqual([]);
195
+ expect(g.asked).toEqual([["main", "plan"]]);
196
+ expect(g.reads).toEqual(["101:.chant/pr/pr-plan.json"]);
197
+ expect(posts.map((p) => [p.pr, p.skipped ?? p.posted!.state])).toEqual([[REPORT.pr, prStatusState(REPORT)], [8, "the plan job is running"]]);
198
+ expect(g.statuses).toEqual([{ sha: HEAD, status: expect.objectContaining({ context: "chant/plan", state: prStatusState(REPORT), url: JOB_URL(101) }), url: JOB_URL(101) }]);
199
+ expect(g.notes.map((n) => n.pr)).toEqual([REPORT.pr]);
200
+ expect(g.notes[0]!.body).toContain("Op `drift` is overdue");
201
+ });
202
+
203
+ test("a job whose result is already posted is not posted again", async () => {
204
+ const g = gitlab({ jobs: [{ pr: REPORT.pr!, head: HEAD, id: 101, status: "success" }], artifacts: { 101: JSON.stringify(REPORT) }, posted: { [HEAD]: JOB_URL(101) } });
205
+ const { posts } = await postOpenPlanReports({ forge: g.forge, branch: "main", job: "plan", artifactDir: ".chant/pr", env: REPORT.env });
206
+ expect(posts).toEqual([{ pr: REPORT.pr, head: HEAD, skipped: "this plan job's result is already posted" }]);
207
+ expect(g.statuses).toEqual([]);
208
+ });
209
+
210
+ test("the pull request and head are GitLab's: a report for another head, a missing one and a failed job each fail the status", async () => {
211
+ const g = gitlab({
212
+ jobs: [
213
+ { pr: REPORT.pr!, head: OTHER, id: 101, status: "success" },
214
+ { pr: 8, head: HEAD, id: 102, status: "failed" },
215
+ { pr: 9, head: HEAD, id: 103, status: "failed" },
216
+ ],
217
+ artifacts: { 101: JSON.stringify(REPORT), 103: JSON.stringify({ ...REPORT, pr: 9 }) },
218
+ });
219
+ const { posts } = await postOpenPlanReports({ forge: g.forge, branch: "main", job: "plan", artifactDir: ".chant/pr", env: REPORT.env, overdue: g.overdue });
220
+ expect(posts.map((p) => [p.pr, p.posted!.state, p.posted!.refused ?? ""])).toEqual([
221
+ [REPORT.pr, "failure", expect.stringMatching(/the report is for head/)],
222
+ [8, "failure", "the plan job wrote no report"],
223
+ [9, "failure", ""],
224
+ ]);
225
+ expect(g.notes.map((n) => n.pr)).toEqual([9]);
226
+ // The check runs once, however many merge requests it posts for.
227
+ expect(g.overdueCalls()).toBe(1);
228
+ });
229
+
230
+ test("a forge that cannot list merge request jobs is refused", async () => {
231
+ await expect(postOpenPlanReports({ forge: fakeForge().forge, branch: "main", job: "plan", artifactDir: ".chant/pr", env: REPORT.env })).rejects.toThrow(/it is for GitLab/);
232
+ });
233
+ });
234
+
235
+ describe("chant components pr-post --open refuses what it cannot post for (#4328)", () => {
236
+
237
+ test("the flags parse", () => {
238
+ expect(parseArgs(["components", "pr-post", "--open", "--env", "prod", "--forge", "gitlab", "--branch", "main"])).toMatchObject({ open: true, env: "prod", forge: "gitlab", branch: "main" });
239
+ });
240
+ });
package/src/pr-post.ts CHANGED
@@ -14,9 +14,16 @@
14
14
  * every hidden HTML comment after its first line is defused, so a forged
15
15
  * marker cannot claim another environment's note. A plan job that failed
16
16
  * fails the status whatever its report says.
17
+ *
18
+ * On GitLab the post runs in a pipeline of the target branch (#4328): a
19
+ * merge request pipeline is built from the merge request's own files, so a
20
+ * job there that holds `CHANT_FORGE_TOKEN` runs whatever the change wrote.
21
+ * `pr-post --open` ({@link postOpenPlanReports}) asks GitLab for each open
22
+ * merge request's plan job and reads the report from its artifacts.
17
23
  */
18
24
 
19
- import { existsSync, readFileSync } from "node:fs";
25
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
26
+ import { tmpdir } from "node:os";
20
27
  import { join } from "node:path";
21
28
  import { prNoteMarker, prStatusContext, prStatusDescription, prStatusState, renderPrNote, type PrOverdue, type PrReport } from "./pr-loop";
22
29
  import { forgePrincipalOf, type PrForge } from "./pr-forge";
@@ -157,3 +164,84 @@ export async function postPlanReport(input: PlanPostInput): Promise<PlanPostResu
157
164
  }
158
165
  return { state, description: cut(description), noted: note !== undefined, ...(refused ? { refused } : {}), warnings };
159
166
  }
167
+
168
+ /** What {@link postOpenPlanReports} posts for. */
169
+ export interface OpenPlanPostInput {
170
+ /** A GitLab client with {@link PrForge.mergeRequestJobs}, {@link PrForge.jobArtifact} and {@link PrForge.statusUrl}. */
171
+ forge: PrForge;
172
+ /** The branch the merge requests merge into: the pipeline's own. */
173
+ branch: string;
174
+ /** The plan job's name in the merge request pipeline: `plan`, or `<member>-plan`. */
175
+ job: string;
176
+ /** The report directory from the repository root, as the plan job kept it as its artifact. */
177
+ artifactDir: string;
178
+ env: string;
179
+ member?: string;
180
+ overdue?: () => Promise<PrOverdue[]>;
181
+ }
182
+
183
+ /** What {@link postOpenPlanReports} did for one merge request. */
184
+ export interface OpenPlanPost {
185
+ pr: number;
186
+ head: string;
187
+ /** What was posted, or why nothing was: the plan job has not settled, or its result is already posted. */
188
+ posted?: PlanPostResult;
189
+ skipped?: string;
190
+ }
191
+
192
+ /**
193
+ * Post the plan note and status of every open merge request into `branch`
194
+ * whose plan job settled (#4328), from a pipeline of `branch`. GitLab says
195
+ * which merge requests are open, their heads, and their newest pipeline's
196
+ * plan job; the report is the one file `pr-plan.json` of that job's
197
+ * artifacts, read as data as {@link postPlanReport} reads any. The status
198
+ * links to the plan job, and a status that already links to it is the
199
+ * job's result posted, so a job is posted for once. The overdue check runs
200
+ * once for every merge request.
201
+ */
202
+ export async function postOpenPlanReports(input: OpenPlanPostInput): Promise<{ posts: OpenPlanPost[]; warnings: string[] }> {
203
+ const { forge } = input;
204
+ if (!forge.mergeRequestJobs || !forge.jobArtifact || !forge.statusUrl) {
205
+ throw new Error(`pr-post --open reads the plan jobs of open merge requests, which ${forge.kind} cannot say; it is for GitLab`);
206
+ }
207
+ const context = prStatusContext("plan", input.member);
208
+ const warnings: string[] = [];
209
+ let overdue: Promise<PrOverdue[]> | undefined;
210
+ const overdueOnce = input.overdue ? () => (overdue ??= input.overdue!()) : undefined;
211
+ const posts: OpenPlanPost[] = [];
212
+ for (const job of await forge.mergeRequestJobs(input.branch, input.job)) {
213
+ const settled = job.status === "success" ? "success" : job.status === "failed" ? "failure" : job.status === "canceled" ? "cancelled" : undefined;
214
+ if (!settled) {
215
+ posts.push({ pr: job.pr, head: job.head, skipped: `the plan job is ${job.status}` });
216
+ continue;
217
+ }
218
+ try {
219
+ if (job.url && (await forge.statusUrl(job.head, context)) === job.url) {
220
+ posts.push({ pr: job.pr, head: job.head, skipped: "this plan job's result is already posted" });
221
+ continue;
222
+ }
223
+ const dir = mkdtempSync(join(tmpdir(), "chant-pr-post-"));
224
+ try {
225
+ const text = await forge.jobArtifact(job.id, `${input.artifactDir.replace(/\/+$/, "")}/${PR_PLAN_REPORT_FILE}`);
226
+ if (text !== undefined) writeFileSync(join(dir, PR_PLAN_REPORT_FILE), text);
227
+ const posted = await postPlanReport({
228
+ // The status links to the plan job it reports on, which is how the next run knows it is posted.
229
+ forge: job.url ? { ...forge, runUrl: job.url } : forge,
230
+ dir,
231
+ pr: job.pr,
232
+ head: job.head,
233
+ env: input.env,
234
+ ...(input.member ? { member: input.member } : {}),
235
+ result: settled,
236
+ ...(overdueOnce ? { overdue: overdueOnce } : {}),
237
+ });
238
+ posts.push({ pr: job.pr, head: job.head, posted });
239
+ } finally {
240
+ rmSync(dir, { recursive: true, force: true });
241
+ }
242
+ } catch (err) {
243
+ warnings.push(`!${job.pr}: ${err instanceof Error ? err.message : String(err)}`);
244
+ }
245
+ }
246
+ return { posts, warnings };
247
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { FilterError, matchesWhere, parseInstant, parseSince, parseWhere, type FilterFields } from "./read-filter";
3
+
4
+ const fields: FilterFields = { name: "text", count: "number", ok: "boolean", at: "time", labels: "json", "unit.id": "text" };
5
+ const now = new Date("2026-10-23T12:00:00.000Z");
6
+ const keep = (rows: { name: string }[], ...where: string[]) => rows.filter((r) => matchesWhere(r, parseWhere(where, fields, now))).map((r) => r.name);
7
+
8
+ describe("typed filters (#4363)", () => {
9
+ const rows = [
10
+ { name: "web", count: 3, ok: true, at: "2026-10-22T00:00:00.000Z", labels: { team: "core", size: 2 }, unit: { id: "W-1" } },
11
+ { name: "worker", count: 10, ok: false, at: "2026-10-01T00:00:00.000Z", labels: '{"team":"edge","size":12}', unit: null },
12
+ { name: "db", count: null, ok: true, at: null, labels: null, unit: { id: "W-2" } },
13
+ ];
14
+
15
+ it("filters each type with its operators, every clause holding", () => {
16
+ expect(keep(rows, "name=web")).toEqual(["web"]);
17
+ expect(keep(rows, "name~w")).toEqual(["web", "worker"]);
18
+ expect(keep(rows, "count>=3", "count<10")).toEqual(["web"]);
19
+ expect(keep(rows, "ok=false")).toEqual(["worker"]);
20
+ expect(keep(rows, "at>7d")).toEqual(["web"]);
21
+ expect(keep(rows, "at<2026-10-10")).toEqual(["worker"]);
22
+ expect(keep(rows, "unit.id=W-2")).toEqual(["db"]);
23
+ });
24
+
25
+ it("reads a key under a json field, parsing JSON text, as the type it holds", () => {
26
+ expect(keep(rows, "labels.team=edge")).toEqual(["worker"]);
27
+ expect(keep(rows, "labels.size>5")).toEqual(["worker"]);
28
+ });
29
+
30
+ it("matches null with = and !=, and a null field only with !=", () => {
31
+ expect(keep(rows, "count=null")).toEqual(["db"]);
32
+ expect(keep(rows, "count!=null")).toEqual(["web", "worker"]);
33
+ expect(keep(rows, "count!=3")).toEqual(["worker", "db"]);
34
+ expect(keep(rows, "count<100")).toEqual(["web", "worker"]);
35
+ });
36
+
37
+ it("refuses a field, operator or value that doesn't fit, before reading", () => {
38
+ expect(() => parseWhere(["nope=1"], fields)).toThrow(/no such field; this read filters on name, count/);
39
+ expect(() => parseWhere(["count=many"], fields)).toThrow(FilterError);
40
+ expect(() => parseWhere(["ok>true"], fields)).toThrow(/= or != only/);
41
+ expect(() => parseWhere(["count~1"], fields)).toThrow(/takes a text field/);
42
+ expect(() => parseWhere(["at=yesterday"], fields)).toThrow(/is a time/);
43
+ expect(() => parseWhere(["labels=x"], fields)).toThrow(/name a key under it/);
44
+ expect(() => parseWhere(["name>=null"], fields)).toThrow(/null takes = or != only/);
45
+ expect(() => parseWhere(["just-a-word"], fields)).toThrow(/<field><op><value>/);
46
+ });
47
+
48
+ it("reads --since as an ISO time or a span back from now", () => {
49
+ expect(parseSince("7d", now)).toBe("2026-10-16T12:00:00.000Z");
50
+ expect(parseSince("2026-10-01", now)).toBe("2026-10-01T00:00:00.000Z");
51
+ expect(parseInstant("90m", now)).toBe(now.getTime() - 90 * 60_000);
52
+ expect(() => parseSince("last week", now)).toThrow(FilterError);
53
+ });
54
+ });
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Typed filters for the contract reads (#4363): `--where <field><op><value>`
3
+ * and `--since <time>`, in place of an embedded SQL engine.
4
+ *
5
+ * Each read declares the fields it can be filtered on and their types
6
+ * ({@link FilterFields}). A clause naming a field the read doesn't declare, an
7
+ * operator its type doesn't take, or a value that isn't of its type is a
8
+ * {@link FilterError} before anything is read, so a typo never filters to an
9
+ * empty answer. Several `--where` clauses all have to hold.
10
+ *
11
+ * Operators: `=` and `!=` for every type; `<`, `<=`, `>` and `>=` for
12
+ * numbers, times and text (text compares by code point); `~` for text, which
13
+ * holds when the field contains the value. The value `null` matches a field
14
+ * that is null or absent, with `=` and `!=` only.
15
+ *
16
+ * A field of type `json` holds an object: a clause can name a key under it
17
+ * with dots (`labels.team=core`), and the value is compared as the type it
18
+ * holds there (a number with a number, a boolean with `true` or `false`, any
19
+ * other value as text). A JSON text is parsed first, so a ledger row that
20
+ * keeps the object as text filters the same way.
21
+ *
22
+ * A null or absent field matches only `=null` and `!=` with a value other
23
+ * than null; the other operators never hold for it.
24
+ *
25
+ * Pure: no I/O, so a bundle that filters rows loads nothing else.
26
+ */
27
+
28
+ export type FilterFieldType = "text" | "number" | "boolean" | "time" | "json";
29
+
30
+ /** The fields a read can be filtered on, by name, with their types. */
31
+ export type FilterFields = Readonly<Record<string, FilterFieldType>>;
32
+
33
+ /** Every operator, longest first so `!=` is not read as `=`. */
34
+ export const WHERE_OPERATORS = ["!=", "<=", ">=", "=", "<", ">", "~"] as const;
35
+ export type WhereOperator = (typeof WHERE_OPERATORS)[number];
36
+
37
+ /** One parsed `--where` clause. */
38
+ export interface WhereClause {
39
+ /** The field as written, with any path under a `json` field. */
40
+ field: string;
41
+ /** The declared field the clause starts at. */
42
+ base: string;
43
+ /** The keys under a `json` field, or `[]`. */
44
+ path: string[];
45
+ type: FilterFieldType;
46
+ op: WhereOperator;
47
+ /** The value as written. */
48
+ raw: string;
49
+ /** The value read as the field's type: a number for `number` and `time` (epoch ms), a boolean, a string, or null. */
50
+ value: string | number | boolean | null;
51
+ }
52
+
53
+ /** A clause or a `--since` that can't be applied. The message is for a person. */
54
+ export class FilterError extends Error {}
55
+
56
+ const ORDERED: readonly WhereOperator[] = ["<", "<=", ">", ">="];
57
+
58
+ /**
59
+ * An instant from an ISO 8601 date or time, or from a span back from `now`:
60
+ * `<n>m`, `<n>h`, `<n>d` or `<n>w`. Epoch milliseconds, or null when `text`
61
+ * is neither.
62
+ */
63
+ export function parseInstant(text: string, now: Date = new Date()): number | null {
64
+ const span = /^(\d+)([mhdw])$/.exec(text.trim());
65
+ if (span) {
66
+ const unit = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }[span[2] as "m" | "h" | "d" | "w"];
67
+ return now.getTime() - Number(span[1]) * unit;
68
+ }
69
+ if (!/^\d{4}-\d{2}-\d{2}/.test(text.trim())) return null;
70
+ const ms = Date.parse(text);
71
+ return Number.isNaN(ms) ? null : ms;
72
+ }
73
+
74
+ /** `--since <time>` as an ISO instant, or a {@link FilterError}. */
75
+ export function parseSince(text: string, now: Date = new Date()): string {
76
+ const ms = parseInstant(text, now);
77
+ if (ms === null) throw new FilterError(`--since takes an ISO 8601 date or time, or a span back from now such as 24h or 7d, not ${JSON.stringify(text)}`);
78
+ return new Date(ms).toISOString();
79
+ }
80
+
81
+ /** The declared field a dotted name starts at: the whole name, or its longest declared `json` prefix. */
82
+ function resolveField(name: string, fields: FilterFields): { base: string; path: string[]; type: FilterFieldType } | null {
83
+ if (Object.prototype.hasOwnProperty.call(fields, name)) return { base: name, path: [], type: fields[name] };
84
+ const parts = name.split(".");
85
+ for (let i = parts.length - 1; i > 0; i--) {
86
+ const base = parts.slice(0, i).join(".");
87
+ if (Object.prototype.hasOwnProperty.call(fields, base) && fields[base] === "json") return { base, path: parts.slice(i), type: "json" };
88
+ }
89
+ return null;
90
+ }
91
+
92
+ /** Parse each `--where` against the fields a read declares. Throws a {@link FilterError}. */
93
+ export function parseWhere(exprs: readonly string[], fields: FilterFields, now: Date = new Date()): WhereClause[] {
94
+ const names = Object.keys(fields).join(", ");
95
+ return exprs.map((expr) => {
96
+ const m = /^([A-Za-z_][\w.-]*)\s*(!=|<=|>=|=|<|>|~)(.*)$/s.exec(expr);
97
+ if (!m) throw new FilterError(`--where takes <field><op><value>, with op one of ${WHERE_OPERATORS.join(" ")}, not ${JSON.stringify(expr)}`);
98
+ const [, field, op, rawValue] = m as unknown as [string, string, WhereOperator, string];
99
+ const raw = rawValue.trim();
100
+ const found = resolveField(field, fields);
101
+ if (!found) throw new FilterError(`--where ${field}: no such field; this read filters on ${names}`);
102
+ const { base, path, type } = found;
103
+ if (type === "json" && path.length === 0) throw new FilterError(`--where ${field}: holds an object; name a key under it, such as ${field}.<key>`);
104
+ const clause = (value: WhereClause["value"]): WhereClause => ({ field, base, path, type, op, raw, value });
105
+ if (raw === "null") {
106
+ if (op !== "=" && op !== "!=") throw new FilterError(`--where ${field}${op}null: null takes = or != only`);
107
+ return clause(null);
108
+ }
109
+ if (op === "~" && type !== "text" && type !== "json") throw new FilterError(`--where ${field}~: ~ (contains) takes a text field; ${field} is ${type}`);
110
+ switch (type) {
111
+ case "number": {
112
+ const n = Number(raw);
113
+ if (raw === "" || !Number.isFinite(n)) throw new FilterError(`--where ${field}: ${field} is a number, not ${JSON.stringify(raw)}`);
114
+ return clause(n);
115
+ }
116
+ case "boolean": {
117
+ if (ORDERED.includes(op)) throw new FilterError(`--where ${field}${op}: a boolean takes = or != only`);
118
+ if (raw !== "true" && raw !== "false") throw new FilterError(`--where ${field}: ${field} is true or false, not ${JSON.stringify(raw)}`);
119
+ return clause(raw === "true");
120
+ }
121
+ case "time": {
122
+ const ms = parseInstant(raw, now);
123
+ if (ms === null) throw new FilterError(`--where ${field}: ${field} is a time; give an ISO 8601 date or time, or a span back from now such as 24h or 7d, not ${JSON.stringify(raw)}`);
124
+ return clause(ms);
125
+ }
126
+ default:
127
+ return clause(raw);
128
+ }
129
+ });
130
+ }
131
+
132
+ /** The value at a clause's field in `row`, parsing a JSON text on the way into a `json` field. */
133
+ function valueAt(row: unknown, c: WhereClause): unknown {
134
+ let v: unknown = row;
135
+ for (const key of c.base.split(".")) v = v !== null && typeof v === "object" ? (v as Record<string, unknown>)[key] : undefined;
136
+ for (const key of c.path) {
137
+ if (typeof v === "string") {
138
+ try {
139
+ v = JSON.parse(v);
140
+ } catch {
141
+ return undefined;
142
+ }
143
+ }
144
+ v = v !== null && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>)[key] : undefined;
145
+ }
146
+ return v;
147
+ }
148
+
149
+ function compare(a: number | string, b: number | string, op: WhereOperator): boolean {
150
+ switch (op) {
151
+ case "=": return a === b;
152
+ case "!=": return a !== b;
153
+ case "<": return a < b;
154
+ case "<=": return a <= b;
155
+ case ">": return a > b;
156
+ case ">=": return a >= b;
157
+ case "~": return String(a).includes(String(b));
158
+ }
159
+ }
160
+
161
+ /** Whether one clause holds for `row`. */
162
+ export function clauseHolds(row: unknown, c: WhereClause): boolean {
163
+ const v = valueAt(row, c);
164
+ if (c.value === null) return c.op === "=" ? v === null || v === undefined : v !== null && v !== undefined;
165
+ if (v === null || v === undefined) return c.op === "!=";
166
+ switch (c.type) {
167
+ case "number":
168
+ return typeof v === "number" && compare(v, c.value as number, c.op);
169
+ case "boolean":
170
+ return typeof v === "boolean" ? (c.op === "=") === (v === c.value) : false;
171
+ case "time": {
172
+ const ms = typeof v === "string" ? Date.parse(v) : typeof v === "number" ? v : NaN;
173
+ return !Number.isNaN(ms) && compare(ms, c.value as number, c.op);
174
+ }
175
+ case "text":
176
+ return typeof v === "string" && compare(v, c.value as string, c.op);
177
+ case "json": {
178
+ // Compared as the type the value holds there.
179
+ if (typeof v === "number" && c.op !== "~") {
180
+ const n = Number(c.raw);
181
+ return Number.isFinite(n) && compare(v, n, c.op);
182
+ }
183
+ if (typeof v === "boolean") return (c.raw === "true" || c.raw === "false") && (c.op === "=" || c.op === "!=") ? (c.op === "=") === (String(v) === c.raw) : false;
184
+ const s = typeof v === "string" ? v : JSON.stringify(v);
185
+ return compare(s, c.raw, c.op);
186
+ }
187
+ }
188
+ }
189
+
190
+ /** Whether every clause holds for `row`. */
191
+ export function matchesWhere(row: unknown, clauses: readonly WhereClause[]): boolean {
192
+ return clauses.every((c) => clauseHolds(row, c));
193
+ }
package/src/run-event.ts CHANGED
@@ -58,7 +58,7 @@ export type RunEventStepStatus = "ok" | "failed" | "skipped" | "cached" | "gated
58
58
  export type RunEventSkipReason = "receipt" | "resumed" | "refused" | "not-run";
59
59
 
60
60
  /** How a gate's approval passed, when it was more than one person's approval of that gate. */
61
- export type RunEventGateVia = "quorum" | "policy" | "point" | "alias" | "empty";
61
+ export type RunEventGateVia = "quorum" | "policy" | "point" | "alias" | "empty" | "workspace";
62
62
 
63
63
  /** The run every event of a stream names: the same on every line. */
64
64
  export interface RunEventRun {
@@ -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
  }