@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
@@ -1,14 +1,19 @@
1
1
  /**
2
2
  * TerraformApplyOp composite tests (#2086) — the phase names, the gate, the
3
- * plan-to-apply reference, and the compensation refusal.
3
+ * plan-to-apply reference, and the compensation refusal. A live root builds
4
+ * the same four phases (#2106 follow-up), and what is particular to it is
5
+ * below, in its own describe block.
4
6
  */
5
7
 
6
- import { describe, test, expect } from "vitest";
8
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { afterEach, describe, test, expect } from "vitest";
7
12
  import {
8
- findGate,
13
+ isGated,
9
14
  isStepOutputRef,
10
15
  runOpLocally,
11
- LocalGateUnsupportedError,
16
+ memoryGateLedgerPort,
12
17
  type ActivityStep,
13
18
  type GateStep,
14
19
  type OpConfig,
@@ -73,20 +78,28 @@ describe("TerraformApplyOp gate (#2086)", () => {
73
78
  test("signal name defaults to approve-<name>", () => {
74
79
  const op = props({ name: "prod-apply", root: "app" });
75
80
  const gate = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
76
- expect(gate.signalName).toBe("approve-prod-apply");
81
+ expect(gate.gate).toBe("approve-prod-apply");
77
82
  });
78
83
 
79
- test("an explicit signalName, timeout and description win", () => {
84
+ // #2202: `gate` on this config already means the gate MODE, so the gate's
85
+ // name is `gateName` here. `signalName` is read through 0.59.0.
86
+ test("the deprecated `signalName` option still names the gate", () => {
87
+ const op = props({ name: "prod-apply", root: "app", signalName: "approve-terraform" });
88
+ const gate = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
89
+ expect(gate.gate).toBe("approve-terraform");
90
+ });
91
+
92
+ test("an explicit gateName, timeout and description win", () => {
80
93
  const op = props({
81
94
  name: "prod-apply",
82
95
  root: "app",
83
- signalName: "approve-terraform",
96
+ gateName: "approve-terraform",
84
97
  gateTimeout: "72h",
85
98
  gateDescription: "Change window only",
86
99
  });
87
100
  const gate = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
88
101
  expect(gate).toMatchObject({
89
- signalName: "approve-terraform",
102
+ gate: "approve-terraform",
90
103
  timeout: "72h",
91
104
  description: "Change window only",
92
105
  });
@@ -95,7 +108,7 @@ describe("TerraformApplyOp gate (#2086)", () => {
95
108
  test("the Gate phase reports the plan's destroy count before it waits", () => {
96
109
  // GateStep has no condition field, so "on-destroy" cannot branch at build
97
110
  // time. The approver is told instead: a `show` of the saved plan runs
98
- // first and surfaces `destroys` as a search attribute.
111
+ // first and surfaces `destroys` as a run outcome.
99
112
  const op = props({ name: "prod-apply", root: "app" });
100
113
  const steps = op.phases.find((p) => p.name === "Gate")!.steps;
101
114
  const show = steps[0] as ActivityStep;
@@ -168,26 +181,173 @@ describe("TerraformApplyOp compensation — total or refused (#2086)", () => {
168
181
  });
169
182
  });
170
183
 
171
- describe("TerraformApplyOp on the local executor (#2086)", () => {
172
- test("a gated Op is refused up front, naming the signal the local executor cannot wait for", async () => {
184
+ describe("TerraformApplyOp on the local executor (#2086, gate-as-fact #2119)", () => {
185
+ test("a gated Op stops at the gate and records the pending fact, naming the signal", async () => {
173
186
  const op = props({ name: "prod-apply", root: "app" });
174
- await expect(runOpLocally(op, new Map(), {})).rejects.toBeInstanceOf(LocalGateUnsupportedError);
187
+ const ran: string[] = [];
188
+ const stub = (fn: string) => [fn, async () => { ran.push(fn); return {}; }] as const;
189
+ const activities = new Map(
190
+ ["terraformInit", "terraformPlan", "terraformShow", "terraformApply"].map(stub),
191
+ );
192
+ const gates = memoryGateLedgerPort();
193
+ const result = await runOpLocally(op, activities, {}, undefined, { gates, now: "2026-09-05T12:00:00.000Z" });
194
+
195
+ expect(result.status).toBe("gated");
196
+ expect(result.gate?.op).toBe("prod-apply");
197
+ expect(result.gate?.gate).toBe(op.phases.flatMap((p) => p.steps).find(isGate)?.gate);
198
+ expect(gates.appended).toHaveLength(1);
199
+ // Init, Plan and the pre-gate `show` ran; Apply is behind the gate and did not.
200
+ expect(ran).toEqual(["terraformInit", "terraformPlan", "terraformShow"]);
175
201
  });
176
202
 
177
- test('gate: "never" clears that refusal, which is what makes `chant run` viable', () => {
178
- expect(findGate(props({ name: "prod-apply", root: "app", gate: "never" }))).toBeUndefined();
203
+ test('gate: "never" means there is no gate to stop at', () => {
204
+ expect(isGated(props({ name: "prod-apply", root: "app", gate: "never" }))).toBe(false);
205
+ expect(isGated(props({ name: "prod-apply", root: "app" }))).toBe(true);
179
206
  });
180
207
  });
181
208
 
182
209
  describe("TerraformApplyOp Op metadata (#2086)", () => {
183
- test("task queue defaults to the name and search attributes name the root", () => {
210
+ test("labels name the root, and no task queue rides on the Op (#2118)", () => {
184
211
  const op = props({ name: "app-apply", root: "app" });
185
212
  expect(op.name).toBe("app-apply");
186
- expect(op.taskQueue).toBe("app-apply");
187
- expect(op.searchAttributes).toEqual({ Apply: "true", TerraformRoot: "app" });
213
+ expect(op.labels).toEqual({ Apply: "true", TerraformRoot: "app" });
214
+ expect(op).not.toHaveProperty("taskQueue");
188
215
  });
216
+ });
217
+
218
+ // ── Live root (#2106) ────────────────────────────────────────────────────────
219
+
220
+ const dirs: string[] = [];
221
+
222
+ afterEach(() => {
223
+ for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
224
+ });
225
+
226
+ /** A throwaway project whose one root is live, stock, or live with a policy block, per `policy`. */
227
+ function project(policy?: string): string {
228
+ const dir = mkdtempSync(join(tmpdir(), "chant-tf-apply-op-live-"));
229
+ dirs.push(dir);
230
+ mkdirSync(join(dir, "root"), { recursive: true });
231
+ writeFileSync(
232
+ join(dir, "root", "main.tf"),
233
+ [
234
+ "terraform {",
235
+ " live {",
236
+ ' estate = "fixture-estate"',
237
+ ...(policy ? [" policy {", ` ${policy}`, " }"] : []),
238
+ " }",
239
+ "}",
240
+ "",
241
+ 'resource "null_resource" "x" {}',
242
+ "",
243
+ ].join("\n"),
244
+ );
245
+ writeFileSync(
246
+ join(dir, "chant.config.json"),
247
+ JSON.stringify({ terraform: { binary: "choudoufu", roots: { estate: { dir: "./root" } } } }),
248
+ );
249
+ return dir;
250
+ }
251
+
252
+ describe("TerraformApplyOp on a live root (#2106 follow-up)", () => {
253
+ test("the same Init, Plan, Gate, Apply as a stock root, with the plan file paired through", () => {
254
+ const dir = project();
255
+ const op = props({ name: "estate-apply", root: "estate", cwd: dir });
256
+ expect(phaseNames(op)).toEqual(["Init", "Plan", "Gate", "Apply"]);
189
257
 
190
- test("an explicit taskQueue wins", () => {
191
- expect(props({ name: "app-apply", root: "app", taskQueue: "infra" }).taskQueue).toBe("infra");
258
+ const activitySteps = op.phases.flatMap((p) => p.steps.filter(isActivity));
259
+ expect(activitySteps.map((s) => s.fn)).toEqual([
260
+ "terraformInit",
261
+ "terraformPlan",
262
+ "terraformShow",
263
+ "terraformApply",
264
+ ]);
265
+
266
+ const plan = op.phases.find((p) => p.name === "Plan")!.steps[0] as ActivityStep;
267
+ expect(plan.args?.planFile).toBe("chant.tfplan");
268
+
269
+ // The Apply step names the Plan step's own output, never a literal path:
270
+ // on a live root that reference is the approval artifact itself.
271
+ const apply = op.phases.find((p) => p.name === "Apply")!.steps[0] as ActivityStep;
272
+ expect(isStepOutputRef(apply.args?.planFile)).toBe(true);
273
+ expect(apply.args?.planFile).toMatchObject({ step: "plan", path: "planFile" });
274
+ });
275
+
276
+ test("no choudoufuLivePlan step anywhere: the watch and adopt Ops still use it, this one does not", () => {
277
+ const dir = project();
278
+ const op = props({ name: "estate-apply", root: "estate", cwd: dir });
279
+ const fns = op.phases.flatMap((p) => p.steps.filter(isActivity)).map((s) => s.fn);
280
+ expect(fns).not.toContain("choudoufuLivePlan");
281
+ });
282
+
283
+ test('gate: "never" drops the Gate phase, leaving Init, Plan, Apply', () => {
284
+ const dir = project();
285
+ const op = props({ name: "estate-apply", root: "estate", cwd: dir, gate: "never" });
286
+ expect(phaseNames(op)).toEqual(["Init", "Plan", "Apply"]);
287
+ expect(op.phases.flatMap((p) => p.steps).some(isGate)).toBe(false);
288
+ });
289
+
290
+ test("Plan reports Changed; the Gate's show step reports Destroys off the saved plan", () => {
291
+ const dir = project();
292
+ const op = props({ name: "estate-apply", root: "estate", cwd: dir });
293
+ const plan = op.phases.find((p) => p.name === "Plan")!.steps[0] as ActivityStep;
294
+ expect(plan.outcomeAttribute).toEqual({ name: "Changed", from: "changed" });
295
+
296
+ const gateSteps = op.phases.find((p) => p.name === "Gate")!.steps;
297
+ const show = gateSteps[0] as ActivityStep;
298
+ expect(show.fn).toBe("terraformShow");
299
+ expect(show.outcomeAttribute).toEqual({ name: "Destroys", from: "destroys" });
300
+ expect(gateSteps[1].kind).toBe("gate");
301
+ });
302
+
303
+ test("the gate description tells the approver what the exit-3 refusal does", () => {
304
+ const dir = project();
305
+ const op = props({ name: "estate-apply", root: "estate", cwd: dir });
306
+ const gateStep = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
307
+ expect(gateStep.description).toContain("live root");
308
+ expect(gateStep.description).toContain("exit status 3");
309
+ expect(gateStep.description).toContain("choudoufu #878");
310
+ });
311
+
312
+ test("a stock root's gate description says nothing about a live re-plan", () => {
313
+ const dir = mkdtempSync(join(tmpdir(), "chant-tf-apply-op-live-"));
314
+ dirs.push(dir);
315
+ mkdirSync(join(dir, "root"), { recursive: true });
316
+ writeFileSync(join(dir, "root", "main.tf"), 'resource "null_resource" "x" {}\n');
317
+ writeFileSync(
318
+ join(dir, "chant.config.json"),
319
+ JSON.stringify({ terraform: { binary: "terraform", roots: { estate: { dir: "./root" } } } }),
320
+ );
321
+ const op = props({ name: "estate-apply", root: "estate", cwd: dir });
322
+ const gateStep = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
323
+ expect(gateStep.description).not.toContain("exit status 3");
324
+ expect(gateStep.description).not.toContain("live root");
325
+ });
326
+
327
+ test("refuses to build when the root's policy sets undeclared_untagged = \"delete\"", () => {
328
+ const dir = project('undeclared_untagged = "delete"');
329
+ expect(() => TerraformApplyOp({ name: "estate-apply", root: "estate", cwd: dir })).toThrow(
330
+ /TerraformApplyOp "estate-apply".*undeclared_untagged = "delete"/s,
331
+ );
332
+ });
333
+
334
+ test("builds fine when the root's policy sets undeclared_untagged to something else", () => {
335
+ const dir = project('undeclared_untagged = "report"');
336
+ expect(() => TerraformApplyOp({ name: "estate-apply", root: "estate", cwd: dir })).not.toThrow();
337
+ });
338
+
339
+ test("a choudoufu root with no declared estate builds the same phases and skips the policy read", () => {
340
+ const dir = mkdtempSync(join(tmpdir(), "chant-tf-apply-op-live-"));
341
+ dirs.push(dir);
342
+ mkdirSync(join(dir, "root"), { recursive: true });
343
+ writeFileSync(join(dir, "root", "main.tf"), 'resource "null_resource" "x" {}\n');
344
+ writeFileSync(
345
+ join(dir, "chant.config.json"),
346
+ JSON.stringify({ terraform: { binary: "choudoufu", roots: { estate: { dir: "./root" } } } }),
347
+ );
348
+
349
+ const op = props({ name: "estate-apply", root: "estate", cwd: dir, gate: "never" });
350
+ const fns = op.phases.flatMap((p) => p.steps.filter(isActivity)).map((s) => s.fn);
351
+ expect(fns).toEqual(["terraformInit", "terraformPlan", "terraformApply"]);
192
352
  });
193
353
  });
@@ -1,17 +1,66 @@
1
1
  /**
2
- * TerraformApplyOp composite (#2086) — init, plan, approve, apply for one root
2
+ * TerraformApplyOp composite (#2086), init, plan, approve, apply for one root
3
3
  * module named in `terraform.roots`.
4
4
  *
5
5
  * Authority stays with terraform: chant runs the binary, reads its plan, and
6
- * hands the same saved plan to apply. The Apply step never re-plans, because
7
- * it takes the Plan step's own `planFile` as a `StepOutputRef` — whatever was
8
- * approved is what runs.
6
+ * hands the same saved plan to apply. The Apply step never applies something
7
+ * no gate saw, because it takes the Plan step's own `planFile` as a
8
+ * `StepOutputRef`.
9
9
  *
10
- * Phases: Init, Plan, Gate, Apply.
10
+ * Phases: Init, Plan, Gate, Apply. One shape, both kinds of root. A live root
11
+ * (#2103, `terraform.binary: "choudoufu"` plus a declared estate) ran a
12
+ * different shape for one release, because choudoufu refused `-out` and
13
+ * `apply <planfile>`; choudoufu v0.13.0 admits both under a `live` block
14
+ * ([choudoufu #878](https://github.com/INTENTIUS/choudoufu/issues/878),
15
+ * PR 889), so the plan-file discipline is back and the two shapes are one
16
+ * again.
11
17
  *
12
- * The gate is durable-runtime work, so an Op that emits one needs Temporal;
13
- * `packages/core/src/op/local-executor.ts` refuses any Op containing a gate.
14
- * `gate: "never"` is what makes the Op runnable with `chant run` locally.
18
+ * **What the approval covers on a live root.** `plan -out=<planFile>` writes
19
+ * stock's own plan file, and that file is what the approver reads and what
20
+ * the Apply step names. `apply <planFile>` does not replay it: prior state on
21
+ * a live root is a projection rebuilt from the live system every run, so the
22
+ * apply re-plans against the live system the way every live-markers run does,
23
+ * then compares its fresh plan with the file's. Same addresses, same actions,
24
+ * same live objects, and the same planned values, or it refuses before
25
+ * anything changes:
26
+ *
27
+ * - `The approved plan no longer matches the live system`, and
28
+ * - `The approved plan belongs to a different estate`,
29
+ *
30
+ * both with exit status **3**, which is neither 1 (any ordinary failure) nor
31
+ * `-detailed-exitcode`'s 2. `terraformApply` maps that status to a named
32
+ * result rather than a thrown error (`refused: "approval-mismatch" |
33
+ * "wrong-estate"`, with choudoufu's message in `refusal`), so a workflow
34
+ * reading the Apply step's output can route the run back to review instead of
35
+ * treating it as a broken step. The seam #2106 left named and unused on the
36
+ * apply args is now the plan file itself.
37
+ *
38
+ * TF101 therefore fires on a live root's `terraformApply` call again: there
39
+ * is a plan file to pair once more, and pairing it is what the refusal above
40
+ * has to compare against.
41
+ *
42
+ * **Delete modes.** chant's `delete: "never" | "owned-only" | "gated"`
43
+ * (`terraform.roots.<name>.delete`, `../config.ts`) maps onto choudoufu's
44
+ * `policy` block. `"owned-only"` is choudoufu's own default verb for the
45
+ * `undeclared_tagged` quadrant (an orphaned marked resource) and needs
46
+ * nothing; `"gated"` needs nothing beyond this composite's own approval gate;
47
+ * `"never"` requires the root's `policy` block to set `undeclared_tagged` to
48
+ * `"keep"`, `"untag"` or `"report"`, and TF026 (`../lint/post-synth/tf026.ts`)
49
+ * enforces that at build time, naming the setting to add. Separately, and
50
+ * regardless of `delete`, this composite refuses outright to build against a
51
+ * root whose `policy` sets `undeclared_untagged = "delete"` (account-scoped
52
+ * reconciliation, which needs a `scope` block): that quadrant deletes
53
+ * resources the estate never claimed, anywhere the scope reaches, and chant
54
+ * never proposes deleting a resource it does not own, not for a root, and not
55
+ * for an Op it generated.
56
+ *
57
+ * A gate is a fact on the gate ledger, not a wait (#2119). Reaching one,
58
+ * `chant run` consults the ledger (`packages/core/src/op/gate.ts`): a
59
+ * resolution newer than the gate's newest pending fact lets the run walk
60
+ * through, and anything else records the pending fact and ends the run with
61
+ * status `gated` and exit 3. `chant approve <op> <gate>` writes the
62
+ * resolution, and the next run reads it. `gate: "never"` drops the Gate phase
63
+ * entirely, so a run never stops.
15
64
  *
16
65
  * @example
17
66
  * ```typescript
@@ -23,6 +72,8 @@
23
72
 
24
73
  import { Op, phase, gate, activity, OpResource } from "@intentius/chant/op";
25
74
  import { DEFAULT_PLAN_FILE } from "../op/activities/terraform";
75
+ import { detectLivePolicyVerbs } from "../op/activities/live-detect";
76
+ import { resolveRootModeSync } from "../op/resolve-root-mode";
26
77
  import {
27
78
  terraformInit as initStep,
28
79
  terraformPlan as planStep,
@@ -38,14 +89,14 @@ import {
38
89
  * cannot branch at build time on a count the plan only produces at run time.
39
90
  * The workable v1 is that `"on-destroy"` and `"always"` emit the same Gate
40
91
  * phase, and the approver is told what is at stake instead: the phase reports
41
- * the plan's `destroys` count as the `Destroys` search attribute before it
42
- * waits, and the gate description says so. A gate that skips itself when the
43
- * plan turns out additive is a new step kind, out of scope here.
92
+ * the plan's `destroys` count as the `Destroys` run outcome before the
93
+ * gate is reached, and the gate description says so. A gate that skips itself
94
+ * when the plan turns out additive is a new step kind, out of scope here.
44
95
  */
45
96
  export type TerraformGateMode = "on-destroy" | "always" | "never";
46
97
 
47
98
  export interface TerraformApplyOpConfig {
48
- /** Op name (kebab-case). Also the default task queue and gate signal suffix. */
99
+ /** Op name (kebab-case). `gateName` defaults to `approve-<name>`. */
49
100
  name: string;
50
101
  /** Key into the project's `terraform.roots`. The root carries dir, workspace, var files and backend config. */
51
102
  root: string;
@@ -57,9 +108,15 @@ export interface TerraformApplyOpConfig {
57
108
  * same phase.
58
109
  */
59
110
  gate?: TerraformGateMode;
60
- /** Gate signal name. Default: `approve-<name>`, as `ApplyOp` does. */
111
+ /** The gate's name. Default: `approve-<name>`, as `ApplyOp` does. */
112
+ gateName?: string;
113
+ /**
114
+ * @deprecated The gate's name was `signalName` through 0.58.0 and is
115
+ * `gateName` since #2202 (`gate` is taken here by {@link TerraformGateMode}).
116
+ * Still read; removed in 0.60.0.
117
+ */
61
118
  signalName?: string;
62
- /** Temporal duration the gate waits before timing out. Default: core's own (48h). */
119
+ /** How long a recorded pending gate stays valid, as a duration string. Default: core's own (48h). */
63
120
  gateTimeout?: string;
64
121
  /** Override the gate description shown to the approver. */
65
122
  gateDescription?: string;
@@ -78,15 +135,13 @@ export interface TerraformApplyOpConfig {
78
135
  * Terraform has no automatic rollback: a half-applied plan is undone by
79
136
  * planning and applying the inverse, which is a decision about the estate
80
137
  * rather than something chant can synthesize. So compensation here is
81
- * total or refused, the same stance `ApplyOp` takes (`lexicons/temporal/src/
138
+ * total or refused, the same stance `ApplyOp` takes (`packages/core/src/op/
82
139
  * composites/apply-op.ts`) — `true`, or an object with no `command`, throws
83
140
  * when the Op is built, naming the Op, rather than warning at the moment a
84
141
  * rollback is already needed. Supply `compensate: { command: "..." }` with a
85
142
  * rollback of your own, or leave it unset.
86
143
  */
87
144
  compensate?: boolean | { command?: string };
88
- /** Override the task queue. Defaults to `name`. */
89
- taskQueue?: string;
90
145
  }
91
146
 
92
147
  export interface TerraformApplyOpResources {
@@ -95,7 +150,6 @@ export interface TerraformApplyOpResources {
95
150
  }
96
151
 
97
152
  export function TerraformApplyOp(config: TerraformApplyOpConfig): TerraformApplyOpResources {
98
- const taskQueue = config.taskQueue ?? config.name;
99
153
  const planFile = config.planFile ?? DEFAULT_PLAN_FILE;
100
154
  const gateMode: TerraformGateMode = config.gate ?? "on-destroy";
101
155
 
@@ -116,31 +170,71 @@ export function TerraformApplyOp(config: TerraformApplyOpConfig): TerraformApply
116
170
  // travels with each of them rather than being read off the process once.
117
171
  const where = config.cwd ? { cwd: config.cwd } : {};
118
172
 
119
- // `id` is what makes `plan.out` legal, and `plan.out.planFile` is how the
120
- // Apply step below names this step's saved plan.
121
- const plan = planStep(config.root, { planFile, ...where, id: "plan" });
122
- plan.outcomeAttribute = { name: "Changed", from: "changed" };
173
+ // Best-effort, synchronous (chant.config.json only, see resolveRootModeSync's
174
+ // own doc comment). The mode no longer changes which steps are emitted; it
175
+ // words the gate below, and it decides whether the policy refusal is even
176
+ // asked. "Unknown" reads as stock, which for the gate wording is harmless
177
+ // and for the refusal is the permissive direction, so the refusal is not
178
+ // this function's to guarantee: on a chant.config.ts project, which is
179
+ // every project in this repository, the mode never resolves and the
180
+ // refusal below never runs (#2216).
181
+ const resolved = resolveRootModeSync(config.root, config.cwd);
182
+ const live = resolved?.mode === "live";
183
+
184
+ if (live) {
185
+ // Regardless of `delete`: an account-scoped purge is never something
186
+ // chant proposes on an Op's own initiative. TF026 handles the narrower,
187
+ // config-driven `delete: "never"` requirement; this is unconditional.
188
+ //
189
+ // TF027 (`../lint/post-synth/tf027.ts`) makes the same refusal off the
190
+ // parsed HCL, whatever the config file is written in, and is the check
191
+ // that guarantees it. This throw is kept for the projects whose mode does
192
+ // resolve here, where failing at the moment the Op is built names the Op
193
+ // as well as the root.
194
+ const verbs = detectLivePolicyVerbs(resolved!.dir);
195
+ if (verbs?.undeclaredUntagged === "delete") {
196
+ throw new Error(
197
+ `TerraformApplyOp "${config.name}": root "${config.root}"'s policy block sets ` +
198
+ `undeclared_untagged = "delete" (account-scoped reconciliation, scoped by a \`scope\` block). ` +
199
+ "chant never proposes deleting a resource it does not own, and neither does an Op it " +
200
+ "generates. Remove that setting from the policy block, or narrow the estate's ownership " +
201
+ "answer instead of the account's.",
202
+ );
203
+ }
204
+ }
123
205
 
124
206
  const phases = [
125
207
  phase("Init", [initStep(config.root, { ...where, ...(config.upgrade ? { upgrade: true } : {}) })]),
126
- phase("Plan", [plan]),
127
208
  ];
128
209
 
210
+ // `id` is what makes `plan.out` legal, and `plan.out.planFile` is how the
211
+ // Apply step below names this step's saved plan. On a live root that file
212
+ // is also the approval artifact the apply re-plans against, so the same
213
+ // reference carries the approval as well as the path.
214
+ const plan = planStep(config.root, { planFile, ...where, id: "plan" });
215
+ plan.outcomeAttribute = { name: "Changed", from: "changed" };
216
+ phases.push(phase("Plan", [plan]));
217
+
129
218
  if (gateMode !== "never") {
130
- // Re-render the saved plan just before the wait, so the approver's
219
+ // Re-render the saved plan just before the gate, so the approver's
131
220
  // `Destroys` attribute comes off the plan that will actually apply.
132
- // `show` against a plan file calls no provider.
221
+ // `show` against a plan file calls no provider, on either kind of root.
133
222
  const show = showStep(config.root, { ...where, planFile: plan.out.planFile });
134
223
  show.outcomeAttribute = { name: "Destroys", from: "destroys" };
135
224
  phases.push(
136
225
  phase("Gate", [
137
226
  show,
138
- gate(config.signalName ?? `approve-${config.name}`, {
227
+ gate(config.gateName ?? config.signalName ?? `approve-${config.name}`, {
139
228
  ...(config.gateTimeout ? { timeout: config.gateTimeout } : {}),
140
229
  description:
141
230
  config.gateDescription ??
142
- `Approve terraform apply of root "${config.root}" (gate: ${gateMode}). ` +
143
- `The Destroys search attribute on this phase is the plan's destroy count.`,
231
+ `Approve terraform apply of ${live ? "live " : ""}root "${config.root}" (gate: ${gateMode}). ` +
232
+ `The Destroys run outcome from this phase is the plan's destroy count.` +
233
+ (live
234
+ ? " The apply that follows re-plans against the live system and applies this plan file only" +
235
+ " if its own fresh plan agrees; otherwise it refuses with exit status 3 and the Apply" +
236
+ " step reports a named refusal for review (choudoufu #878)."
237
+ : ""),
144
238
  }),
145
239
  ]),
146
240
  );
@@ -151,8 +245,7 @@ export function TerraformApplyOp(config: TerraformApplyOpConfig): TerraformApply
151
245
  const op = Op({
152
246
  name: config.name,
153
247
  overview: `Init, plan and apply the "${config.root}" terraform root`,
154
- taskQueue,
155
- searchAttributes: {
248
+ labels: {
156
249
  Apply: "true",
157
250
  TerraformRoot: config.root,
158
251
  },