@intentius/chant-lexicon-terraform 0.124.52 → 0.124.55

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 (641) hide show
  1. package/README.md +14 -10
  2. package/dist/__fixtures__/pr-loop/estate.component.d.ts +7 -0
  3. package/dist/__fixtures__/pr-loop/estate.component.d.ts.map +1 -0
  4. package/dist/__fixtures__/pr-loop/estate.component.js +7 -0
  5. package/dist/__fixtures__/report/generate.d.ts +26 -0
  6. package/dist/__fixtures__/report/generate.d.ts.map +1 -0
  7. package/dist/__fixtures__/report/generate.js +58 -0
  8. package/dist/applied-state.d.ts +27 -0
  9. package/dist/applied-state.d.ts.map +1 -0
  10. package/dist/applied-state.js +44 -0
  11. package/dist/backend-config.d.ts +53 -0
  12. package/dist/backend-config.d.ts.map +1 -0
  13. package/dist/backend-config.js +136 -0
  14. package/dist/behaviour/kinds.d.ts.map +1 -1
  15. package/dist/behaviour/kinds.js +4 -0
  16. package/dist/behaviour/predict.d.ts.map +1 -1
  17. package/dist/behaviour/predict.js +6 -2
  18. package/dist/behaviour/reference-catalog.d.ts +16 -5
  19. package/dist/behaviour/reference-catalog.d.ts.map +1 -1
  20. package/dist/behaviour/reference-catalog.js +16 -5
  21. package/dist/behaviour/request.d.ts.map +1 -1
  22. package/dist/behaviour/request.js +3 -2
  23. package/dist/changed-roots.d.ts +6 -4
  24. package/dist/changed-roots.d.ts.map +1 -1
  25. package/dist/changed-roots.js +14 -18
  26. package/dist/ci-setup.d.ts +11 -0
  27. package/dist/ci-setup.d.ts.map +1 -0
  28. package/dist/ci-setup.js +41 -0
  29. package/dist/commands.d.ts.map +1 -1
  30. package/dist/commands.js +4 -3
  31. package/dist/components/capability-plugin.d.ts +105 -9
  32. package/dist/components/capability-plugin.d.ts.map +1 -1
  33. package/dist/components/capability-plugin.js +357 -36
  34. package/dist/components/compose-group.d.ts +145 -0
  35. package/dist/components/compose-group.d.ts.map +1 -0
  36. package/dist/components/compose-group.js +407 -0
  37. package/dist/components/placeholder-preview.d.ts +78 -0
  38. package/dist/components/placeholder-preview.d.ts.map +1 -0
  39. package/dist/components/placeholder-preview.js +254 -0
  40. package/dist/components/release-inputs.d.ts +29 -0
  41. package/dist/components/release-inputs.d.ts.map +1 -0
  42. package/dist/components/release-inputs.js +57 -0
  43. package/dist/components/trace-facts.d.ts +16 -0
  44. package/dist/components/trace-facts.d.ts.map +1 -0
  45. package/dist/components/trace-facts.js +19 -0
  46. package/dist/compose/apply-events.d.ts +46 -0
  47. package/dist/compose/apply-events.d.ts.map +1 -0
  48. package/dist/compose/apply-events.js +77 -0
  49. package/dist/compose/estate.d.ts +194 -0
  50. package/dist/compose/estate.d.ts.map +1 -0
  51. package/dist/compose/estate.js +363 -0
  52. package/dist/compose/hcl.d.ts +99 -0
  53. package/dist/compose/hcl.d.ts.map +1 -0
  54. package/dist/compose/hcl.js +302 -0
  55. package/dist/compose/module-address.d.ts +24 -0
  56. package/dist/compose/module-address.d.ts.map +1 -0
  57. package/dist/compose/module-address.js +43 -0
  58. package/dist/compose/root.d.ts +127 -0
  59. package/dist/compose/root.d.ts.map +1 -0
  60. package/dist/compose/root.js +586 -0
  61. package/dist/compose/run.d.ts +183 -0
  62. package/dist/compose/run.d.ts.map +1 -0
  63. package/dist/compose/run.js +447 -0
  64. package/dist/compose/slice.d.ts +102 -0
  65. package/dist/compose/slice.d.ts.map +1 -0
  66. package/dist/compose/slice.js +249 -0
  67. package/dist/compose/state-address.d.ts +28 -0
  68. package/dist/compose/state-address.d.ts.map +1 -0
  69. package/dist/compose/state-address.js +154 -0
  70. package/dist/compose/state-merge.d.ts +52 -0
  71. package/dist/compose/state-merge.d.ts.map +1 -0
  72. package/dist/compose/state-merge.js +135 -0
  73. package/dist/compose.d.ts +179 -0
  74. package/dist/compose.d.ts.map +1 -0
  75. package/dist/compose.js +482 -0
  76. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  77. package/dist/composites/terraform-apply-op.js +7 -3
  78. package/dist/cone.d.ts +53 -0
  79. package/dist/cone.d.ts.map +1 -0
  80. package/dist/cone.js +119 -0
  81. package/dist/config.d.ts +59 -13
  82. package/dist/config.d.ts.map +1 -1
  83. package/dist/config.js +43 -9
  84. package/dist/cost-hold.d.ts +27 -0
  85. package/dist/cost-hold.d.ts.map +1 -0
  86. package/dist/cost-hold.js +21 -0
  87. package/dist/describe-resources.d.ts +74 -12
  88. package/dist/describe-resources.d.ts.map +1 -1
  89. package/dist/describe-resources.js +374 -66
  90. package/dist/drift-read.d.ts +33 -0
  91. package/dist/drift-read.d.ts.map +1 -0
  92. package/dist/drift-read.js +243 -0
  93. package/dist/drift-record.d.ts +147 -0
  94. package/dist/drift-record.d.ts.map +1 -0
  95. package/dist/drift-record.js +383 -0
  96. package/dist/drift-rows.d.ts +61 -0
  97. package/dist/drift-rows.d.ts.map +1 -0
  98. package/dist/drift-rows.js +108 -0
  99. package/dist/exit-codes.d.ts +55 -0
  100. package/dist/exit-codes.d.ts.map +1 -0
  101. package/dist/exit-codes.js +48 -0
  102. package/dist/hcl/edges.d.ts.map +1 -1
  103. package/dist/hcl/edges.js +24 -1
  104. package/dist/hcl/files.d.ts +61 -0
  105. package/dist/hcl/files.d.ts.map +1 -0
  106. package/dist/hcl/files.js +221 -0
  107. package/dist/hcl/instances.d.ts +82 -0
  108. package/dist/hcl/instances.d.ts.map +1 -0
  109. package/dist/hcl/instances.js +390 -0
  110. package/dist/hcl/parse.d.ts +81 -9
  111. package/dist/hcl/parse.d.ts.map +1 -1
  112. package/dist/hcl/parse.js +114 -16
  113. package/dist/hcl/roots.d.ts.map +1 -1
  114. package/dist/hcl/roots.js +5 -0
  115. package/dist/hcl/suppressions.d.ts.map +1 -1
  116. package/dist/hcl/suppressions.js +8 -2
  117. package/dist/index.d.ts +13 -2
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +24 -2
  120. package/dist/integrity.json +8 -5
  121. package/dist/lint/audit-catalog.d.ts.map +1 -1
  122. package/dist/lint/audit-catalog.js +27 -3
  123. package/dist/lint/audit-lineage.d.ts +14 -2
  124. package/dist/lint/audit-lineage.d.ts.map +1 -1
  125. package/dist/lint/post-synth/index.d.ts.map +1 -1
  126. package/dist/lint/post-synth/index.js +4 -0
  127. package/dist/lint/post-synth/record-store.d.ts +54 -0
  128. package/dist/lint/post-synth/record-store.d.ts.map +1 -0
  129. package/dist/lint/post-synth/record-store.js +104 -0
  130. package/dist/lint/post-synth/tf001.d.ts +2 -2
  131. package/dist/lint/post-synth/tf001.js +2 -2
  132. package/dist/lint/post-synth/tf024.d.ts +35 -11
  133. package/dist/lint/post-synth/tf024.d.ts.map +1 -1
  134. package/dist/lint/post-synth/tf024.js +92 -20
  135. package/dist/lint/post-synth/tf046.d.ts +64 -0
  136. package/dist/lint/post-synth/tf046.d.ts.map +1 -0
  137. package/dist/lint/post-synth/tf046.js +175 -0
  138. package/dist/lint/post-synth/tf047.d.ts +36 -0
  139. package/dist/lint/post-synth/tf047.d.ts.map +1 -0
  140. package/dist/lint/post-synth/tf047.js +94 -0
  141. package/dist/live-plan-reasons.d.ts +21 -0
  142. package/dist/live-plan-reasons.d.ts.map +1 -0
  143. package/dist/live-plan-reasons.js +47 -0
  144. package/dist/lock-rows.d.ts +36 -0
  145. package/dist/lock-rows.d.ts.map +1 -0
  146. package/dist/lock-rows.js +60 -0
  147. package/dist/lock-timeout.d.ts +22 -0
  148. package/dist/lock-timeout.d.ts.map +1 -0
  149. package/dist/lock-timeout.js +32 -0
  150. package/dist/lsp/option-keys.d.ts.map +1 -1
  151. package/dist/lsp/option-keys.js +2 -0
  152. package/dist/manifest.json +1 -1
  153. package/dist/module-catalog/index.d.ts +69 -0
  154. package/dist/module-catalog/index.d.ts.map +1 -0
  155. package/dist/module-catalog/index.js +110 -0
  156. package/dist/module-catalog/refresh-cli.d.ts +4 -0
  157. package/dist/module-catalog/refresh-cli.d.ts.map +1 -0
  158. package/dist/module-catalog/refresh-cli.js +64 -0
  159. package/dist/module-catalog/static-tier.d.ts +6 -0
  160. package/dist/module-catalog/static-tier.d.ts.map +1 -0
  161. package/dist/module-catalog/static-tier.js +1650 -0
  162. package/dist/module-catalog/types.d.ts +65 -0
  163. package/dist/module-catalog/types.d.ts.map +1 -0
  164. package/dist/module-catalog/types.js +33 -0
  165. package/dist/okf/index.md +3 -1
  166. package/dist/okf/rules/TF024.md +2 -2
  167. package/dist/okf/rules/TF046.md +11 -0
  168. package/dist/okf/rules/TF047.md +11 -0
  169. package/dist/op/activities/index.d.ts +6 -2
  170. package/dist/op/activities/index.d.ts.map +1 -1
  171. package/dist/op/activities/index.js +5 -1
  172. package/dist/op/activities/live-detect.d.ts +4 -3
  173. package/dist/op/activities/live-detect.d.ts.map +1 -1
  174. package/dist/op/activities/live-detect.js +57 -32
  175. package/dist/op/activities/pin-rollout.d.ts +2 -0
  176. package/dist/op/activities/pin-rollout.d.ts.map +1 -1
  177. package/dist/op/activities/pin-rollout.js +1 -0
  178. package/dist/op/activities/terraform.d.ts +182 -5
  179. package/dist/op/activities/terraform.d.ts.map +1 -1
  180. package/dist/op/activities/terraform.js +224 -24
  181. package/dist/op/activity-contracts.d.ts +27 -0
  182. package/dist/op/activity-contracts.d.ts.map +1 -1
  183. package/dist/op/activity-contracts.js +19 -1
  184. package/dist/op/builders.d.ts +7 -1
  185. package/dist/op/builders.d.ts.map +1 -1
  186. package/dist/op/builders.js +9 -0
  187. package/dist/pin/consumers.d.ts +35 -0
  188. package/dist/pin/consumers.d.ts.map +1 -0
  189. package/dist/pin/consumers.js +70 -0
  190. package/dist/pin/edit.d.ts.map +1 -1
  191. package/dist/pin/edit.js +35 -0
  192. package/dist/pin/rollout.d.ts +2 -0
  193. package/dist/pin/rollout.d.ts.map +1 -1
  194. package/dist/pin/rollout.js +14 -11
  195. package/dist/plan-shape.d.ts +30 -0
  196. package/dist/plan-shape.d.ts.map +1 -0
  197. package/dist/plan-shape.js +125 -0
  198. package/dist/plugin.d.ts.map +1 -1
  199. package/dist/plugin.js +37 -18
  200. package/dist/registry/address.d.ts +63 -0
  201. package/dist/registry/address.d.ts.map +1 -0
  202. package/dist/registry/address.js +65 -0
  203. package/dist/registry/catalog-read.d.ts +14 -0
  204. package/dist/registry/catalog-read.d.ts.map +1 -0
  205. package/dist/registry/catalog-read.js +58 -0
  206. package/dist/registry/deprecated.d.ts +37 -0
  207. package/dist/registry/deprecated.d.ts.map +1 -0
  208. package/dist/registry/deprecated.js +43 -0
  209. package/dist/registry/docs.d.ts +96 -0
  210. package/dist/registry/docs.d.ts.map +1 -0
  211. package/dist/registry/docs.js +210 -0
  212. package/dist/registry/edge/edge.d.ts +46 -0
  213. package/dist/registry/edge/edge.d.ts.map +1 -0
  214. package/dist/registry/edge/edge.js +194 -0
  215. package/dist/registry/edge/index.d.ts +11 -0
  216. package/dist/registry/edge/index.d.ts.map +1 -0
  217. package/dist/registry/edge/index.js +10 -0
  218. package/dist/registry/edge/lambda-edge.d.ts +76 -0
  219. package/dist/registry/edge/lambda-edge.d.ts.map +1 -0
  220. package/dist/registry/edge/lambda-edge.js +95 -0
  221. package/dist/registry/edge/sigv4-web.d.ts +33 -0
  222. package/dist/registry/edge/sigv4-web.d.ts.map +1 -0
  223. package/dist/registry/edge/sigv4-web.js +56 -0
  224. package/dist/registry/edge/token-source.d.ts +73 -0
  225. package/dist/registry/edge/token-source.d.ts.map +1 -0
  226. package/dist/registry/edge/token-source.js +115 -0
  227. package/dist/registry/edge/worker.d.ts +45 -0
  228. package/dist/registry/edge/worker.d.ts.map +1 -0
  229. package/dist/registry/edge/worker.js +26 -0
  230. package/dist/registry/handler.d.ts +25 -0
  231. package/dist/registry/handler.d.ts.map +1 -0
  232. package/dist/registry/handler.js +75 -0
  233. package/dist/registry/headers-file.d.ts +21 -0
  234. package/dist/registry/headers-file.d.ts.map +1 -0
  235. package/dist/registry/headers-file.js +57 -0
  236. package/dist/registry/index.d.ts +31 -0
  237. package/dist/registry/index.d.ts.map +1 -0
  238. package/dist/registry/index.js +30 -0
  239. package/dist/registry/mirror-paths.d.ts +9 -0
  240. package/dist/registry/mirror-paths.d.ts.map +1 -0
  241. package/dist/registry/mirror-paths.js +8 -0
  242. package/dist/registry/mirror.d.ts +130 -0
  243. package/dist/registry/mirror.d.ts.map +1 -0
  244. package/dist/registry/mirror.js +319 -0
  245. package/dist/registry/proxies.d.ts +73 -0
  246. package/dist/registry/proxies.d.ts.map +1 -0
  247. package/dist/registry/proxies.js +111 -0
  248. package/dist/registry/reader.d.ts +57 -0
  249. package/dist/registry/reader.d.ts.map +1 -0
  250. package/dist/registry/reader.js +198 -0
  251. package/dist/registry/semver.d.ts +16 -0
  252. package/dist/registry/semver.d.ts.map +1 -0
  253. package/dist/registry/semver.js +24 -0
  254. package/dist/registry/serve.d.ts +24 -0
  255. package/dist/registry/serve.d.ts.map +1 -0
  256. package/dist/registry/serve.js +64 -0
  257. package/dist/registry/source-forms.d.ts +79 -0
  258. package/dist/registry/source-forms.d.ts.map +1 -0
  259. package/dist/registry/source-forms.js +202 -0
  260. package/dist/registry/targets/artifactory.d.ts +28 -0
  261. package/dist/registry/targets/artifactory.d.ts.map +1 -0
  262. package/dist/registry/targets/artifactory.js +206 -0
  263. package/dist/registry/targets/fake-artifactory.d.ts +30 -0
  264. package/dist/registry/targets/fake-artifactory.d.ts.map +1 -0
  265. package/dist/registry/targets/fake-artifactory.js +142 -0
  266. package/dist/registry/targets/fake-gitlab.d.ts +39 -0
  267. package/dist/registry/targets/fake-gitlab.d.ts.map +1 -0
  268. package/dist/registry/targets/fake-gitlab.js +136 -0
  269. package/dist/registry/targets/fake-hcp.d.ts +50 -0
  270. package/dist/registry/targets/fake-hcp.d.ts.map +1 -0
  271. package/dist/registry/targets/fake-hcp.js +202 -0
  272. package/dist/registry/targets/fake-http.d.ts +31 -0
  273. package/dist/registry/targets/fake-http.d.ts.map +1 -0
  274. package/dist/registry/targets/fake-http.js +87 -0
  275. package/dist/registry/targets/fake-nexus.d.ts +27 -0
  276. package/dist/registry/targets/fake-nexus.d.ts.map +1 -0
  277. package/dist/registry/targets/fake-nexus.js +137 -0
  278. package/dist/registry/targets/gitlab.d.ts +53 -0
  279. package/dist/registry/targets/gitlab.d.ts.map +1 -0
  280. package/dist/registry/targets/gitlab.js +187 -0
  281. package/dist/registry/targets/hcp.d.ts +29 -0
  282. package/dist/registry/targets/hcp.d.ts.map +1 -0
  283. package/dist/registry/targets/hcp.js +310 -0
  284. package/dist/registry/targets/hosted.d.ts +62 -0
  285. package/dist/registry/targets/hosted.d.ts.map +1 -0
  286. package/dist/registry/targets/hosted.js +74 -0
  287. package/dist/registry/targets/index.d.ts +18 -0
  288. package/dist/registry/targets/index.d.ts.map +1 -0
  289. package/dist/registry/targets/index.js +17 -0
  290. package/dist/registry/targets/module-target.d.ts +68 -0
  291. package/dist/registry/targets/module-target.d.ts.map +1 -0
  292. package/dist/registry/targets/module-target.js +63 -0
  293. package/dist/registry/targets/nexus.d.ts +43 -0
  294. package/dist/registry/targets/nexus.d.ts.map +1 -0
  295. package/dist/registry/targets/nexus.js +250 -0
  296. package/dist/registry/targets/zip.d.ts +55 -0
  297. package/dist/registry/targets/zip.d.ts.map +1 -0
  298. package/dist/registry/targets/zip.js +278 -0
  299. package/dist/registry/writer.d.ts +101 -0
  300. package/dist/registry/writer.d.ts.map +1 -0
  301. package/dist/registry/writer.js +109 -0
  302. package/dist/report.d.ts +15 -6
  303. package/dist/report.d.ts.map +1 -1
  304. package/dist/report.js +28 -55
  305. package/dist/rules/record-store.ts +122 -0
  306. package/dist/rules/tf001.ts +2 -2
  307. package/dist/rules/tf024.ts +105 -22
  308. package/dist/rules/tf046.ts +204 -0
  309. package/dist/rules/tf047.ts +114 -0
  310. package/dist/skills/chant-terraform.md +1 -1
  311. package/dist/state/azure-blob.d.ts +9 -0
  312. package/dist/state/azure-blob.d.ts.map +1 -0
  313. package/dist/state/azure-blob.js +8 -0
  314. package/dist/state/backend.d.ts +158 -0
  315. package/dist/state/backend.d.ts.map +1 -0
  316. package/dist/state/backend.js +406 -0
  317. package/dist/state/exec.d.ts +39 -0
  318. package/dist/state/exec.d.ts.map +1 -0
  319. package/dist/state/exec.js +80 -0
  320. package/dist/state/gcs.d.ts +8 -0
  321. package/dist/state/gcs.d.ts.map +1 -0
  322. package/dist/state/gcs.js +7 -0
  323. package/dist/state/index.d.ts +15 -0
  324. package/dist/state/index.d.ts.map +1 -0
  325. package/dist/state/index.js +14 -0
  326. package/dist/state/locks.d.ts +105 -0
  327. package/dist/state/locks.d.ts.map +1 -0
  328. package/dist/state/locks.js +130 -0
  329. package/dist/state/object-store.d.ts +8 -0
  330. package/dist/state/object-store.d.ts.map +1 -0
  331. package/dist/state/object-store.js +7 -0
  332. package/dist/state/root-states.d.ts +131 -0
  333. package/dist/state/root-states.d.ts.map +1 -0
  334. package/dist/state/root-states.js +241 -0
  335. package/dist/state/s3.d.ts +21 -0
  336. package/dist/state/s3.d.ts.map +1 -0
  337. package/dist/state/s3.js +19 -0
  338. package/dist/state/store.d.ts +51 -0
  339. package/dist/state/store.d.ts.map +1 -0
  340. package/dist/state/store.js +185 -0
  341. package/dist/state-ops.d.ts +255 -0
  342. package/dist/state-ops.d.ts.map +1 -0
  343. package/dist/state-ops.js +305 -0
  344. package/dist/state-reads.d.ts +57 -0
  345. package/dist/state-reads.d.ts.map +1 -0
  346. package/dist/state-reads.js +104 -0
  347. package/dist/terragrunt/affected.d.ts.map +1 -1
  348. package/dist/terragrunt/affected.js +2 -1
  349. package/dist/used-sources.d.ts +54 -0
  350. package/dist/used-sources.d.ts.map +1 -0
  351. package/dist/used-sources.js +157 -0
  352. package/package.json +24 -4
  353. package/src/__fixtures__/ACCEPTANCE.md +69 -7
  354. package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
  355. package/src/__fixtures__/compose/state-app.json +66 -0
  356. package/src/__fixtures__/compose/state-edge.json +40 -0
  357. package/src/__fixtures__/compose/state-net.json +58 -0
  358. package/src/__fixtures__/compose/three-app.plan.json +184 -0
  359. package/src/__fixtures__/compose/three-composed.plan.json +509 -0
  360. package/src/__fixtures__/compose/three-edge.plan.json +144 -0
  361. package/src/__fixtures__/compose/three-net.plan.json +174 -0
  362. package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
  363. package/src/__fixtures__/compose/working-after-apply.json +145 -0
  364. package/src/__fixtures__/config-blocks/app/main.tf +61 -0
  365. package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
  366. package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
  367. package/src/__fixtures__/config-files/shadow/main.tf +3 -0
  368. package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
  369. package/src/__fixtures__/config-files/shadow/other.tf +3 -0
  370. package/src/__fixtures__/config-files/tf/main.tf +11 -0
  371. package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
  372. package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
  373. package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
  374. package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
  375. package/src/__fixtures__/drift-record/README.md +16 -0
  376. package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
  377. package/src/__fixtures__/drift-rows/live-root-plan.json +70 -0
  378. package/src/__fixtures__/drift-rows/refresh-plan.json +61 -0
  379. package/src/__fixtures__/instance-forms/app/main.tf +110 -0
  380. package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
  381. package/src/__fixtures__/instance-vars/app/main.tf +52 -0
  382. package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
  383. package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
  384. package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
  385. package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
  386. package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
  387. package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
  388. package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
  389. package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
  390. package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
  391. package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
  392. package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
  393. package/src/__fixtures__/pr-loop/README.md +50 -0
  394. package/src/__fixtures__/pr-loop/chant.config.json +13 -0
  395. package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
  396. package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
  397. package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
  398. package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
  399. package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
  400. package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
  401. package/src/__fixtures__/report/applied.show.json +51 -0
  402. package/src/__fixtures__/report/generate.ts +63 -0
  403. package/src/__fixtures__/report/plan.show.json +200 -0
  404. package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
  405. package/src/__fixtures__/report/tf-apply/report.json +338 -0
  406. package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
  407. package/src/__fixtures__/report/tf-plan/report.json +320 -0
  408. package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
  409. package/src/__fixtures__/used-sources/root/extra.tofu +1 -0
  410. package/src/__fixtures__/used-sources/root/main.tf +31 -0
  411. package/src/__fixtures__/used-sources/root/modules/net/main.tf +14 -0
  412. package/src/__fixtures__/used-sources/root/modules/net/versions.tf.json +7 -0
  413. package/src/__fixtures__/used-sources/unit/terragrunt.hcl +3 -0
  414. package/src/applied-state.ts +52 -0
  415. package/src/backend-config.test.ts +105 -0
  416. package/src/backend-config.ts +133 -0
  417. package/src/behaviour/kinds.ts +4 -0
  418. package/src/behaviour/predict.ts +5 -2
  419. package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
  420. package/src/behaviour/reference-catalog.ts +16 -5
  421. package/src/behaviour/request.ts +3 -2
  422. package/src/change-set-instances.test.ts +98 -0
  423. package/src/changed-roots.ts +14 -16
  424. package/src/ci-setup.ts +42 -0
  425. package/src/commands.ts +4 -3
  426. package/src/components/capability-plugin.test.ts +198 -19
  427. package/src/components/capability-plugin.ts +383 -35
  428. package/src/components/change-set-graph.e2e.test.ts +11 -5
  429. package/src/components/compose-group.test.ts +486 -0
  430. package/src/components/compose-group.ts +489 -0
  431. package/src/components/demo-composed-roots.e2e.test.ts +440 -0
  432. package/src/components/member-outputs.test.ts +76 -0
  433. package/src/components/placeholder-preview.test.ts +147 -0
  434. package/src/components/placeholder-preview.ts +257 -0
  435. package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
  436. package/src/components/pr-loop.tofu.test.ts +113 -57
  437. package/src/components/release-inputs.ts +69 -0
  438. package/src/components/trace-facts.test.ts +21 -0
  439. package/src/components/trace-facts.ts +27 -0
  440. package/src/compose/apply-events.test.ts +62 -0
  441. package/src/compose/apply-events.ts +100 -0
  442. package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
  443. package/src/compose/estate.test.ts +421 -0
  444. package/src/compose/estate.ts +443 -0
  445. package/src/compose/hcl.ts +311 -0
  446. package/src/compose/module-address.ts +49 -0
  447. package/src/compose/root.test.ts +416 -0
  448. package/src/compose/root.ts +592 -0
  449. package/src/compose/run.test.ts +440 -0
  450. package/src/compose/run.tofu.test.ts +176 -0
  451. package/src/compose/run.ts +527 -0
  452. package/src/compose/slice.test.ts +184 -0
  453. package/src/compose/slice.ts +290 -0
  454. package/src/compose/state-address.ts +157 -0
  455. package/src/compose/state-merge.test.ts +131 -0
  456. package/src/compose/state-merge.ts +148 -0
  457. package/src/compose.test.ts +248 -0
  458. package/src/compose.ts +627 -0
  459. package/src/composites/terraform-apply-op.test.ts +9 -0
  460. package/src/composites/terraform-apply-op.ts +7 -3
  461. package/src/cone.ts +138 -0
  462. package/src/config-file-listers.test.ts +265 -0
  463. package/src/config.ts +44 -9
  464. package/src/cost-hold.ts +29 -0
  465. package/src/describe-resources.engine-facts.test.ts +411 -0
  466. package/src/describe-resources.live-module-keyed.test.ts +290 -0
  467. package/src/describe-resources.ts +416 -70
  468. package/src/drift-read.ts +284 -0
  469. package/src/drift-record.test.ts +223 -0
  470. package/src/drift-record.ts +455 -0
  471. package/src/drift-rows.test.ts +135 -0
  472. package/src/drift-rows.ts +137 -0
  473. package/src/exit-codes.test.ts +23 -0
  474. package/src/exit-codes.ts +59 -0
  475. package/src/hcl/config-blocks.test.ts +174 -0
  476. package/src/hcl/edges.ts +21 -1
  477. package/src/hcl/files.ts +234 -0
  478. package/src/hcl/instance-forms.test.ts +94 -0
  479. package/src/hcl/instances.test.ts +137 -0
  480. package/src/hcl/instances.ts +422 -0
  481. package/src/hcl/parse.test.ts +139 -1
  482. package/src/hcl/parse.ts +149 -16
  483. package/src/hcl/roots.ts +6 -0
  484. package/src/hcl/suppressions.ts +8 -2
  485. package/src/index.ts +79 -0
  486. package/src/lint/audit-catalog.ts +29 -3
  487. package/src/lint/audit-lineage.ts +14 -2
  488. package/src/lint/post-synth/fixtures/TF024/live-with-record-store/main.tf +18 -0
  489. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/estate.chdf.hcl +1 -0
  490. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/main.tf +10 -0
  491. package/src/lint/post-synth/fixtures/TF046/azurerm-no-resource-group/main.tf +15 -0
  492. package/src/lint/post-synth/fixtures/TF046/backend-config-only/main.tf +14 -0
  493. package/src/lint/post-synth/fixtures/TF046/derived-s3/backend.tf +7 -0
  494. package/src/lint/post-synth/fixtures/TF046/derived-s3/main.tf +9 -0
  495. package/src/lint/post-synth/fixtures/TF046/explicit-local/main.tf +16 -0
  496. package/src/lint/post-synth/fixtures/TF046/explicit-s3-beside-local-backend/main.tf +15 -0
  497. package/src/lint/post-synth/fixtures/TF046/implied-local/main.tf +9 -0
  498. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/estate.chdf.hcl +1 -0
  499. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/main.tf +5 -0
  500. package/src/lint/post-synth/fixtures/TF047/clear/main.tf +12 -0
  501. package/src/lint/post-synth/fixtures/TF047/refused/main.tf +12 -0
  502. package/src/lint/post-synth/fixtures/TF047/unknown-module/main.tf +12 -0
  503. package/src/lint/post-synth/fixtures/TF047/unknown-version/main.tf +12 -0
  504. package/src/lint/post-synth/index.ts +4 -0
  505. package/src/lint/post-synth/record-store.ts +122 -0
  506. package/src/lint/post-synth/tf001.ts +2 -2
  507. package/src/lint/post-synth/tf024.test.ts +26 -5
  508. package/src/lint/post-synth/tf024.ts +105 -22
  509. package/src/lint/post-synth/tf046.test.ts +137 -0
  510. package/src/lint/post-synth/tf046.ts +204 -0
  511. package/src/lint/post-synth/tf047.test.ts +56 -0
  512. package/src/lint/post-synth/tf047.ts +114 -0
  513. package/src/live-plan-reasons.ts +54 -0
  514. package/src/lock-rows.test.ts +70 -0
  515. package/src/lock-rows.ts +82 -0
  516. package/src/lock-timeout.ts +34 -0
  517. package/src/lsp/completions.test.ts +1 -1
  518. package/src/lsp/option-keys.ts +2 -0
  519. package/src/module-catalog/index.test.ts +93 -0
  520. package/src/module-catalog/index.ts +159 -0
  521. package/src/module-catalog/refresh-cli.ts +68 -0
  522. package/src/module-catalog/static-tier.ts +1659 -0
  523. package/src/module-catalog/types.ts +88 -0
  524. package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
  525. package/src/op/activities/index.ts +9 -0
  526. package/src/op/activities/live-detect.ts +55 -26
  527. package/src/op/activities/pin-rollout.ts +3 -0
  528. package/src/op/activities/registry.test.ts +2 -1
  529. package/src/op/activities/terraform.test.ts +55 -0
  530. package/src/op/activities/terraform.ts +312 -27
  531. package/src/op/activity-contracts.test.ts +1 -0
  532. package/src/op/activity-contracts.ts +24 -1
  533. package/src/op/builders.ts +14 -0
  534. package/src/op-wave-step.test.ts +109 -0
  535. package/src/pin/__fixtures__/consumers/live/dev/main.tf +4 -0
  536. package/src/pin/__fixtures__/consumers/live/prod/main.tf +21 -0
  537. package/src/pin/__fixtures__/consumers/units/vpc/terragrunt.hcl +7 -0
  538. package/src/pin/bundle.test.ts +2 -0
  539. package/src/pin/consumers.test.ts +57 -0
  540. package/src/pin/consumers.ts +95 -0
  541. package/src/pin/edit.ts +30 -0
  542. package/src/pin/rollout.ts +16 -11
  543. package/src/plan-shape.test.ts +148 -0
  544. package/src/plan-shape.ts +128 -0
  545. package/src/plugin.ts +39 -19
  546. package/src/registry/__fixtures__/docs/documented/.terraform-docs.yml +3 -0
  547. package/src/registry/__fixtures__/docs/documented/README.md +1 -0
  548. package/src/registry/__fixtures__/docs/documented/main.tf +8 -0
  549. package/src/registry/__fixtures__/docs/json/main.tf.json +14 -0
  550. package/src/registry/__fixtures__/docs/vpc/README.md +3 -0
  551. package/src/registry/__fixtures__/docs/vpc/examples/complete/main.tf +5 -0
  552. package/src/registry/__fixtures__/docs/vpc/examples/simple/README.md +1 -0
  553. package/src/registry/__fixtures__/docs/vpc/examples/simple/main.tf +4 -0
  554. package/src/registry/__fixtures__/docs/vpc/main.tf +14 -0
  555. package/src/registry/__fixtures__/docs/vpc/outputs.tf +9 -0
  556. package/src/registry/__fixtures__/docs/vpc/variables.tf +16 -0
  557. package/src/registry/__fixtures__/proxied-root/main.tf +12 -0
  558. package/src/registry/address.ts +100 -0
  559. package/src/registry/catalog-read.ts +56 -0
  560. package/src/registry/deprecated.test.ts +139 -0
  561. package/src/registry/deprecated.ts +76 -0
  562. package/src/registry/docs.test.ts +140 -0
  563. package/src/registry/docs.ts +299 -0
  564. package/src/registry/edge/bundle.test.ts +50 -0
  565. package/src/registry/edge/edge.test.ts +176 -0
  566. package/src/registry/edge/edge.ts +211 -0
  567. package/src/registry/edge/index.ts +10 -0
  568. package/src/registry/edge/lambda-edge.ts +127 -0
  569. package/src/registry/edge/sigv4-web.ts +79 -0
  570. package/src/registry/edge/token-source.test.ts +258 -0
  571. package/src/registry/edge/token-source.ts +169 -0
  572. package/src/registry/edge/worker.ts +61 -0
  573. package/src/registry/handler.ts +89 -0
  574. package/src/registry/headers-file.ts +61 -0
  575. package/src/registry/index.ts +31 -0
  576. package/src/registry/mirror-paths.ts +10 -0
  577. package/src/registry/mirror.test.ts +198 -0
  578. package/src/registry/mirror.ts +395 -0
  579. package/src/registry/proxies.test.ts +153 -0
  580. package/src/registry/proxies.ts +132 -0
  581. package/src/registry/reader.ts +225 -0
  582. package/src/registry/registry.test.ts +262 -0
  583. package/src/registry/semver.ts +34 -0
  584. package/src/registry/serve.ts +82 -0
  585. package/src/registry/source-forms.test.ts +87 -0
  586. package/src/registry/source-forms.ts +216 -0
  587. package/src/registry/targets/artifactory.test.ts +248 -0
  588. package/src/registry/targets/artifactory.ts +257 -0
  589. package/src/registry/targets/fake-artifactory.ts +158 -0
  590. package/src/registry/targets/fake-gitlab.ts +161 -0
  591. package/src/registry/targets/fake-hcp.ts +233 -0
  592. package/src/registry/targets/fake-http.ts +97 -0
  593. package/src/registry/targets/fake-nexus.ts +152 -0
  594. package/src/registry/targets/gitlab.test.ts +208 -0
  595. package/src/registry/targets/gitlab.ts +223 -0
  596. package/src/registry/targets/hcp.test.ts +230 -0
  597. package/src/registry/targets/hcp.ts +360 -0
  598. package/src/registry/targets/hosted.ts +117 -0
  599. package/src/registry/targets/index.ts +17 -0
  600. package/src/registry/targets/module-target.ts +131 -0
  601. package/src/registry/targets/nexus.test.ts +220 -0
  602. package/src/registry/targets/nexus.ts +314 -0
  603. package/src/registry/targets/zip.test.ts +65 -0
  604. package/src/registry/targets/zip.ts +287 -0
  605. package/src/registry/writer.ts +155 -0
  606. package/src/report.test.ts +82 -1
  607. package/src/report.ts +31 -54
  608. package/src/signal-only-root.test.ts +72 -0
  609. package/src/skills/chant-terraform.md +1 -1
  610. package/src/state/azure-blob.ts +8 -0
  611. package/src/state/backend-verdict.test.ts +187 -0
  612. package/src/state/backend.test.ts +127 -0
  613. package/src/state/backend.ts +488 -0
  614. package/src/state/exec.ts +111 -0
  615. package/src/state/gcs.ts +7 -0
  616. package/src/state/index.ts +15 -0
  617. package/src/state/locks.test.ts +84 -0
  618. package/src/state/locks.ts +201 -0
  619. package/src/state/object-store.ts +7 -0
  620. package/src/state/root-states-floci.e2e.test.ts +66 -0
  621. package/src/state/root-states.test.ts +462 -0
  622. package/src/state/root-states.ts +309 -0
  623. package/src/state/s3.ts +21 -0
  624. package/src/state/store.ts +225 -0
  625. package/src/state-ops.test.ts +182 -0
  626. package/src/state-ops.ts +462 -0
  627. package/src/state-reads.ts +116 -0
  628. package/src/terragrunt/affected.ts +2 -1
  629. package/src/used-sources.test.ts +75 -0
  630. package/src/used-sources.ts +204 -0
  631. package/src/workspace-kinds.test.ts +4 -17
  632. package/workspace-kinds.json +1 -24
  633. package/dist/workspace-member.d.ts +0 -30
  634. package/dist/workspace-member.d.ts.map +0 -1
  635. package/dist/workspace-member.js +0 -91
  636. package/schemas/pulumi.schema.json +0 -34
  637. package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
  638. package/src/testdata/pulumi-member/stack-export.json +0 -53
  639. package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
  640. package/src/workspace-member.test.ts +0 -89
  641. package/src/workspace-member.ts +0 -126
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The working state of a composed run (#4442): merged from each member's
3
+ * pulled state, split back by module address. Fixtures are `state pull`
4
+ * output in `../__fixtures__/compose`, and the working state an apply of the
5
+ * three-module plan left (net's tags changed, app and edge as they were).
6
+ * The lineage and serial cases are carried over from terragucci's rules
7
+ * (`migrate.test.ts` and `compose.ts` at c493e0e).
8
+ */
9
+
10
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { afterAll, describe, expect, test } from "vitest";
14
+ import type { Composition } from "../compose";
15
+ import type { StateFile, StateResource } from "../state/exec";
16
+ import { COMPOSE_LINEAGE, derivedLineage, mergeStates, splitState, splitStates } from "./state-merge";
17
+
18
+ const FIX = join(import.meta.dirname, "..", "__fixtures__", "compose");
19
+ const state = (name: string): StateFile => JSON.parse(readFileSync(join(FIX, name), "utf-8")) as StateFile;
20
+
21
+ const made: string[] = [];
22
+ afterAll(() => {
23
+ for (const d of made) rmSync(d, { recursive: true, force: true });
24
+ });
25
+
26
+ type StateComposition = Pick<Composition, "order" | "modules" | "dirs" | "roots">;
27
+ const composition = (members: string[], dirs: Record<string, string> = {}): StateComposition => ({
28
+ order: members,
29
+ modules: new Map(members.map((m) => [m, m])),
30
+ dirs: new Map(Object.entries(dirs)),
31
+ roots: new Map(members.map((m) => [m, `roots/${m}`])),
32
+ });
33
+
34
+ const c = composition(["net", "app", "edge"]);
35
+ const pulled = new Map<string, StateFile | null>([
36
+ ["net", state("state-net.json")],
37
+ ["app", state("state-app.json")],
38
+ ["edge", state("state-edge.json")],
39
+ ]);
40
+
41
+ describe("mergeStates", () => {
42
+ const working = mergeStates(c, pulled);
43
+
44
+ test("each member's resources under module.<m>, a nested module under it, outputs as <m>__<name>", () => {
45
+ expect(working.resources.map((r) => `${r.module}.${r.type}.${r.name}`)).toEqual([
46
+ "module.net.terraform_data.vpc",
47
+ "module.app.module.logs.terraform_data.group",
48
+ "module.app.terraform_data.service",
49
+ "module.edge.terraform_data.dns",
50
+ ]);
51
+ expect(Object.keys(working.outputs ?? {})).toEqual(["net__vpc_id", "app__service_id", "edge__name"]);
52
+ expect(working.check_results).toBeNull();
53
+ });
54
+
55
+ test("serial 1 and a lineage derived from the members in order", () => {
56
+ expect(working.serial).toBe(1);
57
+ expect(working.lineage).toBe(derivedLineage(COMPOSE_LINEAGE, "net", "app", "edge"));
58
+ expect(working.lineage).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-8[0-9a-f]{3}-[0-9a-f]{12}$/);
59
+ });
60
+
61
+ test("the highest terraform_version pulled", () => {
62
+ const newer = { ...state("state-edge.json"), terraform_version: "1.11.0" };
63
+ expect(mergeStates(c, new Map([...pulled, ["edge", newer]])).terraform_version).toBe("1.11.0");
64
+ });
65
+
66
+ test("a resource whose root configures its provider names the module's provider configuration", () => {
67
+ const dir = mkdtempSync(join(tmpdir(), "chant-compose-state-"));
68
+ made.push(dir);
69
+ mkdirSync(join(dir, "net"));
70
+ writeFileSync(join(dir, "net", "providers.tf"), 'provider "aws" {\n region = "us-east-1"\n}\n');
71
+ const res: StateResource = { mode: "managed", type: "aws_vpc", name: "main", provider: 'provider["registry.terraform.io/hashicorp/aws"]', instances: [] };
72
+ const one = composition(["net", "app"], { net: join(dir, "net"), app: join(dir, "missing") });
73
+ const s = (r: StateResource): StateFile => ({ version: 4, serial: 1, lineage: "L", resources: [r] });
74
+ const merged = mergeStates(one, new Map([["net", s(res)], ["app", s(res)]]));
75
+ expect(merged.resources.map((r) => r.provider)).toEqual(['module.net.provider["registry.terraform.io/hashicorp/aws"]', 'provider["registry.terraform.io/hashicorp/aws"]']);
76
+ const back = splitState(merged, one, "net", s(res));
77
+ expect(back.state.resources[0]!.provider).toBe(res.provider);
78
+ expect(back.same).toBe(true);
79
+ });
80
+ });
81
+
82
+ describe("splitState", () => {
83
+ test("with no apply, every member's state comes back as pulled: same", () => {
84
+ const working = mergeStates(c, pulled);
85
+ for (const [m, s] of splitStates(working, c, pulled)) {
86
+ expect(s.same, m).toBe(true);
87
+ expect(s.state.resources).toEqual(pulled.get(m)!.resources.map((r) => expect.objectContaining(r)));
88
+ expect(s.state.outputs).toEqual(pulled.get(m)!.outputs);
89
+ }
90
+ });
91
+
92
+ test("after an apply that changed net, net alone comes back changed", () => {
93
+ const split = splitStates(state("working-after-apply.json"), c, pulled);
94
+ expect(split.get("net")!.same).toBe(false);
95
+ expect(split.get("app")!.same).toBe(true);
96
+ expect(split.get("edge")!.same).toBe(true);
97
+ const net = split.get("net")!.state;
98
+ expect((net.resources[0]!.instances![0] as any).attributes.input.value.name).toBe("core-v2");
99
+ expect(net.resources[0]!.module).toBeUndefined();
100
+ expect(split.get("app")!.state.resources.find((r) => r.name === "group")!.module).toBe("module.logs");
101
+ });
102
+
103
+ test("a split state keeps its pulled lineage and takes the pulled serial plus one", () => {
104
+ const split = splitStates(state("working-after-apply.json"), c, pulled);
105
+ expect(split.get("net")!.state).toMatchObject({ version: 4, lineage: "2b1f6c0a-6a1e-4c55-9f3d-7e0a1b2c3d4e", serial: 8, terraform_version: "1.10.5", check_results: null });
106
+ expect(split.get("app")!.state).toMatchObject({ lineage: "8d4e2f1a-3b5c-4d6e-9f7a-1c2b3d4e5f60", serial: 4 });
107
+ expect(split.get("edge")!.state).toMatchObject({ lineage: "c0ffee00-1234-4abc-8def-0123456789ab", serial: 13 });
108
+ });
109
+
110
+ test("a member with no state gets serial 1 and a lineage derived from the composed run and its root", () => {
111
+ const fresh = new Map<string, StateFile | null>([...pulled, ["app", null]]);
112
+ const split = splitStates(state("working-after-apply.json"), c, fresh);
113
+ expect(split.get("app")!.state).toMatchObject({ serial: 1, lineage: derivedLineage(COMPOSE_LINEAGE, "roots/app") });
114
+ expect(split.get("app")!.same).toBe(false);
115
+ expect(derivedLineage(COMPOSE_LINEAGE, "roots/app")).toBe(derivedLineage(COMPOSE_LINEAGE, "roots/app"));
116
+ expect(derivedLineage(COMPOSE_LINEAGE, "roots/app")).not.toBe(derivedLineage(COMPOSE_LINEAGE, "roots/edge"));
117
+ });
118
+
119
+ test("a member with no state that the apply left nothing in is same", () => {
120
+ const empty = composition(["net", "new"]);
121
+ const working = mergeStates(empty, new Map([["net", state("state-net.json")], ["new", null]]));
122
+ expect(splitState(working, empty, "new", null)).toMatchObject({ same: true, state: { resources: [], outputs: {}, serial: 1 } });
123
+ });
124
+
125
+ test("a module whose name starts with another's takes only its own resources", () => {
126
+ const two = composition(["app", "app2"]);
127
+ const working = mergeStates(two, new Map([["app", state("state-app.json")], ["app2", state("state-edge.json")]]));
128
+ expect(splitState(working, two, "app", state("state-app.json")).state.resources.map((r) => r.name)).toEqual(["group", "service"]);
129
+ expect(splitState(working, two, "app2", state("state-edge.json")).same).toBe(true);
130
+ });
131
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The working state of a composed run (chant#4442, epic #4419): each
3
+ * member's pulled state moved under `module.<m>` in one state the composed
4
+ * root plans and applies against, and, once the apply is done or has failed,
5
+ * split back into one state per member by module address.
6
+ *
7
+ * Lifted from terragucci's `packages/terragucci/src/compose.ts` at c493e0e
8
+ * (`mergeStates`, `splitState`) and `migrate.ts` (`derivedLineage`), with
9
+ * chant's {@link Composition} in place of terragucci's: members are
10
+ * components, and a member's directory is `Composition.dirs`. Pulling and
11
+ * writing the states is `../state` (#4437).
12
+ *
13
+ * The lineage and serial rules are terragucci's: a split state keeps its
14
+ * pulled lineage and takes the pulled serial plus one; a member with no
15
+ * state yet gets a lineage derived from the composed run and its root, and
16
+ * serial 1. The working state itself has serial 1 and a lineage derived from
17
+ * the members in order.
18
+ */
19
+
20
+ import { createHash } from "node:crypto";
21
+ import type { Composition } from "../compose";
22
+ import type { StateFile, StateResource } from "../state/exec";
23
+ import { rootTexts } from "./hcl";
24
+ import { inModule, stripModule } from "./module-address";
25
+
26
+ /** What the derived lineages are derived from, beside the roots. */
27
+ export const COMPOSE_LINEAGE = "chant terraform.compose";
28
+
29
+ /** A lineage in the form Terraform writes (a version 4 UUID), derived from `parts`, so the same parts always give the same lineage. */
30
+ export function derivedLineage(...parts: string[]): string {
31
+ const h = createHash("sha256").update(parts.join("\n")).digest("hex");
32
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
33
+ }
34
+
35
+ /** Whether a root's own files configure the provider of type `type` (a `provider "<type>"` block): its resources then name the module's configuration. */
36
+ function ownsProvider(dir: string | undefined, binary: string | undefined, type: string): boolean {
37
+ if (!dir) return false;
38
+ return rootTexts(dir, binary).some((t) => (t.json ? new RegExp(`"provider"\\s*:\\s*\\{[^]*"${type}"`).test(t.text) : new RegExp(`\\bprovider\\s+"${type}"\\s*\\{`).test(t.text)));
39
+ }
40
+
41
+ function versionAbove(a: string, b: string): boolean {
42
+ const pa = a.split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
43
+ const pb = b.split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
44
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
45
+ return false;
46
+ }
47
+
48
+ /** The members whose state the working state holds, in apply order. */
49
+ type StateComposition = Pick<Composition, "order" | "modules" | "dirs"> & Partial<Pick<Composition, "roots" | "binary">>;
50
+
51
+ /**
52
+ * The working state: each member's resources with `module.<m>` in front of
53
+ * their module, each output as `<m>__<name>` (the composed root's outputs, so
54
+ * an output that does not change plans no change), check results left out.
55
+ * A resource whose root configures its provider itself names the module's
56
+ * provider configuration. `pulled` is by member; a member with no state
57
+ * (null or absent) adds nothing. The highest `terraform_version` pulled wins.
58
+ */
59
+ export function mergeStates(c: StateComposition, pulled: ReadonlyMap<string, StateFile | null>): StateFile {
60
+ const resources: StateResource[] = [];
61
+ const outputs: Record<string, unknown> = {};
62
+ let tfVersion: string | undefined;
63
+ for (const member of c.order) {
64
+ const s = pulled.get(member);
65
+ if (!s) continue;
66
+ const m = c.modules.get(member)!;
67
+ if (s.terraform_version && (!tfVersion || versionAbove(s.terraform_version, tfVersion))) tfVersion = s.terraform_version;
68
+ for (const res of s.resources) {
69
+ const provider = typeof res.provider === "string" ? res.provider : undefined;
70
+ const type = provider?.match(/provider\["([^"]+)"\]/)?.[1]?.split("/").pop();
71
+ const own = provider && type && !provider.startsWith("module.") && ownsProvider(c.dirs.get(member), c.binary, type);
72
+ resources.push({
73
+ ...structuredClone(res),
74
+ module: res.module ? `module.${m}.${res.module}` : `module.${m}`,
75
+ ...(provider ? { provider: own || provider.startsWith("module.") ? `module.${m}.${provider}` : provider } : {}),
76
+ });
77
+ }
78
+ for (const [k, v] of Object.entries(s.outputs ?? {})) outputs[`${m}__${k}`] = structuredClone(v);
79
+ }
80
+ return { version: 4, terraform_version: tfVersion ?? "1.0.0", serial: 1, lineage: derivedLineage(COMPOSE_LINEAGE, ...c.order), outputs, resources, check_results: null };
81
+ }
82
+
83
+ /** JSON with every object's keys sorted, so two objects that differ only in key order print the same. */
84
+ function stable(v: unknown): string {
85
+ if (Array.isArray(v)) return `[${v.map(stable).join(",")}]`;
86
+ if (v && typeof v === "object") {
87
+ return `{${Object.keys(v)
88
+ .filter((k) => (v as Record<string, unknown>)[k] !== undefined)
89
+ .sort()
90
+ .map((k) => `${JSON.stringify(k)}:${stable((v as Record<string, unknown>)[k])}`)
91
+ .join(",")}}`;
92
+ }
93
+ return JSON.stringify(v) ?? "null";
94
+ }
95
+
96
+ /**
97
+ * A state's resources and outputs in one order, for comparing two states.
98
+ * Keys are sorted too: a split resource in a nested module carries `module`
99
+ * after its other keys, where `state pull` prints it first.
100
+ */
101
+ const canonical = (s: Pick<StateFile, "resources" | "outputs">): string =>
102
+ JSON.stringify({
103
+ resources: [...s.resources].map(stable).sort(),
104
+ outputs: stable(s.outputs ?? {}),
105
+ });
106
+
107
+ /** One member's state out of the working state, and whether it holds what was pulled. */
108
+ export interface SplitState {
109
+ state: StateFile;
110
+ /** It holds what was pulled (for a member with no state: nothing), so there is nothing to write. */
111
+ same: boolean;
112
+ }
113
+
114
+ /**
115
+ * One member's state out of the working state: its resources with the module
116
+ * taken off, its outputs from `<m>__*`, its pulled lineage and its pulled
117
+ * serial plus one (a member with no state yet gets a lineage of its own,
118
+ * derived from its root, and serial 1). `same` when it holds what was pulled.
119
+ */
120
+ export function splitState(working: StateFile, c: StateComposition, member: string, pulled: StateFile | null): SplitState {
121
+ const m = c.modules.get(member)!;
122
+ const resources: StateResource[] = [];
123
+ for (const res of working.resources) {
124
+ if (!inModule(res.module, m)) continue;
125
+ const module = res.module === `module.${m}` ? undefined : stripModule(res.module!);
126
+ const { module: _m, ...rest } = structuredClone(res);
127
+ const provider = typeof rest.provider === "string" && inModule(rest.provider, m) ? stripModule(rest.provider) : rest.provider;
128
+ resources.push({ ...rest, ...(module ? { module } : {}), ...(provider !== undefined ? { provider } : {}) });
129
+ }
130
+ const outputs: Record<string, unknown> = {};
131
+ for (const [k, v] of Object.entries(working.outputs ?? {})) if (k.startsWith(`${m}__`)) outputs[k.slice(m.length + 2)] = v;
132
+ const state: StateFile = {
133
+ version: 4,
134
+ terraform_version: working.terraform_version,
135
+ serial: (pulled?.serial ?? 0) + 1,
136
+ lineage: pulled?.lineage || derivedLineage(COMPOSE_LINEAGE, c.roots?.get(member) ?? member),
137
+ outputs,
138
+ resources,
139
+ check_results: null,
140
+ };
141
+ const same = pulled ? canonical(state) === canonical(pulled) : resources.length === 0 && Object.keys(outputs).length === 0;
142
+ return { state, same };
143
+ }
144
+
145
+ /** Every member's split state, by member, in apply order. */
146
+ export function splitStates(working: StateFile, c: StateComposition, pulled: ReadonlyMap<string, StateFile | null>): Map<string, SplitState> {
147
+ return new Map(c.order.map((member) => [member, splitState(working, c, member, pulled.get(member) ?? null)] as const));
148
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Which roots a change composes, in what order, and why the others fall back
3
+ * (#4438). Each case writes the smallest project that produces it.
4
+ */
5
+
6
+ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { afterAll, describe, expect, test } from "vitest";
11
+ import type { DriverComponent, DriverPhase } from "@intentius/chant/components/driver";
12
+ import { stackOutput } from "@intentius/chant/components/component";
13
+ import { composeOff, compositionFor, compositionIfEnabled, fallbackLine, moduleNames, withPlanFailed, type CompositionInput } from "./compose";
14
+ import type { TerraformConfig } from "./config";
15
+ import { LOCKS } from "./state/backend";
16
+
17
+ const made: string[] = [];
18
+ afterAll(() => {
19
+ for (const d of made) rmSync(d, { recursive: true, force: true });
20
+ });
21
+
22
+ /** A project with `files` under it, each path relative to it. */
23
+ function project(files: Record<string, string>): string {
24
+ const dir = mkdtempSync(join(tmpdir(), "chant-compose-"));
25
+ made.push(dir);
26
+ for (const [path, text] of Object.entries(files)) {
27
+ mkdirSync(dirname(join(dir, path)), { recursive: true });
28
+ writeFileSync(join(dir, path), text);
29
+ }
30
+ return dir;
31
+ }
32
+
33
+ const EMPTY = 'resource "terraform_data" "x" {}\n';
34
+
35
+ /** A component whose deploy is one terraform-apply step. */
36
+ function tf(name: string, opts: { root?: string; dependsOn?: string[]; vars?: Record<string, unknown>; before?: DriverPhase["steps"] } = {}): DriverComponent {
37
+ return {
38
+ name,
39
+ dependsOn: opts.dependsOn ?? [],
40
+ deploy: [{ phase: "Apply", steps: [...(opts.before ?? []), { kind: "terraform-apply", root: opts.root ?? name, ...(opts.vars ? { vars: opts.vars } : {}) }] }],
41
+ };
42
+ }
43
+
44
+ /** Roots `names`, each at `roots/<name>`. */
45
+ const rootsOf = (...names: string[]): TerraformConfig["roots"] => Object.fromEntries(names.map((n) => [n, { dir: `roots/${n}` }]));
46
+
47
+ function input(projectRoot: string, components: DriverComponent[], over: Omit<Partial<CompositionInput>, "terraform"> & { terraform?: Partial<TerraformConfig> } = {}): CompositionInput {
48
+ const { terraform, ...rest } = over;
49
+ const roots = rootsOf(...components.flatMap((c) => c.deploy.flatMap((p) => p.steps)).flatMap((s) => {
50
+ const root = (s as { root?: unknown }).root;
51
+ return typeof root === "string" ? [root] : [];
52
+ }));
53
+ return { projectRoot, components, env: {}, terraform: { roots, compose: { enabled: true }, ...terraform }, ...rest };
54
+ }
55
+
56
+ /** Three independent roots a, b and c. */
57
+ const threeRoots = (extra: Record<string, string> = {}): string => project({ "roots/a/main.tf": EMPTY, "roots/b/main.tf": EMPTY, "roots/c/main.tf": EMPTY, ...extra });
58
+
59
+ describe("compositionFor: order, modules and reads", () => {
60
+ test("B reads A and C reads B: order A, B, C, module names valid HCL identifiers and stable across runs", () => {
61
+ const dir = project({ "roots/net-core/main.tf": EMPTY, "roots/2app/main.tf": EMPTY, "roots/edge.v1/main.tf": EMPTY });
62
+ const components = [
63
+ tf("c", { root: "edge.v1", dependsOn: ["b"], vars: { app_url: stackOutput("b", "url") } }),
64
+ tf("a", { root: "net-core" }),
65
+ tf("b", { root: "2app", dependsOn: ["a"], vars: { vpc_id: stackOutput("a", "vpc_id") } }),
66
+ ];
67
+ const first = compositionFor(input(dir, components));
68
+ expect(first.fallback).toBeUndefined();
69
+ expect(first.order).toEqual(["a", "b", "c"]);
70
+ for (const name of first.modules.values()) expect(name).toMatch(/^[A-Za-z_][A-Za-z0-9_]*$/);
71
+ expect(Object.fromEntries(first.modules)).toEqual({ a: "net_core", b: "r_2app", c: "edge_v1" });
72
+ const again = compositionFor(input(dir, [...components].reverse()));
73
+ expect(Object.fromEntries(again.modules)).toEqual(Object.fromEntries(first.modules));
74
+ expect(again.order).toEqual(first.order);
75
+ });
76
+
77
+ test("moduleNames takes `_2` on a clash, in name order", () => {
78
+ expect(Object.fromEntries(moduleNames(["a-b", "a_b", "."]))).toEqual({ ".": "root", "a-b": "a_b", a_b: "a_b_2" });
79
+ });
80
+
81
+ test("a read through vars and a read through terraform_remote_state both appear in reads", () => {
82
+ const dir = project({
83
+ "roots/a/main.tf": EMPTY,
84
+ "roots/b/main.tf": 'data "terraform_remote_state" "net" {\n backend = "local"\n config = {\n path = "../a/terraform.tfstate"\n }\n}\n',
85
+ "roots/c/main.tf": EMPTY,
86
+ });
87
+ const c = compositionFor(input(dir, [tf("a"), tf("b", { dependsOn: ["a"] }), tf("c", { dependsOn: ["a"], vars: { vpc: stackOutput("a", "vpc_id") } })]));
88
+ expect(c.fallback).toBeUndefined();
89
+ expect(c.reads.get("b")).toEqual([{ via: "remote-state", upstream: "a", name: "net" }]);
90
+ expect(c.reads.get("c")).toEqual([{ via: "vars", upstream: "a", variable: "vpc", output: "vpc_id" }]);
91
+ expect(c.order[0]).toBe("a");
92
+ });
93
+ });
94
+
95
+ describe("compositionFor: each fallback", () => {
96
+ test("too-many-roots", () => {
97
+ const c = compositionFor(input(threeRoots(), [tf("a"), tf("b"), tf("c")], { terraform: { compose: { enabled: true, max: 2 } } }));
98
+ expect(c.members).toEqual([]);
99
+ expect(c.fallback).toEqual({ kind: "too-many-roots", reason: "this change composes 3 roots, more than `terraform.compose.max` (2)" });
100
+ });
101
+
102
+ test("one-root", () => {
103
+ const c = compositionFor(input(project({ "roots/a/main.tf": EMPTY }), [tf("a")]));
104
+ expect(c.fallback).toEqual({ kind: "one-root", member: "a", reason: "this change reaches one root, `a`, which applies in its own run" });
105
+ expect(fallbackLine(c.fallback!)).toBe("**Applies in waves** (`terraform.compose`): this change reaches one root, `a`, which applies in its own run.");
106
+ });
107
+
108
+ test("binary", () => {
109
+ const c = compositionFor(input(threeRoots(), [tf("a"), tf("b"), tf("c")], { binary: (m) => ({ name: "tofu", version: m === "c" ? "1.9.0" : "1.8.0" }) }));
110
+ expect(c.members).toEqual(["a", "b"]);
111
+ expect(c.fallbacks).toEqual([{ kind: "binary", member: "c", reason: "`c` runs tofu 1.9.0 and `a` tofu 1.8.0, and one run has one binary" }]);
112
+ });
113
+
114
+ test("identity", () => {
115
+ const c = compositionFor(input(threeRoots(), [tf("a"), tf("b"), tf("c")], { identity: (m) => (m === "c" ? "role/other" : "role/apply") }));
116
+ expect(c.members).toEqual(["a", "b"]);
117
+ expect(c.fallbacks).toEqual([{ kind: "identity", member: "c", reason: "`c` applies under another cloud identity (`ci.identity.cloud`) than `a`, `b`, and one run has one set of credentials" }]);
118
+ });
119
+
120
+ test("workspace", () => {
121
+ const dir = threeRoots();
122
+ const c = compositionFor(input(dir, [tf("a"), tf("b"), tf("c")], { terraform: { roots: { ...rootsOf("a", "b"), c: { dir: "roots/c", workspace: "prod" } } } }));
123
+ expect(c.fallbacks).toEqual([{ kind: "workspace", member: "c", reason: "`c` plans in workspace prod, and a composed run plans in default" }]);
124
+ });
125
+
126
+ test("backend: a type chant does not lock", () => {
127
+ const c = compositionFor(input(threeRoots({ "roots/c/main.tf": 'terraform {\n backend "consul" {\n path = "c"\n }\n}\n' }), [tf("a"), tf("b"), tf("c")]));
128
+ expect(c.fallbacks).toEqual([{ kind: "backend", member: "c", reason: `\`c\`: the root's backend is consul; ${LOCKS}` }]);
129
+ });
130
+
131
+ test("backend: s3 with no lock file", () => {
132
+ const c = compositionFor(input(threeRoots({ "roots/c/main.tf": 'terraform {\n backend "s3" {\n bucket = "b"\n key = "c"\n }\n}\n' }), [tf("a"), tf("b"), tf("c")]));
133
+ expect(c.fallbacks).toEqual([{ kind: "backend", member: "c", reason: `\`c\`: the s3 backend takes no lock file (use_lockfile = true is unset), so chant cannot hold its lock; ${LOCKS}` }]);
134
+ });
135
+
136
+ test("provider-lock", () => {
137
+ const lock = (v: string): string => `provider "registry.terraform.io/hashicorp/aws" {\n version = "${v}"\n}\n`;
138
+ const c = compositionFor(input(threeRoots({ "roots/a/.terraform.lock.hcl": lock("5.0.0"), "roots/c/.terraform.lock.hcl": lock("5.1.0") }), [tf("a"), tf("b"), tf("c")]));
139
+ expect(c.fallbacks).toEqual([{ kind: "provider-lock", member: "c", reason: "`c` locks registry.terraform.io/hashicorp/aws at 5.1.0 and `a` at 5.0.0, and one run installs one version" }]);
140
+ });
141
+
142
+ test("cli-args", () => {
143
+ const c = compositionFor(input(threeRoots(), [tf("a"), tf("b"), tf("c")], { env: { TF_CLI_ARGS_plan: "-target=aws_s3_bucket.x" } }));
144
+ expect(c.members).toEqual([]);
145
+ expect(c.fallback).toEqual({ kind: "cli-args", reason: "`TF_CLI_ARGS_plan` carries `-target`, which names a root's own addresses or variables, not the composed run's" });
146
+ });
147
+
148
+ test("read", () => {
149
+ const dir = threeRoots({
150
+ "roots/b/main.tf": 'data "terraform_remote_state" "net" {\n count = 1\n backend = "local"\n config = {\n path = "../a/terraform.tfstate"\n }\n}\n',
151
+ });
152
+ const c = compositionFor(input(dir, [tf("a"), tf("b", { dependsOn: ["a"] }), tf("c")]));
153
+ expect(c.members).toEqual(["a", "c"]);
154
+ expect(c.fallbacks).toEqual([{ kind: "read", member: "b", reason: "`b` reads `a` through a block with count or for_each (`data.terraform_remote_state.net`)" }]);
155
+ });
156
+
157
+ test("order-only: two roots ordered only by dependsOn, with no read", () => {
158
+ const c = compositionFor(input(threeRoots(), [tf("a"), tf("b", { dependsOn: ["a"] }), tf("c")]));
159
+ expect(c.members).toEqual(["a", "c"]);
160
+ expect(c.fallbacks).toEqual([
161
+ { kind: "order-only", member: "b", reason: "`b` depends on `a` (`dependsOn`) with no read between them, and a module with its own providers takes no depends_on" },
162
+ ]);
163
+ });
164
+
165
+ test("import", () => {
166
+ const c = compositionFor(input(threeRoots({ "roots/c/main.tf": 'import {\n to = terraform_data.x\n id = var.id\n}\n' }), [tf("a"), tf("b"), tf("c")]));
167
+ expect(c.fallbacks).toEqual([{ kind: "import", member: "c", reason: "an `import` block of `c` takes no literal string `id`, so it cannot be lifted into the composed root" }]);
168
+ });
169
+
170
+ test("path", () => {
171
+ const c = compositionFor(input(threeRoots({ "roots/c/main.tf": 'locals {\n here = path.root\n}\n' }), [tf("a"), tf("b"), tf("c")]));
172
+ expect(c.fallbacks).toEqual([{ kind: "path", member: "c", reason: "`c` uses `path.root` or `path.cwd`, which would name the composed root instead of `roots/c`" }]);
173
+ });
174
+
175
+ test("plan-failed", () => {
176
+ const c = withPlanFailed(compositionFor(input(threeRoots(), [tf("a"), tf("b"), tf("c")])), "Error: boom\nat line 3");
177
+ expect(c.members).toEqual([]);
178
+ expect(c.fallback).toEqual({ kind: "plan-failed", reason: "the composed plan failed: Error: boom" });
179
+ });
180
+
181
+ test("targets: a member given targets falls back with its own kind", () => {
182
+ const c = compositionFor(input(threeRoots(), [tf("a"), tf("b"), tf("c")], { targets: { c: ["aws_s3_bucket.x"] } }));
183
+ expect(c.members).toEqual(["a", "b"]);
184
+ expect(c.fallbacks).toEqual([{ kind: "targets", member: "c", reason: "`c` is given 1 target (`--target`), and a composed run plans each member whole" }]);
185
+ expect(fallbackLine(c.fallbacks[0]!)).toBe("**`c` applies in waves** (`terraform.compose`): `c` is given 1 target (`--target`), and a composed run plans each member whole.");
186
+ });
187
+
188
+ test("steps: a deploy of more than the one step", () => {
189
+ const c = compositionFor(input(threeRoots(), [tf("a"), tf("b"), tf("c", { before: [{ kind: "gate", gate: "ok" }] })]));
190
+ expect(c.fallbacks).toEqual([{ kind: "steps", member: "c", reason: "`c`'s deploy has 2 steps, and only a component whose deploy is one terraform-apply step composes" }]);
191
+ });
192
+
193
+ test("same-root", () => {
194
+ const c = compositionFor(input(threeRoots(), [tf("a"), tf("b"), tf("c"), tf("d", { root: "c" })]));
195
+ expect(c.members).toEqual(["a", "b", "c"]);
196
+ expect(c.fallbacks).toEqual([{ kind: "same-root", member: "d", reason: "`d` deploys root `c`, which `c` deploys too, and a composed run calls each root once" }]);
197
+ });
198
+
199
+ test("split: a component of another kind between two roots", () => {
200
+ const stack: DriverComponent = { name: "s", dependsOn: ["a"], deploy: [{ phase: "Deploy", steps: [{ kind: "cfn-deploy", vars: { v: stackOutput("a", "vpc_id") } }] }] };
201
+ const c = compositionFor(input(threeRoots(), [tf("a"), tf("b"), stack, tf("c", { dependsOn: ["s"], vars: { q: stackOutput("s", "QueueUrl") } })]));
202
+ expect(c.members).toEqual(["a", "b"]);
203
+ expect(c.fallbacks).toEqual([
204
+ { kind: "split", member: "c", reason: "`c` depends on `a` through `s`, a cfn-deploy component that applies between them" },
205
+ ]);
206
+ });
207
+
208
+ test("split: a root left out between two roots", () => {
209
+ const c = compositionFor(
210
+ input(threeRoots(), [tf("a"), tf("b", { dependsOn: ["a"], vars: { v: stackOutput("a", "x") } }), tf("c", { dependsOn: ["b"], vars: { v: stackOutput("b", "y") } })], {
211
+ targets: { b: ["terraform_data.x"] },
212
+ }),
213
+ );
214
+ expect(c.fallbacks.map((f) => f.kind)).toEqual(["targets", "split"]);
215
+ expect(c.fallbacks[1]).toEqual({ kind: "split", member: "c", reason: "`c` depends on `a` through `b`, which is left out of the composed run and applies between them" });
216
+ expect(c.fallback).toEqual({ kind: "one-root", member: "a", reason: "only `a` of this change can compose, and one root applies in its own run" });
217
+ });
218
+ });
219
+
220
+ describe("terraform.compose is off by default", () => {
221
+ test("compositionIfEnabled returns nothing with the setting unset or off", () => {
222
+ const dir = threeRoots();
223
+ const base = input(dir, [tf("a"), tf("b"), tf("c")]);
224
+ expect(compositionIfEnabled({ ...base, terraform: { roots: base.terraform.roots } })).toBeUndefined();
225
+ expect(compositionIfEnabled({ ...base, terraform: { roots: base.terraform.roots, compose: { enabled: false } } })).toBeUndefined();
226
+ expect(compositionIfEnabled(base)?.members).toEqual(["a", "b", "c"]);
227
+ });
228
+
229
+ test("composeOff answers core's groupFallback with kind off until the setting is on", () => {
230
+ expect(composeOff({ roots: {} })).toEqual({ kind: "off", reason: "`terraform.compose` is not enabled" });
231
+ expect(composeOff({ roots: {}, compose: { enabled: false } })?.kind).toBe("off");
232
+ expect(composeOff({ roots: {}, compose: { enabled: true } })).toBeUndefined();
233
+ });
234
+
235
+ test("nothing in the lexicon calls compositionFor except through compositionIfEnabled", () => {
236
+ const src = dirname(fileURLToPath(import.meta.url));
237
+ const callers: string[] = [];
238
+ const walk = (dir: string): void => {
239
+ for (const n of readdirSync(dir)) {
240
+ const p = join(dir, n);
241
+ if (statSync(p).isDirectory()) walk(p);
242
+ else if (/\.ts$/.test(n) && !/\.test\.ts$/.test(n) && p !== join(src, "compose.ts") && /\bcompositionFor\s*\(/.test(readFileSync(p, "utf-8"))) callers.push(p);
243
+ }
244
+ };
245
+ walk(src);
246
+ expect(callers).toEqual([]);
247
+ });
248
+ });