@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
@@ -4,7 +4,10 @@
4
4
  * only the `-no-color` plan text can reach an issue or PR body.
5
5
  */
6
6
 
7
- import { describe, test, expect } from "vitest";
7
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, describe, test, expect } from "vitest";
8
11
  import {
9
12
  isStepOutputRef,
10
13
  collectStepOutputRefs,
@@ -45,7 +48,7 @@ describe("TerraformWatchOp phases (#2087)", () => {
45
48
  }
46
49
  });
47
50
 
48
- test("the Plan step reports drift as a search attribute", () => {
51
+ test("the Plan step reports drift as a run outcome", () => {
49
52
  const plan = props({ name: "app-watch", root: "app" }).phases[1].steps[0] as ActivityStep;
50
53
  expect(plan.fn).toBe("terraformPlan");
51
54
  expect(plan.id).toBe("plan");
@@ -71,7 +74,7 @@ describe("TerraformWatchOp phases (#2087)", () => {
71
74
  });
72
75
 
73
76
  describe("TerraformWatchOp finding modes (#2087)", () => {
74
- const findingStep = (mode: "issue" | "pull-request"): ActivityStep => {
77
+ const findingStep = (mode: "issue" | "comment" | "pull-request"): ActivityStep => {
75
78
  const op = props({ name: "app-watch", root: "app", findingMode: mode });
76
79
  expect(phaseNames(op)).toEqual(["Init", "Plan", "Report"]);
77
80
  return op.phases[2].steps[0] as ActivityStep;
@@ -92,6 +95,14 @@ describe("TerraformWatchOp finding modes (#2087)", () => {
92
95
  expect(step.outcomeAttribute).toEqual({ name: "PR", from: "prUrl" });
93
96
  });
94
97
 
98
+ test("comment mode posts on the triggering pull request and surfaces the comment URL (#2231)", () => {
99
+ const step = findingStep("comment");
100
+ expect(step.fn).toBe("reconcilePr");
101
+ expect(step.args?.mode).toBe("comment");
102
+ expect(step.args?.env).toBe("app");
103
+ expect(step.outcomeAttribute).toEqual({ name: "Comment", from: "commentUrl" });
104
+ });
105
+
95
106
  test("the finding step derives no plan of its own", () => {
96
107
  // `entries: []` is what stops reconcilePr shelling to `chant lifecycle
97
108
  // plan --json`: the finding is already written by the time it runs.
@@ -114,7 +125,7 @@ describe("TerraformWatchOp finding modes (#2087)", () => {
114
125
  });
115
126
 
116
127
  describe("TerraformWatchOp posts the human plan and never the plan JSON (#2087)", () => {
117
- for (const mode of ["issue", "pull-request"] as const) {
128
+ for (const mode of ["issue", "comment", "pull-request"] as const) {
118
129
  test(`${mode}: the body is a reference to the Plan step's -no-color text`, () => {
119
130
  const op = props({ name: "app-watch", root: "app", findingMode: mode });
120
131
  const report = op.phases[2].steps[0] as ActivityStep;
@@ -145,40 +156,169 @@ describe("TerraformWatchOp posts the human plan and never the plan JSON (#2087)"
145
156
  });
146
157
  });
147
158
 
148
- describe("TerraformWatchOp schedule (#2087)", () => {
149
- test("omitting `schedule` returns no schedule", () => {
150
- const result = TerraformWatchOp({ name: "app-watch", root: "app" });
151
- expect(result.schedule).toBeUndefined();
152
- expect(Object.prototype.hasOwnProperty.call(result, "schedule")).toBe(false);
159
+ describe("TerraformWatchOp on a live root (#2105)", () => {
160
+ const live = (over: Partial<Parameters<typeof TerraformWatchOp>[0]> = {}): OpConfig =>
161
+ props({ name: "estate-watch", root: "estate", live: true, ...over });
162
+
163
+ test("the Plan step is choudoufuLivePlan, and Init is unchanged", () => {
164
+ const op = live();
165
+ expect(phaseNames(op)).toEqual(["Init", "Plan"]);
166
+ expect(steps(op).map((s) => s.fn)).toEqual(["terraformInit", "choudoufuLivePlan"]);
153
167
  });
154
168
 
155
- test("a cron returns a Temporal::Schedule beside the Op", () => {
156
- const { op, schedule } = TerraformWatchOp({
157
- name: "app-watch",
158
- root: "app",
159
- schedule: "0 6 * * *",
169
+ test("one live read publishes Drift, Unowned and Adoptable", () => {
170
+ const plan = live().phases[1].steps[0] as ActivityStep;
171
+ expect(plan.id).toBe("plan");
172
+ // `drift` off `-detailed-exitcode`, `unowned`/`adoptable` off the same
173
+ // run's #788 document: three answers, one estate-wide sweep.
174
+ expect(plan.outcomeAttribute).toEqual([
175
+ { name: "Drift", from: "drift" },
176
+ { name: "Unowned", from: "unowned" },
177
+ { name: "Adoptable", from: "adoptable" },
178
+ ]);
179
+ });
180
+
181
+ test("the Op is marked live, and no plan file is named anywhere", () => {
182
+ const op = live();
183
+ expect(op.labels).toMatchObject({ TerraformMode: "live" });
184
+ expect(JSON.stringify(op)).not.toContain("chant.tfplan");
185
+ });
186
+
187
+ test("planFile on a live root is refused at build time, quoting choudoufu", () => {
188
+ expect(() => live({ planFile: "estate.tfplan" })).toThrow(/planFile is refused on a live root/);
189
+ });
190
+
191
+ test("estate off a live root is refused: there is no live plan to name one for", () => {
192
+ expect(() => props({ name: "app-watch", root: "app", estate: "prod" })).toThrow(/not on a live root/);
193
+ });
194
+
195
+ test("an explicit estate rides the Plan step", () => {
196
+ const plan = live({ estate: "prod-networking" }).phases[1].steps[0] as ActivityStep;
197
+ expect(plan.args?.estate).toBe("prod-networking");
198
+ });
199
+
200
+ test("a stock root is untouched: still terraformPlan, still one Drift attribute", () => {
201
+ const plan = props({ name: "app-watch", root: "app" }).phases[1].steps[0] as ActivityStep;
202
+ expect(plan.fn).toBe("terraformPlan");
203
+ expect(plan.outcomeAttribute).toEqual({ name: "Drift", from: "changed" });
204
+ });
205
+
206
+ for (const mode of ["issue", "comment", "pull-request"] as const) {
207
+ test(`${mode}: the body is the plan text plus the adoption ledger, as one field`, () => {
208
+ const op = live({ findingMode: mode });
209
+ expect(phaseNames(op)).toEqual(["Init", "Plan", "Report"]);
210
+ const report = op.phases[2].steps[0] as ActivityStep;
211
+ expect(report.fn).toBe("reconcilePr");
212
+ expect(isStepOutputRef(report.args?.body)).toBe(true);
213
+ expect(report.args?.body).toMatchObject({ step: "plan", path: "finding" });
160
214
  });
161
- expect(schedule).toBeDefined();
162
- expect(schedule!.entityType).toBe("Temporal::Schedule");
163
- expect(schedule!.lexicon).toBe("temporal");
164
- expect((schedule as unknown as { props: Record<string, unknown> }).props).toEqual({
165
- scheduleId: "app-watch-schedule",
166
- spec: { cronExpressions: ["0 6 * * *"] },
167
- action: { workflowType: "appWatchWorkflow", taskQueue: "app-watch" },
215
+
216
+ test(`${mode}: no reference to the live-plan document reaches the body`, () => {
217
+ // On a live root the JSON is choudoufu's bound/omissions/unowned
218
+ // document: live identities and tag values for every resource the
219
+ // estate touched. Same rule as the stock plan's JSON, same test.
220
+ const op = live({ findingMode: mode });
221
+ const report = op.phases[2].steps[0] as ActivityStep;
222
+ const paths = collectStepOutputRefs(report.args).map((r) => r.path);
223
+ expect(paths).toEqual(["finding"]);
224
+ expect(paths).not.toContain("json");
225
+ expect(JSON.stringify(report.args)).not.toContain("json");
168
226
  });
169
- expect((op as unknown as { props: OpConfig }).props.taskQueue).toBe("app-watch");
227
+ }
228
+
229
+ test("the whole live Op references exactly one field of the plan", () => {
230
+ const op = live({ findingMode: "issue" });
231
+ const refs = op.phases.flatMap((p) => p.steps.filter(isActivity)).flatMap((s) => collectStepOutputRefs(s.args));
232
+ expect(refs).toHaveLength(1);
233
+ expect(refs[0]).toMatchObject({ step: "plan", path: "finding" });
234
+ });
235
+ });
236
+
237
+ describe("TerraformWatchOp schedule (#2087, #2120)", () => {
238
+ test("omitting `schedule` leaves the Op with no cadence", () => {
239
+ const { op } = TerraformWatchOp({ name: "app-watch", root: "app" });
240
+ expect((op as unknown as { props: OpConfig }).props).not.toHaveProperty("schedule");
170
241
  });
171
242
 
172
- test("an explicit taskQueue reaches both the Op and the schedule action", () => {
173
- const { op, schedule } = TerraformWatchOp({
243
+ test("a cron lands on the Op itself", () => {
244
+ const { op } = TerraformWatchOp({
174
245
  name: "app-watch",
175
246
  root: "app",
176
247
  schedule: "0 6 * * *",
177
- taskQueue: "infra",
178
248
  });
179
- expect((op as unknown as { props: OpConfig }).props.taskQueue).toBe("infra");
180
- expect(
181
- ((schedule as unknown as { props: { action: { taskQueue: string } } }).props.action).taskQueue,
182
- ).toBe("infra");
249
+ expect((op as unknown as { props: OpConfig }).props.schedule).toEqual({
250
+ cron: "0 6 * * *",
251
+ overlap: "skip",
252
+ });
253
+ });
254
+ });
255
+
256
+ /**
257
+ * The `live` flag against the root it names (#2216). Best-effort: the mode
258
+ * resolves from a `chant.config.json` only, so these projects are written in
259
+ * that format. TF028 (`../lint/post-synth/tf028.ts`) is what reports the same
260
+ * mismatch on a `chant.config.ts` project, where the mode does not resolve
261
+ * here at all.
262
+ */
263
+ describe("TerraformWatchOp cross-checks live against the root's mode (#2216)", () => {
264
+ const dirs: string[] = [];
265
+
266
+ afterEach(() => {
267
+ for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
268
+ });
269
+
270
+ const LIVE_ROOT = [
271
+ "terraform {",
272
+ " live {",
273
+ ' estate = "prod-estate"',
274
+ " }",
275
+ "}",
276
+ "",
277
+ ].join("\n");
278
+
279
+ /** A project on a `chant.config.json` whose root `estate` is `mainTf`, run on `binary`. */
280
+ function project(binary: string, mainTf: string): string {
281
+ const dir = mkdtempSync(join(tmpdir(), "chant-tf-watch-mode-"));
282
+ dirs.push(dir);
283
+ mkdirSync(join(dir, "estate"), { recursive: true });
284
+ writeFileSync(join(dir, "estate", "main.tf"), mainTf);
285
+ writeFileSync(
286
+ join(dir, "chant.config.json"),
287
+ JSON.stringify({ terraform: { binary, roots: { estate: { dir: "./estate" } } } }),
288
+ );
289
+ return dir;
290
+ }
291
+
292
+ test("refuses a live root watched in stock mode, naming the Op and the root", () => {
293
+ const cwd = project("choudoufu", LIVE_ROOT);
294
+ expect(() => TerraformWatchOp({ name: "estate-watch", root: "estate", cwd })).toThrow(
295
+ /TerraformWatchOp "estate-watch": root "estate" runs choudoufu with a declared estate/,
296
+ );
297
+ });
298
+
299
+ test("accepts the same root with live: true", () => {
300
+ const cwd = project("choudoufu", LIVE_ROOT);
301
+ expect(() => TerraformWatchOp({ name: "estate-watch", root: "estate", live: true, cwd })).not.toThrow();
302
+ });
303
+
304
+ test("refuses live: true on a root that declares no estate", () => {
305
+ const cwd = project("choudoufu", 'resource "null_resource" "first" {}\n');
306
+ expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", live: true, cwd })).toThrow(
307
+ /declares no estate/,
308
+ );
309
+ });
310
+
311
+ test("refuses live: true when the binary is not choudoufu, whatever the root declares", () => {
312
+ const cwd = project("terraform", LIVE_ROOT);
313
+ expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", live: true, cwd })).toThrow(
314
+ /declares no estate/,
315
+ );
316
+ });
317
+
318
+ test("says nothing when the mode does not resolve: no config to read", () => {
319
+ const dir = mkdtempSync(join(tmpdir(), "chant-tf-watch-mode-"));
320
+ dirs.push(dir);
321
+ expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", cwd: dir })).not.toThrow();
322
+ expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", live: true, cwd: dir })).not.toThrow();
183
323
  });
184
324
  });
@@ -7,7 +7,7 @@
7
7
  * the plan found. `terraform plan -detailed-exitcode` is the whole mechanism.
8
8
  * Exit 0 is "the world matches the configuration", exit 2 is drift, and
9
9
  * `terraformPlan` turns that into the boolean `changed`, which rides out of
10
- * the Op as the `Drift` search attribute.
10
+ * the Op as the run's `Drift` outcome.
11
11
  *
12
12
  * Phases: Init, Plan, and (for a finding mode that opens something) Report.
13
13
  *
@@ -23,37 +23,59 @@
23
23
  * post the JSON, and `terraform-watch-op.test.ts` asserts no reference to it
24
24
  * reaches the finding step.
25
25
  *
26
+ * The same rule holds on a live root, where the JSON is choudoufu's own
27
+ * bound/omissions/unowned document rather than a plan representation, and
28
+ * carries live identities and tag values for every resource the estate
29
+ * touched. `choudoufuLivePlan` composes the plan text and the adoption ledger
30
+ * into one `finding` string for exactly this reason: the body is one
31
+ * reference to one text field, so there is no arrangement of the args in
32
+ * which the document reaches a body.
33
+ *
34
+ * ## A live root reports three things, not one (#2105)
35
+ *
36
+ * `live: true` swaps the Plan step for `choudoufuLivePlan`. On a choudoufu
37
+ * root ownership is a pair of tags on the resource rather than an entry in a
38
+ * state file, so a plan against the live system can answer three questions
39
+ * where a stock plan answers one: whether the estate drifted (the
40
+ * `-detailed-exitcode` exit, `Drift` as before), how many live resources sit
41
+ * at a declared identity carrying no marker (`Unowned`), and how many of those
42
+ * an exact content match makes claimable (`Adoptable`). All three come off a
43
+ * single live read, published as three run outcomes from the one step.
44
+ *
45
+ * The finding modes then carry the adoption ledger under the plan text: one
46
+ * line per adoptable match with its address, its live identity and the
47
+ * `tofu-estate`/`tofu-address` values that claim it, in the row form
48
+ * `live-plan -adoption-only` prints, followed by any contested address. That
49
+ * is a report, not an action. `TerraformAdoptOp` next door is what writes the
50
+ * markers, and it gates first.
51
+ *
26
52
  * ## Findings reuse `reconcilePr`
27
53
  *
28
- * `issue` and `pull-request` call the temporal lexicon's `reconcilePr`
29
- * activity, the one place in chant that shells to `gh issue create` /
30
- * `gh pr create`, rather than growing a second copy of those calls here.
54
+ * `issue`, `comment` and `pull-request` call core's `reconcilePr` activity,
55
+ * the one place in chant that shells to `gh`, rather than growing a second
56
+ * copy of those calls here.
31
57
  * That activity built its own change-set summary and had no way to be handed
32
58
  * one, so #2087 added a single field to it (`ReconcilePrArgs.body`), which is
33
59
  * what carries the `-no-color` plan through. Note that the pull-request mode
34
60
  * of `reconcilePr` regenerates chant TypeScript via `chant import`; a
35
- * terraform-native regeneration of HCL from live state is #2089, so until
36
- * then `findingMode: "issue"` is the mode with an end-to-end answer and
37
- * `"pull-request"` opens a PR whose body is the plan.
61
+ * terraform-native regeneration of HCL from live state is #2089, so
62
+ * `findingMode: "issue"` is the mode with an end-to-end answer and
63
+ * `"pull-request"` opens a PR whose body is the plan. `"comment"` (#2231) is
64
+ * the mode for the plan-on-pull-request shape: the plan is posted onto the
65
+ * pull request that triggered the run and re-edited there on the next push,
66
+ * which is where a reviewer is already looking. It needs that trigger; a run
67
+ * without one fails the Report step by name rather than posting elsewhere. On a live root #2089's
68
+ * question has a different answer rather than a pending one, and it is not
69
+ * regeneration: see `TerraformAdoptOp`.
38
70
  *
39
71
  * ## The schedule
40
72
  *
41
- * A cron makes this a `{ op, schedule }` pair, the shape
42
- * `lexicons/temporal/src/composites/workflow-audit-op.ts` and
43
- * `reconcile-op.ts` return. The schedule resource is `Temporal::Schedule`,
44
- * built here through core's own `createResource` rather than imported from
45
- * `@intentius/chant-lexicon-temporal`: no lexicon in this repo imports
46
- * another lexicon's package at runtime, and a static import would mean a
47
- * project that installs terraform and runs `chant run` on the local executor
48
- * could not load `@intentius/chant-lexicon-terraform` at all without also
49
- * installing temporal. The declarable is the same one temporal's
50
- * `TemporalSchedule` produces (`createResource("Temporal::Schedule",
51
- * "temporal", {})`, `lexicons/temporal/src/resources.ts`), so temporal's
52
- * serializer renders it unchanged when a project has both.
53
- *
54
- * A project with no Temporal at all runs the same Op on a CI cron instead:
55
- * `generateOpsPipeline` against the github lexicon turns the Op plus its cron
56
- * into a workflow. `examples/scheduled-watch/` is that recipe.
73
+ * A cron lands on the Op itself as `schedule` (#2120), the same field
74
+ * `WatchOp` and `ConvergeOp` set. It is runtime-neutral data: `chant
75
+ * operator` reads it as this Op's tick cadence, `generateOpsPipeline` against
76
+ * the github lexicon renders it as a CI cron, and a hosting lexicon hands it
77
+ * to its own scheduler. The one-shot `chant run app-watch` path ignores it.
78
+ * `examples/scheduled-watch/` is the CI recipe.
57
79
  *
58
80
  * @example
59
81
  * ```typescript
@@ -62,59 +84,100 @@
62
84
  * // one-shot, local executor: chant run app-watch
63
85
  * export const { op } = TerraformWatchOp({ name: "app-watch", root: "app" });
64
86
  *
87
+ * // the plan as a sticky comment on the pull request that triggered the run
88
+ * export const { op } = TerraformWatchOp({
89
+ * name: "app-plan",
90
+ * root: "app",
91
+ * findingMode: "comment",
92
+ * });
93
+ *
65
94
  * // nightly, opening an issue when the plan is non-empty
66
- * export const { op, schedule } = TerraformWatchOp({
95
+ * export const { op } = TerraformWatchOp({
67
96
  * name: "app-watch",
68
97
  * root: "app",
69
98
  * schedule: "0 6 * * *",
70
99
  * findingMode: "issue",
71
100
  * });
101
+ *
102
+ * // a choudoufu estate: drift, unowned and adoptable, with the ledger in the issue
103
+ * export const { op } = TerraformWatchOp({
104
+ * name: "estate-watch",
105
+ * root: "estate",
106
+ * live: true,
107
+ * findingMode: "issue",
108
+ * });
72
109
  * ```
73
110
  */
74
111
 
75
112
  import { Op, phase, OpResource, type ActivityStep } from "@intentius/chant/op";
76
- import { createResource } from "@intentius/chant/runtime";
77
- import type { Declarable } from "@intentius/chant/declarable";
78
- import { DEFAULT_PLAN_FILE } from "../op/activities/terraform";
79
- import { terraformInit as initStep, terraformPlan as planStep } from "../op/builders";
113
+ import { resolveRootModeSync } from "../op/resolve-root-mode";
114
+ import { CHOUDOUFU_LIVE_PLAN_OUT_REFUSAL, DEFAULT_PLAN_FILE } from "../op/activities/terraform";
115
+ import {
116
+ terraformInit as initStep,
117
+ terraformPlan as planStep,
118
+ choudoufuLivePlan as livePlanStep,
119
+ } from "../op/builders";
80
120
 
81
121
  /**
82
122
  * What to do with a non-empty plan. A subset of core's `OpFindingMode`
83
123
  * (`packages/core/src/lexicon.ts`): `merge-request` is GitLab's spelling of
84
124
  * `pull-request` and is produced by the CI generator, not chosen here.
125
+ *
126
+ * `comment` (#2231) is the mode a plan-on-pull-request Op wants: the plan
127
+ * lands as one comment on the pull request that triggered the run and the
128
+ * next push edits that same comment. It needs a `pull_request` trigger on the
129
+ * `ScheduledOpSpec` the workflow is generated from, and the github generator
130
+ * refuses it by name on any other trigger.
85
131
  */
86
- export type TerraformFindingMode = "report" | "issue" | "pull-request";
87
-
88
- /**
89
- * `Temporal::Schedule`, built without importing the temporal lexicon. See the
90
- * module doc for why. Identical to `TemporalSchedule` in
91
- * `lexicons/temporal/src/resources.ts`.
92
- */
93
- const ScheduleResource = createResource("Temporal::Schedule", "temporal", {});
94
-
95
- /** `app-watch` becomes `appWatchWorkflow`, matching the temporal serializer's naming. */
96
- function kebabToCamel(s: string): string {
97
- return s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
98
- }
132
+ export type TerraformFindingMode = "report" | "issue" | "comment" | "pull-request";
99
133
 
100
134
  export interface TerraformWatchOpConfig {
101
- /** Op name (kebab-case). Also the default task queue and schedule id base. */
135
+ /** Op name (kebab-case). */
102
136
  name: string;
103
137
  /** Key into the project's `terraform.roots`. The root carries dir, workspace, var files and backend config. */
104
138
  root: string;
105
139
  /**
106
- * Cron expression. When set, a `Temporal::Schedule` is returned alongside
107
- * the Op; omit for one-shot `chant run` on the local executor, or for a CI
108
- * cron built by `generateOpsPipeline`.
140
+ * Cron expression. When set, it lands on the Op as `schedule` — the cadence
141
+ * `chant operator` ticks on and `generateOpsPipeline` renders as a CI cron.
142
+ * Omit for one-shot `chant run` on the local executor.
109
143
  */
110
144
  schedule?: string;
111
145
  /**
112
146
  * What to do when the plan proposes changes. Default: `"report"`, which
113
- * opens nothing. The `Drift` search attribute and the run's own log are
147
+ * opens nothing. The `Drift` run outcome and the run's own log are
114
148
  * the report.
115
149
  */
116
150
  findingMode?: TerraformFindingMode;
117
- /** Plan file the Plan step writes, relative to the root dir. Default: `chant.tfplan`. */
151
+ /**
152
+ * Watch a live root (#2105): the root runs `terraform.binary: "choudoufu"`
153
+ * and declares an estate, so the Plan phase is `choudoufuLivePlan` rather
154
+ * than `terraformPlan`. See the module doc for what changes.
155
+ *
156
+ * Declared here rather than detected, because a composite is built by
157
+ * `chant build` without reading a root module's `.tf` files. The activities
158
+ * detect the estate at run time (`op/activities/live-detect.ts`), but by then
159
+ * the phases are already serialized.
160
+ *
161
+ * Two things check the answer against the root's real mode, since a wrong
162
+ * one is otherwise silent (#2216). This function refuses a mismatch outright
163
+ * when it can resolve the mode synchronously, which is a
164
+ * `chant.config.json` project (`op/resolve-root-mode.ts`); TF028
165
+ * (`lint/post-synth/tf028.ts`) reports the same mismatch off the parsed HCL
166
+ * for every project, `chant.config.ts` included.
167
+ */
168
+ live?: boolean;
169
+ /**
170
+ * The estate to plan against, for a live root whose estate this Op should
171
+ * name explicitly. Omitted, `choudoufuLivePlan` auto-detects it from the
172
+ * root's own `live` block or `estate.chdf.hcl` sidecar, which is the usual
173
+ * case. Refused on a stock root.
174
+ */
175
+ estate?: string;
176
+ /**
177
+ * Plan file the Plan step writes, relative to the root dir. Default:
178
+ * `chant.tfplan`. Refused on a live root, where choudoufu takes no plan file
179
+ * at all.
180
+ */
118
181
  planFile?: string;
119
182
  /** `-upgrade` on the Init step: re-resolve provider and module versions. */
120
183
  upgrade?: boolean;
@@ -129,27 +192,72 @@ export interface TerraformWatchOpConfig {
129
192
  title?: string;
130
193
  /** Branch the pull-request mode opens from. Default: `reconcilePr`'s own. */
131
194
  branch?: string;
132
- /** Override the task queue. Defaults to `name`. */
133
- taskQueue?: string;
134
195
  }
135
196
 
136
197
  export interface TerraformWatchOpResources {
137
- /** Op resource. Generates the Init/Plan/[Report] workflow. */
198
+ /** Op resource. Generates the Init/Plan/[Report] phases. */
138
199
  op: InstanceType<typeof OpResource>;
139
- /** `Temporal::Schedule`, present only when `schedule` was given. */
140
- schedule?: Declarable;
141
200
  }
142
201
 
143
202
  export function TerraformWatchOp(config: TerraformWatchOpConfig): TerraformWatchOpResources {
144
- const taskQueue = config.taskQueue ?? config.name;
145
- const planFile = config.planFile ?? DEFAULT_PLAN_FILE;
146
203
  const findingMode: TerraformFindingMode = config.findingMode ?? "report";
147
204
  const where = config.cwd ? { cwd: config.cwd } : {};
148
205
 
149
- // `id` is what makes `plan.out` legal, and `plan.out.text` is the only
150
- // channel by which any part of the plan leaves this Op.
151
- const plan = planStep(config.root, { planFile, ...where, id: "plan" });
152
- plan.outcomeAttribute = { name: "Drift", from: "changed" };
206
+ if (config.live && config.planFile !== undefined) {
207
+ throw new Error(
208
+ `TerraformWatchOp "${config.name}": planFile is refused on a live root. choudoufu: ` +
209
+ `"${CHOUDOUFU_LIVE_PLAN_OUT_REFUSAL}". A live plan writes no saved plan, so there is no file to name; ` +
210
+ "drop planFile, or drop live if this root runs stock.",
211
+ );
212
+ }
213
+ if (!config.live && config.estate !== undefined) {
214
+ throw new Error(
215
+ `TerraformWatchOp "${config.name}": estate names the estate a live plan looks for markers of, and ` +
216
+ "this Op is not on a live root. Set live: true, or drop estate.",
217
+ );
218
+ }
219
+
220
+ // The `live` flag against the root it names. Best-effort and synchronous:
221
+ // `resolveRootModeSync` reads a `chant.config.json` only and returns
222
+ // undefined for anything else, so a `chant.config.ts` project reaches
223
+ // neither branch and TF028 is what reports the mismatch there. Where the
224
+ // mode does resolve, refusing at build time names the Op, which a
225
+ // post-synth diagnostic does too but a run-time failure at the first step
226
+ // does not.
227
+ const resolvedMode = resolveRootModeSync(config.root, config.cwd)?.mode;
228
+ if (resolvedMode === "live" && !config.live) {
229
+ throw new Error(
230
+ `TerraformWatchOp "${config.name}": root "${config.root}" runs choudoufu with a declared estate, ` +
231
+ "but this Op watches it in stock mode. A stock plan reports drift alone and stays quiet about " +
232
+ "every unowned and adoptable resource in the estate. Set live: true.",
233
+ );
234
+ }
235
+ if (resolvedMode === "state" && config.live) {
236
+ throw new Error(
237
+ `TerraformWatchOp "${config.name}": live: true, but root "${config.root}" declares no estate (or ` +
238
+ "terraform.binary is not \"choudoufu\"), so there is no live system to plan against. Drop " +
239
+ "live, or declare the root's estate.",
240
+ );
241
+ }
242
+
243
+ // `id` is what makes `plan.out` legal, and one field of `plan.out` is the
244
+ // only channel by which any part of the plan leaves this Op.
245
+ //
246
+ // A live root reads the live system instead of a state file, so the three
247
+ // things a watch reports come off one `choudoufuLivePlan`: `drift` from
248
+ // `-detailed-exitcode`, and `unowned`/`adoptable` from the `-json` document
249
+ // the same run wrote. Three attributes, one live read: see
250
+ // `ActivityStep.outcomeAttribute`'s array form (#2105).
251
+ const plan = config.live
252
+ ? livePlanStep(config.root, { ...where, ...(config.estate ? { estate: config.estate } : {}), id: "plan" })
253
+ : planStep(config.root, { planFile: config.planFile ?? DEFAULT_PLAN_FILE, ...where, id: "plan" });
254
+ plan.outcomeAttribute = config.live
255
+ ? [
256
+ { name: "Drift", from: "drift" },
257
+ { name: "Unowned", from: "unowned" },
258
+ { name: "Adoptable", from: "adoptable" },
259
+ ]
260
+ : { name: "Drift", from: "changed" };
153
261
 
154
262
  const phases = [
155
263
  phase("Init", [initStep(config.root, { ...where, ...(config.upgrade ? { upgrade: true } : {}) })]),
@@ -169,36 +277,36 @@ export function TerraformWatchOp(config: TerraformWatchOpConfig): TerraformWatch
169
277
  mode: findingMode,
170
278
  entries: [],
171
279
  title: config.title ?? `Terraform drift in root "${config.root}"`,
172
- body: plan.out.text,
280
+ // Stock: the `-no-color` plan. Live: the same plan text with the
281
+ // adoption ledger under it, which `choudoufuLivePlan` composes into
282
+ // one `finding` field precisely so that a body is one reference and
283
+ // the `-json` document has no path into one at all.
284
+ body: config.live ? plan.out.finding : plan.out.text,
173
285
  ...(config.branch ? { branch: config.branch } : {}),
174
286
  },
175
287
  outcomeAttribute:
176
- findingMode === "pull-request" ? { name: "PR", from: "prUrl" } : { name: "Issue", from: "issueUrl" },
288
+ findingMode === "pull-request"
289
+ ? { name: "PR", from: "prUrl" }
290
+ : findingMode === "comment"
291
+ ? { name: "Comment", from: "commentUrl" }
292
+ : { name: "Issue", from: "issueUrl" },
177
293
  };
178
294
  phases.push(phase("Report", [report]));
179
295
  }
180
296
 
181
297
  const op = Op({
182
298
  name: config.name,
183
- overview: `Plan the "${config.root}" terraform root and report drift`,
184
- taskQueue,
185
- searchAttributes: {
299
+ overview: config.live
300
+ ? `Live-plan the "${config.root}" choudoufu estate and report drift, unowned and adoptable resources`
301
+ : `Plan the "${config.root}" terraform root and report drift`,
302
+ labels: {
186
303
  Watch: "true",
187
304
  TerraformRoot: config.root,
305
+ ...(config.live ? { TerraformMode: "live" } : {}),
188
306
  },
307
+ ...(config.schedule ? { schedule: { cron: config.schedule, overlap: "skip" as const } } : {}),
189
308
  phases,
190
309
  });
191
310
 
192
- if (!config.schedule) return { op };
193
-
194
- const schedule = new ScheduleResource({
195
- scheduleId: `${config.name}-schedule`,
196
- spec: { cronExpressions: [config.schedule] },
197
- action: {
198
- workflowType: kebabToCamel(config.name) + "Workflow",
199
- taskQueue,
200
- },
201
- });
202
-
203
- return { op, schedule };
311
+ return { op };
204
312
  }
package/src/config.ts CHANGED
@@ -25,6 +25,20 @@
25
25
  import { z } from "zod";
26
26
  import type { ChantConfig } from "@intentius/chant/config";
27
27
 
28
+ /**
29
+ * chant's cross-lexicon delete-mode vocabulary (`lexicons/k8s/src/op/
30
+ * activities/kubectl.ts`'s `ApplyDeleteMode`, `packages/core/src/op/
31
+ * activities/apply.ts`'s `DeleteMode`), read here for a live root and mapped
32
+ * onto choudoufu's `policy` block (#2106): `"never"` requires the root's
33
+ * `policy` to set `undeclared_tagged` to `"keep"`, `"untag"` or `"report"`
34
+ * (TF026 enforces this at build time); `"owned-only"` is choudoufu's own
35
+ * default verb for that quadrant (`delete`) and needs nothing; `"gated"`
36
+ * needs nothing beyond `TerraformApplyOp`'s own approval gate. Inert on a
37
+ * stock root: choudoufu's `policy` block does not exist there.
38
+ */
39
+ export const terraformDeleteModeSchema = z.enum(["never", "owned-only", "gated"]);
40
+ export type TerraformDeleteMode = z.infer<typeof terraformDeleteModeSchema>;
41
+
28
42
  /**
29
43
  * One Terraform root module chant reads. `dir` is the only required field:
30
44
  * everything else names how the root is invoked rather than what it declares.
@@ -43,14 +57,29 @@ export const terraformRootSchema = z.strictObject({
43
57
  varFiles: z.array(z.string()).optional(),
44
58
  /** `-backend-config` key/value pairs handed to `init`. */
45
59
  backendConfig: z.record(z.string(), z.string()).optional(),
60
+ /** This root's declared delete mode. See {@link terraformDeleteModeSchema}. */
61
+ delete: terraformDeleteModeSchema.optional(),
46
62
  });
47
63
 
48
64
  export const terraformConfigSchema = z.strictObject({
49
65
  /**
50
- * Which CLI drives the roots. The two are wire-compatible for everything
51
- * this lexicon does, so the choice is recorded rather than inferred.
66
+ * Which CLI drives the roots. `terraform` and `tofu` are wire-compatible
67
+ * for everything this lexicon does, so the choice is recorded rather than
68
+ * inferred. `choudoufu` (#2103) is an OpenTofu fork: a root running it also
69
+ * needs a declared estate (a `live` block or an `estate.chdf.hcl` sidecar,
70
+ * seen by the shared HCL parse) before it counts as live. Declaring the
71
+ * binary alone runs the root stock, exactly as `terraform`/`tofu` do.
72
+ */
73
+ binary: z.enum(["terraform", "tofu", "choudoufu"]).optional(),
74
+ /**
75
+ * How far a root's parse follows its `module` calls (#2112), spelled the
76
+ * way tflint spells it. `local` (the default) reads modules sourced from a
77
+ * relative path, so TF014, TF015 and TF020 see inside them; `none` reads
78
+ * only each root's own directory; `all` is reserved for fetching registry
79
+ * and git modules and is refused with a message, since chant fetches
80
+ * nothing. See `hcl/descend.ts`.
52
81
  */
53
- binary: z.enum(["terraform", "tofu"]).optional(),
82
+ callModuleType: z.enum(["local", "none", "all"]).optional(),
54
83
  /** Named root modules. The name is the entity-key prefix, so keep it stable. */
55
84
  roots: z.record(z.string(), terraformRootSchema),
56
85
  });