@intentius/chant-lexicon-terraform 0.124.51 → 0.124.54

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 (338) hide show
  1. package/README.md +7 -6
  2. package/dist/__fixtures__/pr-loop/estate.component.d.ts +7 -0
  3. package/dist/__fixtures__/pr-loop/estate.component.d.ts.map +1 -0
  4. package/dist/__fixtures__/pr-loop/estate.component.js +7 -0
  5. package/dist/__fixtures__/report/generate.d.ts +26 -0
  6. package/dist/__fixtures__/report/generate.d.ts.map +1 -0
  7. package/dist/__fixtures__/report/generate.js +58 -0
  8. package/dist/applied-state.d.ts +27 -0
  9. package/dist/applied-state.d.ts.map +1 -0
  10. package/dist/applied-state.js +44 -0
  11. package/dist/backend-config.d.ts +53 -0
  12. package/dist/backend-config.d.ts.map +1 -0
  13. package/dist/backend-config.js +136 -0
  14. package/dist/behaviour/kinds.d.ts.map +1 -1
  15. package/dist/behaviour/kinds.js +4 -0
  16. package/dist/behaviour/predict.d.ts.map +1 -1
  17. package/dist/behaviour/predict.js +6 -2
  18. package/dist/behaviour/reference-catalog.d.ts +16 -5
  19. package/dist/behaviour/reference-catalog.d.ts.map +1 -1
  20. package/dist/behaviour/reference-catalog.js +16 -5
  21. package/dist/behaviour/request.d.ts.map +1 -1
  22. package/dist/behaviour/request.js +3 -2
  23. package/dist/changed-roots.d.ts +6 -4
  24. package/dist/changed-roots.d.ts.map +1 -1
  25. package/dist/changed-roots.js +14 -18
  26. package/dist/commands.d.ts +5 -0
  27. package/dist/commands.d.ts.map +1 -1
  28. package/dist/commands.js +87 -4
  29. package/dist/components/capability-plugin.d.ts +65 -3
  30. package/dist/components/capability-plugin.d.ts.map +1 -1
  31. package/dist/components/capability-plugin.js +197 -13
  32. package/dist/components/compose-group.d.ts +145 -0
  33. package/dist/components/compose-group.d.ts.map +1 -0
  34. package/dist/components/compose-group.js +407 -0
  35. package/dist/components/placeholder-preview.d.ts +78 -0
  36. package/dist/components/placeholder-preview.d.ts.map +1 -0
  37. package/dist/components/placeholder-preview.js +254 -0
  38. package/dist/compose/apply-events.d.ts +46 -0
  39. package/dist/compose/apply-events.d.ts.map +1 -0
  40. package/dist/compose/apply-events.js +77 -0
  41. package/dist/compose/estate.d.ts +194 -0
  42. package/dist/compose/estate.d.ts.map +1 -0
  43. package/dist/compose/estate.js +363 -0
  44. package/dist/compose/hcl.d.ts +99 -0
  45. package/dist/compose/hcl.d.ts.map +1 -0
  46. package/dist/compose/hcl.js +302 -0
  47. package/dist/compose/module-address.d.ts +24 -0
  48. package/dist/compose/module-address.d.ts.map +1 -0
  49. package/dist/compose/module-address.js +43 -0
  50. package/dist/compose/root.d.ts +127 -0
  51. package/dist/compose/root.d.ts.map +1 -0
  52. package/dist/compose/root.js +586 -0
  53. package/dist/compose/run.d.ts +183 -0
  54. package/dist/compose/run.d.ts.map +1 -0
  55. package/dist/compose/run.js +447 -0
  56. package/dist/compose/slice.d.ts +102 -0
  57. package/dist/compose/slice.d.ts.map +1 -0
  58. package/dist/compose/slice.js +249 -0
  59. package/dist/compose/state-address.d.ts +28 -0
  60. package/dist/compose/state-address.d.ts.map +1 -0
  61. package/dist/compose/state-address.js +154 -0
  62. package/dist/compose/state-merge.d.ts +52 -0
  63. package/dist/compose/state-merge.d.ts.map +1 -0
  64. package/dist/compose/state-merge.js +135 -0
  65. package/dist/compose.d.ts +179 -0
  66. package/dist/compose.d.ts.map +1 -0
  67. package/dist/compose.js +482 -0
  68. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  69. package/dist/composites/terraform-apply-op.js +7 -3
  70. package/dist/config.d.ts +59 -13
  71. package/dist/config.d.ts.map +1 -1
  72. package/dist/config.js +43 -9
  73. package/dist/describe-resources.d.ts +74 -12
  74. package/dist/describe-resources.d.ts.map +1 -1
  75. package/dist/describe-resources.js +374 -66
  76. package/dist/drift-read.d.ts +33 -0
  77. package/dist/drift-read.d.ts.map +1 -0
  78. package/dist/drift-read.js +243 -0
  79. package/dist/drift-record.d.ts +147 -0
  80. package/dist/drift-record.d.ts.map +1 -0
  81. package/dist/drift-record.js +383 -0
  82. package/dist/hcl/edges.d.ts.map +1 -1
  83. package/dist/hcl/edges.js +24 -1
  84. package/dist/hcl/files.d.ts +61 -0
  85. package/dist/hcl/files.d.ts.map +1 -0
  86. package/dist/hcl/files.js +221 -0
  87. package/dist/hcl/instances.d.ts +82 -0
  88. package/dist/hcl/instances.d.ts.map +1 -0
  89. package/dist/hcl/instances.js +390 -0
  90. package/dist/hcl/parse.d.ts +81 -9
  91. package/dist/hcl/parse.d.ts.map +1 -1
  92. package/dist/hcl/parse.js +114 -16
  93. package/dist/hcl/roots.d.ts.map +1 -1
  94. package/dist/hcl/roots.js +5 -0
  95. package/dist/hcl/suppressions.d.ts.map +1 -1
  96. package/dist/hcl/suppressions.js +8 -2
  97. package/dist/index.d.ts +11 -2
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +21 -2
  100. package/dist/integrity.json +3 -3
  101. package/dist/live-plan-reasons.d.ts +21 -0
  102. package/dist/live-plan-reasons.d.ts.map +1 -0
  103. package/dist/live-plan-reasons.js +47 -0
  104. package/dist/lsp/option-keys.d.ts.map +1 -1
  105. package/dist/lsp/option-keys.js +2 -0
  106. package/dist/manifest.json +1 -1
  107. package/dist/op/activities/index.d.ts +6 -2
  108. package/dist/op/activities/index.d.ts.map +1 -1
  109. package/dist/op/activities/index.js +5 -1
  110. package/dist/op/activities/live-detect.d.ts +4 -3
  111. package/dist/op/activities/live-detect.d.ts.map +1 -1
  112. package/dist/op/activities/live-detect.js +57 -32
  113. package/dist/op/activities/pin-rollout.d.ts +2 -0
  114. package/dist/op/activities/pin-rollout.d.ts.map +1 -1
  115. package/dist/op/activities/pin-rollout.js +1 -0
  116. package/dist/op/activities/terraform.d.ts +153 -5
  117. package/dist/op/activities/terraform.d.ts.map +1 -1
  118. package/dist/op/activities/terraform.js +205 -20
  119. package/dist/op/activity-contracts.d.ts +27 -0
  120. package/dist/op/activity-contracts.d.ts.map +1 -1
  121. package/dist/op/activity-contracts.js +19 -1
  122. package/dist/op/builders.d.ts +7 -1
  123. package/dist/op/builders.d.ts.map +1 -1
  124. package/dist/op/builders.js +9 -0
  125. package/dist/pin/edit.d.ts.map +1 -1
  126. package/dist/pin/edit.js +35 -0
  127. package/dist/pin/rollout.d.ts +2 -0
  128. package/dist/pin/rollout.d.ts.map +1 -1
  129. package/dist/pin/rollout.js +14 -11
  130. package/dist/plan-shape.d.ts +30 -0
  131. package/dist/plan-shape.d.ts.map +1 -0
  132. package/dist/plan-shape.js +125 -0
  133. package/dist/plugin.d.ts.map +1 -1
  134. package/dist/plugin.js +32 -18
  135. package/dist/report.d.ts +216 -0
  136. package/dist/report.d.ts.map +1 -0
  137. package/dist/report.js +666 -0
  138. package/dist/skills/chant-terraform.md +1 -1
  139. package/dist/state/azure-blob.d.ts +68 -0
  140. package/dist/state/azure-blob.d.ts.map +1 -0
  141. package/dist/state/azure-blob.js +201 -0
  142. package/dist/state/backend.d.ts +158 -0
  143. package/dist/state/backend.d.ts.map +1 -0
  144. package/dist/state/backend.js +406 -0
  145. package/dist/state/exec.d.ts +39 -0
  146. package/dist/state/exec.d.ts.map +1 -0
  147. package/dist/state/exec.js +80 -0
  148. package/dist/state/gcs.d.ts +62 -0
  149. package/dist/state/gcs.d.ts.map +1 -0
  150. package/dist/state/gcs.js +224 -0
  151. package/dist/state/index.d.ts +15 -0
  152. package/dist/state/index.d.ts.map +1 -0
  153. package/dist/state/index.js +14 -0
  154. package/dist/state/locks.d.ts +105 -0
  155. package/dist/state/locks.d.ts.map +1 -0
  156. package/dist/state/locks.js +130 -0
  157. package/dist/state/object-store.d.ts +37 -0
  158. package/dist/state/object-store.d.ts.map +1 -0
  159. package/dist/state/object-store.js +20 -0
  160. package/dist/state/root-states.d.ts +131 -0
  161. package/dist/state/root-states.d.ts.map +1 -0
  162. package/dist/state/root-states.js +241 -0
  163. package/dist/state/s3.d.ts +68 -0
  164. package/dist/state/s3.d.ts.map +1 -0
  165. package/dist/state/s3.js +170 -0
  166. package/dist/state/store.d.ts +51 -0
  167. package/dist/state/store.d.ts.map +1 -0
  168. package/dist/state/store.js +185 -0
  169. package/dist/terragrunt/affected.d.ts.map +1 -1
  170. package/dist/terragrunt/affected.js +2 -1
  171. package/package.json +7 -3
  172. package/src/__fixtures__/ACCEPTANCE.md +69 -7
  173. package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
  174. package/src/__fixtures__/compose/state-app.json +66 -0
  175. package/src/__fixtures__/compose/state-edge.json +40 -0
  176. package/src/__fixtures__/compose/state-net.json +58 -0
  177. package/src/__fixtures__/compose/three-app.plan.json +184 -0
  178. package/src/__fixtures__/compose/three-composed.plan.json +509 -0
  179. package/src/__fixtures__/compose/three-edge.plan.json +144 -0
  180. package/src/__fixtures__/compose/three-net.plan.json +174 -0
  181. package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
  182. package/src/__fixtures__/compose/working-after-apply.json +145 -0
  183. package/src/__fixtures__/config-blocks/app/main.tf +61 -0
  184. package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
  185. package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
  186. package/src/__fixtures__/config-files/shadow/main.tf +3 -0
  187. package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
  188. package/src/__fixtures__/config-files/shadow/other.tf +3 -0
  189. package/src/__fixtures__/config-files/tf/main.tf +11 -0
  190. package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
  191. package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
  192. package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
  193. package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
  194. package/src/__fixtures__/drift-record/README.md +11 -0
  195. package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
  196. package/src/__fixtures__/instance-forms/app/main.tf +110 -0
  197. package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
  198. package/src/__fixtures__/instance-vars/app/main.tf +52 -0
  199. package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
  200. package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
  201. package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
  202. package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
  203. package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
  204. package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
  205. package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
  206. package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
  207. package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
  208. package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
  209. package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
  210. package/src/__fixtures__/pr-loop/README.md +50 -0
  211. package/src/__fixtures__/pr-loop/chant.config.json +13 -0
  212. package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
  213. package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
  214. package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
  215. package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
  216. package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
  217. package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
  218. package/src/__fixtures__/report/applied.show.json +51 -0
  219. package/src/__fixtures__/report/generate.ts +63 -0
  220. package/src/__fixtures__/report/plan.show.json +200 -0
  221. package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
  222. package/src/__fixtures__/report/tf-apply/report.json +338 -0
  223. package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
  224. package/src/__fixtures__/report/tf-plan/report.json +320 -0
  225. package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
  226. package/src/applied-state.ts +52 -0
  227. package/src/backend-config.test.ts +105 -0
  228. package/src/backend-config.ts +133 -0
  229. package/src/behaviour/kinds.ts +4 -0
  230. package/src/behaviour/predict.ts +5 -2
  231. package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
  232. package/src/behaviour/reference-catalog.ts +16 -5
  233. package/src/behaviour/request.ts +3 -2
  234. package/src/change-set-instances.test.ts +98 -0
  235. package/src/changed-roots.ts +14 -16
  236. package/src/commands.ts +84 -4
  237. package/src/components/capability-plugin.test.ts +172 -5
  238. package/src/components/capability-plugin.ts +212 -11
  239. package/src/components/change-set-graph.e2e.test.ts +11 -5
  240. package/src/components/compose-group.test.ts +486 -0
  241. package/src/components/compose-group.ts +489 -0
  242. package/src/components/demo-composed-roots.e2e.test.ts +440 -0
  243. package/src/components/member-outputs.test.ts +76 -0
  244. package/src/components/placeholder-preview.test.ts +147 -0
  245. package/src/components/placeholder-preview.ts +257 -0
  246. package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
  247. package/src/components/pr-loop.tofu.test.ts +112 -56
  248. package/src/compose/apply-events.test.ts +62 -0
  249. package/src/compose/apply-events.ts +100 -0
  250. package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
  251. package/src/compose/estate.test.ts +421 -0
  252. package/src/compose/estate.ts +443 -0
  253. package/src/compose/hcl.ts +311 -0
  254. package/src/compose/module-address.ts +49 -0
  255. package/src/compose/root.test.ts +416 -0
  256. package/src/compose/root.ts +592 -0
  257. package/src/compose/run.test.ts +440 -0
  258. package/src/compose/run.tofu.test.ts +176 -0
  259. package/src/compose/run.ts +527 -0
  260. package/src/compose/slice.test.ts +184 -0
  261. package/src/compose/slice.ts +290 -0
  262. package/src/compose/state-address.ts +157 -0
  263. package/src/compose/state-merge.test.ts +131 -0
  264. package/src/compose/state-merge.ts +148 -0
  265. package/src/compose.test.ts +248 -0
  266. package/src/compose.ts +627 -0
  267. package/src/composites/terraform-apply-op.test.ts +9 -0
  268. package/src/composites/terraform-apply-op.ts +7 -3
  269. package/src/composites/terraform-pin-rollout-op.test.ts +2 -2
  270. package/src/config-file-listers.test.ts +265 -0
  271. package/src/config.ts +44 -9
  272. package/src/describe-resources.engine-facts.test.ts +412 -0
  273. package/src/describe-resources.live-module-keyed.test.ts +290 -0
  274. package/src/describe-resources.ts +416 -70
  275. package/src/drift-read.ts +284 -0
  276. package/src/drift-record.test.ts +223 -0
  277. package/src/drift-record.ts +455 -0
  278. package/src/hcl/config-blocks.test.ts +174 -0
  279. package/src/hcl/edges.ts +21 -1
  280. package/src/hcl/files.ts +234 -0
  281. package/src/hcl/instance-forms.test.ts +94 -0
  282. package/src/hcl/instances.test.ts +137 -0
  283. package/src/hcl/instances.ts +422 -0
  284. package/src/hcl/parse.test.ts +139 -1
  285. package/src/hcl/parse.ts +149 -16
  286. package/src/hcl/roots.ts +6 -0
  287. package/src/hcl/suppressions.ts +8 -2
  288. package/src/index.ts +76 -0
  289. package/src/live-plan-reasons.ts +54 -0
  290. package/src/lsp/completions.test.ts +1 -1
  291. package/src/lsp/option-keys.ts +2 -0
  292. package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
  293. package/src/op/activities/index.ts +9 -0
  294. package/src/op/activities/live-detect.ts +55 -26
  295. package/src/op/activities/pin-rollout.ts +3 -0
  296. package/src/op/activities/registry.test.ts +2 -1
  297. package/src/op/activities/terraform.test.ts +55 -0
  298. package/src/op/activities/terraform.ts +266 -22
  299. package/src/op/activity-contracts.test.ts +1 -0
  300. package/src/op/activity-contracts.ts +24 -1
  301. package/src/op/builders.ts +14 -0
  302. package/src/pin/bundle.test.ts +2 -0
  303. package/src/pin/edit.ts +30 -0
  304. package/src/pin/rollout.ts +16 -11
  305. package/src/plan-shape.test.ts +148 -0
  306. package/src/plan-shape.ts +128 -0
  307. package/src/plugin.ts +33 -19
  308. package/src/report.test.ts +423 -0
  309. package/src/report.ts +752 -0
  310. package/src/signal-only-root.test.ts +72 -0
  311. package/src/skills/chant-terraform.md +1 -1
  312. package/src/state/azure-blob.ts +223 -0
  313. package/src/state/backend-verdict.test.ts +187 -0
  314. package/src/state/backend.test.ts +127 -0
  315. package/src/state/backend.ts +488 -0
  316. package/src/state/exec.ts +111 -0
  317. package/src/state/gcs.ts +254 -0
  318. package/src/state/index.ts +15 -0
  319. package/src/state/locks.test.ts +84 -0
  320. package/src/state/locks.ts +201 -0
  321. package/src/state/object-store.ts +35 -0
  322. package/src/state/root-states-floci.e2e.test.ts +66 -0
  323. package/src/state/root-states.test.ts +462 -0
  324. package/src/state/root-states.ts +309 -0
  325. package/src/state/s3.ts +193 -0
  326. package/src/state/store.ts +225 -0
  327. package/src/terragrunt/affected.ts +2 -1
  328. package/src/workspace-kinds.test.ts +4 -17
  329. package/workspace-kinds.json +1 -24
  330. package/dist/workspace-member.d.ts +0 -30
  331. package/dist/workspace-member.d.ts.map +0 -1
  332. package/dist/workspace-member.js +0 -91
  333. package/schemas/pulumi.schema.json +0 -34
  334. package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
  335. package/src/testdata/pulumi-member/stack-export.json +0 -53
  336. package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
  337. package/src/workspace-member.test.ts +0 -89
  338. package/src/workspace-member.ts +0 -126
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Engine facts from `choudoufu live-graph -json -from-plan=<file>` on a live
3
+ * root (chant #4528, designed on #4520 section 4 and ruled there).
4
+ *
5
+ * Two documents are read here, neither produced by a run:
6
+ *
7
+ * - chant #4527's schema fixture,
8
+ * `packages/core/src/graph-schema/fixtures/choudoufu.engine-graph.json`,
9
+ * the shape choudoufu#2285's own tests decode against. It carries every
10
+ * fact field and two `grain: "block"` edges, one out of an instance under
11
+ * an expanded module call.
12
+ * - `__fixtures__/live-module-keyed/live-graph.json`, written by hand for the
13
+ * #4513 fixture root to what choudoufu#2285 prints for it from the
14
+ * `live-plan.json` beside it, read on choudoufu
15
+ * `origin/integration/consolidation-graph` (3b40fc5683),
16
+ * internal/live/livegraph/: one node per block with its `sourceLoc`
17
+ * (`collect`); instance nodes for the literal `count = 2` call and the
18
+ * block's own `count = 2`, and for the `for_each = toset(...)` call only
19
+ * through the plan's rows, since a function call is not a literal
20
+ * (`literalKeys`, `nodeForAddr`); facts by `planFacts` (`marker` binds
21
+ * carry `markers.EscapeAddress`, a `derived` bind is `ownedBy: "name"`,
22
+ * ABSENT is `pending`, an unowned row with `tofu_estate` is `foreign`);
23
+ * no `observedAt`, because the plan document has no `observed_at`; no
24
+ * engine version, as on a dev build; continuation keys
25
+ * `tofu-address-2..4`; and no edges, because no block in that root reads
26
+ * another.
27
+ *
28
+ * Nothing here runs choudoufu: the activities are injected.
29
+ */
30
+
31
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
32
+ import { createRequire } from "node:module";
33
+ import { tmpdir } from "node:os";
34
+ import { dirname, join } from "node:path";
35
+ import { fileURLToPath } from "node:url";
36
+ import { afterEach, describe, expect, it } from "vitest";
37
+ import { normalizeObservation } from "@intentius/chant/observation";
38
+ import type { Declarable } from "@intentius/chant/declarable";
39
+ import { buildLiveGraphIr } from "@intentius/chant/graph-ir";
40
+ import { ENGINE_FACTS_FLOOR } from "@intentius/chant/engine-facts";
41
+ import { NEXT_RELEASE } from "@intentius/chant/release-floors";
42
+ import {
43
+ describeResources,
44
+ readEngineGraph,
45
+ TERRAFORM_LIVE_PLAN_DIR_ENV,
46
+ withEngineFacts,
47
+ type TerraformReadDeps,
48
+ } from "./describe-resources";
49
+ import { renderTerraformRoots } from "./hcl/roots";
50
+ import {
51
+ choudoufuLiveGraphCommand,
52
+ classifyLiveGraphFailure,
53
+ MIN_CHOUDOUFU_GRAPH_VERSION,
54
+ MIN_CHOUDOUFU_VERSION,
55
+ isOlderVersion,
56
+ } from "./op/activities/terraform";
57
+
58
+ const here = dirname(fileURLToPath(import.meta.url));
59
+ const SCHEMA_DIR = join(here, "..", "..", "..", "packages", "core", "src", "graph-schema");
60
+ const SCHEMA = JSON.parse(readFileSync(join(SCHEMA_DIR, "graph.schema.json"), "utf-8")) as { $id: string };
61
+ const CORE_FIXTURE: unknown = JSON.parse(readFileSync(join(SCHEMA_DIR, "fixtures", "choudoufu.engine-graph.json"), "utf-8"));
62
+ const FIXTURE = join(here, "__fixtures__", "live-module-keyed");
63
+ const ROOT_DIR = join(FIXTURE, "root");
64
+ const LIVE_PLAN: unknown = JSON.parse(readFileSync(join(FIXTURE, "live-plan.json"), "utf-8"));
65
+ const LIVE_GRAPH: unknown = JSON.parse(readFileSync(join(FIXTURE, "live-graph.json"), "utf-8"));
66
+ const ESTATE = "module-keyed-estate";
67
+
68
+ // Core's own ajv (v8, draft 2020-12), which the schema test uses; the copy at
69
+ // the workspace root is another major. Loaded through core's package so the
70
+ // two tests validate with the same build.
71
+ interface Validate {
72
+ (doc: unknown): boolean;
73
+ errors?: unknown;
74
+ }
75
+ interface Ajv {
76
+ addSchema(schema: unknown): void;
77
+ getSchema(ref: string): Validate | undefined;
78
+ }
79
+ const Ajv2020 = (createRequire(join(SCHEMA_DIR, "..", "..", "package.json"))("ajv/dist/2020") as { default: new (opts: object) => Ajv }).default;
80
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
81
+ ajv.addSchema(SCHEMA);
82
+ const validEngineGraph = ajv.getSchema(`${SCHEMA.$id}#/$defs/EngineGraphDocument`)!;
83
+
84
+ type Entities = Map<string, { entityType: string; props: Record<string, unknown> }>;
85
+
86
+ async function render(): Promise<Entities> {
87
+ const { entities: declarables } = await renderTerraformRoots({
88
+ projectRoot: FIXTURE,
89
+ roots: { estate: { dir: "./root" } },
90
+ binary: "choudoufu",
91
+ });
92
+ const entities: Entities = new Map();
93
+ for (const [key, entity] of declarables) {
94
+ entities.set(key, { entityType: entity.entityType, props: (entity as Declarable & { props: Record<string, unknown> }).props });
95
+ }
96
+ return entities;
97
+ }
98
+
99
+ function options(entities: Entities) {
100
+ return { environment: "prod", buildOutput: "", entityNames: [...entities.keys()].sort(), entities };
101
+ }
102
+
103
+ const never = ((args: { root: string }) => {
104
+ throw new Error(`unexpected activity for root "${args.root}"`);
105
+ }) as never;
106
+
107
+ type LiveGraph = NonNullable<TerraformReadDeps["liveGraph"]>;
108
+ type LiveGraphCall = Parameters<LiveGraph>[0] & { document: unknown };
109
+
110
+ /** `live-plan -json` answers from the fixture; `live-graph` answers with `graph`, recording what it was handed. */
111
+ function deps(graph?: (call: LiveGraphCall) => Awaited<ReturnType<LiveGraph>>, calls: LiveGraphCall[] = []): TerraformReadDeps {
112
+ return {
113
+ init: never,
114
+ show: never,
115
+ liveLs: never,
116
+ livePlan: (async () => ({ json: LIVE_PLAN, dir: ROOT_DIR, estate: ESTATE })) as unknown as TerraformReadDeps["livePlan"],
117
+ ...(graph
118
+ ? {
119
+ liveGraph: (async (args: Parameters<LiveGraph>[0]) => {
120
+ // The file is read while it exists: a scratch copy is removed after the call.
121
+ const call = { ...args, document: JSON.parse(readFileSync(args.fromPlan, "utf-8")) as unknown };
122
+ calls.push(call);
123
+ return graph(call);
124
+ }) as LiveGraph,
125
+ }
126
+ : {}),
127
+ };
128
+ }
129
+
130
+ const answers = (json: unknown) => () => ({ supported: true as const, json, dir: ROOT_DIR });
131
+
132
+ const saved = process.env[TERRAFORM_LIVE_PLAN_DIR_ENV];
133
+ afterEach(() => {
134
+ if (saved === undefined) delete process.env[TERRAFORM_LIVE_PLAN_DIR_ENV];
135
+ else process.env[TERRAFORM_LIVE_PLAN_DIR_ENV] = saved;
136
+ });
137
+
138
+ describe("the engine graph documents (#4528)", () => {
139
+ it("both validate against chant #4527's schema", () => {
140
+ expect(validEngineGraph(CORE_FIXTURE), JSON.stringify(validEngineGraph.errors)).toBe(true);
141
+ expect(validEngineGraph(LIVE_GRAPH), JSON.stringify(validEngineGraph.errors)).toBe(true);
142
+ });
143
+ });
144
+
145
+ describe("readEngineGraph over #4527's schema fixture", () => {
146
+ const read = readEngineGraph("web", CORE_FIXTURE);
147
+
148
+ it("puts each block node's facts on its entity key", () => {
149
+ expect([...read.blocks.keys()].sort()).toEqual(["web/aws_s3_bucket.logs", "web/aws_sns_topic.alerts", "web/aws_sqs_queue.jobs"]);
150
+ expect(read.blocks.get("web/aws_s3_bucket.logs")).toEqual({
151
+ ownership: "bound",
152
+ ownedBy: "record",
153
+ cloudIds: [
154
+ { kind: "import-id", value: "web-logs" },
155
+ { kind: "arn", value: "arn:aws:s3:::web-logs" },
156
+ ],
157
+ recordVersions: {
158
+ kept: true,
159
+ versions: [
160
+ { id: "3sL4kqtJlcpXroDTDmJ.rmSpXd3dIbrHY", writtenAt: "2026-10-26T11:58:02Z", current: true, deleted: false },
161
+ { id: "2bE8x0l1BqzDmoPTmD7M7bGqFrsTFTQS", writtenAt: "2026-10-20T09:14:40Z", current: false, deleted: false },
162
+ ],
163
+ },
164
+ observedAt: "2026-10-26T12:00:00Z",
165
+ });
166
+ expect(read.blocks.get("web/aws_sqs_queue.jobs")).toEqual({
167
+ ownership: "foreign",
168
+ markers: { owner: "billing", encoding: "escaped", source: "live" },
169
+ observedAt: "2026-10-26T12:00:00Z",
170
+ });
171
+ expect(read.blocks.get("web/aws_sns_topic.alerts")).toEqual({ ownership: "pending", observedAt: "2026-10-26T12:00:00Z" });
172
+ });
173
+
174
+ it("puts an instance node's facts under its block's entity key, by instance address", () => {
175
+ expect([...read.instances.keys()]).toEqual(["web/module.app/aws_eip.pool"]);
176
+ expect(read.instances.get("web/module.app/aws_eip.pool")!.get("module.app[0].aws_eip.pool[1]")).toEqual({
177
+ ownership: "bound",
178
+ ownedBy: "marker",
179
+ markers: { owner: "web", address: "module.app:0.aws_eip.pool:1", encoding: "escaped", source: "live" },
180
+ cloudIds: [{ kind: "import-id", value: "eipalloc-0abc" }],
181
+ observedAt: "2026-10-26T12:00:00Z",
182
+ });
183
+ });
184
+
185
+ it("maps block edges to live graph ids and keeps their grain", () => {
186
+ expect(read.edges).toEqual([
187
+ { from: "web/module.app[0]/aws_eip.pool[1]", to: "web/module.app/aws_instance.web", kind: "ref", viaAttr: "dependencies", grain: "block" },
188
+ { from: "web/module.app/aws_instance.web", to: "web/aws_s3_bucket.logs", kind: "ref", viaAttr: "dependencies", grain: "block" },
189
+ ]);
190
+ });
191
+
192
+ it("refuses a document that is not an engine graph", () => {
193
+ expect(() => readEngineGraph("web", { version: 1, nodes: [], edges: [] })).toThrow(/not an engine graph document/);
194
+ expect(() => readEngineGraph("web", "nope")).toThrow(/not an engine graph document/);
195
+ });
196
+
197
+ it("carries the facts and edges onto the live graph", () => {
198
+ const resources = withEngineFacts(
199
+ {
200
+ "web/aws_s3_bucket.logs": { type: "Terraform::Resource", status: "bound", physicalId: "web-logs", ownership: "owned" },
201
+ "web/module.app/aws_instance.web": { type: "Terraform::Resource", status: "bound", ownership: "owned" },
202
+ "web/module.app/aws_eip.pool": {
203
+ type: "Terraform::Resource",
204
+ status: "bound",
205
+ ownership: "owned",
206
+ instances: [
207
+ { key: "[1]", scope: "module.app[0]", address: "module.app[0].aws_eip.pool[1]", verdict: "present", ownership: "owned", physicalId: "eipalloc-0abc" },
208
+ ],
209
+ },
210
+ },
211
+ read,
212
+ );
213
+ const ir = buildLiveGraphIr([{ lexicon: "terraform", resources, edges: read.edges }]);
214
+ const node = (id: string) => ir.nodes.find((n) => n.id === id);
215
+ expect(node("web/aws_s3_bucket.logs")!.facts!.ownedBy).toBe("record");
216
+ expect(node("web/module.app[0]/aws_eip.pool[1]")!.facts!.markers!.source).toBe("live");
217
+ // The block node of an expanded block has no facts of its own; its instance does.
218
+ expect(node("web/module.app/aws_eip.pool")!.facts).toBeUndefined();
219
+ // Both endpoints of each were observed, so buildLiveGraphIr keeps both (its sort is localeCompare's).
220
+ expect(ir.edges.map((e) => `${e.from} -> ${e.to} (${e.grain})`).sort()).toEqual([
221
+ "web/module.app/aws_instance.web -> web/aws_s3_bucket.logs (block)",
222
+ "web/module.app[0]/aws_eip.pool[1] -> web/module.app/aws_instance.web (block)",
223
+ ]);
224
+ // ownership keeps its own meaning beside the facts.
225
+ expect(node("web/aws_s3_bucket.logs")!.ownership).toBe("owned");
226
+ });
227
+ });
228
+
229
+ describe("describeResources reads live-graph after live-plan on the module-keyed root (#4528, #4513)", () => {
230
+ it("hands live-graph the same live-plan document, in a scratch file when no directory was asked for", async () => {
231
+ delete process.env[TERRAFORM_LIVE_PLAN_DIR_ENV];
232
+ const calls: LiveGraphCall[] = [];
233
+ await describeResources(options(await render()), deps(answers(LIVE_GRAPH), calls));
234
+ expect(calls).toHaveLength(1);
235
+ expect(calls[0]).toMatchObject({ root: "estate", estate: ESTATE });
236
+ expect(calls[0]!.document).toEqual(LIVE_PLAN);
237
+ expect(calls[0]!.fromPlan.startsWith(tmpdir())).toBe(true);
238
+ expect(calls[0]!.fromPlan.endsWith("estate.live-plan.json")).toBe(true);
239
+ });
240
+
241
+ it("hands it the <root>.live-plan.json under CHANT_TERRAFORM_LIVE_PLAN_DIR when that is set", async () => {
242
+ const out = mkdtempSync(join(tmpdir(), "chant-4528-plan-dir-"));
243
+ try {
244
+ process.env[TERRAFORM_LIVE_PLAN_DIR_ENV] = out;
245
+ const calls: LiveGraphCall[] = [];
246
+ await describeResources(options(await render()), deps(answers(LIVE_GRAPH), calls));
247
+ expect(calls.map((c) => c.fromPlan)).toEqual([join(out, "estate.live-plan.json")]);
248
+ expect(calls[0]!.document).toEqual(LIVE_PLAN);
249
+ } finally {
250
+ rmSync(out, { recursive: true, force: true });
251
+ }
252
+ });
253
+
254
+ it("sets each module-keyed instance's facts, by marker, by name, pending, unowned and foreign", async () => {
255
+ const { resources } = normalizeObservation(await describeResources(options(await render()), deps(answers(LIVE_GRAPH))));
256
+ const pool = resources["estate/module.app/aws_eip.pool"]!;
257
+ expect(pool.facts).toBeUndefined();
258
+ expect(pool.instances!.map((i) => [i.address, i.facts?.ownership, i.facts?.ownedBy, i.facts?.markers?.address, i.facts?.cloudIds?.[0]?.value])).toEqual([
259
+ ["module.app[0].aws_eip.pool[0]", "bound", "marker", "module.app:0.aws_eip.pool:0", "eipalloc-0a41c7d2e9b3f5061"],
260
+ ["module.app[0].aws_eip.pool[1]", "bound", "marker", "module.app:0.aws_eip.pool:1", "eipalloc-0b52d8e3fac4061f2"],
261
+ ["module.app[1].aws_eip.pool[0]", "bound", "marker", "module.app:1.aws_eip.pool:0", "eipalloc-0c63e9f40bd5172a3"],
262
+ ["module.app[1].aws_eip.pool[1]", "bound", "marker", "module.app:1.aws_eip.pool:1", "eipalloc-0d74fa051ce6283b4"],
263
+ ]);
264
+ expect(pool.instances![0]!.facts!.markers).toEqual({ owner: ESTATE, address: "module.app:0.aws_eip.pool:0", encoding: "escaped", source: "live" });
265
+
266
+ const logs = resources["estate/module.app/aws_cloudwatch_log_group.logs"]!;
267
+ expect(logs.instances!.map((i) => [i.address, i.verdict, i.facts])).toEqual([
268
+ ["module.app[0].aws_cloudwatch_log_group.logs", "present", { ownership: "bound", ownedBy: "name", cloudIds: [{ kind: "import-id", value: `/${ESTATE}/app-0` }] }],
269
+ ["module.app[1].aws_cloudwatch_log_group.logs", "absent", { ownership: "pending" }],
270
+ ]);
271
+
272
+ const front = resources["estate/module.edge/aws_cloudwatch_log_group.front"]!;
273
+ expect(front.instances!.map((i) => i.facts)).toEqual([
274
+ { ownership: "unowned", cloudIds: [{ kind: "import-id", value: `/${ESTATE}/edge-east` }] },
275
+ { ownership: "foreign", markers: { owner: "other-estate", encoding: "escaped", source: "live" }, cloudIds: [{ kind: "import-id", value: `/${ESTATE}/edge-west` }] },
276
+ ]);
277
+
278
+ // The verdicts are the live-plan read's, unchanged.
279
+ expect(pool).toMatchObject({ status: "bound", ownership: "owned", marker: { stack: ESTATE } });
280
+ expect(front).toMatchObject({ status: "unowned", ownership: "foreign" });
281
+ });
282
+
283
+ it("draws the facts on the instance nodes the declared graph lists", async () => {
284
+ const { resources } = normalizeObservation(await describeResources(options(await render()), deps(answers(LIVE_GRAPH))));
285
+ const ir = buildLiveGraphIr([{ lexicon: "terraform", resources }]);
286
+ const facts = new Map(ir.nodes.filter((n) => n.facts).map((n) => [n.id, n.facts!.ownership]));
287
+ expect(Object.fromEntries(facts)).toEqual({
288
+ "estate/module.app[0]/aws_cloudwatch_log_group.logs": "bound",
289
+ "estate/module.app[0]/aws_eip.pool[0]": "bound",
290
+ "estate/module.app[0]/aws_eip.pool[1]": "bound",
291
+ "estate/module.app[1]/aws_eip.pool[0]": "bound",
292
+ "estate/module.app[1]/aws_eip.pool[1]": "bound",
293
+ 'estate/module.edge["east"]/aws_cloudwatch_log_group.front': "unowned",
294
+ 'estate/module.edge["west"]/aws_cloudwatch_log_group.front': "foreign",
295
+ });
296
+ });
297
+
298
+ it("puts a block edge on the observation's edges, mapped to the live graph's ids", async () => {
299
+ // live-graph prints no edge for this root, since no block reads another.
300
+ // This one is added by the test, out of an instance under the expanded
301
+ // call into a block, the shape #4527's fixture carries.
302
+ const doc = structuredClone(LIVE_GRAPH) as { edges: unknown[] };
303
+ doc.edges.push({
304
+ from: "module.app[0].aws_eip.pool[1]",
305
+ to: "module.app.aws_cloudwatch_log_group.logs",
306
+ kind: "ref",
307
+ viaAttr: "depends_on",
308
+ grain: "block",
309
+ });
310
+ const raw = await describeResources(options(await render()), deps(answers(doc)));
311
+ const observed = normalizeObservation(raw);
312
+ expect(observed.edges).toEqual([
313
+ { from: "estate/module.app[0]/aws_eip.pool[1]", to: "estate/module.app/aws_cloudwatch_log_group.logs", kind: "ref", viaAttr: "depends_on", grain: "block" },
314
+ ]);
315
+ const ir = buildLiveGraphIr([{ lexicon: "terraform", resources: observed.resources, edges: observed.edges }]);
316
+ expect(ir.edges).toEqual(observed.edges);
317
+ });
318
+
319
+ it("with no edges in the document, the observation carries none", async () => {
320
+ const observed = normalizeObservation(await describeResources(options(await render()), deps(answers(LIVE_GRAPH))));
321
+ expect(observed.edges).toBeUndefined();
322
+ expect(observed.notes.some((n) => n.includes("live-graph"))).toBe(false);
323
+ });
324
+ });
325
+
326
+ describe("an older choudoufu: no facts, one note, the rest unchanged (#4528)", () => {
327
+ async function baseline() {
328
+ return normalizeObservation(await describeResources(options(await render()), deps()));
329
+ }
330
+
331
+ function noFacts(resources: Record<string, { facts?: unknown; instances?: Array<{ facts?: unknown }> }>): boolean {
332
+ return Object.values(resources).every((r) => r.facts === undefined && (r.instances ?? []).every((i) => i.facts === undefined));
333
+ }
334
+
335
+ it("a usage error leaves facts absent and adds exactly one note naming the floor", async () => {
336
+ const before = await baseline();
337
+ const after = normalizeObservation(
338
+ await describeResources(
339
+ options(await render()),
340
+ deps(() => ({ supported: false, detail: 'OpenTofu has no command named "live-graph".', dir: ROOT_DIR })),
341
+ ),
342
+ );
343
+ expect(noFacts(after.resources)).toBe(true);
344
+ expect(after.resources).toEqual(before.resources);
345
+ expect(after.unobserved).toEqual(before.unobserved);
346
+ expect(after.queried).toEqual(before.queried);
347
+ expect(after.sources).toEqual(before.sources);
348
+ expect(after.edges).toBeUndefined();
349
+ const added = after.notes.filter((n) => !before.notes.includes(n));
350
+ expect(added).toHaveLength(1);
351
+ expect(added[0]).toContain("terraform.roots.estate: engine facts are absent");
352
+ expect(added[0]).toContain('OpenTofu has no command named "live-graph".');
353
+ expect(added[0]).toContain(`v${MIN_CHOUDOUFU_GRAPH_VERSION}`);
354
+ });
355
+
356
+ it("any other live-graph failure degrades the same way, with its own note", async () => {
357
+ const before = await baseline();
358
+ const after = normalizeObservation(
359
+ await describeResources(
360
+ options(await render()),
361
+ deps(() => {
362
+ throw new Error("choudoufu live-graph failed in /x (exit 1)\nThe live plan is of another estate");
363
+ }),
364
+ ),
365
+ );
366
+ expect(after.resources).toEqual(before.resources);
367
+ const added = after.notes.filter((n) => !before.notes.includes(n));
368
+ expect(added).toHaveLength(1);
369
+ expect(added[0]).toMatch(/engine facts are absent: `choudoufu live-graph -json -from-plan` failed \(choudoufu live-graph failed in \/x \(exit 1\)\)/);
370
+ });
371
+
372
+ it("a live-plan that failed runs no live-graph at all", async () => {
373
+ const calls: LiveGraphCall[] = [];
374
+ const failing: TerraformReadDeps = {
375
+ ...deps(answers(LIVE_GRAPH), calls),
376
+ livePlan: (async () => {
377
+ throw new Error("choudoufu live-plan failed (exit 1)");
378
+ }) as unknown as TerraformReadDeps["livePlan"],
379
+ };
380
+ const observed = normalizeObservation(await describeResources(options(await render()), failing));
381
+ expect(calls).toEqual([]);
382
+ expect(Object.keys(observed.unobserved).length).toBeGreaterThan(0);
383
+ });
384
+
385
+ it("classifies the usage errors older builds print", () => {
386
+ expect(classifyLiveGraphFailure(1, 'OpenTofu has no command named "live-graph".\n\nTo see all of OpenTofu\'s top-level commands, run:\n choudoufu -help\n')).toBe("unsupported");
387
+ expect(classifyLiveGraphFailure(127, "Usage: choudoufu [global options] <subcommand> [args]")).toBe("unsupported");
388
+ expect(classifyLiveGraphFailure(1, "flag provided but not defined: -from-plan")).toBe("unsupported");
389
+ expect(classifyLiveGraphFailure(1, "Error: The live plan is of another estate")).toBe("failed");
390
+ expect(classifyLiveGraphFailure(1, "Error: Cannot read the live plan")).toBe("failed");
391
+ });
392
+
393
+ it("builds the command with no DIR and no -history", () => {
394
+ expect(choudoufuLiveGraphCommand({ binary: "choudoufu", fromPlan: "/tmp/out/estate.live-plan.json" })).toBe(
395
+ "choudoufu live-graph -json -from-plan=/tmp/out/estate.live-plan.json",
396
+ );
397
+ expect(choudoufuLiveGraphCommand({ binary: "choudoufu", fromPlan: "/tmp/a b/x.json", estate: "web" })).toBe(
398
+ "choudoufu live-graph -json -from-plan='/tmp/a b/x.json' -estate=web",
399
+ );
400
+ });
401
+ });
402
+
403
+ describe("the floors (#4528)", () => {
404
+ it("the choudoufu floor is the release that ships live-graph, above the read floor", () => {
405
+ expect(MIN_CHOUDOUFU_GRAPH_VERSION).toBe("0.32.0");
406
+ expect(isOlderVersion(MIN_CHOUDOUFU_VERSION, MIN_CHOUDOUFU_GRAPH_VERSION)).toBe(true);
407
+ });
408
+
409
+ it("the chant floor for writing facts is the next release", () => {
410
+ expect(ENGINE_FACTS_FLOOR).toBe(NEXT_RELEASE);
411
+ });
412
+ });