@intentius/chant-lexicon-terraform 0.124.31 → 0.124.32

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 (141) hide show
  1. package/dist/__fixtures__/terragrunt/affected-cases.js +111 -0
  2. package/dist/__fixtures__/terragrunt/record.js +57 -0
  3. package/dist/behaviour/index.js +6 -0
  4. package/dist/behaviour/kinds.js +194 -0
  5. package/dist/behaviour/predict.js +297 -0
  6. package/dist/behaviour/reference-catalog.js +178 -0
  7. package/dist/behaviour/request.js +482 -0
  8. package/dist/change-set.js +234 -0
  9. package/dist/changed-roots.js +97 -0
  10. package/dist/codegen/docs-cli.js +3 -0
  11. package/dist/codegen/docs.js +44 -0
  12. package/dist/codegen/generate-cli.js +9 -0
  13. package/dist/codegen/generate.js +59 -0
  14. package/dist/codegen/package.js +43 -0
  15. package/dist/commands.js +106 -0
  16. package/dist/components/capability-plugin.d.ts +38 -2
  17. package/dist/components/capability-plugin.d.ts.map +1 -1
  18. package/dist/components/capability-plugin.js +260 -0
  19. package/dist/composites/terraform-adopt-op.js +152 -0
  20. package/dist/composites/terraform-apply-op.js +183 -0
  21. package/dist/composites/terraform-pin-rollout-op.js +45 -0
  22. package/dist/composites/terraform-watch-op.js +219 -0
  23. package/dist/config.js +118 -0
  24. package/dist/describe-resources.js +984 -0
  25. package/dist/generated/index.js +1 -0
  26. package/dist/hcl/descend.js +208 -0
  27. package/dist/hcl/edges.js +287 -0
  28. package/dist/hcl/jsonencode.js +442 -0
  29. package/dist/hcl/marker.js +212 -0
  30. package/dist/hcl/parse.js +372 -0
  31. package/dist/hcl/references.js +156 -0
  32. package/dist/hcl/roots.js +98 -0
  33. package/dist/hcl/suppressions.js +189 -0
  34. package/dist/hcl/value.js +72 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +46 -0
  38. package/dist/integrity.json +2 -2
  39. package/dist/lint/audit-catalog.js +501 -0
  40. package/dist/lint/audit-lineage.js +442 -0
  41. package/dist/lint/post-synth/blocks.js +87 -0
  42. package/dist/lint/post-synth/fixtures/load.js +94 -0
  43. package/dist/lint/post-synth/flat-attributes.js +135 -0
  44. package/dist/lint/post-synth/index.js +90 -0
  45. package/dist/lint/post-synth/module-source.js +153 -0
  46. package/dist/lint/post-synth/scope.js +104 -0
  47. package/dist/lint/post-synth/terragrunt.js +63 -0
  48. package/dist/lint/post-synth/tf001.js +138 -0
  49. package/dist/lint/post-synth/tf002.js +143 -0
  50. package/dist/lint/post-synth/tf003.js +60 -0
  51. package/dist/lint/post-synth/tf004.js +54 -0
  52. package/dist/lint/post-synth/tf005.js +68 -0
  53. package/dist/lint/post-synth/tf006.js +43 -0
  54. package/dist/lint/post-synth/tf007.js +66 -0
  55. package/dist/lint/post-synth/tf008.js +64 -0
  56. package/dist/lint/post-synth/tf009.js +49 -0
  57. package/dist/lint/post-synth/tf010.js +40 -0
  58. package/dist/lint/post-synth/tf011.js +38 -0
  59. package/dist/lint/post-synth/tf012.js +36 -0
  60. package/dist/lint/post-synth/tf013.js +53 -0
  61. package/dist/lint/post-synth/tf014.js +61 -0
  62. package/dist/lint/post-synth/tf015.js +62 -0
  63. package/dist/lint/post-synth/tf016.js +66 -0
  64. package/dist/lint/post-synth/tf017.js +39 -0
  65. package/dist/lint/post-synth/tf018.js +63 -0
  66. package/dist/lint/post-synth/tf019.js +60 -0
  67. package/dist/lint/post-synth/tf020.js +103 -0
  68. package/dist/lint/post-synth/tf021.js +117 -0
  69. package/dist/lint/post-synth/tf022.js +51 -0
  70. package/dist/lint/post-synth/tf024.js +62 -0
  71. package/dist/lint/post-synth/tf025.js +83 -0
  72. package/dist/lint/post-synth/tf026.js +74 -0
  73. package/dist/lint/post-synth/tf027.js +66 -0
  74. package/dist/lint/post-synth/tf028.js +83 -0
  75. package/dist/lint/post-synth/tf029.js +73 -0
  76. package/dist/lint/post-synth/tf030.js +253 -0
  77. package/dist/lint/post-synth/tf031.js +332 -0
  78. package/dist/lint/post-synth/tf032.js +201 -0
  79. package/dist/lint/post-synth/tf033.js +126 -0
  80. package/dist/lint/post-synth/tf034.js +45 -0
  81. package/dist/lint/post-synth/tf035.js +63 -0
  82. package/dist/lint/post-synth/tf036.js +97 -0
  83. package/dist/lint/post-synth/tf037.js +80 -0
  84. package/dist/lint/post-synth/tf038.js +63 -0
  85. package/dist/lint/post-synth/tf039.js +55 -0
  86. package/dist/lint/post-synth/tf040.js +67 -0
  87. package/dist/lint/post-synth/tf041.js +53 -0
  88. package/dist/lint/post-synth/tf042.js +38 -0
  89. package/dist/lint/post-synth/tf043.js +45 -0
  90. package/dist/lint/post-synth/tf044.js +45 -0
  91. package/dist/lint/post-synth/tf045.js +44 -0
  92. package/dist/lint/rules/fixtures/load.js +50 -0
  93. package/dist/lint/rules/index.js +4 -0
  94. package/dist/lint/rules/plan-before-apply.js +136 -0
  95. package/dist/lint/secret-shape.js +149 -0
  96. package/dist/live-ownership.js +57 -0
  97. package/dist/lsp/completions.js +95 -0
  98. package/dist/lsp/context.js +358 -0
  99. package/dist/lsp/hover.js +45 -0
  100. package/dist/lsp/option-keys.js +93 -0
  101. package/dist/manifest.json +1 -1
  102. package/dist/op/activities/index.js +30 -0
  103. package/dist/op/activities/live-detect.js +143 -0
  104. package/dist/op/activities/pin-rollout.js +45 -0
  105. package/dist/op/activities/terraform.js +1270 -0
  106. package/dist/op/activity-contracts.js +162 -0
  107. package/dist/op/adoption.js +272 -0
  108. package/dist/op/builders.js +125 -0
  109. package/dist/op/resolve-root-mode.js +77 -0
  110. package/dist/package-cli.js +16 -0
  111. package/dist/pin/edit-ts.js +76 -0
  112. package/dist/pin/edit.js +118 -0
  113. package/dist/pin/forge.js +65 -0
  114. package/dist/pin/index.js +16 -0
  115. package/dist/pin/rollout.d.ts.map +1 -1
  116. package/dist/pin/rollout.js +382 -0
  117. package/dist/pin/scan.js +287 -0
  118. package/dist/pin/source.js +171 -0
  119. package/dist/pin/waves.js +109 -0
  120. package/dist/plan-digest.js +67 -0
  121. package/dist/plugin.js +323 -0
  122. package/dist/serializer.js +23 -0
  123. package/dist/state-ownership.js +29 -0
  124. package/dist/terragrunt/affected.d.ts.map +1 -1
  125. package/dist/terragrunt/affected.js +290 -0
  126. package/dist/terragrunt/index.js +10 -0
  127. package/dist/terragrunt/mocks.js +270 -0
  128. package/dist/terragrunt/run.js +264 -0
  129. package/dist/terragrunt/units.js +195 -0
  130. package/dist/terragrunt/wave.js +125 -0
  131. package/dist/validate-cli.js +4 -0
  132. package/dist/validate.js +31 -0
  133. package/package.json +10 -10
  134. package/src/codegen/package.ts +2 -2
  135. package/src/components/capability-plugin.test.ts +97 -2
  136. package/src/components/capability-plugin.ts +117 -12
  137. package/src/components/pr-loop.tofu.test.ts +51 -11
  138. package/src/index.ts +1 -0
  139. package/src/pin/rollout.ts +2 -1
  140. package/src/terragrunt/affected.test.ts +11 -0
  141. package/src/terragrunt/affected.ts +2 -1
@@ -0,0 +1,219 @@
1
+ /**
2
+ * TerraformWatchOp composite (#2087): drift reporting for one root module,
3
+ * on a schedule.
4
+ *
5
+ * The observe position on the lifecycle dial. `TerraformApplyOp` next door
6
+ * changes the estate; this one never does: it inits, plans, and reports what
7
+ * the plan found. `terraform plan -detailed-exitcode` is the whole mechanism.
8
+ * Exit 0 is "the world matches the configuration", exit 2 is drift, and
9
+ * `terraformPlan` turns that into the boolean `changed`, which rides out of
10
+ * the Op as the run's `Drift` outcome.
11
+ *
12
+ * Phases: Init, Plan, and (for a finding mode that opens something) Report.
13
+ *
14
+ * ## Only the human plan is ever posted
15
+ *
16
+ * `terraformPlan` returns two renders of the same plan: `json`, from
17
+ * `terraform show -json`, and `text`, from `terraform show -no-color`. The
18
+ * JSON one carries every resource attribute value the plan touches, including
19
+ * provider credentials that no `sensitive` marking covers, because
20
+ * `sensitive_values` describes the configuration's own declarations rather
21
+ * than everything a provider puts in a plan. So the issue and pull-request
22
+ * modes reference `plan.out.text` and nothing else. There is no option to
23
+ * post the JSON, and `terraform-watch-op.test.ts` asserts no reference to it
24
+ * reaches the finding step.
25
+ *
26
+ * The same rule holds on a live root, where the JSON is choudoufu's own
27
+ * bound/omissions/unowned document rather than a plan representation, and
28
+ * carries live identities and tag values for every resource the estate
29
+ * touched. `choudoufuLivePlan` composes the plan text and the adoption ledger
30
+ * into one `finding` string for exactly this reason: the body is one
31
+ * reference to one text field, so there is no arrangement of the args in
32
+ * which the document reaches a body.
33
+ *
34
+ * ## A live root reports three things, not one (#2105)
35
+ *
36
+ * `live: true` swaps the Plan step for `choudoufuLivePlan`. On a choudoufu
37
+ * root ownership is a pair of tags on the resource rather than an entry in a
38
+ * state file, so a plan against the live system can answer three questions
39
+ * where a stock plan answers one: whether the estate drifted (the
40
+ * `-detailed-exitcode` exit, `Drift` as before), how many live resources sit
41
+ * at a declared identity carrying no marker (`Unowned`), and how many of those
42
+ * an exact content match makes claimable (`Adoptable`). All three come off a
43
+ * single live read, published as three run outcomes from the one step.
44
+ *
45
+ * The finding modes then carry the adoption ledger under the plan text: one
46
+ * line per adoptable match with its address, its live identity and the
47
+ * `tofu-estate`/`tofu-address` values that claim it, in the row form
48
+ * `live-plan -adoption-only` prints, followed by any contested address. That
49
+ * is a report, not an action. `TerraformAdoptOp` next door is what writes the
50
+ * markers, and it gates first.
51
+ *
52
+ * ## Findings reuse `reconcilePr`
53
+ *
54
+ * `issue`, `comment` and `pull-request` call core's `reconcilePr` activity,
55
+ * the one place in chant that shells to `gh`, rather than growing a second
56
+ * copy of those calls here.
57
+ * That activity built its own change-set summary and had no way to be handed
58
+ * one, so #2087 added a single field to it (`ReconcilePrArgs.body`), which is
59
+ * what carries the `-no-color` plan through. Note that the pull-request mode
60
+ * of `reconcilePr` regenerates chant TypeScript via `chant import`; a
61
+ * terraform-native regeneration of HCL from live state is #2089, so
62
+ * `findingMode: "issue"` is the mode with an end-to-end answer and
63
+ * `"pull-request"` opens a PR whose body is the plan. `"comment"` (#2231) is
64
+ * the mode for the plan-on-pull-request shape: the plan is posted onto the
65
+ * pull request that triggered the run and re-edited there on the next push,
66
+ * which is where a reviewer is already looking. It needs that trigger; a run
67
+ * without one fails the Report step by name rather than posting elsewhere. On a live root #2089's
68
+ * question has a different answer rather than a pending one, and it is not
69
+ * regeneration: see `TerraformAdoptOp`.
70
+ *
71
+ * ## The schedule
72
+ *
73
+ * A cron lands on the Op itself as `schedule` (#2120), the same field
74
+ * `WatchOp` and `ConvergeOp` set. It is runtime-neutral data: `chant
75
+ * operator` reads it as this Op's tick cadence, `generateOpsPipeline` against
76
+ * the github lexicon renders it as a CI cron, and a hosting lexicon hands it
77
+ * to its own scheduler. The one-shot `chant run app-watch` path ignores it.
78
+ * `examples/scheduled-watch/` is the CI recipe.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * import { TerraformWatchOp } from "@intentius/chant-lexicon-terraform";
83
+ *
84
+ * // one-shot, local executor: chant run app-watch
85
+ * export const { op } = TerraformWatchOp({ name: "app-watch", root: "app" });
86
+ *
87
+ * // the plan as a sticky comment on the pull request that triggered the run
88
+ * export const { op } = TerraformWatchOp({
89
+ * name: "app-plan",
90
+ * root: "app",
91
+ * findingMode: "comment",
92
+ * });
93
+ *
94
+ * // nightly, opening an issue when the plan is non-empty
95
+ * export const { op } = TerraformWatchOp({
96
+ * name: "app-watch",
97
+ * root: "app",
98
+ * schedule: "0 6 * * *",
99
+ * findingMode: "issue",
100
+ * });
101
+ *
102
+ * // a choudoufu estate: drift, unowned and adoptable, with the ledger in the issue
103
+ * export const { op } = TerraformWatchOp({
104
+ * name: "estate-watch",
105
+ * root: "estate",
106
+ * live: true,
107
+ * findingMode: "issue",
108
+ * });
109
+ * ```
110
+ */
111
+ import { Op, phase } from "@intentius/chant/op";
112
+ import { resolveRootModeSync } from "../op/resolve-root-mode.js";
113
+ import { CHOUDOUFU_LIVE_PLAN_OUT_REFUSAL, DEFAULT_PLAN_FILE } from "../op/activities/terraform.js";
114
+ import { terraformInit as initStep, terraformPlan as planStep, choudoufuLivePlan as livePlanStep, } from "../op/builders.js";
115
+ export function TerraformWatchOp(config) {
116
+ const findingMode = config.findingMode ?? "report";
117
+ const where = config.cwd ? { cwd: config.cwd } : {};
118
+ if (config.live && config.planFile !== undefined) {
119
+ throw new Error(`TerraformWatchOp "${config.name}": planFile is refused on a live root. choudoufu: ` +
120
+ `"${CHOUDOUFU_LIVE_PLAN_OUT_REFUSAL}". A live plan writes no saved plan, so there is no file to name; ` +
121
+ "drop planFile, or drop live if this root runs stock.");
122
+ }
123
+ if (!config.live && config.estate !== undefined) {
124
+ throw new Error(`TerraformWatchOp "${config.name}": estate names the estate a live plan looks for markers of, and ` +
125
+ "this Op is not on a live root. Set live: true, or drop estate.");
126
+ }
127
+ // The `live` flag against the root it names. Best-effort and synchronous:
128
+ // `resolveRootModeSync` reads a `chant.config.json` only and returns
129
+ // undefined for anything else, so a `chant.config.ts` project reaches
130
+ // neither branch and TF028 is what reports the mismatch there. Where the
131
+ // mode does resolve, refusing at build time names the Op, which a
132
+ // post-synth diagnostic does too but a run-time failure at the first step
133
+ // does not.
134
+ const resolvedMode = resolveRootModeSync(config.root, config.cwd)?.mode;
135
+ if (resolvedMode === "live" && !config.live) {
136
+ throw new Error(`TerraformWatchOp "${config.name}": root "${config.root}" runs choudoufu with a declared estate, ` +
137
+ "but this Op watches it in stock mode. A stock plan reports drift alone and stays quiet about " +
138
+ "every unowned and adoptable resource in the estate. Set live: true.");
139
+ }
140
+ if (resolvedMode === "state" && config.live) {
141
+ throw new Error(`TerraformWatchOp "${config.name}": live: true, but root "${config.root}" declares no estate (or ` +
142
+ "terraform.binary is not \"choudoufu\"), so there is no live system to plan against. Drop " +
143
+ "live, or declare the root's estate.");
144
+ }
145
+ // `id` is what makes `plan.out` legal, and one field of `plan.out` is the
146
+ // only channel by which any part of the plan leaves this Op.
147
+ //
148
+ // A live root reads the live system instead of a state file, so the three
149
+ // things a watch reports come off one `choudoufuLivePlan`: `drift` from
150
+ // `-detailed-exitcode`, and `unowned`/`adoptable` from the `-json` document
151
+ // the same run wrote. Three attributes, one live read: see
152
+ // `ActivityStep.outcomeAttribute`'s array form (#2105).
153
+ const plan = config.live
154
+ ? livePlanStep(config.root, { ...where, ...(config.estate ? { estate: config.estate } : {}), id: "plan" })
155
+ : planStep(config.root, { planFile: config.planFile ?? DEFAULT_PLAN_FILE, ...where, id: "plan" });
156
+ plan.outcomeAttribute = config.live
157
+ ? [
158
+ { name: "Drift", from: "drift" },
159
+ { name: "Unowned", from: "unowned" },
160
+ { name: "Adoptable", from: "adoptable" },
161
+ ]
162
+ : { name: "Drift", from: "changed" };
163
+ const phases = [
164
+ phase("Init", [initStep(config.root, { ...where, ...(config.upgrade ? { upgrade: true } : {}) })]),
165
+ phase("Plan", [plan]),
166
+ ];
167
+ if (findingMode !== "report") {
168
+ // `env` is `reconcilePr`'s name for the thing being reconciled; for
169
+ // terraform that is the root, which with its workspace is the deployment
170
+ // target. `entries: []` and `body` together mean the activity opens what
171
+ // it is given rather than running `chant lifecycle plan` to find out.
172
+ //
173
+ // `op` is what keeps the sticky issue this Op's own (#2319). The root
174
+ // alone does not: a stock drift watch and a `live: true` watch over one
175
+ // root is the pairing this composite is built for, and before #2319 both
176
+ // resolved to one env-keyed marker, so each nightly run rewrote the
177
+ // other's issue title and body. Two Ops in a project do not share a raw
178
+ // name — it names the Op's output directory — though the marker carries
179
+ // the slug rather than the raw name, and the slug is not injective; see
180
+ // `issueMarker` for what that leaves and why it is accepted.
181
+ const report = {
182
+ kind: "activity",
183
+ fn: "reconcilePr",
184
+ args: {
185
+ env: config.root,
186
+ op: config.name,
187
+ mode: findingMode,
188
+ entries: [],
189
+ title: config.title ?? `Terraform drift in root "${config.root}"`,
190
+ // Stock: the `-no-color` plan. Live: the same plan text with the
191
+ // adoption ledger under it, which `choudoufuLivePlan` composes into
192
+ // one `finding` field precisely so that a body is one reference and
193
+ // the `-json` document has no path into one at all.
194
+ body: config.live ? plan.out.finding : plan.out.text,
195
+ ...(config.branch ? { branch: config.branch } : {}),
196
+ },
197
+ outcomeAttribute: findingMode === "pull-request"
198
+ ? { name: "PR", from: "prUrl" }
199
+ : findingMode === "comment"
200
+ ? { name: "Comment", from: "commentUrl" }
201
+ : { name: "Issue", from: "issueUrl" },
202
+ };
203
+ phases.push(phase("Report", [report]));
204
+ }
205
+ const op = Op({
206
+ name: config.name,
207
+ overview: config.live
208
+ ? `Live-plan the "${config.root}" choudoufu estate and report drift, unowned and adoptable resources`
209
+ : `Plan the "${config.root}" terraform root and report drift`,
210
+ labels: {
211
+ Watch: "true",
212
+ TerraformRoot: config.root,
213
+ ...(config.live ? { TerraformMode: "live" } : {}),
214
+ },
215
+ ...(config.schedule ? { schedule: { cron: config.schedule, overlap: "skip" } } : {}),
216
+ phases,
217
+ });
218
+ return { op };
219
+ }
package/dist/config.js ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The `terraform` namespace in `chant.config.ts` (#1344).
3
+ *
4
+ * ```ts
5
+ * import type { ChantConfig } from "@intentius/chant/config";
6
+ * import "@intentius/chant-lexicon-terraform"; // brings the `terraform` key into ChantConfig
7
+ *
8
+ * export default {
9
+ * lexicons: ["terraform"],
10
+ * terraform: {
11
+ * binary: "tofu",
12
+ * roots: {
13
+ * app: { dir: "./terraform/app", workspace: "prod", varFiles: ["prod.tfvars"] },
14
+ * },
15
+ * },
16
+ * } satisfies ChantConfig;
17
+ * ```
18
+ *
19
+ * One schema, three consumers: core validates the namespace against it at load,
20
+ * the type below is derived from it, and that derived type is what augments
21
+ * `ChantConfig`, so an unknown key fails at build time and at compile time for
22
+ * the same reason, rather than being silently ignored at both.
23
+ */
24
+ import { z } from "zod";
25
+ /**
26
+ * chant's cross-lexicon delete-mode vocabulary (`lexicons/k8s/src/op/
27
+ * activities/kubectl.ts`'s `ApplyDeleteMode`, `packages/core/src/op/
28
+ * activities/apply.ts`'s `DeleteMode`), read here for a live root and mapped
29
+ * onto choudoufu's `policy` block (#2106): `"never"` requires the root's
30
+ * `policy` to set `undeclared_tagged` to `"keep"`, `"untag"` or `"report"`
31
+ * (TF026 enforces this at build time); `"owned-only"` is choudoufu's own
32
+ * default verb for that quadrant (`delete`) and needs nothing; `"gated"`
33
+ * needs nothing beyond `TerraformApplyOp`'s own approval gate. Inert on a
34
+ * stock root: choudoufu's `policy` block does not exist there.
35
+ */
36
+ export const terraformDeleteModeSchema = z.enum(["never", "owned-only", "gated"]);
37
+ /**
38
+ * One Terraform root module chant reads. `dir` is the only required field:
39
+ * everything else names how the root is invoked rather than what it declares.
40
+ *
41
+ * `strictObject`, not `object`. Core applies `.strict()` to the top level of a
42
+ * declared namespace itself, but nested objects are the lexicon's own to make
43
+ * strict, and a root entry is exactly where a typo (`varfiles`) would
44
+ * otherwise land silently.
45
+ */
46
+ export const terraformRootSchema = z.strictObject({
47
+ /** Root module directory, relative to the project root (where `chant.config.*` lives). */
48
+ dir: z.string(),
49
+ /** Terraform workspace to select for this root. Omitted means `default`. */
50
+ workspace: z.string().optional(),
51
+ /** `-var-file` arguments, in order, relative to `dir`. */
52
+ varFiles: z.array(z.string()).optional(),
53
+ /** `-backend-config` key/value pairs handed to `init`. */
54
+ backendConfig: z.record(z.string(), z.string()).optional(),
55
+ /**
56
+ * The choudoufu estate this root runs against, for a live root whose HCL
57
+ * does not declare one of its own (#2479).
58
+ *
59
+ * This is the environment dimension. A live root has no backend and no
60
+ * state file, so nothing about it is positional: dev, staging and
61
+ * production can be the same directory under different estates, which is
62
+ * why named roots rather than a directory tree are where environments
63
+ * belong here.
64
+ *
65
+ * roots: {
66
+ * "app-staging": { dir: "./terraform", estate: "app-staging", varFiles: ["staging.tfvars"] },
67
+ * "app-prod": { dir: "./terraform", estate: "app-prod", varFiles: ["prod.tfvars"] },
68
+ * }
69
+ *
70
+ * How inputs vary across those environments is `varFiles`, which is
71
+ * already per-root and ordered - the question a directory tree answers
72
+ * with HCL inheritance, answered here by a map entry.
73
+ *
74
+ * Refused (TF028) when the root's own HCL declares an estate in a `live`
75
+ * block or `estate.chdf.hcl` sidecar. choudoufu refuses `-estate` beside a
76
+ * declaration ("This configuration's live block is what names its estate"),
77
+ * and a value here that the declaration then overrode would be a setting
78
+ * written down and silently ignored.
79
+ */
80
+ estate: z.string().optional(),
81
+ /** This root's declared delete mode. See {@link terraformDeleteModeSchema}. */
82
+ delete: terraformDeleteModeSchema.optional(),
83
+ });
84
+ export const terraformConfigSchema = z.strictObject({
85
+ /**
86
+ * Which CLI drives the roots. `terraform` and `tofu` are wire-compatible
87
+ * for everything this lexicon does, so the choice is recorded rather than
88
+ * inferred. `choudoufu` (#2103) is an OpenTofu fork: a root running it also
89
+ * needs a declared estate (a `live` block or an `estate.chdf.hcl` sidecar,
90
+ * seen by the shared HCL parse) before it counts as live. Declaring the
91
+ * binary alone runs the root stock, exactly as `terraform`/`tofu` do.
92
+ */
93
+ binary: z.enum(["terraform", "tofu", "choudoufu"]).optional(),
94
+ /**
95
+ * How far a root's parse follows its `module` calls (#2112), spelled the
96
+ * way tflint spells it. `local` (the default) reads modules sourced from a
97
+ * relative path, so TF014, TF015 and TF020 see inside them; `none` reads
98
+ * only each root's own directory; `all` is reserved for fetching registry
99
+ * and git modules and is refused with a message, since chant fetches
100
+ * nothing. See `hcl/descend.ts`.
101
+ */
102
+ callModuleType: z.enum(["local", "none", "all"]).optional(),
103
+ /**
104
+ * The directory a root's local module sources may resolve within (#2874),
105
+ * absolute or relative to the project root. Unset, it is the project root,
106
+ * or a root's own directory when its `dir` lies outside the project. A
107
+ * source that resolves outside it is refused with a warning, as before.
108
+ *
109
+ * `chant workspace graph` sets it to the workspace root when it reads a
110
+ * `terraform` or `choudoufu` member through a reader project of its own,
111
+ * so a member that calls `../modules/x` keeps its modules. A project
112
+ * rarely needs it: widening the boundary means reading modules the
113
+ * project does not own.
114
+ */
115
+ moduleRoot: z.string().optional(),
116
+ /** Named root modules. The name is the entity-key prefix, so keep it stable. */
117
+ roots: z.record(z.string(), terraformRootSchema),
118
+ });