@intentius/chant-lexicon-terraform 0.124.54 → 0.124.56

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 (351) hide show
  1. package/README.md +7 -4
  2. package/dist/ci-setup.d.ts +11 -0
  3. package/dist/ci-setup.d.ts.map +1 -0
  4. package/dist/ci-setup.js +41 -0
  5. package/dist/components/capability-plugin.d.ts +40 -6
  6. package/dist/components/capability-plugin.d.ts.map +1 -1
  7. package/dist/components/capability-plugin.js +174 -30
  8. package/dist/components/compose-group.d.ts +1 -1
  9. package/dist/components/compose-group.d.ts.map +1 -1
  10. package/dist/components/compose-group.js +1 -1
  11. package/dist/components/release-inputs.d.ts +29 -0
  12. package/dist/components/release-inputs.d.ts.map +1 -0
  13. package/dist/components/release-inputs.js +57 -0
  14. package/dist/components/trace-facts.d.ts +16 -0
  15. package/dist/components/trace-facts.d.ts.map +1 -0
  16. package/dist/components/trace-facts.js +19 -0
  17. package/dist/cone.d.ts +53 -0
  18. package/dist/cone.d.ts.map +1 -0
  19. package/dist/cone.js +119 -0
  20. package/dist/cost-hold.d.ts +27 -0
  21. package/dist/cost-hold.d.ts.map +1 -0
  22. package/dist/cost-hold.js +21 -0
  23. package/dist/drift-rows.d.ts +61 -0
  24. package/dist/drift-rows.d.ts.map +1 -0
  25. package/dist/drift-rows.js +108 -0
  26. package/dist/exit-codes.d.ts +55 -0
  27. package/dist/exit-codes.d.ts.map +1 -0
  28. package/dist/exit-codes.js +48 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/integrity.json +7 -4
  33. package/dist/lint/audit-catalog.d.ts.map +1 -1
  34. package/dist/lint/audit-catalog.js +27 -3
  35. package/dist/lint/audit-lineage.d.ts +14 -2
  36. package/dist/lint/audit-lineage.d.ts.map +1 -1
  37. package/dist/lint/post-synth/index.d.ts.map +1 -1
  38. package/dist/lint/post-synth/index.js +4 -0
  39. package/dist/lint/post-synth/record-store.d.ts +54 -0
  40. package/dist/lint/post-synth/record-store.d.ts.map +1 -0
  41. package/dist/lint/post-synth/record-store.js +104 -0
  42. package/dist/lint/post-synth/tf001.d.ts +2 -2
  43. package/dist/lint/post-synth/tf001.js +2 -2
  44. package/dist/lint/post-synth/tf024.d.ts +35 -11
  45. package/dist/lint/post-synth/tf024.d.ts.map +1 -1
  46. package/dist/lint/post-synth/tf024.js +92 -20
  47. package/dist/lint/post-synth/tf046.d.ts +64 -0
  48. package/dist/lint/post-synth/tf046.d.ts.map +1 -0
  49. package/dist/lint/post-synth/tf046.js +175 -0
  50. package/dist/lint/post-synth/tf047.d.ts +36 -0
  51. package/dist/lint/post-synth/tf047.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf047.js +94 -0
  53. package/dist/lock-rows.d.ts +36 -0
  54. package/dist/lock-rows.d.ts.map +1 -0
  55. package/dist/lock-rows.js +60 -0
  56. package/dist/lock-timeout.d.ts +22 -0
  57. package/dist/lock-timeout.d.ts.map +1 -0
  58. package/dist/lock-timeout.js +32 -0
  59. package/dist/manifest.json +1 -1
  60. package/dist/module-catalog/index.d.ts +69 -0
  61. package/dist/module-catalog/index.d.ts.map +1 -0
  62. package/dist/module-catalog/index.js +110 -0
  63. package/dist/module-catalog/refresh-cli.d.ts +4 -0
  64. package/dist/module-catalog/refresh-cli.d.ts.map +1 -0
  65. package/dist/module-catalog/refresh-cli.js +64 -0
  66. package/dist/module-catalog/static-tier.d.ts +6 -0
  67. package/dist/module-catalog/static-tier.d.ts.map +1 -0
  68. package/dist/module-catalog/static-tier.js +1650 -0
  69. package/dist/module-catalog/types.d.ts +65 -0
  70. package/dist/module-catalog/types.d.ts.map +1 -0
  71. package/dist/module-catalog/types.js +33 -0
  72. package/dist/okf/index.md +3 -1
  73. package/dist/okf/rules/TF024.md +2 -2
  74. package/dist/okf/rules/TF046.md +11 -0
  75. package/dist/okf/rules/TF047.md +11 -0
  76. package/dist/op/activities/terraform.d.ts +29 -0
  77. package/dist/op/activities/terraform.d.ts.map +1 -1
  78. package/dist/op/activities/terraform.js +19 -4
  79. package/dist/pin/consumers.d.ts +35 -0
  80. package/dist/pin/consumers.d.ts.map +1 -0
  81. package/dist/pin/consumers.js +70 -0
  82. package/dist/plugin.d.ts.map +1 -1
  83. package/dist/plugin.js +5 -0
  84. package/dist/registry/address.d.ts +63 -0
  85. package/dist/registry/address.d.ts.map +1 -0
  86. package/dist/registry/address.js +65 -0
  87. package/dist/registry/catalog-read.d.ts +14 -0
  88. package/dist/registry/catalog-read.d.ts.map +1 -0
  89. package/dist/registry/catalog-read.js +58 -0
  90. package/dist/registry/deprecated.d.ts +37 -0
  91. package/dist/registry/deprecated.d.ts.map +1 -0
  92. package/dist/registry/deprecated.js +43 -0
  93. package/dist/registry/docs.d.ts +96 -0
  94. package/dist/registry/docs.d.ts.map +1 -0
  95. package/dist/registry/docs.js +210 -0
  96. package/dist/registry/edge/edge.d.ts +46 -0
  97. package/dist/registry/edge/edge.d.ts.map +1 -0
  98. package/dist/registry/edge/edge.js +194 -0
  99. package/dist/registry/edge/index.d.ts +11 -0
  100. package/dist/registry/edge/index.d.ts.map +1 -0
  101. package/dist/registry/edge/index.js +10 -0
  102. package/dist/registry/edge/lambda-edge.d.ts +76 -0
  103. package/dist/registry/edge/lambda-edge.d.ts.map +1 -0
  104. package/dist/registry/edge/lambda-edge.js +95 -0
  105. package/dist/registry/edge/sigv4-web.d.ts +33 -0
  106. package/dist/registry/edge/sigv4-web.d.ts.map +1 -0
  107. package/dist/registry/edge/sigv4-web.js +56 -0
  108. package/dist/registry/edge/token-source.d.ts +73 -0
  109. package/dist/registry/edge/token-source.d.ts.map +1 -0
  110. package/dist/registry/edge/token-source.js +115 -0
  111. package/dist/registry/edge/worker.d.ts +45 -0
  112. package/dist/registry/edge/worker.d.ts.map +1 -0
  113. package/dist/registry/edge/worker.js +26 -0
  114. package/dist/registry/handler.d.ts +25 -0
  115. package/dist/registry/handler.d.ts.map +1 -0
  116. package/dist/registry/handler.js +75 -0
  117. package/dist/registry/headers-file.d.ts +21 -0
  118. package/dist/registry/headers-file.d.ts.map +1 -0
  119. package/dist/registry/headers-file.js +57 -0
  120. package/dist/registry/index.d.ts +31 -0
  121. package/dist/registry/index.d.ts.map +1 -0
  122. package/dist/registry/index.js +30 -0
  123. package/dist/registry/mirror-paths.d.ts +9 -0
  124. package/dist/registry/mirror-paths.d.ts.map +1 -0
  125. package/dist/registry/mirror-paths.js +8 -0
  126. package/dist/registry/mirror.d.ts +130 -0
  127. package/dist/registry/mirror.d.ts.map +1 -0
  128. package/dist/registry/mirror.js +319 -0
  129. package/dist/registry/proxies.d.ts +73 -0
  130. package/dist/registry/proxies.d.ts.map +1 -0
  131. package/dist/registry/proxies.js +111 -0
  132. package/dist/registry/reader.d.ts +57 -0
  133. package/dist/registry/reader.d.ts.map +1 -0
  134. package/dist/registry/reader.js +198 -0
  135. package/dist/registry/semver.d.ts +16 -0
  136. package/dist/registry/semver.d.ts.map +1 -0
  137. package/dist/registry/semver.js +24 -0
  138. package/dist/registry/serve.d.ts +24 -0
  139. package/dist/registry/serve.d.ts.map +1 -0
  140. package/dist/registry/serve.js +64 -0
  141. package/dist/registry/source-forms.d.ts +79 -0
  142. package/dist/registry/source-forms.d.ts.map +1 -0
  143. package/dist/registry/source-forms.js +202 -0
  144. package/dist/registry/targets/artifactory.d.ts +28 -0
  145. package/dist/registry/targets/artifactory.d.ts.map +1 -0
  146. package/dist/registry/targets/artifactory.js +206 -0
  147. package/dist/registry/targets/fake-artifactory.d.ts +30 -0
  148. package/dist/registry/targets/fake-artifactory.d.ts.map +1 -0
  149. package/dist/registry/targets/fake-artifactory.js +142 -0
  150. package/dist/registry/targets/fake-gitlab.d.ts +39 -0
  151. package/dist/registry/targets/fake-gitlab.d.ts.map +1 -0
  152. package/dist/registry/targets/fake-gitlab.js +136 -0
  153. package/dist/registry/targets/fake-hcp.d.ts +50 -0
  154. package/dist/registry/targets/fake-hcp.d.ts.map +1 -0
  155. package/dist/registry/targets/fake-hcp.js +202 -0
  156. package/dist/registry/targets/fake-http.d.ts +31 -0
  157. package/dist/registry/targets/fake-http.d.ts.map +1 -0
  158. package/dist/registry/targets/fake-http.js +87 -0
  159. package/dist/registry/targets/fake-nexus.d.ts +27 -0
  160. package/dist/registry/targets/fake-nexus.d.ts.map +1 -0
  161. package/dist/registry/targets/fake-nexus.js +137 -0
  162. package/dist/registry/targets/gitlab.d.ts +53 -0
  163. package/dist/registry/targets/gitlab.d.ts.map +1 -0
  164. package/dist/registry/targets/gitlab.js +187 -0
  165. package/dist/registry/targets/hcp.d.ts +29 -0
  166. package/dist/registry/targets/hcp.d.ts.map +1 -0
  167. package/dist/registry/targets/hcp.js +310 -0
  168. package/dist/registry/targets/hosted.d.ts +62 -0
  169. package/dist/registry/targets/hosted.d.ts.map +1 -0
  170. package/dist/registry/targets/hosted.js +74 -0
  171. package/dist/registry/targets/index.d.ts +18 -0
  172. package/dist/registry/targets/index.d.ts.map +1 -0
  173. package/dist/registry/targets/index.js +17 -0
  174. package/dist/registry/targets/module-target.d.ts +68 -0
  175. package/dist/registry/targets/module-target.d.ts.map +1 -0
  176. package/dist/registry/targets/module-target.js +63 -0
  177. package/dist/registry/targets/nexus.d.ts +43 -0
  178. package/dist/registry/targets/nexus.d.ts.map +1 -0
  179. package/dist/registry/targets/nexus.js +250 -0
  180. package/dist/registry/targets/zip.d.ts +55 -0
  181. package/dist/registry/targets/zip.d.ts.map +1 -0
  182. package/dist/registry/targets/zip.js +278 -0
  183. package/dist/registry/writer.d.ts +101 -0
  184. package/dist/registry/writer.d.ts.map +1 -0
  185. package/dist/registry/writer.js +109 -0
  186. package/dist/rules/record-store.ts +122 -0
  187. package/dist/rules/tf001.ts +2 -2
  188. package/dist/rules/tf024.ts +105 -22
  189. package/dist/rules/tf046.ts +204 -0
  190. package/dist/rules/tf047.ts +114 -0
  191. package/dist/state/azure-blob.d.ts +8 -67
  192. package/dist/state/azure-blob.d.ts.map +1 -1
  193. package/dist/state/azure-blob.js +6 -199
  194. package/dist/state/gcs.d.ts +7 -61
  195. package/dist/state/gcs.d.ts.map +1 -1
  196. package/dist/state/gcs.js +5 -222
  197. package/dist/state/object-store.d.ts +4 -33
  198. package/dist/state/object-store.d.ts.map +1 -1
  199. package/dist/state/object-store.js +4 -17
  200. package/dist/state/s3.d.ts +11 -58
  201. package/dist/state/s3.d.ts.map +1 -1
  202. package/dist/state/s3.js +10 -161
  203. package/dist/state/store.js +2 -2
  204. package/dist/state-ops.d.ts +255 -0
  205. package/dist/state-ops.d.ts.map +1 -0
  206. package/dist/state-ops.js +305 -0
  207. package/dist/state-reads.d.ts +57 -0
  208. package/dist/state-reads.d.ts.map +1 -0
  209. package/dist/state-reads.js +104 -0
  210. package/dist/used-sources.d.ts +54 -0
  211. package/dist/used-sources.d.ts.map +1 -0
  212. package/dist/used-sources.js +157 -0
  213. package/package.json +19 -3
  214. package/src/__fixtures__/drift-record/README.md +5 -0
  215. package/src/__fixtures__/drift-rows/live-root-plan.json +70 -0
  216. package/src/__fixtures__/drift-rows/refresh-plan.json +61 -0
  217. package/src/__fixtures__/used-sources/root/extra.tofu +1 -0
  218. package/src/__fixtures__/used-sources/root/main.tf +31 -0
  219. package/src/__fixtures__/used-sources/root/modules/net/main.tf +14 -0
  220. package/src/__fixtures__/used-sources/root/modules/net/versions.tf.json +7 -0
  221. package/src/__fixtures__/used-sources/unit/terragrunt.hcl +3 -0
  222. package/src/ci-setup.ts +42 -0
  223. package/src/components/capability-plugin.test.ts +26 -14
  224. package/src/components/capability-plugin.ts +185 -31
  225. package/src/components/compose-group.ts +1 -1
  226. package/src/components/pr-loop-compose.tofu.e2e.test.ts +1 -1
  227. package/src/components/pr-loop.tofu.test.ts +1 -1
  228. package/src/components/release-inputs.ts +69 -0
  229. package/src/components/trace-facts.test.ts +21 -0
  230. package/src/components/trace-facts.ts +27 -0
  231. package/src/cone.ts +138 -0
  232. package/src/cost-hold.ts +29 -0
  233. package/src/describe-resources.engine-facts.test.ts +2 -3
  234. package/src/drift-rows.test.ts +135 -0
  235. package/src/drift-rows.ts +137 -0
  236. package/src/exit-codes.test.ts +23 -0
  237. package/src/exit-codes.ts +59 -0
  238. package/src/index.ts +3 -0
  239. package/src/lint/audit-catalog.ts +29 -3
  240. package/src/lint/audit-lineage.ts +14 -2
  241. package/src/lint/post-synth/fixtures/TF024/live-with-record-store/main.tf +18 -0
  242. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/estate.chdf.hcl +1 -0
  243. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/main.tf +10 -0
  244. package/src/lint/post-synth/fixtures/TF046/azurerm-no-resource-group/main.tf +15 -0
  245. package/src/lint/post-synth/fixtures/TF046/backend-config-only/main.tf +14 -0
  246. package/src/lint/post-synth/fixtures/TF046/derived-s3/backend.tf +7 -0
  247. package/src/lint/post-synth/fixtures/TF046/derived-s3/main.tf +9 -0
  248. package/src/lint/post-synth/fixtures/TF046/explicit-local/main.tf +16 -0
  249. package/src/lint/post-synth/fixtures/TF046/explicit-s3-beside-local-backend/main.tf +15 -0
  250. package/src/lint/post-synth/fixtures/TF046/implied-local/main.tf +9 -0
  251. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/estate.chdf.hcl +1 -0
  252. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/main.tf +5 -0
  253. package/src/lint/post-synth/fixtures/TF047/clear/main.tf +12 -0
  254. package/src/lint/post-synth/fixtures/TF047/refused/main.tf +12 -0
  255. package/src/lint/post-synth/fixtures/TF047/unknown-module/main.tf +12 -0
  256. package/src/lint/post-synth/fixtures/TF047/unknown-version/main.tf +12 -0
  257. package/src/lint/post-synth/index.ts +4 -0
  258. package/src/lint/post-synth/record-store.ts +122 -0
  259. package/src/lint/post-synth/tf001.ts +2 -2
  260. package/src/lint/post-synth/tf024.test.ts +26 -5
  261. package/src/lint/post-synth/tf024.ts +105 -22
  262. package/src/lint/post-synth/tf046.test.ts +137 -0
  263. package/src/lint/post-synth/tf046.ts +204 -0
  264. package/src/lint/post-synth/tf047.test.ts +56 -0
  265. package/src/lint/post-synth/tf047.ts +114 -0
  266. package/src/lock-rows.test.ts +70 -0
  267. package/src/lock-rows.ts +82 -0
  268. package/src/lock-timeout.ts +34 -0
  269. package/src/module-catalog/index.test.ts +93 -0
  270. package/src/module-catalog/index.ts +159 -0
  271. package/src/module-catalog/refresh-cli.ts +68 -0
  272. package/src/module-catalog/static-tier.ts +1659 -0
  273. package/src/module-catalog/types.ts +88 -0
  274. package/src/op/activities/terraform.ts +46 -5
  275. package/src/op-wave-step.test.ts +109 -0
  276. package/src/pin/__fixtures__/consumers/live/dev/main.tf +4 -0
  277. package/src/pin/__fixtures__/consumers/live/prod/main.tf +21 -0
  278. package/src/pin/__fixtures__/consumers/units/vpc/terragrunt.hcl +7 -0
  279. package/src/pin/consumers.test.ts +57 -0
  280. package/src/pin/consumers.ts +95 -0
  281. package/src/plugin.ts +6 -0
  282. package/src/registry/__fixtures__/docs/documented/.terraform-docs.yml +3 -0
  283. package/src/registry/__fixtures__/docs/documented/README.md +1 -0
  284. package/src/registry/__fixtures__/docs/documented/main.tf +8 -0
  285. package/src/registry/__fixtures__/docs/json/main.tf.json +14 -0
  286. package/src/registry/__fixtures__/docs/vpc/README.md +3 -0
  287. package/src/registry/__fixtures__/docs/vpc/examples/complete/main.tf +5 -0
  288. package/src/registry/__fixtures__/docs/vpc/examples/simple/README.md +1 -0
  289. package/src/registry/__fixtures__/docs/vpc/examples/simple/main.tf +4 -0
  290. package/src/registry/__fixtures__/docs/vpc/main.tf +14 -0
  291. package/src/registry/__fixtures__/docs/vpc/outputs.tf +9 -0
  292. package/src/registry/__fixtures__/docs/vpc/variables.tf +16 -0
  293. package/src/registry/__fixtures__/proxied-root/main.tf +12 -0
  294. package/src/registry/address.ts +100 -0
  295. package/src/registry/catalog-read.ts +56 -0
  296. package/src/registry/deprecated.test.ts +139 -0
  297. package/src/registry/deprecated.ts +76 -0
  298. package/src/registry/docs.test.ts +140 -0
  299. package/src/registry/docs.ts +299 -0
  300. package/src/registry/edge/bundle.test.ts +50 -0
  301. package/src/registry/edge/edge.test.ts +176 -0
  302. package/src/registry/edge/edge.ts +211 -0
  303. package/src/registry/edge/index.ts +10 -0
  304. package/src/registry/edge/lambda-edge.ts +127 -0
  305. package/src/registry/edge/sigv4-web.ts +79 -0
  306. package/src/registry/edge/token-source.test.ts +258 -0
  307. package/src/registry/edge/token-source.ts +169 -0
  308. package/src/registry/edge/worker.ts +61 -0
  309. package/src/registry/handler.ts +89 -0
  310. package/src/registry/headers-file.ts +61 -0
  311. package/src/registry/index.ts +31 -0
  312. package/src/registry/mirror-paths.ts +10 -0
  313. package/src/registry/mirror.test.ts +198 -0
  314. package/src/registry/mirror.ts +395 -0
  315. package/src/registry/proxies.test.ts +153 -0
  316. package/src/registry/proxies.ts +132 -0
  317. package/src/registry/reader.ts +225 -0
  318. package/src/registry/registry.test.ts +262 -0
  319. package/src/registry/semver.ts +34 -0
  320. package/src/registry/serve.ts +82 -0
  321. package/src/registry/source-forms.test.ts +87 -0
  322. package/src/registry/source-forms.ts +216 -0
  323. package/src/registry/targets/artifactory.test.ts +248 -0
  324. package/src/registry/targets/artifactory.ts +257 -0
  325. package/src/registry/targets/fake-artifactory.ts +158 -0
  326. package/src/registry/targets/fake-gitlab.ts +161 -0
  327. package/src/registry/targets/fake-hcp.ts +233 -0
  328. package/src/registry/targets/fake-http.ts +97 -0
  329. package/src/registry/targets/fake-nexus.ts +152 -0
  330. package/src/registry/targets/gitlab.test.ts +208 -0
  331. package/src/registry/targets/gitlab.ts +223 -0
  332. package/src/registry/targets/hcp.test.ts +230 -0
  333. package/src/registry/targets/hcp.ts +360 -0
  334. package/src/registry/targets/hosted.ts +117 -0
  335. package/src/registry/targets/index.ts +17 -0
  336. package/src/registry/targets/module-target.ts +131 -0
  337. package/src/registry/targets/nexus.test.ts +220 -0
  338. package/src/registry/targets/nexus.ts +314 -0
  339. package/src/registry/targets/zip.test.ts +65 -0
  340. package/src/registry/targets/zip.ts +287 -0
  341. package/src/registry/writer.ts +155 -0
  342. package/src/state/azure-blob.ts +6 -221
  343. package/src/state/gcs.ts +5 -252
  344. package/src/state/object-store.ts +4 -32
  345. package/src/state/s3.ts +10 -182
  346. package/src/state/store.ts +2 -2
  347. package/src/state-ops.test.ts +182 -0
  348. package/src/state-ops.ts +462 -0
  349. package/src/state-reads.ts +116 -0
  350. package/src/used-sources.test.ts +75 -0
  351. package/src/used-sources.ts +204 -0
package/src/cone.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * choudoufu's cone plans (#4545, moved from terragucci's `cone.ts`): `plan
3
+ * -cone=<base>` plans only the blocks a change touches and every block that
4
+ * reads them.
5
+ *
6
+ * A wave that applies an open pull request plans a live root as the cone of
7
+ * its change since the pull request's base, as the plan note did. A wave
8
+ * that applies a merge commit and a drift run plan the whole estate, as do
9
+ * a root with no `live` block, an estate file (`estate.chdf.hcl`), whose
10
+ * form choudoufu refuses `-cone` on, and any binary but choudoufu.
11
+ *
12
+ * choudoufu's guard can plan the whole estate anyway (no base, a provider,
13
+ * terraform or live block or the lock file changed, a cone over 30% of the
14
+ * estate's blocks, and so on). It says why in a warning. A cone plan says
15
+ * its size in a warning too, and with `-out FILE` writes `FILE.cone.json`:
16
+ * the cone's base, the commit it named and the addresses it targets.
17
+ *
18
+ * The record's targets join the root's plan digest ({@link conePlanDigest}),
19
+ * so the digest an approval binds names the cone. A wave that plans again
20
+ * after its approval and finds another cone has another digest, and refuses
21
+ * as moved. choudoufu's `apply FILE` checks the same record again.
22
+ *
23
+ * Pure, apart from reading the record beside a plan file.
24
+ */
25
+ import { existsSync, readFileSync } from "node:fs";
26
+ import { basename } from "node:path";
27
+ import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
28
+
29
+ /** The flags choudoufu refuses beside `-cone`, or plans in full with: a plan that carries one gets no `-cone`. */
30
+ export const CONE_EXCLUDES = ["-target", "-exclude", "-destroy", "-refresh-only", "-json", "-estate"] as const;
31
+
32
+ /** What choudoufu writes beside a saved cone plan, `FILE.cone.json`. */
33
+ export interface ConeRecord {
34
+ base: string;
35
+ base_sha: string;
36
+ targets: string[];
37
+ }
38
+
39
+ /** What one root's cone plan did: planned the cone, or planned the whole estate for the guard's reason. */
40
+ export interface PlanCone {
41
+ /** The revision the plan was asked to measure the change from. */
42
+ base: string;
43
+ /** With a cone: its blocks, and the estate's, as choudoufu counted them. Absent when it did not say. */
44
+ blocks?: number;
45
+ of?: number;
46
+ /** With a cone: the record beside the plan file. */
47
+ record?: ConeRecord;
48
+ /** The guard planned the whole estate: its kind and why. */
49
+ fallback?: { kind: string; reason: string };
50
+ }
51
+
52
+ /** Whether `binary` (a name or a path) is choudoufu. */
53
+ export const isChoudoufu = (binary: string): boolean => basename(binary) === "choudoufu";
54
+
55
+ /**
56
+ * The `-cone` argument a plan takes: `-cone=<base>` for choudoufu with a
57
+ * base, on a root that takes it (`live`), when none of `args` is a flag in
58
+ * {@link CONE_EXCLUDES}. Otherwise none, and the plan is full.
59
+ */
60
+ export function coneArgs(binary: string, base: string | undefined, args: readonly string[], cone: boolean): string[] {
61
+ if (!base || !cone || !isChoudoufu(binary)) return [];
62
+ const excluded = args.some((a) => CONE_EXCLUDES.some((f) => a === f || a.startsWith(`${f}=`)));
63
+ return excluded ? [] : [`-cone=${base}`];
64
+ }
65
+
66
+ /** The path of the cone record beside a plan file. */
67
+ export const coneRecordPath = (planFile: string): string => `${planFile}.cone.json`;
68
+
69
+ /** The cone record beside `planFile`, or none when there is none or it does not read as one. */
70
+ export function readConeRecord(planFile: string): ConeRecord | undefined {
71
+ const path = coneRecordPath(planFile);
72
+ if (!existsSync(path)) return undefined;
73
+ try {
74
+ const r = JSON.parse(readFileSync(path, "utf-8")) as Partial<ConeRecord> | null;
75
+ if (!r || typeof r.base !== "string" || !Array.isArray(r.targets) || !r.targets.every((t) => typeof t === "string")) return undefined;
76
+ return { base: r.base, base_sha: typeof r.base_sha === "string" ? r.base_sha : "", targets: [...r.targets].sort() };
77
+ } catch {
78
+ return undefined;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * The detail of the last warning with this summary in a plan's output, its
84
+ * lines joined. The binary prints a warning as `Warning: <summary>`, a blank
85
+ * line, and the detail up to the next blank line or the plan's summary; a box
86
+ * drawn around it with colour on is stripped.
87
+ */
88
+ function warningDetail(text: string, summary: string): string | undefined {
89
+ const lines = text.replace(/\r\n/g, "\n").split("\n").map((l) => l.replace(/^[\s│╷╵]*/, "").trimEnd());
90
+ let found: string | undefined;
91
+ for (let i = 0; i < lines.length; i++) {
92
+ if (lines[i] !== `Warning: ${summary}`) continue;
93
+ let j = i + 1;
94
+ while (j < lines.length && lines[j] === "") j++;
95
+ const detail: string[] = [];
96
+ for (; j < lines.length && lines[j] !== "" && !/^(Plan: |No changes\.|Changes to Outputs:)/.test(lines[j]!); j++) detail.push(lines[j]!);
97
+ found = detail.join(" ").replace(/\s+/g, " ").trim();
98
+ }
99
+ return found;
100
+ }
101
+
102
+ /**
103
+ * What a plan run with `-cone=<base>` did, from its output and the record
104
+ * beside `planFile`. A fallback warning means the whole estate was planned,
105
+ * even after a cone warning. With no fallback, the record is the cone. With
106
+ * neither, choudoufu said nothing this reads, and the plan counts as full.
107
+ */
108
+ export function readPlanCone(output: string, planFile: string, base: string): PlanCone {
109
+ const fell = warningDetail(output, "Cone plan fell back to a full plan");
110
+ if (fell !== undefined) {
111
+ const m = /^This plan covers the whole estate \(guard ([^)]*)\): (.*?)\.?$/.exec(fell);
112
+ return { base, fallback: m ? { kind: m[1]!, reason: m[2]! } : { kind: "unknown", reason: fell } };
113
+ }
114
+ const record = readConeRecord(planFile);
115
+ if (!record) return { base, fallback: { kind: "unknown", reason: "choudoufu wrote no cone record beside the plan" } };
116
+ const size = /: (\d+) of the estate's (\d+) blocks/.exec(warningDetail(output, "Cone plan") ?? "");
117
+ return { base, ...(size ? { blocks: Number(size[1]), of: Number(size[2]) } : {}), record };
118
+ }
119
+
120
+ /** What a cone plan did, in words: its size, or why it planned the whole estate. */
121
+ export function coneText(cone: PlanCone): string {
122
+ if (cone.fallback) return `the whole estate, since ${cone.fallback.reason} (guard ${cone.fallback.kind})`;
123
+ const n = cone.record?.targets.length ?? 0;
124
+ const size = cone.blocks !== undefined && cone.of !== undefined ? `${cone.blocks} of the estate's ${cone.of} blocks` : `${n} target${n === 1 ? "" : "s"}`;
125
+ return `the cone of the change since ${cone.base}, ${size}`;
126
+ }
127
+
128
+ /**
129
+ * A root's plan digest with its cone in it: the plan's own digest, unchanged,
130
+ * when the plan was full; with a cone, the digest over the plan's digest and
131
+ * the cone's targets, so an approval of one cone counts for no other. The
132
+ * digest's kind is terragucci's, so approvals recorded before the move still
133
+ * name the same digest.
134
+ */
135
+ export function conePlanDigest(planDigest: string, cone: PlanCone | undefined): string {
136
+ if (!cone?.record) return planDigest;
137
+ return computePlanDigest("terragucci-cone-plan", { plan: planDigest, targets: [...cone.record.targets].sort() });
138
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The terraform lexicon's preset for an op wave's cost hold (#4545,
3
+ * `holds.cost` on the spec): infracost over each root's plan document, the
4
+ * `show -json` file `terraform-apply`'s plan names as `artifact.planJson`.
5
+ *
6
+ * ```ts
7
+ * const spec = { ..., step: { kind: "terraform-apply", root: "{target}" }, holds: { cost: terraformCostHold({ approveAbove: 100 }) } };
8
+ * ```
9
+ *
10
+ * The shape is core's `OpWaveCostHold`, written out here so the lexicon
11
+ * builds against a core without it.
12
+ */
13
+
14
+ /** infracost's own command over one plan document. */
15
+ export const INFRACOST_ARGV = ["infracost", "breakdown", "--path", "{planJson}", "--format", "json", "--log-level", "error"] as const;
16
+
17
+ export interface TerraformCostHold {
18
+ command: string[];
19
+ approveAbove: number;
20
+ format: "infracost";
21
+ currency?: string;
22
+ }
23
+
24
+ /** A cost hold that prices each root with infracost, waiting over `approveAbove` a month. `command` replaces infracost's argv; it must name `{planJson}`. */
25
+ export function terraformCostHold(options: { approveAbove: number; currency?: string; command?: string[] }): TerraformCostHold {
26
+ const command = options.command ?? [...INFRACOST_ARGV];
27
+ if (!command.some((a) => a.includes("{planJson}"))) throw new Error("terraformCostHold: the command must name {planJson}, the root's plan document");
28
+ return { command, approveAbove: options.approveAbove, format: "infracost", ...(options.currency ? { currency: options.currency } : {}) };
29
+ }
@@ -38,7 +38,6 @@ import { normalizeObservation } from "@intentius/chant/observation";
38
38
  import type { Declarable } from "@intentius/chant/declarable";
39
39
  import { buildLiveGraphIr } from "@intentius/chant/graph-ir";
40
40
  import { ENGINE_FACTS_FLOOR } from "@intentius/chant/engine-facts";
41
- import { NEXT_RELEASE } from "@intentius/chant/release-floors";
42
41
  import {
43
42
  describeResources,
44
43
  readEngineGraph,
@@ -406,7 +405,7 @@ describe("the floors (#4528)", () => {
406
405
  expect(isOlderVersion(MIN_CHOUDOUFU_VERSION, MIN_CHOUDOUFU_GRAPH_VERSION)).toBe(true);
407
406
  });
408
407
 
409
- it("the chant floor for writing facts is the next release", () => {
410
- expect(ENGINE_FACTS_FLOOR).toBe(NEXT_RELEASE);
408
+ it("the chant floor for writing facts shipped in 0.124.54", () => {
409
+ expect(ENGINE_FACTS_FLOOR).toBe("0.124.54");
411
410
  });
412
411
  });
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Drift rows (#4580) over two hand-written plans: a stock root's
3
+ * refresh-only plan with drift and a create row, and a full plan of a root
4
+ * under live resource markers. The count and names are checked against a
5
+ * copy of terragucci's own rules (packages/terragucci/src/report/drift.ts
6
+ * at integration/ci-products), so a product that moves onto these rows
7
+ * reports the same numbers.
8
+ */
9
+
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { describe, expect, test } from "vitest";
13
+ import { terraformPlanDriftCount, terraformPlanDriftNames, terraformPlanDriftRows } from "./drift-rows";
14
+
15
+ const FIX = join(import.meta.dirname, "__fixtures__", "drift-rows");
16
+ const read = (name: string): unknown => JSON.parse(readFileSync(join(FIX, name), "utf-8"));
17
+
18
+ // ── terragucci's rules, copied as they are, for the parity checks ─────────────
19
+
20
+ type Json = Record<string, unknown>;
21
+ const FLIP: Record<string, string> = { create: "delete", delete: "create" };
22
+
23
+ function liveDrift(plan: unknown): unknown {
24
+ if (plan === null || typeof plan !== "object" || Array.isArray(plan)) return plan;
25
+ const p = plan as Json;
26
+ const changes = Array.isArray(p.resource_changes) ? (p.resource_changes as Json[]) : [];
27
+ const resource_drift = changes
28
+ .filter((r) => r.mode !== "data")
29
+ .flatMap((r) => {
30
+ const ch = (r.change ?? {}) as Json;
31
+ const actions = Array.isArray(ch.actions) ? (ch.actions as string[]) : [];
32
+ if (actions.length === 0 || actions.every((a) => a === "no-op" || a === "read")) return [];
33
+ const action = actions.length === 1 ? (FLIP[actions[0]!] ?? actions[0]!) : "update";
34
+ const live = (ch.before ?? null) as Json | null;
35
+ let code = (ch.after ?? null) as Json | null;
36
+ const unknown = ch.after_unknown !== null && typeof ch.after_unknown === "object" ? (ch.after_unknown as Json) : {};
37
+ if (code && live) code = { ...code, ...Object.fromEntries(Object.keys(unknown).filter((k) => unknown[k] === true && k in live).map((k) => [k, live[k]])) };
38
+ return [{
39
+ ...r,
40
+ change: { actions: [action], before: code, after: live, before_sensitive: ch.after_sensitive ?? false, after_sensitive: ch.before_sensitive ?? false },
41
+ }];
42
+ });
43
+ return { ...p, resource_drift };
44
+ }
45
+
46
+ function driftCount(plan: unknown): number {
47
+ const list = plan !== null && typeof plan === "object" ? (plan as Json).resource_drift : undefined;
48
+ return Array.isArray(list) ? list.length : 0;
49
+ }
50
+
51
+ function driftNames(plan: unknown): Map<string, string> {
52
+ const out = new Map<string, string>();
53
+ const list = plan !== null && typeof plan === "object" ? (plan as Json).resource_drift : undefined;
54
+ if (!Array.isArray(list)) return out;
55
+ for (const r of list as Json[]) {
56
+ const ch = (r.change ?? {}) as Json;
57
+ const values = [ch.after, ch.before].filter((v): v is Json => v !== null && typeof v === "object");
58
+ for (const key of ["name", "id", "bucket", "function_name", "arn"]) {
59
+ const v = values.map((x) => x[key]).find((x) => typeof x === "string" && x !== "");
60
+ if (typeof v === "string") {
61
+ out.set(String(r.address), v);
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+
69
+ // ── the rows ──────────────────────────────────────────────────────────────────
70
+
71
+ describe("a stock root's refresh-only plan", () => {
72
+ const plan = read("refresh-plan.json");
73
+ const rows = terraformPlanDriftRows(plan);
74
+
75
+ test("every resource_drift entry is a row, creates and data blocks included", () => {
76
+ expect(rows).toEqual([
77
+ { address: "aws_s3_bucket.logs", action: "update", type: "aws_s3_bucket", name: "acme-logs" },
78
+ { address: "aws_lambda_function.worker", action: "delete", type: "aws_lambda_function", name: "acme-worker" },
79
+ { address: "aws_sqs_queue.jobs", action: "create", type: "aws_sqs_queue", name: "acme-jobs" },
80
+ { address: "aws_iam_role.runner", action: "update", type: "aws_iam_role", name: "arn:aws:iam::000000000000:role/acme-runner" },
81
+ { address: "data.aws_caller_identity.me", action: "update", type: "aws_caller_identity" },
82
+ ]);
83
+ });
84
+
85
+ test("rows carry no attribute values", () => {
86
+ for (const row of rows) expect(Object.keys(row).every((k) => ["address", "action", "type", "name"].includes(k))).toBe(true);
87
+ });
88
+
89
+ test("the count and names match terragucci's report", () => {
90
+ expect(terraformPlanDriftCount(plan)).toBe(driftCount(plan));
91
+ expect(terraformPlanDriftCount(plan)).toBe(5);
92
+ expect(terraformPlanDriftNames(plan)).toEqual(driftNames(plan));
93
+ });
94
+
95
+ test("a delete-and-create pair is a replace", () => {
96
+ const pair = { resource_drift: [{ address: "a.b", change: { actions: ["delete", "create"], before: { id: "x" }, after: { id: "y" } } }] };
97
+ expect(terraformPlanDriftRows(pair)).toEqual([{ address: "a.b", action: "replace", name: "y" }]);
98
+ });
99
+ });
100
+
101
+ describe("a live root's full plan", () => {
102
+ const plan = read("live-root-plan.json");
103
+ const rows = terraformPlanDriftRows(plan, { live: true });
104
+
105
+ test("changes read as drift from the other side, data, no-op and resource_drift left out", () => {
106
+ expect(rows).toEqual([
107
+ { address: "aws_s3_bucket.assets", action: "delete", type: "aws_s3_bucket", name: "acme-assets" },
108
+ { address: "aws_sqs_queue.stray", action: "create", type: "aws_sqs_queue", name: "acme-stray" },
109
+ { address: "aws_lambda_function.api", action: "update", type: "aws_lambda_function", name: "acme-api" },
110
+ { address: "aws_iam_role.deployer", action: "update", type: "aws_iam_role", name: "acme-deployer-old" },
111
+ { address: "aws_security_group.edge", action: "update", type: "aws_security_group", name: "sg-0001" },
112
+ ]);
113
+ });
114
+
115
+ test("the count and names match terragucci's report after liveDrift", () => {
116
+ const drifted = liveDrift(plan);
117
+ expect(terraformPlanDriftCount(plan, { live: true })).toBe(driftCount(drifted));
118
+ expect(terraformPlanDriftCount(plan, { live: true })).toBe(5);
119
+ expect(terraformPlanDriftNames(plan, { live: true })).toEqual(driftNames(drifted));
120
+ });
121
+
122
+ test("read as a stock plan, the same document counts only its resource_drift", () => {
123
+ expect(terraformPlanDriftCount(plan)).toBe(1);
124
+ });
125
+ });
126
+
127
+ describe("documents with nothing to read", () => {
128
+ test("a missing or malformed plan has no rows", () => {
129
+ for (const plan of [undefined, null, "plan", [], {}, { resource_drift: "x" }]) {
130
+ expect(terraformPlanDriftRows(plan)).toEqual([]);
131
+ expect(terraformPlanDriftRows(plan, { live: true })).toEqual([]);
132
+ expect(terraformPlanDriftCount(plan)).toBe(driftCount(plan));
133
+ }
134
+ });
135
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Drift rows (#4580): one row per object a plan found drifted, by address,
3
+ * with its action and the object's real name, counted the way a product's
4
+ * drift report counts. `./drift-record.ts` answers a different question (the
5
+ * status of each instance the plan names), so it skips create rows, carries
6
+ * no names and stays as it is.
7
+ *
8
+ * A stock root's refresh-only plan reports drift as `resource_drift`, and
9
+ * every entry there is one row, data blocks and creates included. A full
10
+ * plan of a root under live resource markers (`live: true`) reads drift from
11
+ * `resource_changes` instead, seen from the other side: a create there is an
12
+ * object that no longer exists (delete), a delete one that exists outside
13
+ * the code (create), a replacement or any other pair of actions an update.
14
+ * Data blocks, no-op and read entries are not drift.
15
+ *
16
+ * These are terragucci's rules (packages/terragucci/src/report/drift.ts:
17
+ * `liveDrift`, `driftCount`, `driftNames`), so a row count here equals the
18
+ * count its report gives for the same plan.
19
+ *
20
+ * A row carries no attribute values. Its `name` is the first non-empty
21
+ * string among `name`, `id`, `bucket`, `function_name` and `arn`, read from
22
+ * the object as found and then as the code has it; a deleted object has no
23
+ * other place its name survives.
24
+ *
25
+ * The module imports nothing, so a product can load it behind a
26
+ * feature-detect without pulling the lexicon's entry point.
27
+ */
28
+
29
+ type Json = Record<string, unknown>;
30
+
31
+ const isObject = (v: unknown): v is Json => v !== null && typeof v === "object" && !Array.isArray(v);
32
+
33
+ /** One drifted object. */
34
+ export interface TerraformDriftRow {
35
+ /** The resource address, `""` when the plan's entry has none. */
36
+ address: string;
37
+ /**
38
+ * What happened outside the code: `update`, `delete` (the object is gone),
39
+ * `create` (an object exists that state or code does not hold), or on a
40
+ * stock root whatever single action the plan's entry names. On a stock
41
+ * root a delete-and-create pair is `replace`, and any other pair `update`.
42
+ */
43
+ action: string;
44
+ /** The resource type, when the entry names one. */
45
+ type?: string;
46
+ /** The object's real name, by the rule in this module's header. */
47
+ name?: string;
48
+ }
49
+
50
+ export interface TerraformPlanDriftRowsOptions {
51
+ /**
52
+ * The plan is a full plan of a root under live resource markers, whose
53
+ * changes are its drift. Default false: a refresh-only plan's
54
+ * `resource_drift`.
55
+ */
56
+ live?: boolean;
57
+ }
58
+
59
+ const FLIP: Record<string, string> = { create: "delete", delete: "create" };
60
+ const NAME_KEYS = ["name", "id", "bucket", "function_name", "arn"] as const;
61
+
62
+ function nameOf(values: readonly unknown[]): string | undefined {
63
+ const objects = values.filter(isObject);
64
+ for (const key of NAME_KEYS) {
65
+ const v = objects.map((x) => x[key]).find((x) => typeof x === "string" && x !== "");
66
+ if (typeof v === "string") return v;
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ function rowOf(entry: unknown, action: string, values: readonly unknown[]): TerraformDriftRow {
72
+ const r = isObject(entry) ? entry : {};
73
+ const name = nameOf(values);
74
+ return {
75
+ address: typeof r.address === "string" ? r.address : "",
76
+ action,
77
+ ...(typeof r.type === "string" && r.type !== "" ? { type: r.type } : {}),
78
+ ...(name !== undefined ? { name } : {}),
79
+ };
80
+ }
81
+
82
+ function stockAction(actions: readonly string[]): string {
83
+ if (actions.length === 1) return actions[0]!;
84
+ if (actions.length === 2 && actions.includes("delete") && actions.includes("create")) return "replace";
85
+ return "update";
86
+ }
87
+
88
+ /** A refresh-only plan's `resource_drift`, one row per entry. */
89
+ function stockRows(doc: Json): TerraformDriftRow[] {
90
+ const list = Array.isArray(doc.resource_drift) ? doc.resource_drift : [];
91
+ return list.map((entry) => {
92
+ const ch = isObject(entry) && isObject(entry.change) ? entry.change : {};
93
+ const actions = Array.isArray(ch.actions) ? ch.actions.map(String) : [];
94
+ return rowOf(entry, stockAction(actions), [ch.after, ch.before]);
95
+ });
96
+ }
97
+
98
+ /** A live root's full plan, its changes read as drift. */
99
+ function liveRows(doc: Json): TerraformDriftRow[] {
100
+ const list = Array.isArray(doc.resource_changes) ? doc.resource_changes : [];
101
+ const out: TerraformDriftRow[] = [];
102
+ for (const entry of list) {
103
+ if (!isObject(entry) || entry.mode === "data") continue;
104
+ const ch = isObject(entry.change) ? entry.change : {};
105
+ const actions = Array.isArray(ch.actions) ? ch.actions.map(String) : [];
106
+ if (actions.length === 0 || actions.every((a) => a === "no-op" || a === "read")) continue;
107
+ const action = actions.length === 1 ? (FLIP[actions[0]!] ?? actions[0]!) : "update";
108
+ const live = isObject(ch.before) ? ch.before : null;
109
+ let code = isObject(ch.after) ? ch.after : null;
110
+ // A value the plan knows only after the apply is not a difference: the live one stands for it.
111
+ const unknown = isObject(ch.after_unknown) ? ch.after_unknown : {};
112
+ if (code && live) code = { ...code, ...Object.fromEntries(Object.keys(unknown).filter((k) => unknown[k] === true && k in live).map((k) => [k, live[k]])) };
113
+ out.push(rowOf(entry, action, [live, code]));
114
+ }
115
+ return out;
116
+ }
117
+
118
+ /**
119
+ * The drift a plan (`show -json`, parsed) found, one row per drifted object
120
+ * in the plan's order. Its length is the drift count.
121
+ */
122
+ export function terraformPlanDriftRows(plan: unknown, options: TerraformPlanDriftRowsOptions = {}): TerraformDriftRow[] {
123
+ if (!isObject(plan)) return [];
124
+ return options.live ? liveRows(plan) : stockRows(plan);
125
+ }
126
+
127
+ /** How many objects a plan found drifted: `terraformPlanDriftRows(plan, options).length`. */
128
+ export function terraformPlanDriftCount(plan: unknown, options: TerraformPlanDriftRowsOptions = {}): number {
129
+ return terraformPlanDriftRows(plan, options).length;
130
+ }
131
+
132
+ /** Each drifted object's real name by address, for the rows that have one. */
133
+ export function terraformPlanDriftNames(plan: unknown, options: TerraformPlanDriftRowsOptions = {}): Map<string, string> {
134
+ const out = new Map<string, string>();
135
+ for (const row of terraformPlanDriftRows(plan, options)) if (row.name !== undefined) out.set(row.address, row.name);
136
+ return out;
137
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { CHANT_EXIT } from "@intentius/chant/exit-codes";
3
+ import { terraformExitOf, terraformPlanMoved } from "./exit-codes";
4
+
5
+ describe("terraform, tofu and choudoufu exit statuses on chant's table (#4553)", () => {
6
+ test("choudoufu's refusals of a moved plan are chant's moved code", () => {
7
+ expect(terraformExitOf({ command: "live-wave-apply", code: 3 })).toBe(CHANT_EXIT.moved);
8
+ expect(terraformExitOf({ command: "apply", code: 3, refusal: "approval-mismatch" })).toBe(CHANT_EXIT.moved);
9
+ expect(terraformPlanMoved({ command: "live-wave-apply", code: 3 })).toBe(true);
10
+ });
11
+
12
+ test("a plan made against another estate is a failure, and so is a root that failed", () => {
13
+ expect(terraformExitOf({ command: "apply", code: 3, refusal: "wrong-estate" })).toBe(CHANT_EXIT.failed);
14
+ expect(terraformExitOf({ command: "live-wave-apply", code: 4 })).toBe(CHANT_EXIT.failed);
15
+ expect(terraformExitOf({ command: "live-plan-set", code: 4 })).toBe(CHANT_EXIT.failed);
16
+ });
17
+
18
+ test("a plan's 2 is drift only under a drift check", () => {
19
+ expect(terraformExitOf({ command: "plan", code: 2 })).toBe(CHANT_EXIT.done);
20
+ expect(terraformExitOf({ command: "plan", code: 2, drift: true })).toBe(CHANT_EXIT.drift);
21
+ expect(terraformExitOf({ command: "plan", code: 1 })).toBe(CHANT_EXIT.failed);
22
+ });
23
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * OpenTofu, Terraform and choudoufu outcomes mapped onto chant's exit-code
3
+ * table (#4553, `CHANT_EXIT` in `@intentius/chant/exit-codes`).
4
+ *
5
+ * The lexicon never picks the process's exit code itself: for a moved plan it
6
+ * throws an error core marks as moved (`planMoved: true`, read by
7
+ * `isPlanMovedError`), and core exits `CHANT_EXIT.moved`. This mapping says
8
+ * which engine outcome is which row of the table, so the throw sites and the
9
+ * reference page agree.
10
+ *
11
+ * | Engine outcome | Table |
12
+ * |---|---|
13
+ * | `plan -detailed-exitcode` 0 or 2 | not an exit: the plan's answer. 2 under a drift check is `drift` |
14
+ * | any 1 | `failed` |
15
+ * | choudoufu `apply <planfile>` 3, approval mismatch | `moved` |
16
+ * | choudoufu `apply <planfile>` 3, plan made against another estate | `failed`: the pipeline handed the wrong file, and no approval fixes that |
17
+ * | choudoufu `live-wave-apply` 3, set moved | `moved` |
18
+ * | choudoufu `live-wave-apply` 4 or `live-plan-set` 4, a root failed | `failed` |
19
+ * | a composed plan that moved | `moved` |
20
+ * | `live-affected` 2, `live-cone` 2 | planner answers, never a job's exit |
21
+ */
22
+ import { CHANT_EXIT, type ChantExitCode } from "@intentius/chant/exit-codes";
23
+ import { CHOUDOUFU_APPROVAL_EXIT_CODE, CHOUDOUFU_WAVE_EXIT, type TerraformApprovalRefusal } from "./op/activities/terraform";
24
+
25
+ /** The command whose exit status is mapped. */
26
+ export type TerraformExitSource =
27
+ /** `terraform|tofu|choudoufu plan -detailed-exitcode`; `drift` says a drift check asked for it. */
28
+ | { command: "plan"; code: number; drift?: boolean }
29
+ /** `choudoufu apply <planfile>` on a live root; `refusal` is `classifyApprovalRefusal`'s answer for its output. */
30
+ | { command: "apply"; code: number; refusal?: TerraformApprovalRefusal }
31
+ /** `choudoufu live-wave-apply`. */
32
+ | { command: "live-wave-apply"; code: number }
33
+ /** `choudoufu live-plan-set`. */
34
+ | { command: "live-plan-set"; code: number };
35
+
36
+ /**
37
+ * The table's code for a terraform, tofu or choudoufu exit status. A plan's
38
+ * 0 and 2 are both `done` unless a drift check asked, when 2 is `drift`.
39
+ */
40
+ export function terraformExitOf(source: TerraformExitSource): ChantExitCode {
41
+ const { code } = source;
42
+ if (code === 0) return CHANT_EXIT.done;
43
+ switch (source.command) {
44
+ case "plan":
45
+ if (code === 2) return source.drift ? CHANT_EXIT.drift : CHANT_EXIT.done;
46
+ return CHANT_EXIT.failed;
47
+ case "apply":
48
+ return code === CHOUDOUFU_APPROVAL_EXIT_CODE && source.refusal === "approval-mismatch" ? CHANT_EXIT.moved : CHANT_EXIT.failed;
49
+ case "live-wave-apply":
50
+ return code === CHOUDOUFU_WAVE_EXIT.setMoved ? CHANT_EXIT.moved : CHANT_EXIT.failed;
51
+ case "live-plan-set":
52
+ return code === 2 ? CHANT_EXIT.done : CHANT_EXIT.failed;
53
+ }
54
+ }
55
+
56
+ /** Whether `source` is a plan that moved since its approval: the outcomes the lexicon throws a `planMoved` error for. */
57
+ export function terraformPlanMoved(source: TerraformExitSource): boolean {
58
+ return terraformExitOf(source) === CHANT_EXIT.moved;
59
+ }
package/src/index.ts CHANGED
@@ -66,6 +66,7 @@ export {
66
66
 
67
67
  // The plan shape of a `show -json` plan (#4436), for core's `coveredRound`.
68
68
  export { terraformPlanShape, type TerraformPlanShapeOptions } from "./plan-shape";
69
+ export { terraformLockRows, terraformLockRowsFromPart, terraformDefaultLockGrain, terraformLockGrainRefusal } from "./lock-rows";
69
70
 
70
71
  // Composed runs (#4419, #4438): which roots compose, in what order, and why
71
72
  // the others fall back. Off unless `terraform.compose.enabled` is set.
@@ -208,3 +209,5 @@ export {
208
209
  type TerraformLiveRead,
209
210
  type TerraformPredictOptions,
210
211
  } from "./behaviour";
212
+ // terraform, tofu and choudoufu exit statuses on chant's exit-code table (#4553).
213
+ export { terraformExitOf, terraformPlanMoved, type TerraformExitSource } from "./exit-codes";
@@ -88,10 +88,11 @@ export const terraformAuditCatalog: Record<string, RuleMeta> = {
88
88
  tier: "merge-worthy",
89
89
  fixKind: "guidance",
90
90
  category: "correctness",
91
- title: "Live root declares a backend or cloud block, which choudoufu refuses",
91
+ title: "Live root declares a backend or cloud block, which choudoufu ignores",
92
92
  remediation:
93
- "Remove the `backend`/`cloud` block from the live root's terraform block; a live root's prior " +
94
- "state is a projection rebuilt from the live system every run, so there is no state to store.",
93
+ "Keep a `backend` block if other roots read this estate's outputs or the record store should be " +
94
+ "derived from its bucket; declare `record_store` to put records elsewhere. Remove a `cloud` block, " +
95
+ "which choudoufu drops entirely, or swap it for a `backend` block.",
95
96
  yamlBased: false,
96
97
  },
97
98
  TF025: {
@@ -551,6 +552,31 @@ export const terraformAuditCatalog: Record<string, RuleMeta> = {
551
552
  'Set `terragrunt_version_constraint = ">= 1.1.0"` in the root config. Off by default; judged only across the configs scanned.',
552
553
  yamlBased: false,
553
554
  },
555
+ TF046: {
556
+ id: "TF046",
557
+ tier: "report-only",
558
+ fixKind: "guidance",
559
+ category: "correctness",
560
+ title: "Live root resolves to the local record store in a CI run",
561
+ remediation:
562
+ "Keep or add a `backend` block of type s3, gcs or azurerm with a literal bucket, so choudoufu " +
563
+ "derives the record store from it, or declare `record_store` in the live block or `estate.chdf.hcl`. " +
564
+ "The rule is off by default and terragucci turns it on for its users; an emulator or test lane " +
565
+ "that keeps the local store on purpose sets `CHANT_ALLOW_LOCAL_RECORD_STORE=1`.",
566
+ yamlBased: false,
567
+ },
568
+ TF047: {
569
+ id: "TF047",
570
+ tier: "report-only",
571
+ fixKind: "guidance",
572
+ category: "correctness",
573
+ title: "Live root pins a module version the known-module catalog has no row for, or one with refusals",
574
+ remediation:
575
+ "Run `choudoufu live-check` on the root to see each refused site, or pin a version whose examples are clear in " +
576
+ "the static tier of choudoufu's module table (https://intentius.io/choudoufu/aws/compatibility/#modules). " +
577
+ "Off by default; terragucci turns it on for its users.",
578
+ yamlBased: false,
579
+ },
554
580
  };
555
581
 
556
582
  // Prior art credits, if any, live beside the rules in ./audit-lineage.ts (see
@@ -76,8 +76,8 @@
76
76
  *
77
77
  * TF024 (#2103, live root declares a backend/cloud block) credits choudoufu's
78
78
  * own `RuleStateBackend`, `internal/live/lint/issue.go` of
79
- * github.com/INTENTIUS/choudoufu: the identical condition, checked the same
80
- * way this fork checks it at `init` before any command runs. TF025 (a live
79
+ * github.com/INTENTIUS/choudoufu: the same condition, which choudoufu has
80
+ * reported as a warning since its #2213 (the block is ignored, not refused). TF025 (a live
81
81
  * root's non-default `terraform.workspace`) has no entry: its workspace half
82
82
  * is refused at the CLI argument-parsing layer there
83
83
  * (`internal/command/live_mode.go`), not by a dedicated lint rule under
@@ -110,6 +110,18 @@
110
110
  * a root's mode, and no surveyed tool has an Op model to read: choudoufu lints
111
111
  * HCL, and every tflint ruleset, tfsec/trivy-checks and the Sentinel policies
112
112
  * do the same. There is nothing upstream to credit.
113
+ *
114
+ * TF046 (chant#4561, a live root on the local record store in a CI run) has
115
+ * no entry. choudoufu prints where records go on every run and reports a
116
+ * `record_store.note` in `live-check -json` (INTENTIUS/choudoufu#2293), but
117
+ * it has no `internal/live/lint` rule for the local store, and no surveyed
118
+ * tool knows about record stores. TF044 is the nearest neighbour, for a
119
+ * Terragrunt state backend.
120
+ *
121
+ * TF047 (chant#4560, a live root pins a module version the known-module
122
+ * catalog has no row for, or one with refusals) has no entry. Its data is
123
+ * choudoufu's own module table (choudoufu#2292), vendored; no surveyed tool
124
+ * checks a module pin against a measured compatibility table.
113
125
  */
114
126
  import type { Lineage } from "@intentius/chant/audit/catalog";
115
127
 
@@ -0,0 +1,18 @@
1
+ terraform {
2
+ required_version = ">= 1.5.0"
3
+
4
+ live {
5
+ estate = "fixture-estate"
6
+
7
+ record_store "s3" {
8
+ bucket = "fixture-records"
9
+ }
10
+ }
11
+
12
+ backend "s3" {
13
+ bucket = "tfstate"
14
+ key = "fixture/terraform.tfstate"
15
+ }
16
+ }
17
+
18
+ resource "null_resource" "first" {}
@@ -0,0 +1 @@
1
+ estate = "fixture-estate"