@intentius/chant-lexicon-terraform 0.57.0 → 0.59.0

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 (408) hide show
  1. package/README.md +9 -0
  2. package/dist/composites/terraform-adopt-op.d.ts +131 -0
  3. package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
  4. package/dist/composites/terraform-apply-op.d.ts +70 -17
  5. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  6. package/dist/composites/terraform-watch-op.d.ts +104 -37
  7. package/dist/composites/terraform-watch-op.d.ts.map +1 -1
  8. package/dist/config.d.ts +49 -2
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/describe-resources.d.ts +256 -16
  11. package/dist/describe-resources.d.ts.map +1 -1
  12. package/dist/hcl/descend.d.ts +98 -0
  13. package/dist/hcl/descend.d.ts.map +1 -0
  14. package/dist/hcl/marker.d.ts +130 -0
  15. package/dist/hcl/marker.d.ts.map +1 -0
  16. package/dist/hcl/parse.d.ts +156 -9
  17. package/dist/hcl/parse.d.ts.map +1 -1
  18. package/dist/hcl/references.d.ts +76 -0
  19. package/dist/hcl/references.d.ts.map +1 -0
  20. package/dist/hcl/roots.d.ts +22 -3
  21. package/dist/hcl/roots.d.ts.map +1 -1
  22. package/dist/hcl/suppressions.d.ts +54 -0
  23. package/dist/hcl/suppressions.d.ts.map +1 -0
  24. package/dist/hcl/value.d.ts +77 -0
  25. package/dist/hcl/value.d.ts.map +1 -0
  26. package/dist/index.d.ts +5 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/integrity.json +34 -5
  29. package/dist/lint/audit-catalog.d.ts +3 -2
  30. package/dist/lint/audit-catalog.d.ts.map +1 -1
  31. package/dist/lint/audit-lineage.d.ts +95 -0
  32. package/dist/lint/audit-lineage.d.ts.map +1 -1
  33. package/dist/lint/post-synth/blocks.d.ts +53 -0
  34. package/dist/lint/post-synth/blocks.d.ts.map +1 -0
  35. package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
  36. package/dist/lint/post-synth/fixtures/load.d.ts.map +1 -0
  37. package/dist/lint/post-synth/index.d.ts.map +1 -1
  38. package/dist/lint/post-synth/module-source.d.ts +38 -0
  39. package/dist/lint/post-synth/module-source.d.ts.map +1 -0
  40. package/dist/lint/post-synth/scope.d.ts +65 -0
  41. package/dist/lint/post-synth/scope.d.ts.map +1 -0
  42. package/dist/lint/post-synth/tf001.d.ts +58 -6
  43. package/dist/lint/post-synth/tf001.d.ts.map +1 -1
  44. package/dist/lint/post-synth/tf002.d.ts +32 -0
  45. package/dist/lint/post-synth/tf002.d.ts.map +1 -0
  46. package/dist/lint/post-synth/tf003.d.ts +22 -0
  47. package/dist/lint/post-synth/tf003.d.ts.map +1 -0
  48. package/dist/lint/post-synth/tf004.d.ts +22 -0
  49. package/dist/lint/post-synth/tf004.d.ts.map +1 -0
  50. package/dist/lint/post-synth/tf005.d.ts +28 -0
  51. package/dist/lint/post-synth/tf005.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf006.d.ts +21 -0
  53. package/dist/lint/post-synth/tf006.d.ts.map +1 -0
  54. package/dist/lint/post-synth/tf007.d.ts +21 -0
  55. package/dist/lint/post-synth/tf007.d.ts.map +1 -0
  56. package/dist/lint/post-synth/tf008.d.ts +22 -0
  57. package/dist/lint/post-synth/tf008.d.ts.map +1 -0
  58. package/dist/lint/post-synth/tf009.d.ts +24 -0
  59. package/dist/lint/post-synth/tf009.d.ts.map +1 -0
  60. package/dist/lint/post-synth/tf010.d.ts +19 -0
  61. package/dist/lint/post-synth/tf010.d.ts.map +1 -0
  62. package/dist/lint/post-synth/tf011.d.ts +18 -0
  63. package/dist/lint/post-synth/tf011.d.ts.map +1 -0
  64. package/dist/lint/post-synth/tf012.d.ts +17 -0
  65. package/dist/lint/post-synth/tf012.d.ts.map +1 -0
  66. package/dist/lint/post-synth/tf013.d.ts +21 -0
  67. package/dist/lint/post-synth/tf013.d.ts.map +1 -0
  68. package/dist/lint/post-synth/tf014.d.ts +25 -0
  69. package/dist/lint/post-synth/tf014.d.ts.map +1 -0
  70. package/dist/lint/post-synth/tf015.d.ts +30 -0
  71. package/dist/lint/post-synth/tf015.d.ts.map +1 -0
  72. package/dist/lint/post-synth/tf016.d.ts +30 -0
  73. package/dist/lint/post-synth/tf016.d.ts.map +1 -0
  74. package/dist/lint/post-synth/tf017.d.ts +19 -0
  75. package/dist/lint/post-synth/tf017.d.ts.map +1 -0
  76. package/dist/lint/post-synth/tf018.d.ts +20 -0
  77. package/dist/lint/post-synth/tf018.d.ts.map +1 -0
  78. package/dist/lint/post-synth/tf019.d.ts +25 -0
  79. package/dist/lint/post-synth/tf019.d.ts.map +1 -0
  80. package/dist/lint/post-synth/tf020.d.ts +38 -0
  81. package/dist/lint/post-synth/tf020.d.ts.map +1 -0
  82. package/dist/lint/post-synth/tf021.d.ts +41 -0
  83. package/dist/lint/post-synth/tf021.d.ts.map +1 -0
  84. package/dist/lint/post-synth/tf022.d.ts +24 -0
  85. package/dist/lint/post-synth/tf022.d.ts.map +1 -0
  86. package/dist/lint/post-synth/tf024.d.ts +24 -0
  87. package/dist/lint/post-synth/tf024.d.ts.map +1 -0
  88. package/dist/lint/post-synth/tf025.d.ts +29 -0
  89. package/dist/lint/post-synth/tf025.d.ts.map +1 -0
  90. package/dist/lint/post-synth/tf026.d.ts +28 -0
  91. package/dist/lint/post-synth/tf026.d.ts.map +1 -0
  92. package/dist/lint/post-synth/tf027.d.ts +30 -0
  93. package/dist/lint/post-synth/tf027.d.ts.map +1 -0
  94. package/dist/lint/post-synth/tf028.d.ts +33 -0
  95. package/dist/lint/post-synth/tf028.d.ts.map +1 -0
  96. package/dist/lint/rules/fixtures/load.d.ts +13 -0
  97. package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
  98. package/dist/lint/rules/plan-before-apply.d.ts +11 -0
  99. package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
  100. package/dist/lint/secret-shape.d.ts +68 -0
  101. package/dist/lint/secret-shape.d.ts.map +1 -0
  102. package/dist/live-ownership.d.ts +55 -0
  103. package/dist/live-ownership.d.ts.map +1 -0
  104. package/dist/lsp/option-keys.d.ts.map +1 -1
  105. package/dist/manifest.json +1 -1
  106. package/dist/okf/index.md +26 -0
  107. package/dist/okf/rules/TF002.md +11 -0
  108. package/dist/okf/rules/TF003.md +11 -0
  109. package/dist/okf/rules/TF004.md +11 -0
  110. package/dist/okf/rules/TF005.md +11 -0
  111. package/dist/okf/rules/TF006.md +11 -0
  112. package/dist/okf/rules/TF007.md +11 -0
  113. package/dist/okf/rules/TF008.md +11 -0
  114. package/dist/okf/rules/TF009.md +11 -0
  115. package/dist/okf/rules/TF010.md +11 -0
  116. package/dist/okf/rules/TF011.md +11 -0
  117. package/dist/okf/rules/TF012.md +11 -0
  118. package/dist/okf/rules/TF013.md +11 -0
  119. package/dist/okf/rules/TF014.md +11 -0
  120. package/dist/okf/rules/TF015.md +11 -0
  121. package/dist/okf/rules/TF016.md +11 -0
  122. package/dist/okf/rules/TF017.md +11 -0
  123. package/dist/okf/rules/TF018.md +11 -0
  124. package/dist/okf/rules/TF019.md +11 -0
  125. package/dist/okf/rules/TF020.md +11 -0
  126. package/dist/okf/rules/TF021.md +11 -0
  127. package/dist/okf/rules/TF022.md +11 -0
  128. package/dist/okf/rules/TF024.md +11 -0
  129. package/dist/okf/rules/TF025.md +11 -0
  130. package/dist/okf/rules/TF026.md +11 -0
  131. package/dist/okf/rules/TF027.md +11 -0
  132. package/dist/okf/rules/TF028.md +11 -0
  133. package/dist/op/activities/index.d.ts +13 -6
  134. package/dist/op/activities/index.d.ts.map +1 -1
  135. package/dist/op/activities/live-detect.d.ts +51 -0
  136. package/dist/op/activities/live-detect.d.ts.map +1 -0
  137. package/dist/op/activities/terraform.d.ts +466 -17
  138. package/dist/op/activities/terraform.d.ts.map +1 -1
  139. package/dist/op/activity-contracts.d.ts +148 -0
  140. package/dist/op/activity-contracts.d.ts.map +1 -0
  141. package/dist/op/adoption.d.ts +118 -0
  142. package/dist/op/adoption.d.ts.map +1 -0
  143. package/dist/op/builders.d.ts +48 -6
  144. package/dist/op/builders.d.ts.map +1 -1
  145. package/dist/op/resolve-root-mode.d.ts +59 -0
  146. package/dist/op/resolve-root-mode.d.ts.map +1 -0
  147. package/dist/plugin.d.ts.map +1 -1
  148. package/dist/rules/blocks.ts +109 -0
  149. package/dist/rules/module-source.ts +121 -0
  150. package/dist/rules/plan-before-apply.ts +11 -0
  151. package/dist/rules/scope.ts +107 -0
  152. package/dist/rules/tf001.ts +98 -18
  153. package/dist/rules/tf002.ts +151 -0
  154. package/dist/rules/tf003.ts +71 -0
  155. package/dist/rules/tf004.ts +63 -0
  156. package/dist/rules/tf005.ts +76 -0
  157. package/dist/rules/tf006.ts +53 -0
  158. package/dist/rules/tf007.ts +78 -0
  159. package/dist/rules/tf008.ts +75 -0
  160. package/dist/rules/tf009.ts +59 -0
  161. package/dist/rules/tf010.ts +51 -0
  162. package/dist/rules/tf011.ts +49 -0
  163. package/dist/rules/tf012.ts +47 -0
  164. package/dist/rules/tf013.ts +63 -0
  165. package/dist/rules/tf014.ts +72 -0
  166. package/dist/rules/tf015.ts +72 -0
  167. package/dist/rules/tf016.ts +75 -0
  168. package/dist/rules/tf017.ts +50 -0
  169. package/dist/rules/tf018.ts +72 -0
  170. package/dist/rules/tf019.ts +72 -0
  171. package/dist/rules/tf020.ts +123 -0
  172. package/dist/rules/tf021.ts +126 -0
  173. package/dist/rules/tf022.ts +61 -0
  174. package/dist/rules/tf024.ts +70 -0
  175. package/dist/rules/tf025.ts +101 -0
  176. package/dist/rules/tf026.ts +84 -0
  177. package/dist/rules/tf027.ts +74 -0
  178. package/dist/rules/tf028.ts +96 -0
  179. package/dist/skills/chant-terraform.md +1 -1
  180. package/dist/validate.d.ts.map +1 -1
  181. package/package.json +7 -2
  182. package/src/__fixtures__/ACCEPTANCE.md +181 -0
  183. package/src/__fixtures__/live/main.tf +28 -0
  184. package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
  185. package/src/__fixtures__/live-adopt/main.tf +21 -0
  186. package/src/__fixtures__/live-estate/README.md +58 -0
  187. package/src/__fixtures__/live-estate/compute.tf +10 -0
  188. package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
  189. package/src/__fixtures__/live-estate/logs.tf +8 -0
  190. package/src/__fixtures__/live-estate/network.tf +24 -0
  191. package/src/__fixtures__/live-estate/rule.tf +11 -0
  192. package/src/__fixtures__/live-estate/unowned.tf +21 -0
  193. package/src/__fixtures__/live-estate/versions.tf +31 -0
  194. package/src/__fixtures__/live-ls.json +98 -0
  195. package/src/__fixtures__/live-plan.json +285 -0
  196. package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
  197. package/src/__fixtures__/live-sidecar/main.tf +24 -0
  198. package/src/__fixtures__/module-tree/README.md +16 -0
  199. package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
  200. package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
  201. package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
  202. package/src/__fixtures__/module-tree/missing/main.tf +9 -0
  203. package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
  204. package/src/__fixtures__/module-tree/root/main.tf +28 -0
  205. package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
  206. package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
  207. package/src/__fixtures__/module-tree/shared/main.tf +5 -0
  208. package/src/__fixtures__/references/main.tf +56 -0
  209. package/src/__fixtures__/remote-backend/main.tf +34 -0
  210. package/src/__fixtures__/with-backend/main.tf +7 -0
  211. package/src/codegen/lint-rules-page.test.ts +45 -0
  212. package/src/composites/terraform-adopt-op.acceptance.test.ts +193 -0
  213. package/src/composites/terraform-adopt-op.test.ts +175 -0
  214. package/src/composites/terraform-adopt-op.ts +202 -0
  215. package/src/composites/terraform-apply-op.acceptance.test.ts +188 -5
  216. package/src/composites/terraform-apply-op.test.ts +179 -19
  217. package/src/composites/terraform-apply-op.ts +123 -30
  218. package/src/composites/terraform-watch-op.test.ts +169 -29
  219. package/src/composites/terraform-watch-op.ts +183 -75
  220. package/src/config.ts +32 -3
  221. package/src/describe-resources.live.test.ts +598 -0
  222. package/src/describe-resources.test.ts +53 -0
  223. package/src/describe-resources.ts +811 -28
  224. package/src/hcl/descend.test.ts +162 -0
  225. package/src/hcl/descend.ts +273 -0
  226. package/src/hcl/marker.test.ts +147 -0
  227. package/src/hcl/marker.ts +235 -0
  228. package/src/hcl/parse.test.ts +69 -0
  229. package/src/hcl/parse.ts +269 -27
  230. package/src/hcl/references.test.ts +116 -0
  231. package/src/hcl/references.ts +172 -0
  232. package/src/hcl/roots.ts +59 -5
  233. package/src/hcl/suppressions.test.ts +147 -0
  234. package/src/hcl/suppressions.ts +204 -0
  235. package/src/hcl/value.test.ts +73 -0
  236. package/src/hcl/value.ts +106 -0
  237. package/src/index.ts +31 -1
  238. package/src/lint/audit-catalog.ts +288 -3
  239. package/src/lint/audit-lineage.ts +384 -1
  240. package/src/lint/audit.test.ts +210 -3
  241. package/src/lint/post-synth/blocks.ts +109 -0
  242. package/src/lint/post-synth/fixtures/TF001/negative-cloud.tf +28 -0
  243. package/src/lint/post-synth/fixtures/TF001/negative.tf +30 -0
  244. package/src/lint/post-synth/fixtures/TF001/positive-local.tf +31 -0
  245. package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
  246. package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
  247. package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
  248. package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
  249. package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
  250. package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
  251. package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
  252. package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
  253. package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
  254. package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
  255. package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
  256. package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
  257. package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
  258. package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
  259. package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
  260. package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
  261. package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
  262. package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
  263. package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
  264. package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
  265. package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
  266. package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
  267. package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
  268. package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
  269. package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
  270. package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
  271. package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
  272. package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
  273. package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
  274. package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
  275. package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
  276. package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
  277. package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
  278. package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
  279. package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
  280. package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
  281. package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
  282. package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
  283. package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
  284. package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
  285. package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
  286. package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
  287. package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
  288. package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
  289. package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
  290. package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
  291. package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
  292. package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
  293. package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
  294. package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
  295. package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
  296. package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
  297. package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
  298. package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
  299. package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
  300. package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
  301. package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
  302. package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
  303. package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
  304. package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
  305. package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
  306. package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
  307. package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
  308. package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
  309. package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
  310. package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
  311. package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
  312. package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
  313. package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
  314. package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
  315. package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
  316. package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
  317. package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
  318. package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
  319. package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
  320. package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
  321. package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
  322. package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
  323. package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
  324. package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
  325. package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
  326. package/src/lint/post-synth/fixtures/TF027/live-default/main.tf +9 -0
  327. package/src/lint/post-synth/fixtures/TF027/live-untagged-delete/main.tf +13 -0
  328. package/src/lint/post-synth/fixtures/TF027/live-untagged-delete-scoped/main.tf +17 -0
  329. package/src/lint/post-synth/fixtures/TF027/live-untagged-report/main.tf +13 -0
  330. package/src/lint/post-synth/fixtures/TF028/live/main.tf +9 -0
  331. package/src/lint/post-synth/fixtures/TF028/stock/main.tf +10 -0
  332. package/src/lint/post-synth/fixtures/load.ts +108 -0
  333. package/src/lint/post-synth/index.ts +52 -0
  334. package/src/lint/post-synth/live-refusals-config-format.test.ts +143 -0
  335. package/src/lint/post-synth/module-rules.test.ts +153 -0
  336. package/src/lint/post-synth/module-source.test.ts +75 -0
  337. package/src/lint/post-synth/module-source.ts +121 -0
  338. package/src/lint/post-synth/post-synth.test.ts +73 -16
  339. package/src/lint/post-synth/scope.ts +107 -0
  340. package/src/lint/post-synth/single-block.test.ts +263 -0
  341. package/src/lint/post-synth/suppression.test.ts +110 -0
  342. package/src/lint/post-synth/tf001.ts +98 -18
  343. package/src/lint/post-synth/tf002.test.ts +93 -0
  344. package/src/lint/post-synth/tf002.ts +151 -0
  345. package/src/lint/post-synth/tf003.test.ts +75 -0
  346. package/src/lint/post-synth/tf003.ts +71 -0
  347. package/src/lint/post-synth/tf004.test.ts +47 -0
  348. package/src/lint/post-synth/tf004.ts +63 -0
  349. package/src/lint/post-synth/tf005.test.ts +57 -0
  350. package/src/lint/post-synth/tf005.ts +76 -0
  351. package/src/lint/post-synth/tf006.ts +53 -0
  352. package/src/lint/post-synth/tf007.ts +78 -0
  353. package/src/lint/post-synth/tf008.ts +75 -0
  354. package/src/lint/post-synth/tf009.ts +59 -0
  355. package/src/lint/post-synth/tf010.ts +51 -0
  356. package/src/lint/post-synth/tf011.ts +49 -0
  357. package/src/lint/post-synth/tf012.ts +47 -0
  358. package/src/lint/post-synth/tf013.ts +63 -0
  359. package/src/lint/post-synth/tf014.ts +72 -0
  360. package/src/lint/post-synth/tf015.ts +72 -0
  361. package/src/lint/post-synth/tf016.ts +75 -0
  362. package/src/lint/post-synth/tf017.ts +50 -0
  363. package/src/lint/post-synth/tf018.ts +72 -0
  364. package/src/lint/post-synth/tf019.ts +72 -0
  365. package/src/lint/post-synth/tf020.ts +123 -0
  366. package/src/lint/post-synth/tf021.ts +126 -0
  367. package/src/lint/post-synth/tf022.ts +61 -0
  368. package/src/lint/post-synth/tf024.test.ts +42 -0
  369. package/src/lint/post-synth/tf024.ts +70 -0
  370. package/src/lint/post-synth/tf025.test.ts +46 -0
  371. package/src/lint/post-synth/tf025.ts +101 -0
  372. package/src/lint/post-synth/tf026.test.ts +56 -0
  373. package/src/lint/post-synth/tf026.ts +84 -0
  374. package/src/lint/post-synth/tf027.test.ts +62 -0
  375. package/src/lint/post-synth/tf027.ts +74 -0
  376. package/src/lint/post-synth/tf028.test.ts +92 -0
  377. package/src/lint/post-synth/tf028.ts +96 -0
  378. package/src/lint/presets.test.ts +72 -0
  379. package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
  380. package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
  381. package/src/lint/rules/fixtures/load.ts +53 -0
  382. package/src/lint/rules/plan-before-apply.test.ts +112 -7
  383. package/src/lint/rules/plan-before-apply.ts +11 -0
  384. package/src/lint/secret-shape.test.ts +150 -0
  385. package/src/lint/secret-shape.ts +159 -0
  386. package/src/live-ownership.ts +60 -0
  387. package/src/lsp/completions.test.ts +3 -2
  388. package/src/lsp/option-keys.ts +12 -6
  389. package/src/op/activities/choudoufu.acceptance.test.ts +127 -0
  390. package/src/op/activities/choudoufu.test.ts +725 -0
  391. package/src/op/activities/index.ts +40 -4
  392. package/src/op/activities/live-detect.test.ts +143 -0
  393. package/src/op/activities/live-detect.ts +162 -0
  394. package/src/op/activities/registry.test.ts +11 -3
  395. package/src/op/activities/terraform.test.ts +7 -3
  396. package/src/op/activities/terraform.ts +861 -69
  397. package/src/op/activity-contracts.test.ts +147 -0
  398. package/src/op/activity-contracts.ts +178 -0
  399. package/src/op/adoption.test.ts +170 -0
  400. package/src/op/adoption.ts +241 -0
  401. package/src/op/builders.test.ts +60 -5
  402. package/src/op/builders.ts +95 -5
  403. package/src/op/resolve-root-mode.test.ts +85 -0
  404. package/src/op/resolve-root-mode.ts +86 -0
  405. package/src/plugin.test.ts +106 -0
  406. package/src/plugin.ts +186 -29
  407. package/src/skills/chant-terraform.md +1 -1
  408. package/src/validate.ts +10 -1
@@ -0,0 +1,202 @@
1
+ /**
2
+ * TerraformAdoptOp composite (#2105): claim the live resources a choudoufu
3
+ * estate's configuration already describes.
4
+ *
5
+ * The reconcile position on the lifecycle dial, for the one backend that has
6
+ * a typed path from live back to the configuration. chant #2089 deferred
7
+ * reconcile for the terraform lexicon because stock Terraform has none: a live
8
+ * resource nobody has imported is knowable only as HCL somebody writes, and
9
+ * generating that is a different problem. choudoufu changes the question
10
+ * rather than answering the old one. Ownership there is two tags on the
11
+ * resource (`live/MARKERS.md`), `live-plan` already names every live resource
12
+ * that exactly matches a declared block and carries no marker, and it prints
13
+ * the two values that would claim it. Cloud-to-code here is "claim what the
14
+ * configuration already describes" — a tag write, not a regeneration.
15
+ *
16
+ * Phases: Check, Ledger, Gate, Adopt.
17
+ *
18
+ * - **Check** runs `live-check`, which makes no cloud calls and says whether
19
+ * this configuration can move under markers at all. It runs first because
20
+ * a root that `live-check` refuses will produce an adoption ledger that
21
+ * reads as authoritative and is not, and the cheapest place to find that
22
+ * out is before the estate-wide sweep.
23
+ * - **Ledger** runs `live-plan` with `adoptionOnly`, so the run's human half
24
+ * is choudoufu's own adoption ledger (GitHub issue #587) and its machine
25
+ * half is the bound/omissions/unowned document (#788). The ledger is the
26
+ * approval subject; the document is what the Adopt step acts on.
27
+ * - **Gate** always. Adoption writes tags onto resources this estate does
28
+ * not yet own, which is the moment the estate's boundary moves, and no
29
+ * reading of that is routine enough to skip. There is no `gate: "never"`
30
+ * here, unlike `TerraformApplyOp`, so every run stops at the gate until
31
+ * someone has approved it. A gate is a fact on the gate ledger (#2119):
32
+ * `chant run` records that the run is waiting, ends with status `gated`
33
+ * and exits 3, `chant approve <op> <gate>` writes the resolution, and the
34
+ * next run reads it and walks through to the Adopt phase.
35
+ * - **Adopt** writes the markers. See `choudoufuAdopt`'s own doc for the
36
+ * mechanism and why it is the tag write rather than choudoufu's `adopt`
37
+ * policy verb.
38
+ *
39
+ * ## The adopt mechanism, and why it is not the policy verb
40
+ *
41
+ * choudoufu's `policy` block has an `adopt` verb for the declared-but-unmarked
42
+ * quadrant, and its migration guide names `policy { declared_untagged =
43
+ * "adopt" }` as the way to claim a whole estate at once. That is the smaller
44
+ * write, and #2105 tried to reach it from outside the configuration. It is not
45
+ * reachable: a `live` block in a terraform override file is dropped in silence
46
+ * (choudoufu's `Module.mergeFile` has no `Lives` case), a second primary file
47
+ * carrying one is refused as a duplicate, and a sidecar beside an in-block
48
+ * form is refused as two sources of truth. What is left is editing the root's
49
+ * own checked-in live configuration around an apply, which leaves an estate
50
+ * declaring "adopt everything declared and unmarked" if the run dies between
51
+ * the two edits.
52
+ *
53
+ * So the mechanism is the tag write, which choudoufu's migration guide calls
54
+ * the whole contract: "There is no `choudoufu adopt` command and no need for
55
+ * one. Two tags is the whole contract, so any tool that writes two tags can
56
+ * adopt a resource." Each write runs the paste-ready command choudoufu itself
57
+ * printed in the ledger, carrying its own provider configuration's region and
58
+ * endpoint. `choudoufuAdopt` has the measurements behind that ruling.
59
+ *
60
+ * ## Ambiguity is reported, never resolved
61
+ *
62
+ * Two live resources at one declared identity are two `unowned` entries with
63
+ * the same address, and no single tag write claims that address. Those
64
+ * candidates never enter the adoptable set; the Adopt step is handed them
65
+ * separately so the Op's own result names what it declined and why, beside
66
+ * what it wrote.
67
+ *
68
+ * ## Compensation
69
+ *
70
+ * Refused without a command, the same stance `TerraformApplyOp` takes.
71
+ * Un-adopting is `untag`: dropping this estate's marker from a live resource,
72
+ * which is a decision about the estate rather than something chant can
73
+ * synthesize. It is also not always the right undo — a resource that was
74
+ * already carrying the markers before this run is not one this run adopted.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * import { TerraformAdoptOp } from "@intentius/chant-lexicon-terraform";
79
+ *
80
+ * export const { op } = TerraformAdoptOp({ name: "estate-adopt", root: "estate" });
81
+ * ```
82
+ */
83
+
84
+ import { Op, phase, gate, activity, OpResource } from "@intentius/chant/op";
85
+ import {
86
+ choudoufuLiveCheck as checkStep,
87
+ choudoufuLivePlan as ledgerStep,
88
+ choudoufuAdopt as adoptStep,
89
+ } from "../op/builders";
90
+
91
+ export interface TerraformAdoptOpConfig {
92
+ /** Op name (kebab-case). `gateName` defaults to `approve-<name>`. */
93
+ name: string;
94
+ /** Key into the project's `terraform.roots`. Must be a live root: choudoufu, with a declared estate. */
95
+ root: string;
96
+ /**
97
+ * The estate whose markers this run looks for. Omitted, `choudoufuLivePlan`
98
+ * auto-detects it from the root's own `live` block or `estate.chdf.hcl`
99
+ * sidecar, which is the usual case.
100
+ */
101
+ estate?: string;
102
+ /** The gate's name. Default: `approve-<name>`, as `TerraformApplyOp` does. */
103
+ gateName?: string;
104
+ /**
105
+ * @deprecated The gate's name was `signalName` through 0.58.0 and is
106
+ * `gateName` since #2202, matching `TerraformApplyOp`. Removed in 0.60.0.
107
+ */
108
+ signalName?: string;
109
+ /** How long a recorded pending gate stays valid, as a duration string. Default: core's own (48h). */
110
+ gateTimeout?: string;
111
+ /** Override the gate description shown to the approver. */
112
+ gateDescription?: string;
113
+ /**
114
+ * Directory each step starts the `chant.config.*` search from, which is what
115
+ * `terraform.roots` and the root's relative `dir` resolve against. Default:
116
+ * the running process's cwd.
117
+ */
118
+ cwd?: string;
119
+ /**
120
+ * Saga-style rollback on a failed adoption, run as an `onFailure` phase.
121
+ *
122
+ * Refused without a command. Undoing an adoption is `untag` — removing this
123
+ * estate's marker from a live resource — which is a decision about the
124
+ * estate, and one this Op cannot even scope correctly: a resource that
125
+ * already carried the markers is not one this run claimed. So `true`, or an
126
+ * object with no `command`, throws when the Op is built, naming the Op,
127
+ * rather than warning at the moment a rollback is already wanted.
128
+ */
129
+ compensate?: boolean | { command?: string };
130
+ }
131
+
132
+ export interface TerraformAdoptOpResources {
133
+ /** Op resource. Generates the Check/Ledger/Gate/Adopt workflow. */
134
+ op: InstanceType<typeof OpResource>;
135
+ }
136
+
137
+ export function TerraformAdoptOp(config: TerraformAdoptOpConfig): TerraformAdoptOpResources {
138
+ const compensateCommand = typeof config.compensate === "object" ? config.compensate.command : undefined;
139
+ if (config.compensate !== undefined && config.compensate !== false && compensateCommand === undefined) {
140
+ throw new Error(
141
+ `TerraformAdoptOp "${config.name}": compensate is enabled, but there is no automatic undo for an ` +
142
+ `adoption — un-adopting is untag, removing this estate's ownership marker from a live resource, ` +
143
+ `which is a decision about the estate rather than something chant can synthesize, and which this ` +
144
+ `Op cannot scope for you (a resource that already carried the markers is not one this run ` +
145
+ `claimed). Either supply compensate: { command: "..." } with an undo of your own, or set ` +
146
+ `compensate: false.`,
147
+ );
148
+ }
149
+
150
+ const where = config.cwd ? { cwd: config.cwd } : {};
151
+ const estate = config.estate ? { estate: config.estate } : {};
152
+
153
+ const check = checkStep(config.root, { ...where });
154
+ check.outcomeAttribute = { name: "Refused", from: "refused" };
155
+
156
+ // `id` is what makes `.out` legal. The Adopt step below reads two fields of
157
+ // this one: the adoptable matches, and the contested candidates it must
158
+ // report and never write.
159
+ const ledger = ledgerStep(config.root, { ...where, ...estate, adoptionOnly: true, id: "ledger" });
160
+ ledger.outcomeAttribute = [
161
+ { name: "Adoptable", from: "adoptable" },
162
+ { name: "Ambiguous", from: "ambiguous" },
163
+ ];
164
+
165
+ const adopt = adoptStep(config.root, {
166
+ ...where,
167
+ adoptions: ledger.out.adoptions,
168
+ contested: ledger.out.contested,
169
+ });
170
+ adopt.outcomeAttribute = { name: "Adopted", from: "adoptedCount" };
171
+
172
+ const op = Op({
173
+ name: config.name,
174
+ overview: `Adopt the live resources the "${config.root}" choudoufu estate already declares`,
175
+ labels: {
176
+ Adopt: "true",
177
+ TerraformRoot: config.root,
178
+ TerraformMode: "live",
179
+ },
180
+ phases: [
181
+ phase("Check", [check]),
182
+ phase("Ledger", [ledger]),
183
+ phase("Gate", [
184
+ gate(config.gateName ?? config.signalName ?? `approve-${config.name}`, {
185
+ ...(config.gateTimeout ? { timeout: config.gateTimeout } : {}),
186
+ description:
187
+ config.gateDescription ??
188
+ `Approve adopting live resources into the "${config.root}" estate. The Ledger phase's ` +
189
+ `adoption ledger is what is being approved: every resource it lists as adoptable gets this ` +
190
+ `estate's tofu-estate and tofu-address tags written onto it. The Adoptable and Ambiguous ` +
191
+ `run outcomes from that phase are its counts; an ambiguous address is never adopted.`,
192
+ }),
193
+ ]),
194
+ phase("Adopt", [adopt]),
195
+ ],
196
+ ...(compensateCommand
197
+ ? { onFailure: [phase("Rollback", [activity("shellCmd", { cmd: compensateCommand })])] }
198
+ : {}),
199
+ });
200
+
201
+ return { op };
202
+ }
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Everything else in this lexicon's Op suite proves the shape of what chant
5
5
  * emits. This proves the shape runs: build a `TerraformApplyOp` with
6
- * `gate: "never"` (the local executor refuses any Op containing a gate), hand
6
+ * `gate: "never"` (a gate ends a local run as `gated` since #2119, so an
7
+ * acceptance run that must reach Apply declares none), hand
7
8
  * it to `runOpLocally` with the activities the registry resolves by
8
9
  * convention, and let a real `terraform` — or `tofu` — init, plan and apply
9
10
  * the #2083 `with-backend` fixture root into a temp directory. Two
@@ -19,17 +20,29 @@
19
20
  *
20
21
  * Gating copied from `lexicons/k3s/src/serializer.acceptance.test.ts`, which
21
22
  * skips the same way when Docker is absent.
23
+ *
24
+ * `../__fixtures__/ACCEPTANCE.md` records what both blocks in this file have
25
+ * last passed against, with the binary version and the date (#2220). Update
26
+ * it in the same change as a run.
22
27
  */
23
28
 
24
29
  import { execSync } from "node:child_process";
25
30
  import { lookup } from "node:dns/promises";
26
- import { cpSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
31
+ import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
27
32
  import { tmpdir } from "node:os";
28
33
  import { join } from "node:path";
29
34
  import { afterAll, describe, expect, it } from "vitest";
30
35
  import { loadActivities, loadProfiles, runOpLocally, type OpConfig } from "@intentius/chant/op";
31
36
  import { TerraformApplyOp } from "./terraform-apply-op";
32
- import { terraformShow } from "../op/activities/terraform";
37
+ import {
38
+ MIN_CHOUDOUFU_VERSION,
39
+ isOlderVersion,
40
+ parseChoudoufuVersion,
41
+ terraformApply,
42
+ terraformInit,
43
+ terraformPlan,
44
+ terraformShow,
45
+ } from "../op/activities/terraform";
33
46
 
34
47
  function onPath(cmd: string): boolean {
35
48
  try {
@@ -113,7 +126,7 @@ describe.skipIf(skipReason !== "")(
113
126
  await loadProfiles(),
114
127
  );
115
128
 
116
- expect(result.ok).toBe(true);
129
+ expect(result.status).toBe("ok");
117
130
  expect(result.records.map((r) => `${r.phase}:${r.status}`)).toEqual([
118
131
  "Init:ok",
119
132
  "Plan:ok",
@@ -121,7 +134,7 @@ describe.skipIf(skipReason !== "")(
121
134
  ]);
122
135
 
123
136
  // The plan proposed two creates, and said so through the outcome the
124
- // Plan step surfaces as the `Changed` search attribute.
137
+ // Plan step surfaces as the `Changed` run outcome.
125
138
  const plan = result.records.find((r) => r.phase === "Plan")!;
126
139
  expect(plan.outcome).toEqual({ name: "Changed", value: true });
127
140
 
@@ -136,3 +149,173 @@ describe.skipIf(skipReason !== "")(
136
149
  );
137
150
  },
138
151
  );
152
+
153
+ /**
154
+ * `TerraformApplyOp` on a live root, against choudoufu's own pinned emulator
155
+ * (#2106 follow-up). Two suites, gated the same way
156
+ * `../op/activities/choudoufu.acceptance.test.ts` is, plus a version floor:
157
+ *
158
+ * - no `choudoufu` on PATH,
159
+ * - a `choudoufu` older than {@link MIN_CHOUDOUFU_VERSION}: v0.13.0 shipped
160
+ * the approval artifact
161
+ * ([choudoufu #878](https://github.com/INTENTIUS/choudoufu/issues/878),
162
+ * PR 889), before which `plan -out` was refused under a live block and
163
+ * this Op could not be built the way it is built now, and v0.14.0 moved
164
+ * the floor again for the document the other two Ops read
165
+ * ([choudoufu #894](https://github.com/INTENTIUS/choudoufu/issues/894)),
166
+ * - `CHOUDOUFU_EMULATOR_ENDPOINT` unset (bring up choudoufu's `just smoke`
167
+ * docker compose stack and export `http://localhost:<mapped port>`).
168
+ *
169
+ * The #894 clause the first version of this suite carried is gone, and stayed
170
+ * gone: that issue was about `live-plan -json`, which the apply Op does not
171
+ * run at all, since the plan half is the stock `plan -out` path.
172
+ *
173
+ * The first test is the happy path end to end through `runOpLocally`. The
174
+ * second is the refusal, and it runs the activities directly rather than
175
+ * through the Op, because the world has to move between the Plan step and the
176
+ * Apply step and there is no seam inside a running Op to do that from.
177
+ */
178
+
179
+ function versionOf(binary: string): string | undefined {
180
+ try {
181
+ return parseChoudoufuVersion(execSync(`${binary} version`, { encoding: "utf8" }));
182
+ } catch {
183
+ return undefined;
184
+ }
185
+ }
186
+
187
+ const LIVE_FIXTURE = join(import.meta.dirname, "..", "__fixtures__", "live");
188
+
189
+ /** A throwaway project holding a copy of the live fixture as its one root. */
190
+ function liveProject(): string {
191
+ const dir = mkdtempSync(join(tmpdir(), "chant-choudoufu-apply-accept-"));
192
+ workspaces.push(dir);
193
+ cpSync(LIVE_FIXTURE, join(dir, "root"), { recursive: true });
194
+ writeFileSync(
195
+ join(dir, "chant.config.json"),
196
+ JSON.stringify(
197
+ { lexicons: ["terraform"], terraform: { binary: "choudoufu", roots: { estate: { dir: "./root" } } } },
198
+ null,
199
+ 2,
200
+ ),
201
+ );
202
+ return dir;
203
+ }
204
+
205
+ /** The estate-wide marker sweep configures the AWS provider whatever the root declares. */
206
+ function pointAtTheEmulator(endpoint: string): void {
207
+ process.env.AWS_ENDPOINT_URL ??= endpoint;
208
+ process.env.AWS_ACCESS_KEY_ID ??= "choudoufu-emulator";
209
+ process.env.AWS_SECRET_ACCESS_KEY ??= "choudoufu-emulator";
210
+ process.env.AWS_DEFAULT_REGION ??= "us-east-1";
211
+ }
212
+
213
+ const choudoufuVersion = onPath("choudoufu") ? versionOf("choudoufu") : undefined;
214
+ const emulatorEndpoint = process.env.CHOUDOUFU_EMULATOR_ENDPOINT;
215
+
216
+ const liveSkipReason: string = !onPath("choudoufu")
217
+ ? "no choudoufu binary on PATH"
218
+ : choudoufuVersion === undefined
219
+ ? "the choudoufu on PATH reports no release version (a dev build), so the approval artifact cannot be assumed"
220
+ : isOlderVersion(choudoufuVersion, MIN_CHOUDOUFU_VERSION)
221
+ ? `choudoufu ${choudoufuVersion} is older than v${MIN_CHOUDOUFU_VERSION}, the lexicon's floor (choudoufu #878's approval artifact in v0.13.0, #894's -json document in v0.14.0)`
222
+ : !emulatorEndpoint
223
+ ? "CHOUDOUFU_EMULATOR_ENDPOINT is not set (bring up choudoufu's `just smoke` emulator stack and export it)"
224
+ : "";
225
+
226
+ describe.skipIf(liveSkipReason !== "")(
227
+ `TerraformApplyOp applies a live root against choudoufu's emulator${liveSkipReason ? ` (skipped: ${liveSkipReason})` : ""}`,
228
+ () => {
229
+ it(
230
+ 'inits, plans to a file, applies that file, gate: "never" — two null_resources applied',
231
+ { timeout: 600_000 },
232
+ async () => {
233
+ pointAtTheEmulator(emulatorEndpoint!);
234
+ const project = liveProject();
235
+
236
+ const { op } = TerraformApplyOp({
237
+ name: "choudoufu-acceptance",
238
+ root: "estate",
239
+ gate: "never",
240
+ cwd: project,
241
+ });
242
+
243
+ // The plan file is what crosses the (skipped) gate, so the built Op
244
+ // has to name one even here, where nothing waits on it.
245
+ const applyStep = (op as unknown as { props: OpConfig }).props.phases
246
+ .find((p) => p.name === "Apply")!
247
+ .steps[0] as { args?: Record<string, unknown> };
248
+ expect(applyStep.args?.planFile).toBeDefined();
249
+
250
+ const activities = await loadActivities(["terraform"]);
251
+ const result = await runOpLocally(
252
+ (op as unknown as { props: OpConfig }).props,
253
+ activities,
254
+ await loadProfiles(),
255
+ );
256
+
257
+ expect(result.status).toBe("ok");
258
+ expect(result.records.map((r) => `${r.phase}:${r.status}`)).toEqual(["Init:ok", "Plan:ok", "Apply:ok"]);
259
+
260
+ // A live root has no state file to read back: the state cache is not
261
+ // stock's `terraform.tfstate` and `show` over it renders nothing. The
262
+ // estate's own answer is the next plan, which is built from the live
263
+ // system, so what was applied is read back the way choudoufu means it
264
+ // to be. Empty, and its prior state holds exactly the two resources.
265
+ const after = await terraformPlan({ root: "estate", cwd: project });
266
+ expect(after.changed).toBe(false);
267
+ expect({ adds: after.adds, changes: after.changes, destroys: after.destroys }).toEqual({
268
+ adds: 0,
269
+ changes: 0,
270
+ destroys: 0,
271
+ });
272
+ const applied = (
273
+ (after.json as { prior_state?: { values?: { root_module?: { resources?: Array<{ address?: string }> } } } })
274
+ .prior_state?.values?.root_module?.resources ?? []
275
+ ).map((r) => r.address);
276
+ expect(applied.sort()).toEqual(["null_resource.first", "null_resource.second"]);
277
+ },
278
+ );
279
+
280
+ it(
281
+ "a plan file the live system has moved past comes back as a named refusal, not a throw",
282
+ { timeout: 600_000 },
283
+ async () => {
284
+ pointAtTheEmulator(emulatorEndpoint!);
285
+ const project = liveProject();
286
+ const mainTf = join(project, "root", "main.tf");
287
+ const original = readFileSync(mainTf, "utf8");
288
+
289
+ await terraformInit({ root: "estate", cwd: project });
290
+ const plan = await terraformPlan({ root: "estate", cwd: project });
291
+ expect(plan.changed).toBe(true);
292
+ expect(plan.adds).toBe(2);
293
+
294
+ // The configuration moves after the approval: one of the two resources
295
+ // the approver read is gone, so the fresh plan the apply builds has a
296
+ // change the file does not, and the file has one the fresh plan does not.
297
+ writeFileSync(mainTf, original.replace(/resource "null_resource" "second" \{[\s\S]*?\n\}\n/, ""));
298
+
299
+ const refusedRun = await terraformApply({ root: "estate", cwd: project, planFile: plan.planFile });
300
+ expect(refusedRun.applied).toBe(false);
301
+ expect(refusedRun.refused).toBe("approval-mismatch");
302
+ expect(refusedRun.refusal).toContain("The approved plan no longer matches the live system");
303
+ expect(refusedRun.refusal).toContain("null_resource.second");
304
+
305
+ // The sibling refusal, from the same exit status: the file was produced
306
+ // against another estate than the one this directory now declares.
307
+ writeFileSync(mainTf, original.replace('estate = "fixture-estate"', 'estate = "renamed-estate"'));
308
+ const wrongEstate = await terraformApply({ root: "estate", cwd: project, planFile: plan.planFile });
309
+ expect(wrongEstate.applied).toBe(false);
310
+ expect(wrongEstate.refused).toBe("wrong-estate");
311
+ expect(wrongEstate.refusal).toContain("The approved plan belongs to a different estate");
312
+
313
+ // And with the world back where the approval found it, the same file applies.
314
+ writeFileSync(mainTf, original);
315
+ const applied = await terraformApply({ root: "estate", cwd: project, planFile: plan.planFile });
316
+ expect(applied.applied).toBe(true);
317
+ expect(applied.refused).toBeUndefined();
318
+ },
319
+ );
320
+ },
321
+ );