@intentius/chant 0.124.51 → 0.124.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (593) hide show
  1. package/dist/audit/discover.d.ts.map +1 -1
  2. package/dist/audit/discover.js +3 -1
  3. package/dist/behaviour.d.ts +1 -1
  4. package/dist/behaviour.d.ts.map +1 -1
  5. package/dist/capabilities/templates.d.ts +65 -0
  6. package/dist/capabilities/templates.d.ts.map +1 -0
  7. package/dist/capabilities/templates.js +193 -0
  8. package/dist/capabilities/templates.json +145 -0
  9. package/dist/change-set-graph.d.ts +35 -3
  10. package/dist/change-set-graph.d.ts.map +1 -1
  11. package/dist/change-set-graph.js +60 -9
  12. package/dist/change-set.d.ts +65 -7
  13. package/dist/change-set.d.ts.map +1 -1
  14. package/dist/change-set.js +23 -12
  15. package/dist/ci-identity.d.ts +7 -2
  16. package/dist/ci-identity.d.ts.map +1 -1
  17. package/dist/ci-identity.js +4 -1
  18. package/dist/cli/commands/import-index.d.ts +52 -0
  19. package/dist/cli/commands/import-index.d.ts.map +1 -0
  20. package/dist/cli/commands/import-index.js +226 -0
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/lint.js +4 -2
  23. package/dist/cli/handlers/components.d.ts.map +1 -1
  24. package/dist/cli/handlers/components.js +11 -2
  25. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  26. package/dist/cli/handlers/fan-out.js +16 -3
  27. package/dist/cli/handlers/graph.d.ts.map +1 -1
  28. package/dist/cli/handlers/graph.js +22 -4
  29. package/dist/cli/handlers/index-cloud.d.ts +14 -0
  30. package/dist/cli/handlers/index-cloud.d.ts.map +1 -0
  31. package/dist/cli/handlers/index-cloud.js +22 -0
  32. package/dist/cli/handlers/index-diff.d.ts +18 -0
  33. package/dist/cli/handlers/index-diff.d.ts.map +1 -0
  34. package/dist/cli/handlers/index-diff.js +142 -0
  35. package/dist/cli/handlers/lifecycle-drift.d.ts +25 -0
  36. package/dist/cli/handlers/lifecycle-drift.d.ts.map +1 -0
  37. package/dist/cli/handlers/lifecycle-drift.js +64 -0
  38. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  39. package/dist/cli/handlers/lifecycle.js +188 -28
  40. package/dist/cli/handlers/misc.d.ts +9 -0
  41. package/dist/cli/handlers/misc.d.ts.map +1 -1
  42. package/dist/cli/handlers/misc.js +77 -0
  43. package/dist/cli/handlers/pr.d.ts.map +1 -1
  44. package/dist/cli/handlers/pr.js +17 -3
  45. package/dist/cli/handlers/promote.d.ts.map +1 -1
  46. package/dist/cli/handlers/promote.js +89 -15
  47. package/dist/cli/handlers/query.d.ts.map +1 -1
  48. package/dist/cli/handlers/query.js +28 -4
  49. package/dist/cli/handlers/scenario.d.ts.map +1 -1
  50. package/dist/cli/handlers/scenario.js +8 -7
  51. package/dist/cli/handlers/search.d.ts +34 -4
  52. package/dist/cli/handlers/search.d.ts.map +1 -1
  53. package/dist/cli/handlers/search.js +216 -36
  54. package/dist/cli/main.d.ts.map +1 -1
  55. package/dist/cli/main.js +55 -1
  56. package/dist/cli/mcp/lifecycle-tools.d.ts.map +1 -1
  57. package/dist/cli/mcp/lifecycle-tools.js +3 -2
  58. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  59. package/dist/cli/mcp/resource-handlers.js +4 -3
  60. package/dist/cli/mcp/workspace-tools.js +1 -1
  61. package/dist/cli/parse-args.d.ts.map +1 -1
  62. package/dist/cli/parse-args.js +55 -2
  63. package/dist/cli/registry.d.ts +19 -1
  64. package/dist/cli/registry.d.ts.map +1 -1
  65. package/dist/cloud-lister/aws.d.ts +35 -0
  66. package/dist/cloud-lister/aws.d.ts.map +1 -0
  67. package/dist/cloud-lister/aws.js +203 -0
  68. package/dist/cloud-lister/azure.d.ts +34 -0
  69. package/dist/cloud-lister/azure.d.ts.map +1 -0
  70. package/dist/cloud-lister/azure.js +129 -0
  71. package/dist/cloud-lister/common.d.ts +40 -0
  72. package/dist/cloud-lister/common.d.ts.map +1 -0
  73. package/dist/cloud-lister/common.js +83 -0
  74. package/dist/cloud-lister/credentials.d.ts +50 -0
  75. package/dist/cloud-lister/credentials.d.ts.map +1 -0
  76. package/dist/cloud-lister/credentials.js +86 -0
  77. package/dist/cloud-lister/fakes.d.ts +27 -0
  78. package/dist/cloud-lister/fakes.d.ts.map +1 -0
  79. package/dist/cloud-lister/fakes.js +50 -0
  80. package/dist/cloud-lister/gcp.d.ts +42 -0
  81. package/dist/cloud-lister/gcp.d.ts.map +1 -0
  82. package/dist/cloud-lister/gcp.js +143 -0
  83. package/dist/cloud-lister/guard.d.ts +49 -0
  84. package/dist/cloud-lister/guard.d.ts.map +1 -0
  85. package/dist/cloud-lister/guard.js +103 -0
  86. package/dist/cloud-lister/ids.d.ts +23 -0
  87. package/dist/cloud-lister/ids.d.ts.map +1 -0
  88. package/dist/cloud-lister/ids.js +42 -0
  89. package/dist/cloud-lister/index.d.ts +67 -0
  90. package/dist/cloud-lister/index.d.ts.map +1 -0
  91. package/dist/cloud-lister/index.js +102 -0
  92. package/dist/cloud-lister/kubernetes.d.ts +64 -0
  93. package/dist/cloud-lister/kubernetes.d.ts.map +1 -0
  94. package/dist/cloud-lister/kubernetes.js +276 -0
  95. package/dist/cloud-lister/markers.d.ts +31 -0
  96. package/dist/cloud-lister/markers.d.ts.map +1 -0
  97. package/dist/cloud-lister/markers.js +73 -0
  98. package/dist/cloud-lister/mask.d.ts +31 -0
  99. package/dist/cloud-lister/mask.d.ts.map +1 -0
  100. package/dist/cloud-lister/mask.js +74 -0
  101. package/dist/cloud-lister/sigv4.d.ts +29 -0
  102. package/dist/cloud-lister/sigv4.d.ts.map +1 -0
  103. package/dist/cloud-lister/sigv4.js +54 -0
  104. package/dist/cloud-lister/transport.d.ts +55 -0
  105. package/dist/cloud-lister/transport.d.ts.map +1 -0
  106. package/dist/cloud-lister/transport.js +86 -0
  107. package/dist/cloud-lister/types.d.ts +121 -0
  108. package/dist/cloud-lister/types.d.ts.map +1 -0
  109. package/dist/cloud-lister/types.js +24 -0
  110. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  111. package/dist/components/capability-plugin-loader.js +6 -0
  112. package/dist/components/capability-plugin.d.ts +9 -0
  113. package/dist/components/capability-plugin.d.ts.map +1 -1
  114. package/dist/components/capability.d.ts +224 -0
  115. package/dist/components/capability.d.ts.map +1 -1
  116. package/dist/components/cli-support.d.ts +21 -0
  117. package/dist/components/cli-support.d.ts.map +1 -1
  118. package/dist/components/cli-support.js +58 -14
  119. package/dist/components/covered-round.d.ts +173 -0
  120. package/dist/components/covered-round.d.ts.map +1 -0
  121. package/dist/components/covered-round.js +200 -0
  122. package/dist/components/declared-reads.d.ts +38 -0
  123. package/dist/components/declared-reads.d.ts.map +1 -0
  124. package/dist/components/declared-reads.js +131 -0
  125. package/dist/components/deploy-units.d.ts.map +1 -1
  126. package/dist/components/deploy-units.js +4 -0
  127. package/dist/components/driver.d.ts +42 -1
  128. package/dist/components/driver.d.ts.map +1 -1
  129. package/dist/components/driver.js +74 -3
  130. package/dist/components/fan-out-run.d.ts +23 -1
  131. package/dist/components/fan-out-run.d.ts.map +1 -1
  132. package/dist/components/fan-out-run.js +82 -14
  133. package/dist/components/fan-out-support.d.ts +6 -0
  134. package/dist/components/fan-out-support.d.ts.map +1 -1
  135. package/dist/components/fan-out-support.js +4 -1
  136. package/dist/components/fan-out.d.ts +28 -0
  137. package/dist/components/fan-out.d.ts.map +1 -1
  138. package/dist/components/groups.d.ts +229 -0
  139. package/dist/components/groups.d.ts.map +1 -0
  140. package/dist/components/groups.js +760 -0
  141. package/dist/components/index.d.ts +7 -2
  142. package/dist/components/index.d.ts.map +1 -1
  143. package/dist/components/index.js +5 -0
  144. package/dist/components/plan-shape-conformance.d.ts +48 -0
  145. package/dist/components/plan-shape-conformance.d.ts.map +1 -0
  146. package/dist/components/plan-shape-conformance.js +122 -0
  147. package/dist/components/pr-run.d.ts +62 -7
  148. package/dist/components/pr-run.d.ts.map +1 -1
  149. package/dist/components/pr-run.js +354 -53
  150. package/dist/components/promote-rounds.d.ts +9 -0
  151. package/dist/components/promote-rounds.d.ts.map +1 -1
  152. package/dist/components/promote-rounds.js +23 -2
  153. package/dist/components/promote.d.ts +17 -0
  154. package/dist/components/promote.d.ts.map +1 -1
  155. package/dist/components/promote.js +36 -6
  156. package/dist/components/release-gate.d.ts +23 -1
  157. package/dist/components/release-gate.d.ts.map +1 -1
  158. package/dist/components/release-gate.js +97 -33
  159. package/dist/components/run-groups.d.ts +58 -0
  160. package/dist/components/run-groups.d.ts.map +1 -0
  161. package/dist/components/run-groups.js +78 -0
  162. package/dist/components/starter-plugin.d.ts +1 -0
  163. package/dist/components/starter-plugin.d.ts.map +1 -1
  164. package/dist/components/starter-plugin.js +6 -1
  165. package/dist/components/verbs/index.d.ts +1 -0
  166. package/dist/components/verbs/index.d.ts.map +1 -1
  167. package/dist/components/verbs/index.js +1 -0
  168. package/dist/components/verbs/member-outputs.d.ts +187 -0
  169. package/dist/components/verbs/member-outputs.d.ts.map +1 -0
  170. package/dist/components/verbs/member-outputs.js +218 -0
  171. package/dist/components/wave-plan.d.ts +2 -0
  172. package/dist/components/wave-plan.d.ts.map +1 -1
  173. package/dist/components/wave-plan.js +4 -1
  174. package/dist/config.d.ts +60 -0
  175. package/dist/config.d.ts.map +1 -1
  176. package/dist/config.js +27 -0
  177. package/dist/deep-observation.d.ts +1 -13
  178. package/dist/deep-observation.d.ts.map +1 -1
  179. package/dist/deep-observation.js +5 -22
  180. package/dist/drift-record.d.ts +542 -0
  181. package/dist/drift-record.d.ts.map +1 -0
  182. package/dist/drift-record.js +886 -0
  183. package/dist/engine-facts.d.ts +144 -0
  184. package/dist/engine-facts.d.ts.map +1 -0
  185. package/dist/engine-facts.js +14 -0
  186. package/dist/graph-ir.d.ts +153 -2
  187. package/dist/graph-ir.d.ts.map +1 -1
  188. package/dist/graph-ir.js +224 -6
  189. package/dist/graph-refs.d.ts +145 -0
  190. package/dist/graph-refs.d.ts.map +1 -1
  191. package/dist/graph-refs.js +305 -0
  192. package/dist/graph-schema/fixtures/adopt.json +15 -0
  193. package/dist/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  194. package/dist/graph-schema/fixtures/index.json +68 -0
  195. package/dist/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  196. package/dist/graph-schema/fixtures.d.ts +23 -0
  197. package/dist/graph-schema/fixtures.d.ts.map +1 -0
  198. package/dist/graph-schema/fixtures.js +246 -0
  199. package/dist/graph-schema/generate.d.ts +15 -0
  200. package/dist/graph-schema/generate.d.ts.map +1 -0
  201. package/dist/graph-schema/generate.js +267 -0
  202. package/dist/graph-schema/graph.schema.json +1387 -0
  203. package/dist/health.schema.json +2 -1
  204. package/dist/identity.d.ts.map +1 -1
  205. package/dist/identity.js +2 -0
  206. package/dist/index.d.ts +1 -0
  207. package/dist/index.d.ts.map +1 -1
  208. package/dist/index.js +2 -0
  209. package/dist/instance-nodes.d.ts +159 -0
  210. package/dist/instance-nodes.d.ts.map +1 -0
  211. package/dist/instance-nodes.js +231 -0
  212. package/dist/lexicon.d.ts +72 -0
  213. package/dist/lexicon.d.ts.map +1 -1
  214. package/dist/lifecycle/adoption-file.d.ts +56 -0
  215. package/dist/lifecycle/adoption-file.d.ts.map +1 -0
  216. package/dist/lifecycle/adoption-file.js +123 -0
  217. package/dist/lifecycle/cloud-index.d.ts +186 -0
  218. package/dist/lifecycle/cloud-index.d.ts.map +1 -0
  219. package/dist/lifecycle/cloud-index.js +618 -0
  220. package/dist/lifecycle/derive-edges.d.ts +59 -0
  221. package/dist/lifecycle/derive-edges.d.ts.map +1 -0
  222. package/dist/lifecycle/derive-edges.js +126 -0
  223. package/dist/lifecycle/git.d.ts +10 -9
  224. package/dist/lifecycle/git.d.ts.map +1 -1
  225. package/dist/lifecycle/git.js +7 -1
  226. package/dist/lifecycle/identity-match.d.ts +133 -0
  227. package/dist/lifecycle/identity-match.d.ts.map +1 -0
  228. package/dist/lifecycle/identity-match.js +147 -0
  229. package/dist/lifecycle/index-diff.d.ts +224 -0
  230. package/dist/lifecycle/index-diff.d.ts.map +1 -0
  231. package/dist/lifecycle/index-diff.js +327 -0
  232. package/dist/lifecycle/index-document.d.ts +130 -0
  233. package/dist/lifecycle/index-document.d.ts.map +1 -0
  234. package/dist/lifecycle/index-document.js +189 -0
  235. package/dist/lifecycle/index-drift.d.ts +121 -0
  236. package/dist/lifecycle/index-drift.d.ts.map +1 -0
  237. package/dist/lifecycle/index-drift.js +178 -0
  238. package/dist/lifecycle/index-import.d.ts +168 -0
  239. package/dist/lifecycle/index-import.d.ts.map +1 -0
  240. package/dist/lifecycle/index-import.js +515 -0
  241. package/dist/lifecycle/index-join.d.ts +129 -0
  242. package/dist/lifecycle/index-join.d.ts.map +1 -0
  243. package/dist/lifecycle/index-join.js +248 -0
  244. package/dist/lifecycle/index.d.ts +4 -0
  245. package/dist/lifecycle/index.d.ts.map +1 -1
  246. package/dist/lifecycle/index.js +4 -0
  247. package/dist/lifecycle/observation-baseline.d.ts +61 -21
  248. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  249. package/dist/lifecycle/observation-baseline.js +144 -19
  250. package/dist/lifecycle/observe.d.ts.map +1 -1
  251. package/dist/lifecycle/observe.js +6 -1
  252. package/dist/lifecycle/query.d.ts +45 -4
  253. package/dist/lifecycle/query.d.ts.map +1 -1
  254. package/dist/lifecycle/query.js +89 -7
  255. package/dist/lifecycle/replay.d.ts +10 -3
  256. package/dist/lifecycle/replay.d.ts.map +1 -1
  257. package/dist/lifecycle/replay.js +10 -6
  258. package/dist/lifecycle/scenario.d.ts +4 -3
  259. package/dist/lifecycle/scenario.d.ts.map +1 -1
  260. package/dist/lifecycle/snapshot-store-fake.d.ts +9 -0
  261. package/dist/lifecycle/snapshot-store-fake.d.ts.map +1 -0
  262. package/dist/lifecycle/snapshot-store-fake.js +21 -0
  263. package/dist/lifecycle/snapshot-store-s3.d.ts +102 -0
  264. package/dist/lifecycle/snapshot-store-s3.d.ts.map +1 -0
  265. package/dist/lifecycle/snapshot-store-s3.js +254 -0
  266. package/dist/lifecycle/snapshot-store.d.ts +170 -0
  267. package/dist/lifecycle/snapshot-store.d.ts.map +1 -0
  268. package/dist/lifecycle/snapshot-store.js +832 -0
  269. package/dist/lifecycle/snapshot.d.ts +11 -0
  270. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  271. package/dist/lifecycle/snapshot.js +18 -5
  272. package/dist/lifecycle/type-listing.d.ts +148 -0
  273. package/dist/lifecycle/type-listing.d.ts.map +1 -0
  274. package/dist/lifecycle/type-listing.js +166 -0
  275. package/dist/lint/component-checks.d.ts +15 -1
  276. package/dist/lint/component-checks.d.ts.map +1 -1
  277. package/dist/lint/component-checks.js +1 -0
  278. package/dist/lint/presets/relaxed.json +2 -1
  279. package/dist/lint/presets/strict.json +2 -1
  280. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts +4 -0
  281. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts.map +1 -0
  282. package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.js +13 -0
  283. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts +4 -0
  284. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts.map +1 -0
  285. package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.js +8 -0
  286. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts +33 -0
  287. package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts.map +1 -0
  288. package/dist/lint/rules/comp/comp008-applies-signal-only-root.js +123 -0
  289. package/dist/lint/rules/comp/index.d.ts +2 -1
  290. package/dist/lint/rules/comp/index.d.ts.map +1 -1
  291. package/dist/lint/rules/comp/index.js +4 -1
  292. package/dist/observation.d.ts +19 -2
  293. package/dist/observation.d.ts.map +1 -1
  294. package/dist/observation.js +18 -1
  295. package/dist/op/activities/activity-contracts.d.ts +1 -1
  296. package/dist/op/activities/activity-contracts.js +1 -1
  297. package/dist/op/activities/change-set.d.ts +6 -4
  298. package/dist/op/activities/change-set.d.ts.map +1 -1
  299. package/dist/op/activities/change-set.js +4 -2
  300. package/dist/op/activity-failure.d.ts +27 -0
  301. package/dist/op/activity-failure.d.ts.map +1 -1
  302. package/dist/op/activity-failure.js +31 -0
  303. package/dist/op/index.d.ts +2 -1
  304. package/dist/op/index.d.ts.map +1 -1
  305. package/dist/op/index.js +1 -1
  306. package/dist/op/local-executor.d.ts +5 -1
  307. package/dist/op/local-executor.d.ts.map +1 -1
  308. package/dist/op/local-executor.js +22 -8
  309. package/dist/pr-loop.d.ts +92 -3
  310. package/dist/pr-loop.d.ts.map +1 -1
  311. package/dist/pr-loop.js +62 -3
  312. package/dist/pulumi-urn.d.ts +101 -0
  313. package/dist/pulumi-urn.d.ts.map +1 -0
  314. package/dist/pulumi-urn.js +131 -0
  315. package/dist/release-floors.d.ts +23 -0
  316. package/dist/release-floors.d.ts.map +1 -0
  317. package/dist/release-floors.js +22 -0
  318. package/dist/sensitive-key.d.ts +23 -0
  319. package/dist/sensitive-key.d.ts.map +1 -0
  320. package/dist/sensitive-key.js +31 -0
  321. package/dist/wave-runtime.mjs +234 -234
  322. package/dist/workspace/adoption-report.d.ts +53 -0
  323. package/dist/workspace/adoption-report.d.ts.map +1 -0
  324. package/dist/workspace/adoption-report.js +76 -0
  325. package/dist/workspace/capabilities-contract.d.ts +7 -0
  326. package/dist/workspace/capabilities-contract.d.ts.map +1 -1
  327. package/dist/workspace/capabilities-contract.js +2 -1
  328. package/dist/workspace/capabilities.schema.json +98 -0
  329. package/dist/workspace/change-set.schema.json +42 -3
  330. package/dist/workspace/ci-cloud-identity.d.ts +13 -1
  331. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  332. package/dist/workspace/ci-cloud-identity.js +30 -1
  333. package/dist/workspace/compose-graph.d.ts +10 -1
  334. package/dist/workspace/compose-graph.d.ts.map +1 -1
  335. package/dist/workspace/compose-graph.js +6 -1
  336. package/dist/workspace/conformance/index.d.ts +1 -1
  337. package/dist/workspace/conformance/index.d.ts.map +1 -1
  338. package/dist/workspace/conformance/index.js +6 -1
  339. package/dist/workspace/drift-cli.d.ts +81 -0
  340. package/dist/workspace/drift-cli.d.ts.map +1 -0
  341. package/dist/workspace/drift-cli.js +317 -0
  342. package/dist/workspace/drift.schema.json +672 -0
  343. package/dist/workspace/graph-cli.d.ts +2 -0
  344. package/dist/workspace/graph-cli.d.ts.map +1 -1
  345. package/dist/workspace/graph-cli.js +3 -0
  346. package/dist/workspace/graph.schema.json +72 -0
  347. package/dist/workspace/init.d.ts +5 -1
  348. package/dist/workspace/init.d.ts.map +1 -1
  349. package/dist/workspace/init.js +55 -9
  350. package/dist/workspace/ledger.schema.json +1 -1
  351. package/dist/workspace/nested-graph.d.ts.map +1 -1
  352. package/dist/workspace/nested-graph.js +2 -0
  353. package/dist/workspace/pr-report.schema.json +76 -3
  354. package/dist/workspace/promote-waves.d.ts +112 -0
  355. package/dist/workspace/promote-waves.d.ts.map +1 -0
  356. package/dist/workspace/promote-waves.js +290 -0
  357. package/dist/workspace/promote.d.ts +84 -2
  358. package/dist/workspace/promote.d.ts.map +1 -1
  359. package/dist/workspace/promote.js +347 -61
  360. package/dist/workspace/reason-codes.d.ts +12 -0
  361. package/dist/workspace/reason-codes.d.ts.map +1 -1
  362. package/dist/workspace/reason-codes.js +14 -0
  363. package/dist/workspace/search-contract.d.ts +3 -3
  364. package/dist/workspace/search-contract.d.ts.map +1 -1
  365. package/dist/workspace/search-contract.js +2 -0
  366. package/dist/workspace/search.schema.json +103 -5
  367. package/package.json +14 -1
  368. package/src/__fixtures__/drift-record/README.md +58 -0
  369. package/src/__fixtures__/drift-record/choudoufu-cone.ndjson +4 -0
  370. package/src/__fixtures__/drift-record/choudoufu-resource.ndjson +3 -0
  371. package/src/__fixtures__/drift-record/component-status-ledger.json +65 -0
  372. package/src/__fixtures__/drift-record/component-status.json +91 -0
  373. package/src/__fixtures__/drift-record/deep-diff.json +259 -0
  374. package/src/__fixtures__/drift-record/lifecycle-diff-live.json +825 -0
  375. package/src/__fixtures__/drift-record/live-diff.json +125 -0
  376. package/src/audit/discover.e2e.test.ts +14 -0
  377. package/src/audit/discover.ts +3 -1
  378. package/src/behaviour.ts +1 -1
  379. package/src/capabilities/templates.json +145 -0
  380. package/src/capabilities/templates.test.ts +212 -0
  381. package/src/capabilities/templates.ts +204 -0
  382. package/src/change-set-bundle.test.ts +83 -0
  383. package/src/change-set-graph.ts +80 -9
  384. package/src/change-set.test.ts +24 -0
  385. package/src/change-set.ts +77 -14
  386. package/src/ci-identity-targets.test.ts +23 -0
  387. package/src/ci-identity.ts +10 -4
  388. package/src/cli/commands/import-index.test.ts +117 -0
  389. package/src/cli/commands/import-index.ts +265 -0
  390. package/src/cli/commands/lint.ts +4 -2
  391. package/src/cli/handlers/components.test.ts +9 -0
  392. package/src/cli/handlers/components.ts +12 -2
  393. package/src/cli/handlers/fan-out.ts +17 -3
  394. package/src/cli/handlers/graph.test.ts +42 -0
  395. package/src/cli/handlers/graph.ts +26 -4
  396. package/src/cli/handlers/index-cloud.ts +32 -0
  397. package/src/cli/handlers/index-diff.test.ts +68 -0
  398. package/src/cli/handlers/index-diff.ts +146 -0
  399. package/src/cli/handlers/ledger-root-member.test.ts +161 -0
  400. package/src/cli/handlers/lifecycle-drift.ts +68 -0
  401. package/src/cli/handlers/lifecycle.test.ts +33 -11
  402. package/src/cli/handlers/lifecycle.ts +205 -30
  403. package/src/cli/handlers/misc.ts +73 -0
  404. package/src/cli/handlers/pr-waves-blocked.test.ts +37 -2
  405. package/src/cli/handlers/pr.ts +16 -3
  406. package/src/cli/handlers/promote.test.ts +64 -0
  407. package/src/cli/handlers/promote.ts +81 -6
  408. package/src/cli/handlers/query.ts +27 -4
  409. package/src/cli/handlers/run-declared-reads-cli.test.ts +187 -0
  410. package/src/cli/handlers/run-groups-cli.test.ts +229 -0
  411. package/src/cli/handlers/scenario.test.ts +13 -0
  412. package/src/cli/handlers/scenario.ts +8 -7
  413. package/src/cli/handlers/search-index.test.ts +255 -0
  414. package/src/cli/handlers/search-json.test.ts +1 -1
  415. package/src/cli/handlers/search-ownership.test.ts +194 -0
  416. package/src/cli/handlers/search.ts +271 -38
  417. package/src/cli/main.test.ts +7 -0
  418. package/src/cli/main.ts +56 -1
  419. package/src/cli/mcp/lifecycle-tools.ts +3 -2
  420. package/src/cli/mcp/resource-handlers.ts +4 -3
  421. package/src/cli/mcp/workspace-tools.ts +1 -1
  422. package/src/cli/parse-args.ts +40 -2
  423. package/src/cli/registry.ts +19 -1
  424. package/src/cloud-lister/aws.test.ts +202 -0
  425. package/src/cloud-lister/aws.ts +226 -0
  426. package/src/cloud-lister/azure.test.ts +132 -0
  427. package/src/cloud-lister/azure.ts +142 -0
  428. package/src/cloud-lister/common.ts +115 -0
  429. package/src/cloud-lister/credentials.ts +110 -0
  430. package/src/cloud-lister/fakes.ts +66 -0
  431. package/src/cloud-lister/gcp.test.ts +121 -0
  432. package/src/cloud-lister/gcp.ts +159 -0
  433. package/src/cloud-lister/guard.test.ts +198 -0
  434. package/src/cloud-lister/guard.ts +111 -0
  435. package/src/cloud-lister/ids.ts +54 -0
  436. package/src/cloud-lister/index.ts +134 -0
  437. package/src/cloud-lister/kubernetes.test.ts +160 -0
  438. package/src/cloud-lister/kubernetes.ts +284 -0
  439. package/src/cloud-lister/markers.ts +74 -0
  440. package/src/cloud-lister/mask.ts +77 -0
  441. package/src/cloud-lister/shape.test.ts +81 -0
  442. package/src/cloud-lister/sigv4.ts +70 -0
  443. package/src/cloud-lister/transport.ts +118 -0
  444. package/src/cloud-lister/types.ts +135 -0
  445. package/src/components/capability-plugin-loader.ts +4 -0
  446. package/src/components/capability-plugin.ts +10 -0
  447. package/src/components/capability.ts +238 -0
  448. package/src/components/cli-support.ts +70 -14
  449. package/src/components/covered-round.test.ts +166 -0
  450. package/src/components/covered-round.ts +293 -0
  451. package/src/components/declared-reads.test.ts +103 -0
  452. package/src/components/declared-reads.ts +134 -0
  453. package/src/components/deploy-units.ts +4 -0
  454. package/src/components/driver.ts +101 -4
  455. package/src/components/fan-out-run.ts +95 -15
  456. package/src/components/fan-out-support.ts +10 -1
  457. package/src/components/fan-out.ts +30 -0
  458. package/src/components/groups.test.ts +320 -0
  459. package/src/components/groups.ts +880 -0
  460. package/src/components/index.ts +59 -0
  461. package/src/components/plan-shape-conformance.ts +130 -0
  462. package/src/components/pr-env-run-bundle.test.ts +2 -0
  463. package/src/components/pr-run.test.ts +728 -4
  464. package/src/components/pr-run.ts +395 -55
  465. package/src/components/promote-rounds.ts +25 -2
  466. package/src/components/promote.ts +55 -5
  467. package/src/components/registry.test.ts +1 -1
  468. package/src/components/release-gate.ts +115 -30
  469. package/src/components/release-groups.test.ts +362 -0
  470. package/src/components/run-groups.ts +113 -0
  471. package/src/components/starter-plugin.ts +6 -0
  472. package/src/components/verbs/index.ts +1 -0
  473. package/src/components/verbs/member-outputs.test.ts +205 -0
  474. package/src/components/verbs/member-outputs.ts +308 -0
  475. package/src/components/wave-plan.ts +5 -2
  476. package/src/config.ts +44 -0
  477. package/src/deep-observation.ts +5 -23
  478. package/src/drift-record.test.ts +596 -0
  479. package/src/drift-record.ts +1206 -0
  480. package/src/engine-facts.guard.test.ts +111 -0
  481. package/src/engine-facts.test.ts +178 -0
  482. package/src/engine-facts.ts +154 -0
  483. package/src/gated-waves-bundle.test.ts +2 -0
  484. package/src/graph-ir-live.test.ts +92 -0
  485. package/src/graph-ir.ts +317 -7
  486. package/src/graph-refs.ts +416 -0
  487. package/src/graph-schema/fixtures/adopt.json +15 -0
  488. package/src/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
  489. package/src/graph-schema/fixtures/index.json +68 -0
  490. package/src/graph-schema/fixtures/plunge.engine-graph.json +158 -0
  491. package/src/graph-schema/fixtures.ts +267 -0
  492. package/src/graph-schema/generate.ts +263 -0
  493. package/src/graph-schema/graph-schema.test.ts +153 -0
  494. package/src/graph-schema/graph.schema.json +1387 -0
  495. package/src/health.schema.json +2 -1
  496. package/src/identity.ts +2 -0
  497. package/src/index.ts +2 -0
  498. package/src/instance-nodes.ts +288 -0
  499. package/src/lexicon.ts +76 -0
  500. package/src/lifecycle/__fixtures__/index-diff/20261026T120000Z-aaaaaaaaaaaa.json +149 -0
  501. package/src/lifecycle/__fixtures__/index-diff/20261026T130000Z-bbbbbbbbbbbb.json +127 -0
  502. package/src/lifecycle/adoption-file.test.ts +100 -0
  503. package/src/lifecycle/adoption-file.ts +154 -0
  504. package/src/lifecycle/cloud-index.test.ts +517 -0
  505. package/src/lifecycle/cloud-index.ts +736 -0
  506. package/src/lifecycle/derive-edges.test.ts +193 -0
  507. package/src/lifecycle/derive-edges.ts +182 -0
  508. package/src/lifecycle/git.ts +10 -4
  509. package/src/lifecycle/identity-match.test.ts +308 -0
  510. package/src/lifecycle/identity-match.ts +257 -0
  511. package/src/lifecycle/index-diff.test.ts +198 -0
  512. package/src/lifecycle/index-diff.ts +476 -0
  513. package/src/lifecycle/index-document.test.ts +64 -0
  514. package/src/lifecycle/index-document.ts +299 -0
  515. package/src/lifecycle/index-drift.test.ts +191 -0
  516. package/src/lifecycle/index-drift.ts +304 -0
  517. package/src/lifecycle/index-import.test.ts +255 -0
  518. package/src/lifecycle/index-import.ts +634 -0
  519. package/src/lifecycle/index-join.test.ts +250 -0
  520. package/src/lifecycle/index-join.ts +304 -0
  521. package/src/lifecycle/index-store-load-bearing.test.ts +187 -0
  522. package/src/lifecycle/index.ts +4 -0
  523. package/src/lifecycle/member-ledger.test.ts +25 -7
  524. package/src/lifecycle/observation-baseline.test.ts +72 -1
  525. package/src/lifecycle/observation-baseline.ts +190 -27
  526. package/src/lifecycle/observe.ts +6 -1
  527. package/src/lifecycle/query.test.ts +69 -1
  528. package/src/lifecycle/query.ts +103 -10
  529. package/src/lifecycle/replay.test.ts +9 -5
  530. package/src/lifecycle/replay.ts +9 -5
  531. package/src/lifecycle/scenario.ts +4 -3
  532. package/src/lifecycle/snapshot-store-fake.ts +29 -0
  533. package/src/lifecycle/snapshot-store-s3.ts +316 -0
  534. package/src/lifecycle/snapshot-store.test.ts +385 -0
  535. package/src/lifecycle/snapshot-store.ts +981 -0
  536. package/src/lifecycle/snapshot.test.ts +13 -0
  537. package/src/lifecycle/snapshot.ts +23 -6
  538. package/src/lifecycle/type-listing.test.ts +166 -0
  539. package/src/lifecycle/type-listing.ts +309 -0
  540. package/src/lint/component-checks.ts +17 -1
  541. package/src/lint/config.test.ts +3 -0
  542. package/src/lint/presets/relaxed.json +2 -1
  543. package/src/lint/presets/strict.json +2 -1
  544. package/src/lint/rules/__fixtures__/comp/comp008/fail/dns.component.ts +15 -0
  545. package/src/lint/rules/__fixtures__/comp/comp008/pass/dns.component.ts +10 -0
  546. package/src/lint/rules/comp/comp.test.ts +2 -1
  547. package/src/lint/rules/comp/comp008-applies-signal-only-root.ts +121 -0
  548. package/src/lint/rules/comp/comp008.test.ts +77 -0
  549. package/src/lint/rules/comp/index.ts +4 -1
  550. package/src/observation-edges.test.ts +33 -0
  551. package/src/observation.ts +36 -1
  552. package/src/op/activities/activity-contracts.ts +1 -1
  553. package/src/op/activities/change-set.ts +8 -5
  554. package/src/op/activity-failure.ts +36 -0
  555. package/src/op/index.ts +2 -1
  556. package/src/op/local-executor.test.ts +80 -0
  557. package/src/op/local-executor.ts +27 -9
  558. package/src/pr-loop-bundle.test.ts +5 -2
  559. package/src/pr-loop.test.ts +57 -1
  560. package/src/pr-loop.ts +154 -6
  561. package/src/pulumi-urn.ts +151 -0
  562. package/src/release-floors.ts +22 -0
  563. package/src/sensitive-key.ts +34 -0
  564. package/src/workspace/adoption-report.test.ts +61 -0
  565. package/src/workspace/adoption-report.ts +103 -0
  566. package/src/workspace/capabilities-contract.test.ts +4 -0
  567. package/src/workspace/capabilities-contract.ts +9 -1
  568. package/src/workspace/capabilities.schema.json +98 -0
  569. package/src/workspace/change-set.schema.json +42 -3
  570. package/src/workspace/ci-cloud-identity.ts +31 -1
  571. package/src/workspace/compose-graph.test.ts +12 -0
  572. package/src/workspace/compose-graph.ts +12 -1
  573. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  574. package/src/workspace/conformance/index.ts +6 -1
  575. package/src/workspace/drift-cli.test.ts +65 -0
  576. package/src/workspace/drift-cli.ts +365 -0
  577. package/src/workspace/drift.schema.json +672 -0
  578. package/src/workspace/graph-cli.ts +5 -0
  579. package/src/workspace/graph.schema.json +72 -0
  580. package/src/workspace/init.test.ts +38 -1
  581. package/src/workspace/init.ts +50 -8
  582. package/src/workspace/intent-gaps.test.ts +18 -2
  583. package/src/workspace/ledger.schema.json +1 -1
  584. package/src/workspace/nested-graph.ts +1 -0
  585. package/src/workspace/pr-report.schema.json +76 -3
  586. package/src/workspace/promote-waves.test.ts +296 -0
  587. package/src/workspace/promote-waves.ts +340 -0
  588. package/src/workspace/promote.test.ts +158 -4
  589. package/src/workspace/promote.ts +398 -57
  590. package/src/workspace/reason-codes.test.ts +4 -1
  591. package/src/workspace/reason-codes.ts +14 -0
  592. package/src/workspace/search-contract.ts +2 -0
  593. package/src/workspace/search.schema.json +103 -5
@@ -0,0 +1,296 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join } from "node:path";
5
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
6
+ import { contentDigest } from "../content-digest";
7
+ import type { GateLedgerPort } from "../op/gate";
8
+ import type { PromoteRunner } from "./promote";
9
+ import { parseStateMappings, planWaves, promoteWavesCommand, stateRefusal, type WavesSummary } from "./promote-waves";
10
+
11
+ const git = (cwd: string, ...a: string[]) => execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@t", ...a], { cwd, encoding: "utf-8" });
12
+
13
+ interface Pending {
14
+ gate: string;
15
+ planDigest?: string;
16
+ description?: string;
17
+ timestamp: string;
18
+ }
19
+
20
+ /** A gate ledger in memory: pending facts as evaluateGate records them, and approvals of the newest one per gate. */
21
+ function ledger(): GateLedgerPort & { pending: Pending[]; approve(gate: string): void } {
22
+ const pending: Pending[] = [];
23
+ const resolutions: unknown[] = [];
24
+ return {
25
+ pending,
26
+ async read() {
27
+ return { resolutions, pending } as never;
28
+ },
29
+ async appendPending(input: Record<string, unknown>) {
30
+ const record = { version: 1, kind: "pending", ...input };
31
+ pending.push(record as unknown as Pending);
32
+ return { record, pushed: false } as never;
33
+ },
34
+ approve(gate: string) {
35
+ const standing = pending.filter((p) => p.gate === gate).at(-1)!;
36
+ const after = new Date(new Date(standing.timestamp).getTime() + 1000).toISOString();
37
+ resolutions.push({ version: 1, op: "workspace-promote", gate, planDigest: standing.planDigest, resolvedBy: "alex", outcome: "approved", timestamp: after });
38
+ },
39
+ } as never;
40
+ }
41
+
42
+ /** The report the fake live-import prints for an estate, in choudoufu's layout: two verified, one unadmitted. */
43
+ const reportFor = (estate: string, extra = "") =>
44
+ [
45
+ `Ratifying /s/${estate}.tfstate for estate "${estate}" against the live system. This was read-only: nothing was written.`,
46
+ "",
47
+ "VERIFIED (2) - verified against the live system:",
48
+ ` aws_s3_bucket.${estate} aws_s3_bucket live id: ${estate}-bucket`,
49
+ " matches",
50
+ ` aws_sqs_queue.${estate} aws_sqs_queue live id: ${estate}-queue`,
51
+ " matches",
52
+ "",
53
+ "UNADMITTED_TYPE (1) - outside the admitted type set:",
54
+ " aws_x.y aws_x live id: -",
55
+ " not admitted",
56
+ "",
57
+ `2 of 3 resource instance(s) are eligible for stamping (VERIFIED or DRIFTED).${extra}`,
58
+ "",
59
+ ].join("\n");
60
+
61
+ describe("planWaves (#4461)", () => {
62
+ test("the canary is wave 1, the rest follow in order at the wave size", () => {
63
+ expect(planWaves(["a", "b", "c", "d", "e"], ["c"], 2)).toEqual([["c"], ["a", "b"], ["d", "e"]]);
64
+ expect(planWaves(["a", "b", "c"], [], 5)).toEqual([["a"], ["b", "c"]]);
65
+ expect(() => planWaves(["a"], ["z"], 1)).toThrow(/--canary z/);
66
+ expect(() => planWaves(["a"], [], 0)).toThrow(/--wave-size/);
67
+ });
68
+ test("--state takes <member>=<path>", () => {
69
+ expect(parseStateMappings(["a=/s/a.tfstate", "b=x=y"])).toEqual({ a: "/s/a.tfstate", b: "x=y" });
70
+ expect(() => parseStateMappings(["/s/a.tfstate"])).toThrow(/<member>=/);
71
+ });
72
+ });
73
+
74
+ describe("chant workspace promote --all (#4461)", () => {
75
+ let root: string;
76
+ let states: string;
77
+ let calls: { argv: string[]; cwd: string }[];
78
+ let failCheck: Set<string>;
79
+ let reportExtra: Map<string, string>;
80
+ /** Members whose next `live-import -approve` fails, once each. */
81
+ let failStampOnce: Set<string>;
82
+
83
+ const memberOf = (argv: string[], cwd: string): string => argv.find((a) => a.startsWith("-estate="))?.slice("-estate=".length) ?? basename(cwd);
84
+ const runner: PromoteRunner = async (argv, cwd) => {
85
+ calls.push({ argv, cwd });
86
+ const member = memberOf(argv, cwd);
87
+ if (argv[1] === "live-check") return failCheck.has(member) ? { exitCode: 2, output: "unsupported construct: provisioner" } : { exitCode: 0, output: "" };
88
+ if (argv.includes("-approve") && failStampOnce.delete(member)) return { exitCode: 1, output: "Error: the live system refused the stamp write" };
89
+ if (argv.includes("-approve")) return { exitCode: 0, output: "2 resource(s) newly stamped, 0 already stamped, 0 newly recorded, 0 re-recorded for sensitivity only, 0 already recorded, 0 failed, 1 skipped.\n" };
90
+ return { exitCode: 0, output: reportFor(member, reportExtra.get(member)) };
91
+ };
92
+
93
+ const v4 = JSON.stringify({ version: 4, terraform_version: "1.5.7", serial: 1, lineage: "l", outputs: {}, resources: [] });
94
+ const members = ["a", "b", "c", "d"];
95
+
96
+ beforeEach(() => {
97
+ vi.spyOn(console, "error").mockImplementation(() => {});
98
+ vi.spyOn(console, "log").mockImplementation(() => {});
99
+ calls = [];
100
+ failCheck = new Set();
101
+ reportExtra = new Map();
102
+ failStampOnce = new Set();
103
+ root = mkdtempSync(join(tmpdir(), "chant-promote-waves-"));
104
+ states = mkdtempSync(join(tmpdir(), "chant-promote-waves-states-"));
105
+ git(root, "init", "-q", "-b", "main");
106
+ for (const m of members) {
107
+ mkdirSync(join(root, m));
108
+ writeFileSync(join(root, m, "main.tf"), "# tf\n");
109
+ writeFileSync(join(states, `${m}.tfstate`), v4);
110
+ }
111
+ writeFileSync(
112
+ join(root, "chant.workspace.json"),
113
+ JSON.stringify({ name: "w", schema: 1, members: members.map((m) => ({ name: m, dir: m, kind: "terraform" })) }, null, 2) + "\n",
114
+ );
115
+ git(root, "add", "-A");
116
+ git(root, "commit", "-qm", "init");
117
+ });
118
+ afterEach(() => {
119
+ rmSync(root, { recursive: true, force: true });
120
+ rmSync(states, { recursive: true, force: true });
121
+ vi.restoreAllMocks();
122
+ });
123
+
124
+ const kinds = () => Object.fromEntries(JSON.parse(readFileSync(join(root, "chant.workspace.json"), "utf-8")).members.map((m: { name: string; kind: string }) => [m.name, m.kind]));
125
+ const outcomes = (s: WavesSummary) => Object.fromEntries(s.roots.map((r) => [r.member, r.outcome]));
126
+ const promote = (l: GateLedgerPort, extra: { canary?: string[]; waveSize?: number } = {}) =>
127
+ promoteWavesCommand({ root, all: true, stateDir: states, canary: ["b"], waveSize: 2, json: true, runner, ledger: l, ...extra });
128
+
129
+ test("a failing canary stops every later wave and leaves the tree alone", async () => {
130
+ const l = ledger();
131
+ failCheck.add("b");
132
+ const s = await promote(l);
133
+ expect(s.waves).toEqual([["b"], ["a", "c"], ["d"]]);
134
+ expect(outcomes(s)).toEqual({ b: "checks-failed", a: "not-run", c: "not-run", d: "not-run" });
135
+ expect(s.exitCode).toBe(1);
136
+ expect(calls.every((c) => basename(c.cwd) === "b")).toBe(true);
137
+ expect(l.pending).toHaveLength(0);
138
+ expect(kinds()).toEqual({ a: "terraform", b: "terraform", c: "terraform", d: "terraform" });
139
+ expect(git(root, "status", "--porcelain")).toBe("");
140
+ });
141
+
142
+ test("one gate per root, each bound to that root's own live-import report digest", async () => {
143
+ const l = ledger();
144
+
145
+ const first = await promote(l);
146
+ expect(outcomes(first)).toEqual({ b: "gated", a: "not-run", c: "not-run", d: "not-run" });
147
+ expect(first.exitCode).toBe(3);
148
+ expect(l.pending.map((p) => p.gate)).toEqual(["b"]);
149
+ const b = first.roots.find((r) => r.member === "b")!;
150
+ expect(b.reportDigest).toBe(contentDigest(reportFor("b").trimEnd()));
151
+ expect(l.pending[0].planDigest).toBe(b.digest);
152
+ expect(l.pending[0].description).toContain(b.reportDigest);
153
+ expect(b.eligible).toEqual({ stampable: 2, total: 3 });
154
+ expect(b.unadmitted).toBe(1);
155
+
156
+ l.approve("b");
157
+ calls = [];
158
+ const second = await promote(l);
159
+ expect(outcomes(second)).toEqual({ b: "applied", a: "gated", c: "gated", d: "not-run" });
160
+ expect(second.roots.find((r) => r.member === "b")!.stamped).toEqual({ newly: 2, already: 0, failed: 0 });
161
+ // The stamp runs in b's staging directory, where its choudoufu init ran, not in the tree (chant#4492).
162
+ const bInit = calls.find((c) => c.argv[1] === "init" && basename(c.cwd) === "b")!;
163
+ expect(calls.filter((c) => c.argv.includes("-approve")).map((c) => c.cwd)).toEqual([bInit.cwd]);
164
+ expect(bInit.cwd).not.toBe(join(realpathSync(root), "b"));
165
+ expect(kinds()).toMatchObject({ b: "choudoufu", a: "terraform", c: "terraform" });
166
+
167
+ const gateOf = (m: string) => l.pending.filter((p) => p.gate === m);
168
+ const rowOf = (s: WavesSummary, m: string) => s.roots.find((r) => r.member === m)!;
169
+ expect(gateOf("a")).toHaveLength(1);
170
+ expect(gateOf("c")).toHaveLength(1);
171
+ expect(gateOf("a")[0].planDigest).toBe(rowOf(second, "a").digest);
172
+ expect(gateOf("c")[0].planDigest).toBe(rowOf(second, "c").digest);
173
+ expect(rowOf(second, "a").digest).not.toBe(rowOf(second, "c").digest);
174
+ expect(rowOf(second, "a").reportDigest).toBe(contentDigest(reportFor("a").trimEnd()));
175
+ expect(rowOf(second, "c").reportDigest).toBe(contentDigest(reportFor("c").trimEnd()));
176
+
177
+ // a's approval is for the report its approver read: a changed report is a new digest and stays gated.
178
+ l.approve("a");
179
+ l.approve("c");
180
+ reportExtra.set("a", "\nDRIFTED (1) - verified, but drifted from the live system:");
181
+ const third = await promote(l);
182
+ expect(outcomes(third)).toMatchObject({ b: "already-promoted", a: "gated", c: "applied", d: "not-run" });
183
+ expect(kinds()).toMatchObject({ a: "terraform", c: "choudoufu" });
184
+ });
185
+
186
+ test("within a wave a failed root leaves the others to run, and the next wave waits", async () => {
187
+ const l = ledger();
188
+ await promote(l);
189
+ l.approve("b");
190
+ failCheck.add("a");
191
+ const s = await promote(l);
192
+ expect(outcomes(s)).toEqual({ b: "applied", a: "checks-failed", c: "gated", d: "not-run" });
193
+ expect(s.exitCode).toBe(1);
194
+ expect(l.pending.filter((p) => p.gate === "a")).toHaveLength(0);
195
+ expect(kinds()).toMatchObject({ a: "terraform", b: "choudoufu", c: "terraform", d: "terraform" });
196
+ expect(s.roots.find((r) => r.member === "d")!.reason).toMatch(/wave 2 did not land \(a checks-failed, c gated\)/);
197
+ });
198
+
199
+ test("re-running over a landed workspace is a no-op", async () => {
200
+ const l = ledger();
201
+ const all = { waveSize: 3 };
202
+ await promote(l, all);
203
+ l.approve("b");
204
+ await promote(l, all);
205
+ for (const m of ["a", "c", "d"]) l.approve(m);
206
+ const landed = await promote(l, all);
207
+ expect(outcomes(landed)).toEqual({ b: "already-promoted", a: "applied", c: "applied", d: "applied" });
208
+ expect(landed.exitCode).toBe(0);
209
+
210
+ calls = [];
211
+ const pendingBefore = l.pending.length;
212
+ const again = await promote(l, all);
213
+ expect(outcomes(again)).toEqual({ b: "already-promoted", a: "already-promoted", c: "already-promoted", d: "already-promoted" });
214
+ expect(again.exitCode).toBe(0);
215
+ expect(again.outcome).toBe("landed");
216
+ expect(calls).toHaveLength(0);
217
+ expect(l.pending).toHaveLength(pendingBefore);
218
+ });
219
+
220
+ test("a re-run after a failed live-import -approve retries the stamp instead of reporting already-promoted (#4485)", async () => {
221
+ const l = ledger();
222
+ await promote(l);
223
+ l.approve("b");
224
+ failStampOnce.add("b");
225
+ const failed = await promote(l);
226
+ const row = (s: WavesSummary, m: string) => s.roots.find((r) => r.member === m)!;
227
+ expect(row(failed, "b").outcome).toBe("stamp-failed");
228
+ expect(row(failed, "b").reason).toContain(`choudoufu live-import -state=${join(states, "b.tfstate")} -estate=b -approve`);
229
+ expect(failed.exitCode).toBe(1);
230
+ // The kind was written; the stamp is kept as pending.
231
+ expect(kinds()).toMatchObject({ b: "choudoufu" });
232
+ expect(existsSync(join(root, ".chant", "workspace-promote", "b.stamp.json"))).toBe(true);
233
+
234
+ calls = [];
235
+ const retried = await promote(l);
236
+ expect(row(retried, "b").outcome).toBe("applied");
237
+ expect(row(retried, "b").stamped).toEqual({ newly: 2, already: 0, failed: 0 });
238
+ // Only the stamp ran for b, in a staging of its own whose init installed the providers (chant#4492): no live-check, no report, no new gate.
239
+ const forB = calls.filter((c) => memberOf(c.argv, c.cwd) === "b");
240
+ expect(forB.map((c) => c.argv.slice(1))).toEqual([
241
+ ["init", "-input=false", "-backend=false", "-no-color"],
242
+ ["live-import", `-state=${join(states, "b.tfstate")}`, "-estate=b", "-approve"],
243
+ ]);
244
+ expect(forB[1]!.cwd).toBe(forB[0]!.cwd);
245
+ expect(forB[1]!.cwd).not.toBe(join(realpathSync(root), "b"));
246
+ expect(l.pending.filter((p) => p.gate === "b")).toHaveLength(1);
247
+ expect(existsSync(join(root, ".chant", "workspace-promote", "b.stamp.json"))).toBe(false);
248
+ // The canary landed, so wave 2 ran.
249
+ expect(outcomes(retried)).toMatchObject({ a: "gated", c: "gated" });
250
+
251
+ // Once stamped, a re-run reports it promoted and runs nothing for it.
252
+ calls = [];
253
+ const again = await promote(l);
254
+ expect(row(again, "b").outcome).toBe("already-promoted");
255
+ expect(calls.some((c) => memberOf(c.argv, c.cwd) === "b")).toBe(false);
256
+ });
257
+
258
+ test("a stamp that fails again stays stamp-failed with the command, and the later waves wait (#4485)", async () => {
259
+ const l = ledger();
260
+ await promote(l);
261
+ l.approve("b");
262
+ failStampOnce.add("b");
263
+ await promote(l);
264
+ failStampOnce.add("b");
265
+ const s = await promote(l);
266
+ const b = s.roots.find((r) => r.member === "b")!;
267
+ expect(b.outcome).toBe("stamp-failed");
268
+ expect(b.reason).toMatch(/failed again; live-import is safe to re-run: `choudoufu live-import -state=.* -estate=b -approve` in b/);
269
+ expect(outcomes(s)).toEqual({ b: "stamp-failed", a: "not-run", c: "not-run", d: "not-run" });
270
+ expect(existsSync(join(root, ".chant", "workspace-promote", "b.stamp.json"))).toBe(true);
271
+ });
272
+
273
+ test("a root without a local v4 state is refused by name and stages nothing", async () => {
274
+ const l = ledger();
275
+ writeFileSync(join(root, "a", "backend.tf"), 'terraform {\n backend "s3" {}\n}\n');
276
+ git(root, "add", "-A");
277
+ git(root, "commit", "-qm", "s3");
278
+ rmSync(join(states, "a.tfstate"));
279
+ writeFileSync(join(states, "c.tfstate"), JSON.stringify({ version: 3, serial: 1 }));
280
+ const s = await promoteWavesCommand({ root, all: true, stateDir: states, canary: ["b"], waveSize: 3, json: true, runner, ledger: l });
281
+ l.approve("b");
282
+ const second = await promoteWavesCommand({ root, all: true, stateDir: states, canary: ["b"], waveSize: 3, json: true, runner, ledger: l });
283
+ expect(outcomes(s).b).toBe("gated");
284
+ const row = (m: string) => second.roots.find((r) => r.member === m)!;
285
+ expect(row("a").outcome).toBe("refused");
286
+ expect(row("a").reason).toMatch(/a uses the s3 backend .*terraform state pull/);
287
+ expect(row("c").outcome).toBe("refused");
288
+ expect(row("c").reason).toMatch(/version 3 state; live-import reads version 4/);
289
+ expect(row("d").outcome).toBe("gated");
290
+ expect(calls.some((c) => memberOf(c.argv, c.cwd) === "a" || memberOf(c.argv, c.cwd) === "c")).toBe(false);
291
+ });
292
+
293
+ test("stateRefusal accepts a local v4 state", () => {
294
+ expect(stateRefusal("a", join(root, "a"), join(states, "a.tfstate"))).toBeUndefined();
295
+ });
296
+ });
@@ -0,0 +1,340 @@
1
+ /**
2
+ * `chant workspace promote --all` (#4461): adopt a workspace of plain
3
+ * Terraform roots into choudoufu in waves, a canary first.
4
+ *
5
+ * Each root goes through {@link promoteCommand} on its own, so each root has
6
+ * its own gate, `workspace-promote/<member>`, whose digest binds that root's
7
+ * own `live-import` report. A root that fails leaves every other root as it
8
+ * was: its staging ran in a worktree, and nothing of it reached the tree.
9
+ *
10
+ * The order:
11
+ *
12
+ * - The canary (`--canary <member>`, else the first candidate in declaration
13
+ * order) is wave 1. Unless every canary lands, no other wave runs.
14
+ * - The rest follow in declaration order, `--wave-size` roots a wave. Every
15
+ * root of a wave is tried even when one of them fails, and the next wave
16
+ * runs only when the whole wave landed.
17
+ *
18
+ * A root lands when it was applied, was already promoted, or (under
19
+ * `--dry-run`) staged cleanly. A gated root has not landed: approve its gate
20
+ * and run the same command again. Roots already of kind `choudoufu` report
21
+ * `already-promoted` and are not staged again, so a re-run over a workspace
22
+ * that has landed is a no-op. A root whose kind was written but whose
23
+ * `live-import -approve` failed is kind `choudoufu` with a pending stamp
24
+ * (#4485): a re-run retries the stamp, and the root lands once it passes or
25
+ * stays `stamp-failed` with the command to run.
26
+ *
27
+ * Before a root is staged its state is checked: `live-import` reads a local
28
+ * tfstate of format version 4, so a missing state under a remote backend, or
29
+ * a state of another version, refuses that root by name.
30
+ */
31
+
32
+ import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
33
+ import { join, resolve } from "node:path";
34
+ import { formatError, formatInfo, formatSuccess, formatWarning } from "../cli/format";
35
+ import type { GateLedgerPort } from "../op/gate";
36
+ import { readDeclaration } from "./declaration";
37
+ import { PROMOTE_GATED_EXIT, PromoteError, promoteCommand, readPendingStamp, retryPendingStamp, stampCommand, type PromoteResult, type PromoteRunner } from "./promote";
38
+ import { workingTree } from "./tree";
39
+
40
+ export const DEFAULT_WAVE_SIZE = 5;
41
+
42
+ export type WaveRootOutcome =
43
+ /** Staged, approved, kind written and markers stamped. */
44
+ | "applied"
45
+ /** Already of kind choudoufu (or above): nothing to do. */
46
+ | "already-promoted"
47
+ /** `--dry-run`: staged and checked, no gate recorded. */
48
+ | "dry-run"
49
+ /** A gate is pending for this root's digest. */
50
+ | "gated"
51
+ /** live-check or the live-import report failed in staging. */
52
+ | "checks-failed"
53
+ /** The state is not a local v4 tfstate. */
54
+ | "refused"
55
+ /** The kind was written but `live-import -approve` failed. */
56
+ | "stamp-failed"
57
+ /** Anything else that stopped the root (uncommitted changes, a patch that no longer applies). */
58
+ | "failed"
59
+ /** An earlier wave did not land. */
60
+ | "not-run";
61
+
62
+ const LANDED: ReadonlySet<WaveRootOutcome> = new Set(["applied", "already-promoted", "dry-run"]);
63
+
64
+ export interface WaveRootResult {
65
+ member: string;
66
+ wave: number;
67
+ canary: boolean;
68
+ outcome: WaveRootOutcome;
69
+ kind: string;
70
+ state?: string;
71
+ digest?: string;
72
+ reportDigest?: string;
73
+ /** From the report: instances eligible for stamping, of the total. */
74
+ eligible?: { stampable: number; total: number };
75
+ /** From the report: instances outside the admitted type set, never stamped. */
76
+ unadmitted?: number;
77
+ /** From the stamp report under -approve. */
78
+ stamped?: { newly: number; already: number; failed: number };
79
+ reason?: string;
80
+ }
81
+
82
+ export interface WavesSummary {
83
+ outcome: "landed" | "gated" | "failed" | "dry-run";
84
+ exitCode: number;
85
+ waves: string[][];
86
+ roots: WaveRootResult[];
87
+ }
88
+
89
+ export interface PromoteWavesOptions {
90
+ root: string;
91
+ /** Every terraform member, and every choudoufu member (which reports already-promoted). */
92
+ all?: boolean;
93
+ /** `--state <member>=<path>`, repeatable. Without `all`, the members named here are the set. */
94
+ states?: Record<string, string>;
95
+ /** `--state-dir <dir>`: a member's state is `<dir>/<member>.tfstate`. */
96
+ stateDir?: string;
97
+ canary?: string[];
98
+ waveSize?: number;
99
+ dryRun?: boolean;
100
+ json?: boolean;
101
+ /** For tests. */
102
+ runner?: PromoteRunner;
103
+ ledger?: GateLedgerPort;
104
+ now?: string;
105
+ }
106
+
107
+ /** Parse `--state <member>=<path>` values. */
108
+ export function parseStateMappings(values: readonly string[]): Record<string, string> {
109
+ const out: Record<string, string> = {};
110
+ for (const v of values) {
111
+ const at = v.indexOf("=");
112
+ if (at <= 0 || at === v.length - 1) throw new PromoteError(`--state with --all takes <member>=<terraform.tfstate>: got "${v}"`);
113
+ const member = v.slice(0, at);
114
+ if (out[member] !== undefined) throw new PromoteError(`--state names ${member} twice`);
115
+ out[member] = v.slice(at + 1);
116
+ }
117
+ return out;
118
+ }
119
+
120
+ /** The waves: the canaries first, then the rest in declaration order, `size` at a time. */
121
+ export function planWaves(candidates: readonly string[], canary: readonly string[], size: number): string[][] {
122
+ if (!Number.isInteger(size) || size < 1) throw new PromoteError(`--wave-size needs a whole number from 1: got ${size}`);
123
+ for (const c of canary) {
124
+ if (!candidates.includes(c)) throw new PromoteError(`--canary ${c} is not one of the roots being promoted (${candidates.join(", ") || "none"})`);
125
+ }
126
+ const first = canary.length > 0 ? [...new Set(canary)] : candidates.slice(0, 1);
127
+ const rest = candidates.filter((m) => !first.includes(m));
128
+ const waves = first.length > 0 ? [first] : [];
129
+ for (let i = 0; i < rest.length; i += size) waves.push(rest.slice(i, i + size));
130
+ return waves;
131
+ }
132
+
133
+ /** The backend a root's .tf files declare, or undefined for none or `local`. */
134
+ export function remoteBackend(dir: string): string | undefined {
135
+ let files: string[];
136
+ try {
137
+ files = readdirSync(dir).filter((f) => f.endsWith(".tf"));
138
+ } catch {
139
+ return undefined;
140
+ }
141
+ for (const f of files) {
142
+ const text = readFileSync(join(dir, f), "utf-8").replace(/^\s*(#|\/\/).*$/gm, "");
143
+ const backend = /\bbackend\s+"([^"]+)"/.exec(text);
144
+ if (backend && backend[1] !== "local") return backend[1];
145
+ if (/^\s*cloud\s*\{/m.test(text)) return "cloud";
146
+ }
147
+ return undefined;
148
+ }
149
+
150
+ /**
151
+ * Why `state` cannot feed `live-import` for `member`, or undefined when it
152
+ * can: it must be a local tfstate of format version 4.
153
+ */
154
+ export function stateRefusal(member: string, memberDir: string, state: string): string | undefined {
155
+ const backend = remoteBackend(memberDir);
156
+ if (!existsSync(state)) {
157
+ if (backend) {
158
+ return `${member} uses the ${backend} backend and has no local state at ${state}: live-import reads a local tfstate, so run \`terraform state pull > ${state}\` in ${memberDir} first (or pass --state ${member}=<pulled file>)`;
159
+ }
160
+ return `${member} has no state at ${state}: pass --state ${member}=<terraform.tfstate> or --state-dir <dir>`;
161
+ }
162
+ let parsed: unknown;
163
+ try {
164
+ parsed = JSON.parse(readFileSync(state, "utf-8"));
165
+ } catch {
166
+ return `${member}: ${state} is not a JSON tfstate`;
167
+ }
168
+ const version = (parsed as { version?: unknown } | null)?.version;
169
+ if (version !== 4) {
170
+ const hint = (parsed as { backend?: unknown } | null)?.backend !== undefined
171
+ ? " It looks like .terraform/terraform.tfstate, the backend's settings: run `terraform state pull` for the state itself."
172
+ : "";
173
+ return `${member}: ${state} is a version ${String(version)} state; live-import reads version 4 only (Terraform 0.12 and later).${hint}`;
174
+ }
175
+ return undefined;
176
+ }
177
+
178
+ function reportCounts(r: PromoteResult): Pick<WaveRootResult, "eligible" | "unadmitted"> {
179
+ if (!r.adoption) return {};
180
+ return {
181
+ eligible: { stampable: r.adoption.claimed, total: r.adoption.total },
182
+ unadmitted: r.adoption.entries.filter((e) => e.status === "UNADMITTED_TYPE").length,
183
+ };
184
+ }
185
+
186
+ function stampCounts(text: string | undefined): WaveRootResult["stamped"] {
187
+ const m = text ? /(\d+) resource\(s\) newly stamped, (\d+) already stamped, .*?(\d+) failed/.exec(text) : null;
188
+ return m ? { newly: Number(m[1]), already: Number(m[2]), failed: Number(m[3]) } : undefined;
189
+ }
190
+
191
+ function rootOutcome(r: PromoteResult): WaveRootOutcome {
192
+ if (r.outcome === "applied") return r.exitCode === 0 ? "applied" : "stamp-failed";
193
+ if (r.outcome === "top-of-ladder") return "already-promoted";
194
+ return r.outcome;
195
+ }
196
+
197
+ export async function promoteWavesCommand(opts: PromoteWavesOptions): Promise<WavesSummary> {
198
+ const root = resolve(opts.root);
199
+ const decl = readDeclaration(workingTree(root));
200
+ const states = opts.states ?? {};
201
+ const say = (m: string): void => {
202
+ if (!opts.json) console.error(m);
203
+ };
204
+
205
+ for (const name of Object.keys(states)) {
206
+ if (!decl.members.some((m) => m.name === name)) throw new PromoteError(`--state names ${name}, which the declaration has no member of`);
207
+ }
208
+ const selected = decl.members.filter((m) => (opts.all ? m.kind === "terraform" || m.kind === "choudoufu" : states[m.name] !== undefined));
209
+ if (selected.length === 0) {
210
+ throw new PromoteError(opts.all ? "the declaration has no member of kind terraform to promote" : "name the roots with --state <member>=<path>, or pass --all");
211
+ }
212
+ for (const m of selected) {
213
+ if (m.kind !== "terraform" && m.kind !== "choudoufu") throw new PromoteError(`${m.name} is kind ${m.kind}; promote --all adopts terraform roots into choudoufu`);
214
+ }
215
+
216
+ // The waves are planned over every selected root, promoted or not, so a
217
+ // re-run keeps the same waves and the same canary: an already promoted
218
+ // root lands in its wave without being staged.
219
+ const waves = planWaves(selected.map((m) => m.name), opts.canary ?? [], opts.waveSize ?? DEFAULT_WAVE_SIZE);
220
+ const byName = new Map(selected.map((m) => [m.name, m]));
221
+ const roots: WaveRootResult[] = [];
222
+
223
+ const stateFor = (name: string): string => {
224
+ const dir = byName.get(name)!.dir;
225
+ if (states[name] !== undefined) return resolve(states[name]);
226
+ if (opts.stateDir) return resolve(opts.stateDir, `${name}.tfstate`);
227
+ return join(root, dir, "terraform.tfstate");
228
+ };
229
+
230
+ let stopped: string | undefined;
231
+ for (const [i, wave] of waves.entries()) {
232
+ const n = i + 1;
233
+ const canary = i === 0;
234
+ if (stopped) {
235
+ for (const name of wave) roots.push({ member: name, wave: n, canary, outcome: "not-run", kind: byName.get(name)!.kind, reason: stopped });
236
+ continue;
237
+ }
238
+ say(formatInfo(`wave ${n}${canary ? " (canary)" : ""}: ${wave.join(", ")}`));
239
+ const results: WaveRootResult[] = [];
240
+ for (const name of wave) {
241
+ const member = byName.get(name)!;
242
+ const state = stateFor(name);
243
+ if (member.kind !== "terraform") {
244
+ const pending = member.kind === "choudoufu" ? readPendingStamp(realpathSync(root), name) : undefined;
245
+ if (!pending || opts.dryRun) {
246
+ const why = pending ? `already kind ${member.kind}; its stamp is pending and --dry-run does not retry it` : `already kind ${member.kind}; nothing staged`;
247
+ results.push({ member: name, wave: n, canary, outcome: pending ? "stamp-failed" : "already-promoted", kind: member.kind, reason: why });
248
+ continue;
249
+ }
250
+ // #4485: the kind was written and the stamp failed; retry the stamp, against the state this run names if it names one.
251
+ const explicit = states[name] !== undefined || opts.stateDir !== undefined ? stateFor(name) : undefined;
252
+ const row: WaveRootResult = { member: name, wave: n, canary, outcome: "stamp-failed", kind: member.kind, state: explicit ?? pending.state };
253
+ try {
254
+ const r = (await retryPendingStamp({ root, member: name, ...(explicit ? { state: explicit } : {}), ...(opts.json !== undefined ? { json: opts.json } : {}), ...(opts.runner ? { runner: opts.runner } : {}) }))!;
255
+ row.outcome = rootOutcome(r);
256
+ const stamped = stampCounts(r.followUpOutput);
257
+ if (stamped) row.stamped = stamped;
258
+ row.reason = row.outcome === "applied"
259
+ ? `kind written by an earlier run; its failed stamp was retried and passed`
260
+ : `kind written, but ${r.followUpFailed} failed again; live-import is safe to re-run: ${stampCommand({ name, dir: member.dir }, row.state!)}`;
261
+ } catch (err) {
262
+ if (!(err instanceof PromoteError)) throw err;
263
+ row.reason = err.message;
264
+ say(formatError({ message: `${name}: ${err.message}` }));
265
+ }
266
+ results.push(row);
267
+ continue;
268
+ }
269
+ const row: WaveRootResult = { member: name, wave: n, canary, outcome: "failed", kind: member.kind, state };
270
+ const refusal = stateRefusal(name, join(root, member.dir), state);
271
+ if (refusal) {
272
+ row.outcome = "refused";
273
+ row.reason = refusal;
274
+ say(formatError({ message: refusal }));
275
+ results.push(row);
276
+ continue;
277
+ }
278
+ try {
279
+ const r = await promoteCommand({
280
+ root,
281
+ member: name,
282
+ state,
283
+ dryRun: opts.dryRun,
284
+ json: opts.json,
285
+ nested: true,
286
+ ...(opts.runner ? { runner: opts.runner } : {}),
287
+ ...(opts.ledger ? { ledger: opts.ledger } : {}),
288
+ ...(opts.now ? { now: opts.now } : {}),
289
+ });
290
+ row.outcome = rootOutcome(r);
291
+ if (r.digest) row.digest = r.digest;
292
+ if (r.reportDigest) row.reportDigest = r.reportDigest;
293
+ Object.assign(row, reportCounts(r));
294
+ const stamped = stampCounts(r.followUpOutput);
295
+ if (stamped) row.stamped = stamped;
296
+ if (row.outcome === "checks-failed") row.reason = r.failureOutput?.split("\n").at(-1) || "a staging check failed";
297
+ if (row.outcome === "stamp-failed") {
298
+ row.reason = `kind written, but ${r.followUpFailed} failed; run this again to retry the stamp, or run it by hand (live-import is safe to re-run): ${stampCommand({ name, dir: member.dir }, state)}`;
299
+ }
300
+ if (row.outcome === "gated") row.reason = `approve with \`chant approve workspace-promote ${name}\`, then run this again`;
301
+ } catch (err) {
302
+ if (!(err instanceof PromoteError)) throw err;
303
+ row.outcome = "failed";
304
+ row.reason = err.message;
305
+ say(formatError({ message: `${name}: ${err.message}` }));
306
+ }
307
+ results.push(row);
308
+ }
309
+ roots.push(...results);
310
+ const short = results.filter((r) => !LANDED.has(r.outcome));
311
+ if (short.length > 0) {
312
+ stopped = `${canary ? "the canary wave" : `wave ${n}`} did not land (${short.map((r) => `${r.member} ${r.outcome}`).join(", ")})`;
313
+ }
314
+ }
315
+
316
+ const failed = roots.some((r) => r.outcome === "refused" || r.outcome === "checks-failed" || r.outcome === "failed" || r.outcome === "stamp-failed");
317
+ const gated = roots.some((r) => r.outcome === "gated");
318
+ const outcome: WavesSummary["outcome"] = failed ? "failed" : gated ? "gated" : opts.dryRun ? "dry-run" : "landed";
319
+ const exitCode = failed ? 1 : gated ? PROMOTE_GATED_EXIT : 0;
320
+ const summary: WavesSummary = { outcome, exitCode, waves, roots };
321
+
322
+ if (opts.json) {
323
+ console.log(JSON.stringify(summary, null, 2));
324
+ } else {
325
+ say("");
326
+ for (const r of roots) {
327
+ const counts = [
328
+ r.eligible ? `${r.eligible.stampable} of ${r.eligible.total} stampable` : "",
329
+ r.unadmitted ? `${r.unadmitted} unadmitted, not stamped` : "",
330
+ r.stamped ? `${r.stamped.newly} newly stamped, ${r.stamped.already} already stamped, ${r.stamped.failed} failed` : "",
331
+ ].filter(Boolean).join("; ");
332
+ const where = `wave ${r.wave}${r.canary ? " canary" : ""}`;
333
+ const line = `${r.member} ${where} ${r.outcome}${counts ? ` (${counts})` : ""}${r.reason ? `: ${r.reason}` : ""}`;
334
+ say(LANDED.has(r.outcome) ? formatInfo(line) : r.outcome === "gated" || r.outcome === "not-run" ? formatWarning({ message: line }) : formatError({ message: line }));
335
+ }
336
+ if (outcome === "landed") say(formatSuccess(`every root landed. Review and commit the declaration.`));
337
+ else if (outcome === "gated") say(formatInfo("approve the pending gates and run the same command again; landed roots are not staged twice"));
338
+ }
339
+ return summary;
340
+ }