@intentius/chant-lexicon-terraform 0.124.31 → 0.124.33

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 (149) 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.d.ts +2 -0
  20. package/dist/composites/terraform-adopt-op.d.ts.map +1 -1
  21. package/dist/composites/terraform-adopt-op.js +153 -0
  22. package/dist/composites/terraform-apply-op.d.ts +2 -0
  23. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  24. package/dist/composites/terraform-apply-op.js +184 -0
  25. package/dist/composites/terraform-pin-rollout-op.js +45 -0
  26. package/dist/composites/terraform-watch-op.js +219 -0
  27. package/dist/config.js +118 -0
  28. package/dist/describe-resources.js +984 -0
  29. package/dist/generated/index.js +1 -0
  30. package/dist/hcl/descend.js +208 -0
  31. package/dist/hcl/edges.js +287 -0
  32. package/dist/hcl/jsonencode.js +442 -0
  33. package/dist/hcl/marker.js +212 -0
  34. package/dist/hcl/parse.js +372 -0
  35. package/dist/hcl/references.js +156 -0
  36. package/dist/hcl/roots.js +98 -0
  37. package/dist/hcl/suppressions.js +189 -0
  38. package/dist/hcl/value.js +72 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +46 -0
  42. package/dist/integrity.json +2 -2
  43. package/dist/lint/audit-catalog.js +501 -0
  44. package/dist/lint/audit-lineage.js +442 -0
  45. package/dist/lint/post-synth/blocks.js +87 -0
  46. package/dist/lint/post-synth/fixtures/load.js +94 -0
  47. package/dist/lint/post-synth/flat-attributes.js +135 -0
  48. package/dist/lint/post-synth/index.js +90 -0
  49. package/dist/lint/post-synth/module-source.js +153 -0
  50. package/dist/lint/post-synth/scope.js +104 -0
  51. package/dist/lint/post-synth/terragrunt.js +63 -0
  52. package/dist/lint/post-synth/tf001.js +138 -0
  53. package/dist/lint/post-synth/tf002.js +143 -0
  54. package/dist/lint/post-synth/tf003.js +60 -0
  55. package/dist/lint/post-synth/tf004.js +54 -0
  56. package/dist/lint/post-synth/tf005.js +68 -0
  57. package/dist/lint/post-synth/tf006.js +43 -0
  58. package/dist/lint/post-synth/tf007.js +66 -0
  59. package/dist/lint/post-synth/tf008.js +64 -0
  60. package/dist/lint/post-synth/tf009.js +49 -0
  61. package/dist/lint/post-synth/tf010.js +40 -0
  62. package/dist/lint/post-synth/tf011.js +38 -0
  63. package/dist/lint/post-synth/tf012.js +36 -0
  64. package/dist/lint/post-synth/tf013.js +53 -0
  65. package/dist/lint/post-synth/tf014.js +61 -0
  66. package/dist/lint/post-synth/tf015.js +62 -0
  67. package/dist/lint/post-synth/tf016.js +66 -0
  68. package/dist/lint/post-synth/tf017.js +39 -0
  69. package/dist/lint/post-synth/tf018.js +63 -0
  70. package/dist/lint/post-synth/tf019.js +60 -0
  71. package/dist/lint/post-synth/tf020.js +103 -0
  72. package/dist/lint/post-synth/tf021.js +117 -0
  73. package/dist/lint/post-synth/tf022.js +51 -0
  74. package/dist/lint/post-synth/tf024.js +62 -0
  75. package/dist/lint/post-synth/tf025.js +83 -0
  76. package/dist/lint/post-synth/tf026.js +74 -0
  77. package/dist/lint/post-synth/tf027.js +66 -0
  78. package/dist/lint/post-synth/tf028.js +83 -0
  79. package/dist/lint/post-synth/tf029.js +73 -0
  80. package/dist/lint/post-synth/tf030.js +253 -0
  81. package/dist/lint/post-synth/tf031.js +332 -0
  82. package/dist/lint/post-synth/tf032.js +201 -0
  83. package/dist/lint/post-synth/tf033.js +126 -0
  84. package/dist/lint/post-synth/tf034.js +45 -0
  85. package/dist/lint/post-synth/tf035.js +63 -0
  86. package/dist/lint/post-synth/tf036.js +97 -0
  87. package/dist/lint/post-synth/tf037.js +80 -0
  88. package/dist/lint/post-synth/tf038.js +63 -0
  89. package/dist/lint/post-synth/tf039.js +55 -0
  90. package/dist/lint/post-synth/tf040.js +67 -0
  91. package/dist/lint/post-synth/tf041.js +53 -0
  92. package/dist/lint/post-synth/tf042.js +38 -0
  93. package/dist/lint/post-synth/tf043.js +45 -0
  94. package/dist/lint/post-synth/tf044.js +45 -0
  95. package/dist/lint/post-synth/tf045.js +44 -0
  96. package/dist/lint/rules/fixtures/load.js +50 -0
  97. package/dist/lint/rules/index.js +4 -0
  98. package/dist/lint/rules/plan-before-apply.js +136 -0
  99. package/dist/lint/secret-shape.js +149 -0
  100. package/dist/live-ownership.js +57 -0
  101. package/dist/lsp/completions.js +95 -0
  102. package/dist/lsp/context.js +358 -0
  103. package/dist/lsp/hover.js +45 -0
  104. package/dist/lsp/option-keys.js +93 -0
  105. package/dist/manifest.json +1 -1
  106. package/dist/op/activities/index.js +30 -0
  107. package/dist/op/activities/live-detect.js +143 -0
  108. package/dist/op/activities/pin-rollout.js +45 -0
  109. package/dist/op/activities/terraform.js +1270 -0
  110. package/dist/op/activity-contracts.js +162 -0
  111. package/dist/op/adoption.js +272 -0
  112. package/dist/op/builders.js +125 -0
  113. package/dist/op/resolve-root-mode.js +77 -0
  114. package/dist/package-cli.js +16 -0
  115. package/dist/pin/edit-ts.js +76 -0
  116. package/dist/pin/edit.js +118 -0
  117. package/dist/pin/forge.js +65 -0
  118. package/dist/pin/index.js +16 -0
  119. package/dist/pin/rollout.d.ts.map +1 -1
  120. package/dist/pin/rollout.js +382 -0
  121. package/dist/pin/scan.js +287 -0
  122. package/dist/pin/source.js +171 -0
  123. package/dist/pin/waves.js +109 -0
  124. package/dist/plan-digest.js +67 -0
  125. package/dist/plugin.js +323 -0
  126. package/dist/serializer.js +23 -0
  127. package/dist/state-ownership.js +29 -0
  128. package/dist/terragrunt/affected.d.ts.map +1 -1
  129. package/dist/terragrunt/affected.js +290 -0
  130. package/dist/terragrunt/index.js +10 -0
  131. package/dist/terragrunt/mocks.js +270 -0
  132. package/dist/terragrunt/run.js +264 -0
  133. package/dist/terragrunt/units.js +195 -0
  134. package/dist/terragrunt/wave.js +125 -0
  135. package/dist/validate-cli.js +4 -0
  136. package/dist/validate.js +31 -0
  137. package/package.json +10 -10
  138. package/src/codegen/package.ts +2 -2
  139. package/src/components/capability-plugin.test.ts +97 -2
  140. package/src/components/capability-plugin.ts +117 -12
  141. package/src/components/pr-loop.tofu.test.ts +51 -11
  142. package/src/composites/terraform-adopt-op.test.ts +6 -0
  143. package/src/composites/terraform-adopt-op.ts +7 -2
  144. package/src/composites/terraform-apply-op.test.ts +6 -0
  145. package/src/composites/terraform-apply-op.ts +7 -2
  146. package/src/index.ts +1 -0
  147. package/src/pin/rollout.ts +2 -1
  148. package/src/terragrunt/affected.test.ts +11 -0
  149. package/src/terragrunt/affected.ts +2 -1
@@ -0,0 +1,482 @@
1
+ /**
2
+ * A behaviour engine's request from a terraform estate, declared or live (#2360).
3
+ *
4
+ * The epic (#2355) wants two predictions shown as a delta: what the file
5
+ * declares, and what the account holds. For a choudoufu root the second is
6
+ * readable — `live-ls -json` lists every resource the account holds under the
7
+ * estate's marker, `live-plan -json` says which declared instance each one is
8
+ * bound to and which declarations nothing answers for — and this module turns
9
+ * either reading into the request `PredictBehaviourOptions` describes:
10
+ * `entityNames`, `entities`, `edges`, `edgeCoverage`, `traffic`.
11
+ *
12
+ * One producer for both sides, on purpose. A delta is only a statement about
13
+ * the estate if both sides were assembled the same way, and the third review
14
+ * comment on #2360 is about exactly the asymmetry this avoids: the declared
15
+ * path had no containment producer, so a "one zone lost" verdict differed
16
+ * between the sides for a reason that was not drift. Here the same reference
17
+ * catalog (`./reference-catalog.ts`) runs over the same reconstruction
18
+ * (`reconstructEdges`, `packages/core/src/graph-refs.ts`) on both sides, and
19
+ * `edgeCoverage.containmentEdges` is populated the same way on each.
20
+ *
21
+ * ## What the live side is, and is not
22
+ *
23
+ * The two documents carry identities, types, addresses and markers. Neither
24
+ * carries a resource's attributes: a subnet's `vpc_id` as the account holds it
25
+ * is in no section of either. So a live node's arguments are the declaration's
26
+ * arguments — the body choudoufu applied — and its **references** are the
27
+ * declaration's references resolved against what is bound: a reference whose
28
+ * target the plan bound resolves to that target's live identity, and one whose
29
+ * target is absent from the account stays unresolved and is reported as
30
+ * `dangling`, naming the node, the argument and the address it points at. A
31
+ * resource the listing holds that nothing declares has no body at all; it is a
32
+ * node with an identity and a type, and its kind is counted into
33
+ * `unresolvedKinds` unless the catalog knows that kind references nothing.
34
+ *
35
+ * The drift this side sees is therefore **membership** drift: a resource added
36
+ * out of band under the estate's marker, a declared resource never applied, a
37
+ * resource at a declared identity that another estate holds. An attribute
38
+ * changed live — an instance resized by hand — is not in either document and
39
+ * is not claimed here; the size an engine is sent is the declared one on both
40
+ * sides. `describe-resources.ts`'s module doc has the same boundary for the
41
+ * observation read, for the same reason.
42
+ *
43
+ * One consequence of that worth stating outright, because it is the one a
44
+ * reader will hit first: a block with `count` or `for_each` is **one** node on
45
+ * both sides, whatever the account holds. The request's unit is the entity the
46
+ * caller asked about, and the caller asked about `aws_eip.pool`, not about two
47
+ * slots — inventing `aws_eip.pool[0]` and `[1]` as entities would put names in
48
+ * the report that are in no `entityNames` list and in no build. Every instance
49
+ * address the plan named rides on `props.live.instances` instead, so an engine
50
+ * that prices per instance has the count and one that does not is not silently
51
+ * handed a multiplier. An estate whose drift is a changed `count` is therefore
52
+ * a delta this path does not yet show.
53
+ *
54
+ * ## `edgeCoverage`, honestly
55
+ *
56
+ * `partial` whenever `dangling` or `unresolvedKinds` is non-empty, `complete`
57
+ * only when both are empty, and `dangling` carries `reconstructEdges`'s
58
+ * `DanglingRef` records through unflattened — `from` is the field that says
59
+ * which node's argument leaves the estate. `containmentEdges` comes from the
60
+ * reconstruction's `containmentEdges`, the traversable shape, and never from
61
+ * the `containment` pairs beside it. All three of those are #2360's review
62
+ * comments, applied.
63
+ */
64
+ import { reconstructEdges } from "@intentius/chant/graph-refs";
65
+ import { classifyLiveInstance, declaredOf, entityKeyFor, instancesOf, } from "../describe-resources.js";
66
+ import { RESOURCE_TYPE } from "../hcl/parse.js";
67
+ import { isUnresolvedKind, TERRAFORM_REFERENCE_CATALOG } from "./reference-catalog.js";
68
+ /** Code-unit order, so the request's bytes do not depend on the machine's locale. */
69
+ const byCodeUnit = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
70
+ /** The block a document instance address belongs to: `aws_eip.pool[0]` is `aws_eip.pool`. */
71
+ export function blockAddressOf(instance) {
72
+ return instance.replace(/\[[^\]]*\]$/, "");
73
+ }
74
+ /** The region an ARN names, or undefined for a global one (`arn:aws:s3:::bucket`). */
75
+ export function regionOfArn(arn) {
76
+ const parts = arn.split(":");
77
+ return parts[0] === "arn" && parts.length >= 6 && parts[3] ? parts[3] : undefined;
78
+ }
79
+ function asString(value) {
80
+ return typeof value === "string" && value.length > 0 ? value : undefined;
81
+ }
82
+ function bodyOf(props) {
83
+ const body = props.body;
84
+ return typeof body === "object" && body !== null && !Array.isArray(body) ? body : {};
85
+ }
86
+ function referencesOf(entity) {
87
+ return Array.isArray(entity?.references) ? entity.references : [];
88
+ }
89
+ /** True when a body holds any `${…}` string at all, at any depth. */
90
+ function holdsReference(value, depth = 0) {
91
+ if (depth > 12)
92
+ return false;
93
+ if (typeof value === "string")
94
+ return value.includes("${");
95
+ if (Array.isArray(value))
96
+ return value.some((v) => holdsReference(v, depth + 1));
97
+ if (typeof value === "object" && value !== null) {
98
+ return Object.values(value).some((v) => holdsReference(v, depth + 1));
99
+ }
100
+ return false;
101
+ }
102
+ /** `${addr}` followed by an attribute, an index, or the closing brace — the address itself, not a longer one. */
103
+ function mentions(text, address) {
104
+ const escaped = address.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
105
+ return new RegExp(`\\$\\{${escaped}(?:[.\\[}\\s)\\],]|$)`).test(text);
106
+ }
107
+ /** One `${…}` and nothing else: the value the account holds *is* the target's identity. */
108
+ const WHOLE_INTERPOLATION = /^\$\{[^{}]*\}$/;
109
+ /**
110
+ * The body with every resolvable reference replaced by the identifier it
111
+ * stands for, so `reconstructEdges` can match it against the node that owns
112
+ * that identifier.
113
+ *
114
+ * A leaf string is replaced only when the reference **is** the whole value:
115
+ * the string is one `${…}` and nothing else, exactly one of the block's
116
+ * references under the same top-level argument is the one it names, and that
117
+ * reference resolves. Then the value in the account really is the target's
118
+ * identity, and matching it against the node holding that identity is a
119
+ * statement about the estate.
120
+ *
121
+ * Everything else is left alone, and the two cases it leaves alone are
122
+ * different. `"${a.id}-${b.id}"` and `"${a.id}-suffix"` are composed values —
123
+ * a name derived from an identity is not that identity, and substituting the
124
+ * bare identity into one would hand the resolver a value the account does not
125
+ * hold and get back an edge nothing points along. A string naming a reference
126
+ * that does not resolve keeps its `${…}`, which is what makes a reference to a
127
+ * target the account is missing come back from the resolver as `dangling` with
128
+ * the address in `value` rather than vanishing.
129
+ */
130
+ export function substituteReferences(body, references, addressOf, resolve) {
131
+ const leaf = (value, via) => {
132
+ if (!WHOLE_INTERPOLATION.test(value))
133
+ return value;
134
+ const matched = references.filter((r) => {
135
+ if ((r.viaAttr ?? "") !== via)
136
+ return false;
137
+ const address = addressOf(r.to);
138
+ return address !== undefined && mentions(value, address);
139
+ });
140
+ if (matched.length !== 1)
141
+ return value;
142
+ return resolve(matched[0].to) ?? value;
143
+ };
144
+ const walk = (value, via, depth) => {
145
+ if (depth > 12)
146
+ return value;
147
+ if (typeof value === "string")
148
+ return leaf(value, via);
149
+ if (Array.isArray(value))
150
+ return value.map((v) => walk(v, via, depth + 1));
151
+ if (typeof value === "object" && value !== null) {
152
+ const out = {};
153
+ for (const [key, inner] of Object.entries(value)) {
154
+ out[key] = walk(inner, depth === 0 ? key : via, depth + 1);
155
+ }
156
+ return out;
157
+ }
158
+ return value;
159
+ };
160
+ return walk(body, "", 0);
161
+ }
162
+ /**
163
+ * The provider type a resource block declares — `aws_subnet` — or undefined
164
+ * for any other block.
165
+ *
166
+ * Read off the address, because the block carries no `type` field:
167
+ * `../hcl/parse.ts` composes a resource's address as `${type}.${name}` and
168
+ * keeps it unqualified even inside a descended child module, where the calling
169
+ * chain lives on `props.callers` and the `module.<name>` segments live in the
170
+ * entity's key. An address with no dot is a live row's identity standing in
171
+ * for an address it had none of, and is not a type.
172
+ *
173
+ * This is the kind a node is given for `reconstructEdges`, and it is the same
174
+ * derivation `./kinds.ts`'s `resolveType` runs to look a coverage row up,
175
+ * so the graph and the coverage table agree on what a block is. A live row
176
+ * states its own type instead, on `props.resourceType`, the name
177
+ * `observeAmbient` already uses for it.
178
+ */
179
+ function providerTypeOf(entity) {
180
+ if (entity.entityType !== RESOURCE_TYPE)
181
+ return undefined;
182
+ const stated = asString(entity.props.resourceType);
183
+ if (stated !== undefined)
184
+ return stated;
185
+ const address = asString(entity.props.address);
186
+ if (address === undefined)
187
+ return undefined;
188
+ const dot = address.indexOf(".");
189
+ return dot > 0 ? address.slice(0, dot) : undefined;
190
+ }
191
+ function emptyPart() {
192
+ return { nodes: [], entities: new Map(), sources: {}, absent: [], unpredicted: {}, unresolvedKinds: new Set() };
193
+ }
194
+ function liveVerdictOf(address, plan) {
195
+ const instances = instancesOf(address, plan);
196
+ if (instances.length === 0)
197
+ return { kind: "absent", instances };
198
+ const verdicts = instances.map((a) => classifyLiveInstance(a, plan));
199
+ const unobserved = verdicts.find((v) => v.kind === "unobserved");
200
+ if (unobserved && unobserved.kind === "unobserved") {
201
+ return {
202
+ kind: "unobserved",
203
+ instances,
204
+ // `no-credentials` is an observation reason and not a prediction one;
205
+ // the credential wording stays in the detail.
206
+ reason: unobserved.reason === "no-credentials" ? "read-failed" : unobserved.reason,
207
+ detail: unobserved.detail,
208
+ };
209
+ }
210
+ const foreign = verdicts.find((v) => v.kind === "foreign");
211
+ if (foreign && foreign.kind === "foreign") {
212
+ return {
213
+ kind: "present",
214
+ status: "foreign",
215
+ ownership: "foreign",
216
+ instances,
217
+ ...(foreign.row.identity ? { identity: foreign.row.identity } : {}),
218
+ ...(foreign.row.heldBy ? { heldBy: foreign.row.heldBy } : {}),
219
+ };
220
+ }
221
+ const adoptable = verdicts.find((v) => v.kind === "adoptable");
222
+ if (adoptable && adoptable.kind === "adoptable") {
223
+ return {
224
+ kind: "present",
225
+ status: "adoptable",
226
+ ownership: "unknown",
227
+ instances,
228
+ ...(adoptable.row.identity ? { identity: adoptable.row.identity } : {}),
229
+ };
230
+ }
231
+ const owned = verdicts.filter((v) => v.kind === "owned");
232
+ if (owned.length === 0)
233
+ return { kind: "absent", instances };
234
+ const single = owned.length === 1 && instances.length === 1 ? owned[0] : undefined;
235
+ return {
236
+ kind: "present",
237
+ status: "bound",
238
+ ownership: "owned",
239
+ instances,
240
+ ...(single && single.kind === "owned" && single.row.identity ? { identity: single.row.identity } : {}),
241
+ };
242
+ }
243
+ /**
244
+ * A live root: the declared resources the account holds, at their live
245
+ * identities, plus everything the listing holds that nothing declares.
246
+ */
247
+ function liveRootPart(root, declared, entities, read, owned) {
248
+ const part = emptyPart();
249
+ const { listing, plan } = read;
250
+ const estate = plan.estate || listing.estate;
251
+ // The listing, by block address: a `count` block has one item per instance.
252
+ const listed = new Map();
253
+ for (const item of listing.items) {
254
+ if (!item.address)
255
+ continue;
256
+ const block = blockAddressOf(item.address);
257
+ (listed.get(block) ?? listed.set(block, []).get(block)).push(item);
258
+ }
259
+ // Live identity per entity key, for reference substitution below. Filled as
260
+ // declared blocks are placed; an orphan can be the target of nothing.
261
+ const identity = new Map();
262
+ const placed = [];
263
+ for (const d of declared) {
264
+ const entity = entities.get(d.name);
265
+ if (!entity)
266
+ continue;
267
+ if (entity.entityType !== RESOURCE_TYPE) {
268
+ // A variable, an output, a data block: not a thing the account holds,
269
+ // so the account has nothing to say about it. It rides through as
270
+ // declared, and the coverage rows decline it by name on both sides alike.
271
+ part.sources[d.name] = "live";
272
+ part.entities.set(d.name, entity);
273
+ continue;
274
+ }
275
+ const verdict = liveVerdictOf(d.address, plan);
276
+ if (verdict.kind === "absent") {
277
+ part.absent.push(d.name);
278
+ continue;
279
+ }
280
+ if (verdict.kind === "unobserved") {
281
+ part.unpredicted[d.name] = { type: RESOURCE_TYPE, reason: verdict.reason, detail: verdict.detail };
282
+ continue;
283
+ }
284
+ if (owned && verdict.ownership !== "owned") {
285
+ part.unpredicted[d.name] = {
286
+ type: RESOURCE_TYPE,
287
+ reason: "filtered",
288
+ detail: `this address read \`${verdict.ownership}\` on the root's live markers and --owned was requested`,
289
+ };
290
+ continue;
291
+ }
292
+ const items = listed.get(d.address) ?? [];
293
+ const single = items.length === 1 && verdict.instances.length === 1 ? items[0] : undefined;
294
+ const facts = {
295
+ status: verdict.status,
296
+ ownership: verdict.ownership,
297
+ ...(verdict.identity ? { identity: verdict.identity } : {}),
298
+ ...(single ? { arn: single.id } : {}),
299
+ ...(single && regionOfArn(single.id) ? { region: regionOfArn(single.id) } : {}),
300
+ ...(verdict.instances.length > 1 || verdict.instances[0] !== d.address ? { instances: verdict.instances } : {}),
301
+ ...(verdict.heldBy ? { heldBy: verdict.heldBy } : {}),
302
+ };
303
+ identity.set(d.name, verdict.identity);
304
+ part.sources[d.name] = "live";
305
+ placed.push({ name: d.name, entity, declared: d, facts });
306
+ }
307
+ const addressOf = (to) => asString(entities.get(to)?.props.address);
308
+ // A bound target resolves to its live identity; a target that is live but
309
+ // carries none (a marker read the tagging index had not settled, choudoufu
310
+ // #1014) resolves to its node id; a target not in the account resolves to
311
+ // nothing, and the reference stays in the body for the resolver to report.
312
+ const resolve = (to) => identity.has(to) ? (identity.get(to) ?? to) : undefined;
313
+ for (const { name, entity, declared: d, facts } of placed) {
314
+ const kind = providerTypeOf(entity) ?? "";
315
+ const references = referencesOf(entity);
316
+ const body = bodyOf(entity.props);
317
+ if (references.length === 0 && holdsReference(body))
318
+ part.unresolvedKinds.add(kind);
319
+ else if (isUnresolvedKind(kind))
320
+ part.unresolvedKinds.add(kind);
321
+ part.nodes.push({
322
+ id: name,
323
+ kind,
324
+ lexicon: "terraform",
325
+ attrs: substituteReferences(body, references, addressOf, resolve),
326
+ ...(facts.identity ? { physicalId: facts.identity } : {}),
327
+ });
328
+ part.entities.set(name, {
329
+ entityType: entity.entityType,
330
+ props: { ...entity.props, address: d.address, live: facts },
331
+ ...(entity.references ? { references: entity.references } : {}),
332
+ });
333
+ }
334
+ // What the account holds that nothing declares: the owned orphans. No body,
335
+ // so nothing to look for references in.
336
+ for (const item of listing.items) {
337
+ if (item.declared)
338
+ continue;
339
+ const address = item.address ? blockAddressOf(item.address) : item.id;
340
+ const name = entityKeyFor(root, address);
341
+ if (part.entities.has(name) || entities.has(name))
342
+ continue;
343
+ const facts = {
344
+ status: "orphan",
345
+ ownership: "owned",
346
+ arn: item.id,
347
+ ...(regionOfArn(item.id) ? { region: regionOfArn(item.id) } : {}),
348
+ ...(item.address && item.address !== address ? { instances: [item.address] } : {}),
349
+ tags: item.tags,
350
+ };
351
+ if (isUnresolvedKind(item.type))
352
+ part.unresolvedKinds.add(item.type);
353
+ part.sources[name] = "live";
354
+ part.nodes.push({ id: name, kind: item.type, lexicon: "terraform", attrs: {}, physicalId: item.id });
355
+ part.entities.set(name, {
356
+ entityType: RESOURCE_TYPE,
357
+ props: {
358
+ address,
359
+ root,
360
+ estate,
361
+ // `resourceType`, the name `observeAmbient` gives the same fact
362
+ // (`../describe-resources.ts`). An undeclared resource has no
363
+ // terraform address for a type to be read out of, so the listing's
364
+ // own statement of it is the only one there is.
365
+ resourceType: item.type,
366
+ mode: "live",
367
+ live: facts,
368
+ },
369
+ });
370
+ }
371
+ return part;
372
+ }
373
+ /** A declared root: every entity as declared, references resolved to the entity keys they name. */
374
+ function declaredRootPart(declared, entities) {
375
+ const part = emptyPart();
376
+ const addressOf = (to) => asString(entities.get(to)?.props.address);
377
+ const resolve = (to) => (entities.has(to) ? to : undefined);
378
+ for (const d of declared) {
379
+ const entity = entities.get(d.name);
380
+ if (!entity)
381
+ continue;
382
+ part.sources[d.name] = "declared";
383
+ part.entities.set(d.name, entity);
384
+ const kind = providerTypeOf(entity);
385
+ if (kind === undefined)
386
+ continue;
387
+ const references = referencesOf(entity);
388
+ const body = bodyOf(entity.props);
389
+ if (references.length === 0 && holdsReference(body))
390
+ part.unresolvedKinds.add(kind);
391
+ else if (isUnresolvedKind(kind))
392
+ part.unresolvedKinds.add(kind);
393
+ part.nodes.push({
394
+ id: d.name,
395
+ kind,
396
+ lexicon: "terraform",
397
+ attrs: substituteReferences(body, references, addressOf, resolve),
398
+ });
399
+ }
400
+ return part;
401
+ }
402
+ /**
403
+ * The coverage claim, from the reconstruction and the kinds nothing was
404
+ * looked for on. `partial` names what is missing; `complete` is claimed only
405
+ * when nothing is.
406
+ */
407
+ export function edgeCoverageOf(reconstructed, unresolvedKinds) {
408
+ const dangling = reconstructed.dangling;
409
+ const kinds = [...unresolvedKinds].sort(byCodeUnit);
410
+ return {
411
+ verdict: dangling.length > 0 || kinds.length > 0 ? "partial" : "complete",
412
+ ...(dangling.length > 0 ? { dangling } : {}),
413
+ ...(kinds.length > 0 ? { unresolvedKinds: kinds } : {}),
414
+ containmentEdges: reconstructed.containmentEdges,
415
+ };
416
+ }
417
+ /**
418
+ * Build the request. Pure: the documents are already parsed and nothing here
419
+ * reads a clock, the environment or the filesystem, so the same declaration
420
+ * and the same two documents give the same request forever.
421
+ */
422
+ export function terraformBehaviourRequest(options) {
423
+ const live = new Map((options.live ?? []).map((read) => [read.root, read]));
424
+ const byRoot = new Map();
425
+ const rootless = [];
426
+ for (const name of options.entityNames) {
427
+ const entity = options.entities.get(name);
428
+ const d = declaredOf(name, entity);
429
+ if (!d.root) {
430
+ rootless.push(name);
431
+ continue;
432
+ }
433
+ (byRoot.get(d.root) ?? byRoot.set(d.root, []).get(d.root)).push(d);
434
+ }
435
+ const parts = [];
436
+ for (const [root, declared] of byRoot) {
437
+ const read = live.get(root);
438
+ parts.push(read ? liveRootPart(root, declared, options.entities, read, options.owned) : declaredRootPart(declared, options.entities));
439
+ }
440
+ const entities = new Map();
441
+ const sources = {};
442
+ const absent = [];
443
+ const unpredicted = {};
444
+ const unresolved = new Set();
445
+ const nodes = [];
446
+ for (const part of parts) {
447
+ for (const [name, entity] of part.entities)
448
+ entities.set(name, entity);
449
+ Object.assign(sources, part.sources);
450
+ absent.push(...part.absent);
451
+ Object.assign(unpredicted, part.unpredicted);
452
+ for (const kind of part.unresolvedKinds)
453
+ unresolved.add(kind);
454
+ nodes.push(...part.nodes);
455
+ }
456
+ // A name carrying no root came from somewhere other than `buildRoots()`. It
457
+ // is passed through as it came, so the engine-fronting lexicon reports it
458
+ // rather than this builder dropping it.
459
+ for (const name of rootless) {
460
+ const entity = options.entities.get(name);
461
+ if (entity)
462
+ entities.set(name, entity);
463
+ sources[name] = "declared";
464
+ }
465
+ nodes.sort((a, b) => byCodeUnit(a.id, b.id));
466
+ const reconstructed = reconstructEdges(nodes, TERRAFORM_REFERENCE_CATALOG);
467
+ const entityNames = [...new Set([...entities.keys(), ...rootless])].sort(byCodeUnit);
468
+ const request = {
469
+ environment: options.environment,
470
+ buildOutput: options.buildOutput,
471
+ entityNames,
472
+ entities: entities,
473
+ ...(options.stack !== undefined ? { stack: options.stack } : {}),
474
+ ...(options.region !== undefined ? { region: options.region } : {}),
475
+ ...(options.owned !== undefined ? { owned: options.owned } : {}),
476
+ from: options.from,
477
+ traffic: options.traffic,
478
+ edges: reconstructed.edges,
479
+ edgeCoverage: edgeCoverageOf(reconstructed, [...unresolved]),
480
+ };
481
+ return { request, sources, absent: absent.sort(byCodeUnit), unpredicted };
482
+ }