@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,137 @@
1
+ /**
2
+ * Drift rows (#4580): one row per object a plan found drifted, by address,
3
+ * with its action and the object's real name, counted the way a product's
4
+ * drift report counts. `./drift-record.ts` answers a different question (the
5
+ * status of each instance the plan names), so it skips create rows, carries
6
+ * no names and stays as it is.
7
+ *
8
+ * A stock root's refresh-only plan reports drift as `resource_drift`, and
9
+ * every entry there is one row, data blocks and creates included. A full
10
+ * plan of a root under live resource markers (`live: true`) reads drift from
11
+ * `resource_changes` instead, seen from the other side: a create there is an
12
+ * object that no longer exists (delete), a delete one that exists outside
13
+ * the code (create), a replacement or any other pair of actions an update.
14
+ * Data blocks, no-op and read entries are not drift.
15
+ *
16
+ * These are terragucci's rules (packages/terragucci/src/report/drift.ts:
17
+ * `liveDrift`, `driftCount`, `driftNames`), so a row count here equals the
18
+ * count its report gives for the same plan.
19
+ *
20
+ * A row carries no attribute values. Its `name` is the first non-empty
21
+ * string among `name`, `id`, `bucket`, `function_name` and `arn`, read from
22
+ * the object as found and then as the code has it; a deleted object has no
23
+ * other place its name survives.
24
+ *
25
+ * The module imports nothing, so a product can load it behind a
26
+ * feature-detect without pulling the lexicon's entry point.
27
+ */
28
+
29
+ type Json = Record<string, unknown>;
30
+
31
+ const isObject = (v: unknown): v is Json => v !== null && typeof v === "object" && !Array.isArray(v);
32
+
33
+ /** One drifted object. */
34
+ export interface TerraformDriftRow {
35
+ /** The resource address, `""` when the plan's entry has none. */
36
+ address: string;
37
+ /**
38
+ * What happened outside the code: `update`, `delete` (the object is gone),
39
+ * `create` (an object exists that state or code does not hold), or on a
40
+ * stock root whatever single action the plan's entry names. On a stock
41
+ * root a delete-and-create pair is `replace`, and any other pair `update`.
42
+ */
43
+ action: string;
44
+ /** The resource type, when the entry names one. */
45
+ type?: string;
46
+ /** The object's real name, by the rule in this module's header. */
47
+ name?: string;
48
+ }
49
+
50
+ export interface TerraformPlanDriftRowsOptions {
51
+ /**
52
+ * The plan is a full plan of a root under live resource markers, whose
53
+ * changes are its drift. Default false: a refresh-only plan's
54
+ * `resource_drift`.
55
+ */
56
+ live?: boolean;
57
+ }
58
+
59
+ const FLIP: Record<string, string> = { create: "delete", delete: "create" };
60
+ const NAME_KEYS = ["name", "id", "bucket", "function_name", "arn"] as const;
61
+
62
+ function nameOf(values: readonly unknown[]): string | undefined {
63
+ const objects = values.filter(isObject);
64
+ for (const key of NAME_KEYS) {
65
+ const v = objects.map((x) => x[key]).find((x) => typeof x === "string" && x !== "");
66
+ if (typeof v === "string") return v;
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ function rowOf(entry: unknown, action: string, values: readonly unknown[]): TerraformDriftRow {
72
+ const r = isObject(entry) ? entry : {};
73
+ const name = nameOf(values);
74
+ return {
75
+ address: typeof r.address === "string" ? r.address : "",
76
+ action,
77
+ ...(typeof r.type === "string" && r.type !== "" ? { type: r.type } : {}),
78
+ ...(name !== undefined ? { name } : {}),
79
+ };
80
+ }
81
+
82
+ function stockAction(actions: readonly string[]): string {
83
+ if (actions.length === 1) return actions[0]!;
84
+ if (actions.length === 2 && actions.includes("delete") && actions.includes("create")) return "replace";
85
+ return "update";
86
+ }
87
+
88
+ /** A refresh-only plan's `resource_drift`, one row per entry. */
89
+ function stockRows(doc: Json): TerraformDriftRow[] {
90
+ const list = Array.isArray(doc.resource_drift) ? doc.resource_drift : [];
91
+ return list.map((entry) => {
92
+ const ch = isObject(entry) && isObject(entry.change) ? entry.change : {};
93
+ const actions = Array.isArray(ch.actions) ? ch.actions.map(String) : [];
94
+ return rowOf(entry, stockAction(actions), [ch.after, ch.before]);
95
+ });
96
+ }
97
+
98
+ /** A live root's full plan, its changes read as drift. */
99
+ function liveRows(doc: Json): TerraformDriftRow[] {
100
+ const list = Array.isArray(doc.resource_changes) ? doc.resource_changes : [];
101
+ const out: TerraformDriftRow[] = [];
102
+ for (const entry of list) {
103
+ if (!isObject(entry) || entry.mode === "data") continue;
104
+ const ch = isObject(entry.change) ? entry.change : {};
105
+ const actions = Array.isArray(ch.actions) ? ch.actions.map(String) : [];
106
+ if (actions.length === 0 || actions.every((a) => a === "no-op" || a === "read")) continue;
107
+ const action = actions.length === 1 ? (FLIP[actions[0]!] ?? actions[0]!) : "update";
108
+ const live = isObject(ch.before) ? ch.before : null;
109
+ let code = isObject(ch.after) ? ch.after : null;
110
+ // A value the plan knows only after the apply is not a difference: the live one stands for it.
111
+ const unknown = isObject(ch.after_unknown) ? ch.after_unknown : {};
112
+ if (code && live) code = { ...code, ...Object.fromEntries(Object.keys(unknown).filter((k) => unknown[k] === true && k in live).map((k) => [k, live[k]])) };
113
+ out.push(rowOf(entry, action, [live, code]));
114
+ }
115
+ return out;
116
+ }
117
+
118
+ /**
119
+ * The drift a plan (`show -json`, parsed) found, one row per drifted object
120
+ * in the plan's order. Its length is the drift count.
121
+ */
122
+ export function terraformPlanDriftRows(plan: unknown, options: TerraformPlanDriftRowsOptions = {}): TerraformDriftRow[] {
123
+ if (!isObject(plan)) return [];
124
+ return options.live ? liveRows(plan) : stockRows(plan);
125
+ }
126
+
127
+ /** How many objects a plan found drifted: `terraformPlanDriftRows(plan, options).length`. */
128
+ export function terraformPlanDriftCount(plan: unknown, options: TerraformPlanDriftRowsOptions = {}): number {
129
+ return terraformPlanDriftRows(plan, options).length;
130
+ }
131
+
132
+ /** Each drifted object's real name by address, for the rows that have one. */
133
+ export function terraformPlanDriftNames(plan: unknown, options: TerraformPlanDriftRowsOptions = {}): Map<string, string> {
134
+ const out = new Map<string, string>();
135
+ for (const row of terraformPlanDriftRows(plan, options)) if (row.name !== undefined) out.set(row.address, row.name);
136
+ return out;
137
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { CHANT_EXIT } from "@intentius/chant/exit-codes";
3
+ import { terraformExitOf, terraformPlanMoved } from "./exit-codes";
4
+
5
+ describe("terraform, tofu and choudoufu exit statuses on chant's table (#4553)", () => {
6
+ test("choudoufu's refusals of a moved plan are chant's moved code", () => {
7
+ expect(terraformExitOf({ command: "live-wave-apply", code: 3 })).toBe(CHANT_EXIT.moved);
8
+ expect(terraformExitOf({ command: "apply", code: 3, refusal: "approval-mismatch" })).toBe(CHANT_EXIT.moved);
9
+ expect(terraformPlanMoved({ command: "live-wave-apply", code: 3 })).toBe(true);
10
+ });
11
+
12
+ test("a plan made against another estate is a failure, and so is a root that failed", () => {
13
+ expect(terraformExitOf({ command: "apply", code: 3, refusal: "wrong-estate" })).toBe(CHANT_EXIT.failed);
14
+ expect(terraformExitOf({ command: "live-wave-apply", code: 4 })).toBe(CHANT_EXIT.failed);
15
+ expect(terraformExitOf({ command: "live-plan-set", code: 4 })).toBe(CHANT_EXIT.failed);
16
+ });
17
+
18
+ test("a plan's 2 is drift only under a drift check", () => {
19
+ expect(terraformExitOf({ command: "plan", code: 2 })).toBe(CHANT_EXIT.done);
20
+ expect(terraformExitOf({ command: "plan", code: 2, drift: true })).toBe(CHANT_EXIT.drift);
21
+ expect(terraformExitOf({ command: "plan", code: 1 })).toBe(CHANT_EXIT.failed);
22
+ });
23
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * OpenTofu, Terraform and choudoufu outcomes mapped onto chant's exit-code
3
+ * table (#4553, `CHANT_EXIT` in `@intentius/chant/exit-codes`).
4
+ *
5
+ * The lexicon never picks the process's exit code itself: for a moved plan it
6
+ * throws an error core marks as moved (`planMoved: true`, read by
7
+ * `isPlanMovedError`), and core exits `CHANT_EXIT.moved`. This mapping says
8
+ * which engine outcome is which row of the table, so the throw sites and the
9
+ * reference page agree.
10
+ *
11
+ * | Engine outcome | Table |
12
+ * |---|---|
13
+ * | `plan -detailed-exitcode` 0 or 2 | not an exit: the plan's answer. 2 under a drift check is `drift` |
14
+ * | any 1 | `failed` |
15
+ * | choudoufu `apply <planfile>` 3, approval mismatch | `moved` |
16
+ * | choudoufu `apply <planfile>` 3, plan made against another estate | `failed`: the pipeline handed the wrong file, and no approval fixes that |
17
+ * | choudoufu `live-wave-apply` 3, set moved | `moved` |
18
+ * | choudoufu `live-wave-apply` 4 or `live-plan-set` 4, a root failed | `failed` |
19
+ * | a composed plan that moved | `moved` |
20
+ * | `live-affected` 2, `live-cone` 2 | planner answers, never a job's exit |
21
+ */
22
+ import { CHANT_EXIT, type ChantExitCode } from "@intentius/chant/exit-codes";
23
+ import { CHOUDOUFU_APPROVAL_EXIT_CODE, CHOUDOUFU_WAVE_EXIT, type TerraformApprovalRefusal } from "./op/activities/terraform";
24
+
25
+ /** The command whose exit status is mapped. */
26
+ export type TerraformExitSource =
27
+ /** `terraform|tofu|choudoufu plan -detailed-exitcode`; `drift` says a drift check asked for it. */
28
+ | { command: "plan"; code: number; drift?: boolean }
29
+ /** `choudoufu apply <planfile>` on a live root; `refusal` is `classifyApprovalRefusal`'s answer for its output. */
30
+ | { command: "apply"; code: number; refusal?: TerraformApprovalRefusal }
31
+ /** `choudoufu live-wave-apply`. */
32
+ | { command: "live-wave-apply"; code: number }
33
+ /** `choudoufu live-plan-set`. */
34
+ | { command: "live-plan-set"; code: number };
35
+
36
+ /**
37
+ * The table's code for a terraform, tofu or choudoufu exit status. A plan's
38
+ * 0 and 2 are both `done` unless a drift check asked, when 2 is `drift`.
39
+ */
40
+ export function terraformExitOf(source: TerraformExitSource): ChantExitCode {
41
+ const { code } = source;
42
+ if (code === 0) return CHANT_EXIT.done;
43
+ switch (source.command) {
44
+ case "plan":
45
+ if (code === 2) return source.drift ? CHANT_EXIT.drift : CHANT_EXIT.done;
46
+ return CHANT_EXIT.failed;
47
+ case "apply":
48
+ return code === CHOUDOUFU_APPROVAL_EXIT_CODE && source.refusal === "approval-mismatch" ? CHANT_EXIT.moved : CHANT_EXIT.failed;
49
+ case "live-wave-apply":
50
+ return code === CHOUDOUFU_WAVE_EXIT.setMoved ? CHANT_EXIT.moved : CHANT_EXIT.failed;
51
+ case "live-plan-set":
52
+ return code === 2 ? CHANT_EXIT.done : CHANT_EXIT.failed;
53
+ }
54
+ }
55
+
56
+ /** Whether `source` is a plan that moved since its approval: the outcomes the lexicon throws a `planMoved` error for. */
57
+ export function terraformPlanMoved(source: TerraformExitSource): boolean {
58
+ return terraformExitOf(source) === CHANT_EXIT.moved;
59
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * `moved`, `import`, `removed` and `check` blocks as entities (chant #4458).
3
+ *
4
+ * The fixture is `src/__fixtures__/config-blocks/app/`: one root holding two
5
+ * `moved` blocks, an `import`, a `removed` and a `check` with a scoped `data`
6
+ * block, rendered through `renderTerraformRoots` and core's `buildGraphIr`,
7
+ * the pair `chant graph --format ir` runs.
8
+ */
9
+
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { beforeAll, describe, expect, it } from "vitest";
13
+ import { buildGraphIr, type GraphIR, type IREdge } from "@intentius/chant/graph-ir";
14
+ import type { Declarable } from "@intentius/chant/declarable";
15
+ import { renderTerraformRoots } from "./roots";
16
+ import {
17
+ CHECK_TYPE,
18
+ IMPORT_TYPE,
19
+ MOVED_TYPE,
20
+ REMOVED_TYPE,
21
+ blocksToEntities,
22
+ type TerraformEntity,
23
+ } from "./parse";
24
+
25
+ const TREE = join(dirname(fileURLToPath(import.meta.url)), "..", "__fixtures__", "config-blocks");
26
+
27
+ let entities: Map<string, Declarable>;
28
+ let warnings: string[];
29
+ let ir: GraphIR;
30
+
31
+ beforeAll(async () => {
32
+ const rendered = await renderTerraformRoots({ projectRoot: TREE, roots: { app: { dir: "./app" } } });
33
+ entities = rendered.entities;
34
+ warnings = rendered.warnings;
35
+ ir = buildGraphIr(entities);
36
+ });
37
+
38
+ function entity(key: string): TerraformEntity {
39
+ const e = entities.get(key);
40
+ expect(e, key).toBeDefined();
41
+ return e as TerraformEntity;
42
+ }
43
+
44
+ function edgesFrom(from: string): Array<[string, string]> {
45
+ return ir.edges
46
+ .filter((e: IREdge) => e.from === from)
47
+ .map((e: IREdge) => [e.to, e.viaAttr ?? ""] as [string, string])
48
+ .sort();
49
+ }
50
+
51
+ describe("moved blocks", () => {
52
+ it("become Terraform::Moved entities", () => {
53
+ expect(entity("app/moved").entityType).toBe(MOVED_TYPE);
54
+ expect(entity("app/moved").props.address).toBe("moved");
55
+ });
56
+
57
+ it("get distinct keys when a root holds two", () => {
58
+ expect(entity("app/moved~2").entityType).toBe(MOVED_TYPE);
59
+ expect([...entities.keys()].filter((k) => entities.get(k)!.entityType === MOVED_TYPE).sort()).toEqual([
60
+ "app/moved",
61
+ "app/moved~2",
62
+ ]);
63
+ expect(entity("app/moved").props.line).not.toBe(entity("app/moved~2").props.line);
64
+ });
65
+
66
+ it("have an edge to `to`, and to `from` when `from` is still declared", () => {
67
+ // `aws_instance.old` and `module.legacy` are not declared, so only `to` draws.
68
+ expect(edgesFrom("app/moved")).toEqual([["app/aws_instance.web", "to"]]);
69
+ expect(edgesFrom("app/moved~2")).toEqual([["app/module.network", "to"]]);
70
+ });
71
+
72
+ it("draws the `from` edge when the address it names is declared", async () => {
73
+ const parsed = await blocksToEntities(
74
+ [
75
+ {
76
+ name: "main.tf",
77
+ source: [
78
+ 'resource "aws_instance" "a" {}',
79
+ 'resource "aws_instance" "b" {}',
80
+ "moved {",
81
+ " from = aws_instance.a",
82
+ " to = aws_instance.b",
83
+ "}",
84
+ ].join("\n"),
85
+ },
86
+ ],
87
+ "app",
88
+ );
89
+ const { resolveEntityReferences } = await import("./edges");
90
+ await resolveEntityReferences(parsed);
91
+ const moved = parsed.get("app/moved") as TerraformEntity;
92
+ expect(moved.references).toEqual([
93
+ { to: "app/aws_instance.a", viaAttr: "from" },
94
+ { to: "app/aws_instance.b", viaAttr: "to" },
95
+ ]);
96
+ });
97
+ });
98
+
99
+ describe("import blocks", () => {
100
+ it("become a Terraform::Import entity with an edge to `to`", () => {
101
+ expect(entity("app/import").entityType).toBe(IMPORT_TYPE);
102
+ expect(edgesFrom("app/import")).toEqual([["app/aws_lb.front", "to"]]);
103
+ });
104
+ });
105
+
106
+ describe("removed blocks", () => {
107
+ it("become a Terraform::Removed entity", () => {
108
+ expect(entity("app/removed").entityType).toBe(REMOVED_TYPE);
109
+ expect(entity("app/removed").props.body["from"]).toBe("${aws_instance.gone}");
110
+ });
111
+
112
+ it("draw a `from` edge only when the address is still declared", async () => {
113
+ expect(edgesFrom("app/removed")).toEqual([]);
114
+ const parsed = await blocksToEntities(
115
+ [{ name: "main.tf", source: 'resource "aws_instance" "a" {}\nremoved {\n from = aws_instance.a\n}\n' }],
116
+ "app",
117
+ );
118
+ const { resolveEntityReferences } = await import("./edges");
119
+ await resolveEntityReferences(parsed);
120
+ expect((parsed.get("app/removed") as TerraformEntity).references).toEqual([
121
+ { to: "app/aws_instance.a", viaAttr: "from" },
122
+ ]);
123
+ });
124
+ });
125
+
126
+ describe("check blocks", () => {
127
+ it("become a Terraform::Check entity keyed by its name", () => {
128
+ expect(entity("app/check.health").entityType).toBe(CHECK_TYPE);
129
+ expect(entity("app/check.health").props.address).toBe("check.health");
130
+ });
131
+
132
+ it("have an edge per reference, including the scoped data block's", () => {
133
+ expect(edgesFrom("app/check.health")).toEqual([
134
+ ["app/aws_lb.front", "data"],
135
+ ["app/var.region", "assert"],
136
+ ]);
137
+ });
138
+
139
+ it("read the scoped data block, never the root-level one of the same address", () => {
140
+ expect(ir.edges.some((e) => e.from === "app/check.health" && e.to === "app/data.http.site")).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe("a `from` naming an undeclared address", () => {
145
+ it("does not fail the build", () => {
146
+ expect(warnings).toEqual([]);
147
+ });
148
+
149
+ it("raises no dangling reference", () => {
150
+ expect(ir.dangling ?? []).toEqual([]);
151
+ for (const e of ir.edges) expect(entities.has(e.to)).toBe(true);
152
+ });
153
+ });
154
+
155
+ describe("the same blocks in Terraform's JSON syntax (chant#4457)", () => {
156
+ /** Each config block's entity as the acceptance compares it: type, address and body, by key. */
157
+ const configBlocks = (all: Map<string, Declarable>): Record<string, { type: string; address: string; body: unknown }> =>
158
+ Object.fromEntries(
159
+ [...all]
160
+ .filter(([, e]) => [MOVED_TYPE, IMPORT_TYPE, REMOVED_TYPE, CHECK_TYPE].includes(e.entityType))
161
+ .map(([key, e]) => {
162
+ const p = (e as TerraformEntity).props;
163
+ return [key, { type: e.entityType, address: p.address, body: p.body }];
164
+ }),
165
+ );
166
+
167
+ it("main.tf.json's moved, import, removed and check blocks are the entities main.tf's are", async () => {
168
+ const json = (await renderTerraformRoots({ projectRoot: TREE, roots: { app: { dir: "./app-json" } } })).entities;
169
+ const fromJson = configBlocks(json);
170
+ expect(Object.keys(fromJson).sort()).toEqual(["app/check.health", "app/import", "app/moved", "app/moved~2", "app/removed"]);
171
+ expect(fromJson).toEqual(configBlocks(entities));
172
+ for (const key of Object.keys(fromJson)) expect((json.get(key) as TerraformEntity).props.file).toBe("main.tf.json");
173
+ });
174
+ });
package/src/hcl/edges.ts CHANGED
@@ -88,7 +88,7 @@ import { isResourceDeclarable, type Declarable } from "@intentius/chant/declarab
88
88
  import type { EntityReference } from "@intentius/chant/graph-ir";
89
89
  import { loadHcl2json, type Hcl2Json } from "@intentius/chant/terraform/hcl2json";
90
90
  import { refFromAccessor } from "@intentius/chant/terraform/graph";
91
- import { LOCALS_TYPE, scopeOfKey, type BlockBody, type TerraformEntity } from "./parse";
91
+ import { CHECK_TYPE, LOCALS_TYPE, scopeOfKey, type BlockBody, type TerraformEntity } from "./parse";
92
92
 
93
93
  /** An identifier as Terraform's grammar allows it. Same as `./references.ts`. */
94
94
  const NAME = "[A-Za-z_][A-Za-z0-9_-]*";
@@ -244,8 +244,12 @@ export function referencesOfEntity(
244
244
  const index = scopes.get(scopeOfKey(key));
245
245
  if (!index) return [];
246
246
 
247
+ const shadowed = entity.entityType === CHECK_TYPE ? scopedDataAddresses(bodyOf(entity)) : undefined;
247
248
  const grouped = new Map<string, { to: string; viaAttr: string; attrs: Set<string> }>();
248
249
  for (const raw of rawReferences(bodyOf(entity), accessors)) {
250
+ // An assertion reading the check's own scoped `data` block reads that
251
+ // block, never a root-level data source of the same address (#4458).
252
+ if (shadowed?.has(raw.address)) continue;
249
253
  const to = resolveAddress(raw.address, index);
250
254
  if (!to || to === key) continue;
251
255
  const groupKey = `${to}${raw.via}`;
@@ -270,6 +274,22 @@ export function referencesOfEntity(
270
274
  return out.sort((a, b) => cmp(a.to, b.to) || cmp(a.viaAttr ?? "", b.viaAttr ?? ""));
271
275
  }
272
276
 
277
+ /**
278
+ * The `data.<type>.<name>` addresses a `check` block declares in its own
279
+ * scope (#4458). hcl2json gives them as `data: { <type>: { <name>: [...] } }`
280
+ * inside the check's body.
281
+ */
282
+ function scopedDataAddresses(body: BlockBody): Set<string> {
283
+ const out = new Set<string>();
284
+ const data = body["data"];
285
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return out;
286
+ for (const [type, named] of Object.entries(data as Record<string, unknown>)) {
287
+ if (typeof named !== "object" || named === null || Array.isArray(named)) continue;
288
+ for (const name of Object.keys(named as Record<string, unknown>)) out.add(`data.${type}.${name}`);
289
+ }
290
+ return out;
291
+ }
292
+
273
293
  /** Code-point ordering, so output does not depend on the machine's locale. */
274
294
  function cmp(a: string, b: string): number {
275
295
  return a < b ? -1 : a > b ? 1 : 0;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Which files of a directory are a root's configuration, and how a JSON-syntax
3
+ * one reads (chant#4457). Every lister in the lexicon asks here, so the parser,
4
+ * change detection, the backend readers, the report and the rollout pin all
5
+ * see the same file set for one root.
6
+ *
7
+ * The set follows the root's binary. Terraform reads `.tf` and `.tf.json`.
8
+ * OpenTofu (and choudoufu, which is OpenTofu) also reads `.tofu` and
9
+ * `.tofu.json`, and when `x.tofu` and `x.tf` both exist it reads `x.tofu`
10
+ * only (likewise `x.tofu.json` over `x.tf.json`). A binary that is not given
11
+ * is `terraform`, the config's default.
12
+ *
13
+ * `parse.ts` re-exports all of this; the module is separate so the readers
14
+ * that never parse HCL (`state/backend.ts`, `changed-roots.ts`) do not load
15
+ * the parser's dependencies.
16
+ */
17
+
18
+ import { readdirSync, statSync } from "node:fs";
19
+ import { join } from "node:path";
20
+
21
+ /** The configuration file suffixes Terraform reads. */
22
+ export const TERRAFORM_SUFFIXES: readonly string[] = [".tf", ".tf.json"];
23
+ /** The ones OpenTofu reads as well. */
24
+ export const TOFU_SUFFIXES: readonly string[] = [".tofu", ".tofu.json"];
25
+
26
+ /** Whether a root run by `binary` reads `.tofu` files: `tofu` and `choudoufu` do (by name or path), `terraform` and an unset binary do not. */
27
+ export function readsTofuFiles(binary?: string): boolean {
28
+ const base = (binary ?? "terraform").split(/[\\/]/).pop() ?? "";
29
+ return base.startsWith("tofu") || base.startsWith("choudoufu");
30
+ }
31
+
32
+ /** Whether `name` is a configuration file in Terraform's JSON syntax (`.tf.json`, `.tofu.json`). */
33
+ export function isJsonConfigFile(name: string): boolean {
34
+ return name.endsWith(".tf.json") || name.endsWith(".tofu.json");
35
+ }
36
+
37
+ /** Whether `name` is a `.tofu` or `.tofu.json` file. */
38
+ export function isTofuConfigFile(name: string): boolean {
39
+ return TOFU_SUFFIXES.some((s) => name.endsWith(s));
40
+ }
41
+
42
+ /** Whether `name` is a configuration file a root run by `binary` reads, before the `.tofu`-over-`.tf` rule. */
43
+ export function isConfigFileFor(name: string, binary?: string): boolean {
44
+ if (TERRAFORM_SUFFIXES.some((s) => name.endsWith(s))) return true;
45
+ return readsTofuFiles(binary) && isTofuConfigFile(name);
46
+ }
47
+
48
+ /**
49
+ * The configuration files among `names` a root run by `binary` reads, in
50
+ * filename order. For a tofu or choudoufu root, `x.tf` is dropped when
51
+ * `x.tofu` is present and `x.tf.json` when `x.tofu.json` is, which is
52
+ * OpenTofu's own rule. Names may carry a directory; the rule compares names
53
+ * in the same directory.
54
+ */
55
+ export function selectConfigFiles(names: Iterable<string>, binary?: string): string[] {
56
+ const kept = [...names].filter((n) => isConfigFileFor(n, binary));
57
+ const present = new Set(kept);
58
+ return kept
59
+ .filter((n) => {
60
+ if (n.endsWith(".tf")) return !present.has(`${n.slice(0, -".tf".length)}.tofu`);
61
+ if (n.endsWith(".tf.json")) return !present.has(`${n.slice(0, -".tf.json".length)}.tofu.json`);
62
+ return true;
63
+ })
64
+ .sort();
65
+ }
66
+
67
+ /**
68
+ * The configuration files directly under `dir` a root run by `binary` reads,
69
+ * by name, in filename order. Non-recursive, matching Terraform's module
70
+ * scoping; directories named like a file are left out. Throws when `dir`
71
+ * cannot be read.
72
+ */
73
+ export function listConfigFiles(dir: string, binary?: string): string[] {
74
+ const files = readdirSync(dir).filter((n) => {
75
+ if (!isConfigFileFor(n, binary)) return false;
76
+ try {
77
+ return statSync(join(dir, n)).isFile();
78
+ } catch {
79
+ return false;
80
+ }
81
+ });
82
+ return selectConfigFiles(files, binary);
83
+ }
84
+
85
+ /** {@link listConfigFiles}, empty when `dir` cannot be read. */
86
+ export function listConfigFilesSafe(dir: string, binary?: string): string[] {
87
+ try {
88
+ return listConfigFiles(dir, binary);
89
+ } catch {
90
+ return [];
91
+ }
92
+ }
93
+
94
+ /**
95
+ * The nested blocks a JSON-syntax body can carry that hcl2json would give as
96
+ * arrays, by block type: each entry is the nested block's label count and its
97
+ * own nested blocks. Anything not named here is an attribute and is kept as
98
+ * written, since JSON syntax cannot tell a nested block from an object
99
+ * attribute without the provider's schema.
100
+ */
101
+ interface BlockShape {
102
+ labels: number;
103
+ nested?: Record<string, BlockShape>;
104
+ /**
105
+ * Attributes JSON syntax writes as a bare expression in a string (a type,
106
+ * a reference), where HCL writes the expression itself and hcl2json gives
107
+ * it as `"${...}"`. Their strings are wrapped the same way.
108
+ */
109
+ exprs?: readonly string[];
110
+ }
111
+
112
+ const LIFECYCLE: BlockShape = { labels: 0, exprs: ["ignore_changes", "replace_triggered_by"] };
113
+ const RESOURCE_NESTED: Record<string, BlockShape> = {
114
+ lifecycle: LIFECYCLE,
115
+ provisioner: { labels: 1, nested: { connection: { labels: 0 } } },
116
+ connection: { labels: 0 },
117
+ };
118
+ const REFERS = ["provider", "depends_on"];
119
+
120
+ const TOP_LEVEL: Record<string, BlockShape> = {
121
+ terraform: {
122
+ labels: 0,
123
+ nested: {
124
+ backend: { labels: 1 },
125
+ cloud: { labels: 0, nested: { workspaces: { labels: 0 } } },
126
+ required_providers: { labels: 0 },
127
+ provider_meta: { labels: 1 },
128
+ live: { labels: 0 },
129
+ },
130
+ },
131
+ locals: { labels: 0 },
132
+ provider: { labels: 1 },
133
+ module: { labels: 1, exprs: ["depends_on", "providers"] },
134
+ variable: { labels: 1, nested: { validation: { labels: 0 } }, exprs: ["type"] },
135
+ output: { labels: 1, nested: { precondition: { labels: 0 } }, exprs: ["depends_on"] },
136
+ resource: { labels: 2, nested: RESOURCE_NESTED, exprs: REFERS },
137
+ data: { labels: 2, nested: { lifecycle: LIFECYCLE }, exprs: REFERS },
138
+ moved: { labels: 0, exprs: ["from", "to"] },
139
+ import: { labels: 0, exprs: ["to", "provider"] },
140
+ removed: { labels: 0, nested: { lifecycle: LIFECYCLE, provisioner: RESOURCE_NESTED.provisioner! }, exprs: ["from"] },
141
+ check: { labels: 1, nested: { data: { labels: 2 }, assert: { labels: 0 } } },
142
+ };
143
+
144
+ const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
145
+
146
+ /** A bare expression string as hcl2json writes the expression: `aws.west` becomes `${aws.west}`; a template is kept. */
147
+ function asExpression(v: unknown): unknown {
148
+ if (typeof v === "string") return v.includes("${") ? v : `\${${v}}`;
149
+ if (Array.isArray(v)) return v.map(asExpression);
150
+ if (isObject(v)) return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, asExpression(x)]));
151
+ return v;
152
+ }
153
+
154
+ /** A JSON body with its `"//"` comment keys dropped (Terraform ignores them), its known nested blocks wrapped and its expression attributes written as hcl2json writes them. */
155
+ function normaliseBody(body: unknown, shape: BlockShape): Record<string, unknown> {
156
+ const out: Record<string, unknown> = {};
157
+ if (!isObject(body)) return out;
158
+ for (const [k, v] of Object.entries(body)) {
159
+ if (k === "//") continue;
160
+ const inner = shape.nested?.[k];
161
+ out[k] = inner ? wrapBlock(v, inner) : shape.exprs?.includes(k) ? asExpression(v) : v;
162
+ }
163
+ return out;
164
+ }
165
+
166
+ /**
167
+ * A block in JSON syntax as hcl2json gives the same block in HCL: one object
168
+ * level per label, then an array of bodies. JSON syntax writes a single body
169
+ * as an object or several as an array, and allows an array of objects at a
170
+ * label level to be merged, so both are read.
171
+ */
172
+ function wrapBlock(value: unknown, shape: BlockShape, labels = shape.labels): unknown {
173
+ if (labels === 0) return (Array.isArray(value) ? value : [value]).filter(isObject).map((b) => normaliseBody(b, shape));
174
+ const out: Record<string, unknown> = {};
175
+ for (const part of Array.isArray(value) ? value : [value]) {
176
+ if (!isObject(part)) continue;
177
+ for (const [label, inner] of Object.entries(part)) {
178
+ if (label === "//") continue;
179
+ const wrapped = wrapBlock(inner, shape, labels - 1);
180
+ const had = out[label];
181
+ if (Array.isArray(had) && Array.isArray(wrapped)) out[label] = [...had, ...wrapped];
182
+ else if (isObject(had) && isObject(wrapped)) out[label] = mergeLabelled(had, wrapped);
183
+ else out[label] = wrapped;
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+
189
+ function mergeLabelled(a: Record<string, unknown>, b: Record<string, unknown>): Record<string, unknown> {
190
+ const out: Record<string, unknown> = { ...a };
191
+ for (const [k, v] of Object.entries(b)) {
192
+ const had = out[k];
193
+ if (Array.isArray(had) && Array.isArray(v)) out[k] = [...had, ...v];
194
+ else if (isObject(had) && isObject(v)) out[k] = mergeLabelled(had, v);
195
+ else out[k] = v;
196
+ }
197
+ return out;
198
+ }
199
+
200
+ /**
201
+ * A `.tf.json` or `.tofu.json` document in the tree shape hcl2json gives an
202
+ * HCL file, so `blocksToEntities` reads both the same way. Top-level keys it
203
+ * does not know are kept as written. Throws on malformed JSON, as hcl2json
204
+ * throws on malformed HCL.
205
+ */
206
+ export function jsonConfigTree(source: string): Record<string, unknown> {
207
+ const doc: unknown = JSON.parse(source);
208
+ if (!isObject(doc)) throw new Error("a JSON configuration file must hold an object");
209
+ const out: Record<string, unknown> = {};
210
+ for (const [k, v] of Object.entries(doc)) {
211
+ if (k === "//") continue;
212
+ const shape = TOP_LEVEL[k];
213
+ out[k] = shape ? wrapBlock(v, shape) : v;
214
+ }
215
+ return out;
216
+ }
217
+
218
+ /**
219
+ * The bodies of the `live` blocks nested in the `terraform` blocks of a
220
+ * `.tf.json` or `.tofu.json` document, in the order written (chant#4504).
221
+ * The fast live readers (`report.ts`'s `estateOfDir`, `op/activities/live-detect.ts`)
222
+ * read HCL by text and a JSON file through this. Malformed JSON reads as no
223
+ * `live` block, as those readers treat any file they cannot read.
224
+ */
225
+ export function jsonLiveBlocks(source: string): Record<string, unknown>[] {
226
+ let tree: Record<string, unknown>;
227
+ try {
228
+ tree = jsonConfigTree(source);
229
+ } catch {
230
+ return [];
231
+ }
232
+ const terraform = Array.isArray(tree.terraform) ? tree.terraform : [];
233
+ return terraform.flatMap((t) => (isObject(t) && Array.isArray(t.live) ? t.live.filter(isObject) : []));
234
+ }