@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,9 @@
1
+ module "b" {
2
+ source = "../b"
3
+ }
4
+
5
+ resource "null_resource" "a" {
6
+ triggers = {
7
+ name = "a"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ module "a" {
2
+ source = "../a"
3
+ }
4
+
5
+ resource "null_resource" "b" {
6
+ triggers = {
7
+ name = "b"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ module "gone" {
2
+ source = "./modules/gone"
3
+ }
4
+
5
+ resource "null_resource" "here" {
6
+ triggers = {
7
+ name = "here"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ module "shared" {
2
+ source = "../../shared"
3
+ }
4
+
5
+ resource "null_resource" "here" {
6
+ triggers = {
7
+ name = "here"
8
+ }
9
+ }
@@ -0,0 +1,28 @@
1
+ terraform {
2
+ required_version = ">= 1.5.0"
3
+
4
+ backend "s3" {
5
+ bucket = "tfstate"
6
+ key = "app/terraform.tfstate"
7
+ region = "us-east-1"
8
+ }
9
+ }
10
+
11
+ variable "region" {
12
+ type = string
13
+ default = "us-east-1"
14
+ }
15
+
16
+ module "cdn" {
17
+ source = "./modules/cdn"
18
+ region = var.region
19
+ }
20
+
21
+ module "vpc" {
22
+ source = "terraform-aws-modules/vpc/aws"
23
+ version = "5.1.2"
24
+ }
25
+
26
+ resource "aws_s3_bucket" "root_bucket" {
27
+ bucket = "root-bucket"
28
+ }
@@ -0,0 +1,15 @@
1
+ variable "region" {
2
+ type = string
3
+ }
4
+
5
+ provider "aws" {
6
+ region = var.region
7
+ }
8
+
9
+ module "bucket" {
10
+ source = "./modules/bucket"
11
+ }
12
+
13
+ resource "aws_cloudfront_distribution" "cdn" {
14
+ comment = "cdn in ${var.region}"
15
+ }
@@ -0,0 +1,11 @@
1
+ terraform {
2
+ required_version = ">= 1.5.0"
3
+
4
+ backend "local" {
5
+ path = "bucket.tfstate"
6
+ }
7
+ }
8
+
9
+ resource "aws_s3_bucket" "assets" {
10
+ bucket = "assets"
11
+ }
@@ -0,0 +1,5 @@
1
+ resource "null_resource" "shared" {
2
+ triggers = {
3
+ name = "shared"
4
+ }
5
+ }
@@ -0,0 +1,56 @@
1
+ # Every reference form the index collects, once each, so a test can assert the
2
+ # whole vocabulary against one parse (chant #2112).
3
+ #
4
+ # var.<name> region, subnets
5
+ # local.<name> bucket_name, tags
6
+ # data.<type>.<name> data.aws_ami.ubuntu
7
+ # module.<name> module.cdn
8
+ # provider = <type>.<alias> provider.aws.replica, through both the
9
+ # `provider` meta-argument and a module's
10
+ # `providers` map
11
+
12
+ variable "region" {
13
+ type = string
14
+ }
15
+
16
+ variable "subnets" {
17
+ type = list(string)
18
+ }
19
+
20
+ locals {
21
+ bucket_name = "assets-${var.region}"
22
+ tags = { Team = "platform" }
23
+ }
24
+
25
+ data "aws_ami" "ubuntu" {
26
+ most_recent = true
27
+ }
28
+
29
+ provider "aws" {
30
+ alias = "replica"
31
+ region = "eu-west-1"
32
+ }
33
+
34
+ resource "aws_instance" "web" {
35
+ count = length(var.subnets)
36
+ ami = data.aws_ami.ubuntu.id
37
+ subnet_id = var.subnets[count.index]
38
+ tags = local.tags
39
+ }
40
+
41
+ resource "aws_s3_bucket" "replica" {
42
+ provider = aws.replica
43
+ bucket = "${local.bucket_name}-replica"
44
+ }
45
+
46
+ module "cdn" {
47
+ source = "./modules/cdn"
48
+
49
+ providers = {
50
+ aws = aws.replica
51
+ }
52
+ }
53
+
54
+ output "cdn_url" {
55
+ value = module.cdn.url
56
+ }
@@ -0,0 +1,34 @@
1
+ # The `with-backend` root next door keeps its state local on purpose, so
2
+ # TF001 reports it (#2218). This one is the same configuration with a remote
3
+ # backend, and is what a root TF001 says nothing about looks like. Nothing
4
+ # ever runs `terraform init` here, so the bucket need not exist.
5
+ terraform {
6
+ required_version = ">= 1.5.0"
7
+
8
+ backend "s3" {
9
+ bucket = "tfstate"
10
+ key = "app/terraform.tfstate"
11
+ region = "us-east-1"
12
+ }
13
+
14
+ required_providers {
15
+ null = {
16
+ source = "hashicorp/null"
17
+ version = "~> 3.2"
18
+ }
19
+ }
20
+ }
21
+
22
+ provider "null" {}
23
+
24
+ resource "null_resource" "first" {
25
+ triggers = {
26
+ name = "first"
27
+ }
28
+ }
29
+
30
+ resource "null_resource" "second" {
31
+ triggers = {
32
+ name = "second"
33
+ }
34
+ }
@@ -1,3 +1,10 @@
1
+ # The `terraform-apply-op` acceptance test copies this root into a temp
2
+ # directory and runs a real `terraform init`, `plan` and `apply` against it,
3
+ # so the backend has to be one that works with no credentials and no network
4
+ # beyond the provider registry. That is `local`, which is why TF001 reports
5
+ # this root (#2218): the finding is correct, and the local state is the point
6
+ # of the fixture. `remote-backend/` next door is the same configuration with
7
+ # a backend TF001 says nothing about.
1
8
  terraform {
2
9
  required_version = ">= 1.5.0"
3
10
 
@@ -0,0 +1,45 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, test } from "vitest";
5
+ import { scanRules } from "@intentius/chant/codegen/docs-rule-scanning";
6
+ import { RULE_CATALOG } from "@intentius/chant/audit/catalog";
7
+
8
+ /**
9
+ * `docs/pages/lint-rules.mdx` is authored by hand, one `##` section per rule
10
+ * (#2108). Nothing regenerates those sections, so nothing stops the page
11
+ * lagging the catalog the moment a new rule ships without one. This test
12
+ * reads the same rule scan the generated `rules` table (`All Rules`) is built
13
+ * from and fails the moment a scanned id has no matching heading on the
14
+ * authored page, so the page cannot silently fall behind.
15
+ */
16
+ const pkgDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
17
+
18
+ /**
19
+ * The TF ids core owns rather than this lexicon (#2220). `scanRules` walks
20
+ * this package's `src/`, so it cannot see them: TF023 (Terraform state
21
+ * committed to the repository) lives in `packages/core/src/audit/catalog.ts`
22
+ * because it reads the discovered file list rather than a parsed root module,
23
+ * and its page section here was the one TF section nothing guarded. Read off
24
+ * `RULE_CATALOG` rather than hardcoded, so a second core-owned TF id is
25
+ * covered on the day it lands.
26
+ */
27
+ function coreOwnedTfIds(): string[] {
28
+ return Object.keys(RULE_CATALOG).filter((id) => /^TF\d+$/.test(id));
29
+ }
30
+
31
+ describe("lint-rules.mdx stays in sync with the rule catalog", () => {
32
+ test("every rule id scanned from source has a ## section on the authored page", () => {
33
+ const core = coreOwnedTfIds();
34
+ expect(core, "core owns at least TF023").toContain("TF023");
35
+
36
+ const ids = [...new Set([...scanRules(join(pkgDir, "src")).map((r) => r.id), ...core])].sort();
37
+ expect(ids.length).toBeGreaterThan(0);
38
+
39
+ const page = readFileSync(join(pkgDir, "docs", "pages", "lint-rules.mdx"), "utf-8");
40
+ const headingIds = new Set([...page.matchAll(/^## (TF\d+)\b/gm)].map((m) => m[1]));
41
+
42
+ const missing = ids.filter((id) => !headingIds.has(id));
43
+ expect(missing, `lint-rules.mdx has no section for: ${missing.join(", ")}`).toEqual([]);
44
+ });
45
+ });
@@ -0,0 +1,193 @@
1
+ /**
2
+ * The acceptance test is choudoufu, not a fixture (#2105): adopt a live
3
+ * resource nothing has marked, end to end.
4
+ *
5
+ * `terraform-adopt-op.test.ts` next door proves the shape chant emits.
6
+ * `choudoufu.test.ts` proves the activities' contract against a stubbed child
7
+ * process. This proves the shape does the thing: create an unmarked VPC
8
+ * directly against choudoufu's pinned emulator, so it is a live resource this
9
+ * estate does not own at an identity this root's configuration declares; run
10
+ * the Ledger step and expect exactly one adoptable match with the two marker
11
+ * values on it; run the Adopt step and let it write them; re-plan and expect
12
+ * the estate to own the same VPC, with nothing left adoptable.
13
+ *
14
+ * The Op's phases are not run through `runOpLocally` here, deliberately.
15
+ * `TerraformAdoptOp` always emits a gate — adoption moves the estate's
16
+ * boundary, and there is no ungated form — so a local run would stop at the
17
+ * gate and record a pending fact rather than reach the Adopt phase
18
+ * (`packages/core/src/op/gate.ts`). So this drives the two activities the Op's
19
+ * Ledger and Adopt phases are built from, in the same order and with the same
20
+ * hand-off, which is the part a real binary can falsify. `choudoufu.acceptance.test.ts` takes the same approach for the same
21
+ * reason.
22
+ *
23
+ * ## Why this still skips on choudoufu v0.14.0
24
+ *
25
+ * Not #894 any more. v0.14.0 (choudoufu PR 915) made `live-plan -json`
26
+ * reachable on a configuration that names its own estate, and this suite was
27
+ * run against it on 2026-09-07 for the first time. It got as far as the
28
+ * document and stopped there: `ledger.adoptions` came back empty, because the
29
+ * #788 document carries no adoptable-by-content section at all.
30
+ *
31
+ * The document's `unowned[]` is the resources found at an identity the
32
+ * configuration itself declares. A `aws_cloudwatch_log_group` has one (its
33
+ * name is in the block), so an unmarked live one comes back in `unowned[]`
34
+ * with `adopt_tofu_estate`/`adopt_tofu_address` on it, which is exactly what
35
+ * `../__fixtures__/live-plan.json` recorded and what `readAdoptionLedger`
36
+ * reads. An `aws_vpc` has none: EC2 assigns the id, so the document reports
37
+ * `omissions[].reason = "NEEDS_DISCOVERY"` and leaves `unowned[]` empty. The
38
+ * VPC is matched instead by choudoufu's content matcher during the
39
+ * estate-wide unclaimed sweep, and that match is printed only in the human
40
+ * render's "Adoptable" section. `views.LivePlanDocument` has no field for it,
41
+ * `-adoption-only` is refused alongside `-json` ("-adoption-only and -json
42
+ * cannot be combined"), and `TOFU_LIVE_COLLECT_UNCLAIMED=1` on the `-json`
43
+ * run makes no difference: the text render then prints "Adoptable: 1 live
44
+ * resource matches a declared resource" and the document beside it still says
45
+ * `"unowned": []`.
46
+ *
47
+ * So `choudoufuLivePlan` cannot produce a ledger for a provider-assigned
48
+ * identity on any binary that exists today, and this suite is what would
49
+ * prove it can. Filed upstream as
50
+ * [choudoufu #962](https://github.com/INTENTIUS/choudoufu/issues/962); the
51
+ * measurements are there and on chant #2168.
52
+ *
53
+ * The gate below names that, alongside the three ordinary dependencies: a
54
+ * `choudoufu`, an `aws` CLI, and the emulator's endpoint.
55
+ *
56
+ * Gating copied from `./terraform-apply-op.acceptance.test.ts` (`onPath`),
57
+ * which in turn copies `lexicons/k3s/src/serializer.acceptance.test.ts`'s
58
+ * pattern of skipping with the reason named in the describe title rather than
59
+ * failing when the real dependency is absent.
60
+ *
61
+ * `../__fixtures__/ACCEPTANCE.md` records what this block has last passed
62
+ * against, which is still nothing, and why the reason changed.
63
+ */
64
+
65
+ import { execSync } from "node:child_process";
66
+ import { cpSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
67
+ import { tmpdir } from "node:os";
68
+ import { join } from "node:path";
69
+ import { afterAll, describe, expect, it } from "vitest";
70
+ import { choudoufuAdopt, choudoufuLivePlan, terraformInit } from "../op/activities/terraform";
71
+
72
+ function onPath(cmd: string): boolean {
73
+ try {
74
+ execSync(`command -v ${cmd}`, { stdio: "ignore" });
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * `true` while `live-plan -json`'s document carries no adoptable-by-content
83
+ * section, so a provider-assigned identity like an `aws_vpc` never reaches
84
+ * `readAdoptionLedger`. Filed upstream as
85
+ * [choudoufu #962](https://github.com/INTENTIUS/choudoufu/issues/962), with
86
+ * the measurements against the v0.14.0 release binary on 2026-09-07; flip to
87
+ * `false` when a choudoufu release puts the content matcher's "Adoptable"
88
+ * rows in the document, either in `unowned[]` or in a sibling array.
89
+ * choudoufu #894, which gated this block before, is fixed and gone.
90
+ */
91
+ const CHOUDOUFU_ADOPTABLE_NOT_IN_DOCUMENT = true;
92
+
93
+ const emulatorEndpoint = process.env.CHOUDOUFU_EMULATOR_ENDPOINT;
94
+
95
+ const skipReason = !onPath("choudoufu")
96
+ ? "no choudoufu binary on PATH"
97
+ : !onPath("aws")
98
+ ? "no aws CLI on PATH (the unmarked resource is created with it, and adopted through it)"
99
+ : !emulatorEndpoint
100
+ ? "CHOUDOUFU_EMULATOR_ENDPOINT is not set (bring up choudoufu's `just smoke` emulator stack and export it)"
101
+ : CHOUDOUFU_ADOPTABLE_NOT_IN_DOCUMENT
102
+ ? "choudoufu#962: live-plan -json's document carries no adoptable-by-content section, so the " +
103
+ "fixture's unmarked aws_vpc reaches omissions[NEEDS_DISCOVERY] and never unowned[]; " +
104
+ "measured on choudoufu v0.14.0"
105
+ : "";
106
+
107
+ const FIXTURE = join(import.meta.dirname, "..", "__fixtures__", "live-adopt");
108
+ const ESTATE = "chant-adopt-fixture";
109
+ const CIDR = "10.77.0.0/16";
110
+ const workspaces: string[] = [];
111
+
112
+ afterAll(() => {
113
+ for (const dir of workspaces) rmSync(dir, { recursive: true, force: true });
114
+ });
115
+
116
+ /** A fresh copy of the live-adopt fixture, wired up with a real project config. */
117
+ function project(): string {
118
+ const dir = mkdtempSync(join(tmpdir(), "chant-adopt-accept-"));
119
+ workspaces.push(dir);
120
+ cpSync(FIXTURE, join(dir, "root"), { recursive: true });
121
+ writeFileSync(
122
+ join(dir, "chant.config.json"),
123
+ JSON.stringify(
124
+ { lexicons: ["terraform"], terraform: { binary: "choudoufu", roots: { estate: { dir: "./root" } } } },
125
+ null,
126
+ 2,
127
+ ),
128
+ );
129
+ return dir;
130
+ }
131
+
132
+ describe.skipIf(skipReason !== "")(
133
+ `TerraformAdoptOp adopts an unmarked live resource${skipReason ? ` (skipped: ${skipReason})` : ""}`,
134
+ () => {
135
+ it(
136
+ "ledgers one adoptable VPC, writes its two markers, and re-plans with it owned",
137
+ { timeout: 600_000 },
138
+ async () => {
139
+ const dir = project();
140
+
141
+ process.env.AWS_ENDPOINT_URL ??= emulatorEndpoint;
142
+ process.env.AWS_ACCESS_KEY_ID ??= "choudoufu-emulator";
143
+ process.env.AWS_SECRET_ACCESS_KEY ??= "choudoufu-emulator";
144
+ process.env.AWS_DEFAULT_REGION ??= "us-east-1";
145
+
146
+ // The stock-created resource: a VPC at the cidr this root declares,
147
+ // carrying no ownership marker at all. Nothing about it says which
148
+ // configuration made it, which is exactly the migration situation.
149
+ execSync(
150
+ `aws ec2 create-vpc --cidr-block ${CIDR} --endpoint-url ${emulatorEndpoint} --region us-east-1`,
151
+ { stdio: "pipe" },
152
+ );
153
+
154
+ await terraformInit({ root: "estate", cwd: dir });
155
+
156
+ const ledger = await choudoufuLivePlan({ root: "estate", cwd: dir, adoptionOnly: true });
157
+ expect(ledger.estate).toBe(ESTATE);
158
+ expect(ledger.contested).toEqual([]);
159
+ expect(ledger.adoptions).toHaveLength(1);
160
+ const [candidate] = ledger.adoptions;
161
+ expect(candidate.addr).toBe("aws_vpc.adoptable");
162
+ expect(candidate.markerEstate).toBe(ESTATE);
163
+ expect(candidate.markerAddress).toBe("aws_vpc.adoptable");
164
+ // A VPC is tagged through `ec2 create-tags`, which choudoufu prints a
165
+ // command for; a candidate with none would be refused below instead.
166
+ expect(candidate.command).toContain("create-tags");
167
+ expect(ledger.ledger).toContain("aws_vpc.adoptable <- aws_vpc vpc-");
168
+
169
+ const adopted = await choudoufuAdopt({
170
+ root: "estate",
171
+ cwd: dir,
172
+ adoptions: ledger.adoptions,
173
+ contested: ledger.contested,
174
+ });
175
+ expect(adopted.mechanism).toBe("tag-write");
176
+ expect(adopted.adopted).toEqual(["aws_vpc.adoptable"]);
177
+ expect(adopted.refused).toEqual([]);
178
+
179
+ // The markers are the whole ownership answer, so a re-plan binds the
180
+ // same live VPC instead of proposing a second one: nothing is left
181
+ // unowned at this estate's declared identity, and nothing adoptable.
182
+ const after = await choudoufuLivePlan({ root: "estate", cwd: dir });
183
+ expect(after.adoptable).toBe(0);
184
+ expect(after.unowned).toBe(0);
185
+ expect(after.adoptions).toEqual([]);
186
+
187
+ // And the estate now says so: the VPC is bound at the declared address.
188
+ const bound = (after.json as { bound?: Array<{ addr?: string; identity?: string }> }).bound ?? [];
189
+ expect(bound.map((b) => b.addr)).toContain("aws_vpc.adoptable");
190
+ },
191
+ );
192
+ },
193
+ );
@@ -0,0 +1,175 @@
1
+ /**
2
+ * TerraformAdoptOp composite tests (#2105): the four phases, the gate that is
3
+ * always there, the two references the Adopt step takes off the ledger, and
4
+ * the rule an adoption Op exists to keep — an ambiguous match is reported and
5
+ * never written.
6
+ */
7
+
8
+ import { describe, test, expect } from "vitest";
9
+ import {
10
+ collectStepOutputRefs,
11
+ isStepOutputRef,
12
+ type ActivityStep,
13
+ type GateStep,
14
+ type OpConfig,
15
+ type StepDefinition,
16
+ } from "@intentius/chant/op";
17
+
18
+ import { TerraformAdoptOp } from "./terraform-adopt-op";
19
+
20
+ function props(config: Parameters<typeof TerraformAdoptOp>[0]): OpConfig {
21
+ return (TerraformAdoptOp(config).op as unknown as { props: OpConfig }).props;
22
+ }
23
+
24
+ const phaseNames = (op: OpConfig): string[] => op.phases.map((p) => p.name);
25
+ const isActivity = (s: StepDefinition): s is ActivityStep => s.kind === "activity";
26
+ const steps = (op: OpConfig): ActivityStep[] => op.phases.flatMap((p) => p.steps.filter(isActivity));
27
+ const base = { name: "estate-adopt", root: "estate" };
28
+
29
+ describe("TerraformAdoptOp phases (#2105)", () => {
30
+ test("Check, Ledger, Gate, Adopt, in that order", () => {
31
+ const op = props(base);
32
+ expect(phaseNames(op)).toEqual(["Check", "Ledger", "Gate", "Adopt"]);
33
+ expect(steps(op).map((s) => s.fn)).toEqual(["choudoufuLiveCheck", "choudoufuLivePlan", "choudoufuAdopt"]);
34
+ });
35
+
36
+ test("Check runs live-check first, and reports whether the root was refused", () => {
37
+ const check = props(base).phases[0].steps[0] as ActivityStep;
38
+ expect(check.fn).toBe("choudoufuLiveCheck");
39
+ expect(check.outcomeAttribute).toEqual({ name: "Refused", from: "refused" });
40
+ // No cloud calls, so this is the cheapest place to learn the root cannot
41
+ // move under markers at all.
42
+ expect(check.profile).toBe("fastIdempotent");
43
+ });
44
+
45
+ test("Ledger is the -adoption-only live plan, and publishes both counts", () => {
46
+ const ledger = props(base).phases[1].steps[0] as ActivityStep;
47
+ expect(ledger.fn).toBe("choudoufuLivePlan");
48
+ expect(ledger.id).toBe("ledger");
49
+ expect(ledger.args?.adoptionOnly).toBe(true);
50
+ expect(ledger.outcomeAttribute).toEqual([
51
+ { name: "Adoptable", from: "adoptable" },
52
+ { name: "Ambiguous", from: "ambiguous" },
53
+ ]);
54
+ });
55
+
56
+ test("every step names the configured root", () => {
57
+ for (const step of steps(props(base))) expect(step.args?.root).toBe("estate");
58
+ });
59
+
60
+ test("cwd rides every step, and no cwd means no cwd arg", () => {
61
+ for (const step of steps(props({ ...base, cwd: "/repo" }))) expect(step.args?.cwd).toBe("/repo");
62
+ for (const step of steps(props(base))) expect(step.args && "cwd" in step.args).toBe(false);
63
+ });
64
+
65
+ test("an explicit estate rides the Ledger step only", () => {
66
+ const op = props({ ...base, estate: "prod-networking" });
67
+ expect((op.phases[1].steps[0] as ActivityStep).args?.estate).toBe("prod-networking");
68
+ expect((op.phases[0].steps[0] as ActivityStep).args?.estate).toBeUndefined();
69
+ });
70
+ });
71
+
72
+ describe("TerraformAdoptOp gates on the ledger (#2105)", () => {
73
+ const gateOf = (op: OpConfig): GateStep => {
74
+ const step = op.phases[2].steps[0];
75
+ expect(step.kind).toBe("gate");
76
+ return step as GateStep;
77
+ };
78
+
79
+ test("the gate is always emitted: there is no ungated adoption", () => {
80
+ // Unlike TerraformApplyOp there is no `gate: "never"` here. Adoption moves
81
+ // the estate's boundary onto resources it did not own, which is not a
82
+ // thing to do unattended.
83
+ expect(gateOf(props(base)).gate).toBe("approve-estate-adopt");
84
+ expect(Object.keys(TerraformAdoptOp(base))).toEqual(["op"]);
85
+ });
86
+
87
+ test("the description names the ledger as the approval subject", () => {
88
+ const description = gateOf(props(base)).description!;
89
+ expect(description).toContain("adoption ledger");
90
+ expect(description).toContain("tofu-estate and tofu-address");
91
+ expect(description).toContain("an ambiguous address is never adopted");
92
+ });
93
+
94
+ // #2202: `gateName` is the option; `signalName` is read through 0.59.0.
95
+ test("the deprecated `signalName` option still names the gate", () => {
96
+ expect(gateOf(props({ ...base, signalName: "ok" })).gate).toBe("ok");
97
+ });
98
+
99
+ test("gate name, timeout and description are overridable", () => {
100
+ const gate = gateOf(props({ ...base, gateName: "ok", gateTimeout: "2h", gateDescription: "mine" }));
101
+ expect(gate).toMatchObject({ gate: "ok", timeout: "2h", description: "mine" });
102
+ });
103
+
104
+ test("no timeout given means the gate carries none, so core's own default applies", () => {
105
+ expect(gateOf(props(base)).timeout).toBeUndefined();
106
+ });
107
+ });
108
+
109
+ describe("TerraformAdoptOp adopts the ledger's matches and nothing else (#2105)", () => {
110
+ const adoptStep = (op: OpConfig): ActivityStep => op.phases[3].steps[0] as ActivityStep;
111
+
112
+ test("the Adopt step takes the ledger's adoptable set by reference", () => {
113
+ const adopt = adoptStep(props(base));
114
+ expect(adopt.fn).toBe("choudoufuAdopt");
115
+ expect(isStepOutputRef(adopt.args?.adoptions)).toBe(true);
116
+ expect(adopt.args?.adoptions).toMatchObject({ step: "ledger", path: "adoptions" });
117
+ });
118
+
119
+ test("an ambiguous match is handed over as `contested`, which is a separate field from `adoptions`", () => {
120
+ // `readAdoptionLedger` never puts a contested candidate in `adoptions`, so
121
+ // the Adopt step cannot write one however it behaves; `contested` is
122
+ // passed so the Op's own result names what it declined and why.
123
+ const adopt = adoptStep(props(base));
124
+ expect(adopt.args?.contested).toMatchObject({ step: "ledger", path: "contested" });
125
+ expect(adopt.args?.contested).not.toEqual(adopt.args?.adoptions);
126
+ });
127
+
128
+ test("the Adopt step reports how many resources it claimed", () => {
129
+ expect(adoptStep(props(base)).outcomeAttribute).toEqual({ name: "Adopted", from: "adoptedCount" });
130
+ });
131
+
132
+ test("the Op references exactly the two ledger fields and no document", () => {
133
+ const op = props(base);
134
+ const refs = op.phases.flatMap((p) => p.steps.filter(isActivity)).flatMap((s) => collectStepOutputRefs(s.args));
135
+ expect(refs.map((r) => r.path).sort()).toEqual(["adoptions", "contested"]);
136
+ expect(refs.every((r) => r.step === "ledger")).toBe(true);
137
+ // The bound/omissions/unowned document never leaves the Ledger step.
138
+ expect(JSON.stringify(op)).not.toContain('"json"');
139
+ });
140
+ });
141
+
142
+ describe("TerraformAdoptOp compensation (#2105)", () => {
143
+ test("compensate: true throws, because un-adopting is untag", () => {
144
+ expect(() => TerraformAdoptOp({ ...base, compensate: true })).toThrow(/un-adopting is untag/);
145
+ });
146
+
147
+ test("an object with no command throws the same way", () => {
148
+ expect(() => TerraformAdoptOp({ ...base, compensate: {} })).toThrow(/TerraformAdoptOp "estate-adopt"/);
149
+ });
150
+
151
+ test("a supplied command becomes an onFailure Rollback phase", () => {
152
+ const op = props({ ...base, compensate: { command: "./untag.sh" } });
153
+ expect(op.onFailure?.map((p) => p.name)).toEqual(["Rollback"]);
154
+ expect((op.onFailure![0].steps[0] as ActivityStep).args?.cmd).toBe("./untag.sh");
155
+ });
156
+
157
+ test("compensate: false and omitting it both leave no onFailure", () => {
158
+ expect(props(base).onFailure).toBeUndefined();
159
+ expect(props({ ...base, compensate: false }).onFailure).toBeUndefined();
160
+ });
161
+ });
162
+
163
+ describe("TerraformAdoptOp labels (#2105, #2118)", () => {
164
+ test("marks the Op as an adoption on a live root", () => {
165
+ expect(props(base).labels).toEqual({
166
+ Adopt: "true",
167
+ TerraformRoot: "estate",
168
+ TerraformMode: "live",
169
+ });
170
+ });
171
+
172
+ test("no task queue rides on the Op (#2118)", () => {
173
+ expect(props(base)).not.toHaveProperty("taskQueue");
174
+ });
175
+ });