@intentius/chant-lexicon-terraform 0.75.1 → 0.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/behaviour/index.d.ts +1 -1
  2. package/dist/behaviour/index.d.ts.map +1 -1
  3. package/dist/behaviour/kinds.d.ts +9 -0
  4. package/dist/behaviour/kinds.d.ts.map +1 -1
  5. package/dist/behaviour/predict.d.ts +67 -9
  6. package/dist/behaviour/predict.d.ts.map +1 -1
  7. package/dist/behaviour/request.d.ts +1 -1
  8. package/dist/behaviour/request.d.ts.map +1 -1
  9. package/dist/describe-resources.d.ts +3 -2
  10. package/dist/describe-resources.d.ts.map +1 -1
  11. package/dist/hcl/descend.d.ts +1 -1
  12. package/dist/hcl/parse.d.ts +2 -2
  13. package/dist/hcl/roots.d.ts +1 -1
  14. package/dist/hcl/suppressions.d.ts +1 -1
  15. package/dist/hcl/value.d.ts +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/integrity.json +3 -3
  19. package/dist/lint/post-synth/tf016.d.ts +1 -1
  20. package/dist/manifest.json +1 -1
  21. package/dist/op/activities/live-detect.d.ts +1 -1
  22. package/dist/op/activities/terraform.d.ts +38 -0
  23. package/dist/op/activities/terraform.d.ts.map +1 -1
  24. package/dist/plugin.d.ts.map +1 -1
  25. package/dist/rules/tf016.ts +1 -1
  26. package/package.json +5 -5
  27. package/src/behaviour/index.ts +3 -0
  28. package/src/behaviour/kinds.ts +6 -1
  29. package/src/behaviour/predict.test.ts +171 -3
  30. package/src/behaviour/predict.ts +171 -9
  31. package/src/behaviour/request.test.ts +8 -1
  32. package/src/behaviour/request.ts +2 -1
  33. package/src/describe-resources.ts +4 -3
  34. package/src/hcl/descend.ts +1 -1
  35. package/src/hcl/parse.ts +2 -2
  36. package/src/hcl/roots.ts +1 -1
  37. package/src/hcl/suppressions.ts +1 -1
  38. package/src/hcl/value.ts +1 -1
  39. package/src/index.ts +3 -1
  40. package/src/lint/post-synth/tf016.ts +1 -1
  41. package/src/op/activities/live-detect.ts +1 -1
  42. package/src/op/activities/terraform.test.ts +117 -0
  43. package/src/op/activities/terraform.ts +85 -28
  44. package/src/plugin.test.ts +23 -1
  45. package/src/plugin.ts +13 -0
@@ -33,10 +33,12 @@ import {
33
33
  type PredictedBehaviour,
34
34
  type UnpredictedEntity,
35
35
  } from "@intentius/chant/behaviour";
36
+ import { behaviourRequestFromIr } from "@intentius/chant/behaviour-graph";
36
37
  import type { Declarable } from "@intentius/chant/declarable";
38
+ import { buildGraphIr } from "@intentius/chant/graph-ir";
37
39
  import type { TerraformReadDeps } from "../describe-resources";
38
40
  import { renderTerraformRoots } from "../hcl/roots";
39
- import { liveRootsOf, predictTerraformBehaviour, TERRAFORM } from "./predict";
41
+ import { liveRootsOf, predictTerraformBehaviour, TERRAFORM, terraformPredictOptionsFrom } from "./predict";
40
42
  import type { TerraformBehaviourEntity } from "./request";
41
43
 
42
44
  const fixtures = join(dirname(fileURLToPath(import.meta.url)), "..", "__fixtures__");
@@ -49,6 +51,8 @@ const key = (address: string): string => `${ROOT}/${address}`;
49
51
 
50
52
  let entities: Map<string, TerraformBehaviourEntity>;
51
53
  let entityNames: string[];
54
+ /** The build's own entities, as `chant graph` is handed them. */
55
+ let built: Map<string, Declarable>;
52
56
 
53
57
  beforeAll(async () => {
54
58
  const rendered = await renderTerraformRoots({
@@ -56,6 +60,7 @@ beforeAll(async () => {
56
60
  roots: { [ROOT]: { dir: "./live-estate" } },
57
61
  binary: "choudoufu",
58
62
  });
63
+ built = rendered.entities;
59
64
  entities = new Map();
60
65
  for (const [name, entity] of rendered.entities) {
61
66
  const e = entity as Declarable & { props: Record<string, unknown>; references?: readonly never[] };
@@ -183,7 +188,7 @@ async function predict(
183
188
  ): Promise<{ result: BehaviourResult; calls: Call[] }> {
184
189
  const d = deps(overrides?.deps);
185
190
  const result = await predictTerraformBehaviour(
186
- { ...common, entityNames, entities, ...(overrides?.owned ? { owned: true } : {}) },
191
+ { ...common, from: "live", entityNames, entities, ...(overrides?.owned ? { owned: true } : {}) },
187
192
  { liveLs: d.deps.liveLs, livePlan: d.deps.livePlan, predict: tariff },
188
193
  );
189
194
  return { result, calls: d.calls };
@@ -325,7 +330,7 @@ describe("the screen runs before the engine", () => {
325
330
  });
326
331
  const d = deps();
327
332
  const result = await predictTerraformBehaviour(
328
- { ...common, entityNames, entities: tainted },
333
+ { ...common, from: "live", entityNames, entities: tainted },
329
334
  {
330
335
  liveLs: d.deps.liveLs,
331
336
  livePlan: d.deps.livePlan,
@@ -378,4 +383,167 @@ describe("the delta the epic asks for", () => {
378
383
  // not computed over a graph the other side does not have.
379
384
  expect(live.meta.edgeCoverage.verdict).toBe(declared.meta.edgeCoverage.verdict);
380
385
  });
386
+
387
+ it("tells the engine which estate each side is about (#2494)", async () => {
388
+ const asked: Array<PredictBehaviourOptions["from"]> = [];
389
+ const listening = async (options: PredictBehaviourOptions): Promise<BehaviourResult> => {
390
+ asked.push(options.from);
391
+ return tariff(options);
392
+ };
393
+ const d = deps();
394
+ for (const from of ["live", "declared"] as const) {
395
+ await predictTerraformBehaviour(
396
+ { ...common, from, entityNames, entities },
397
+ { liveLs: d.deps.liveLs, livePlan: d.deps.livePlan, predict: listening },
398
+ );
399
+ }
400
+ expect(asked).toEqual(["live", "declared"]);
401
+ });
402
+ });
403
+
404
+ describe("the contract's options, read as this producer's (#2495)", () => {
405
+ /** What `chant graph --traffic` hands a plugin: the graph reshaped, references left behind on the edges. */
406
+ const throughTheGraph = (from: "live" | "declared"): PredictBehaviourOptions =>
407
+ behaviourRequestFromIr(buildGraphIr(built, fixtures), {
408
+ ...common,
409
+ from,
410
+ edgeCoverage: { verdict: "unknown" },
411
+ });
412
+
413
+ /** Run the producer and keep the request the engine was sent. */
414
+ async function sent(options: Parameters<typeof predictTerraformBehaviour>[0]): Promise<PredictBehaviourOptions> {
415
+ let request: PredictBehaviourOptions | undefined;
416
+ const d = deps();
417
+ await predictTerraformBehaviour(options, {
418
+ liveLs: d.deps.liveLs,
419
+ livePlan: d.deps.livePlan,
420
+ predict: async (o) => {
421
+ request = o;
422
+ return tariff(o);
423
+ },
424
+ });
425
+ if (!request) throw new Error("the engine was never asked");
426
+ return request;
427
+ }
428
+
429
+ /** A reader that must not be reached: nothing on the declared side, or on a live side that kept its `mode`, reads the project again. */
430
+ const neverRedeclared = async (): Promise<never> => {
431
+ throw new Error("the declaration was read again");
432
+ };
433
+
434
+ it("the graph drops every reference, and the edges give each one back", async () => {
435
+ const graphed = throughTheGraph("declared");
436
+ const referencing = [...entities].filter(([, e]) => (e.references ?? []).length > 0);
437
+ expect(referencing.length).toBeGreaterThan(0);
438
+ for (const [name] of referencing) {
439
+ expect(Object.prototype.hasOwnProperty.call(graphed.entities.get(name), "references"), name).toBe(false);
440
+ }
441
+ const read = await terraformPredictOptionsFrom(graphed, neverRedeclared);
442
+ for (const [name, entity] of referencing) {
443
+ expect(read.entities.get(name)?.references, name).toEqual(entity.references);
444
+ }
445
+ });
446
+
447
+ it.each(["live", "declared"] as const)("sends the engine the %s request the build's own entities would have", async (from) => {
448
+ const direct = await sent({ ...common, from, entityNames, entities });
449
+ const graphed = await sent(await terraformPredictOptionsFrom(throughTheGraph(from), neverRedeclared));
450
+ expect(graphed.edges).toEqual(direct.edges);
451
+ expect(graphed.edgeCoverage).toEqual(direct.edgeCoverage);
452
+ expect(graphed.entityNames).toEqual(direct.entityNames);
453
+ expect(graphed.from).toBe(from);
454
+ });
455
+
456
+ it("keeps the references an entity still carries over the edges' copy", async () => {
457
+ const [name, entity] = [...entities].find(([, e]) => (e.references ?? []).length > 0)!;
458
+ const read = await terraformPredictOptionsFrom({
459
+ ...common,
460
+ from: "declared",
461
+ entityNames,
462
+ entities: entities as PredictBehaviourOptions["entities"],
463
+ edges: [{ from: name, to: "somewhere/else", kind: "ref" }],
464
+ edgeCoverage: { verdict: "unknown" },
465
+ }, neverRedeclared);
466
+ expect(read.entities.get(name)?.references).toEqual(entity.references);
467
+ });
468
+
469
+ it("does not pass the caller's edges or coverage claim on", async () => {
470
+ const read = (await terraformPredictOptionsFrom(throughTheGraph("declared"), neverRedeclared)) as unknown as Record<
471
+ string,
472
+ unknown
473
+ >;
474
+ expect(Object.prototype.hasOwnProperty.call(read, "edges")).toBe(false);
475
+ expect(Object.prototype.hasOwnProperty.call(read, "edgeCoverage")).toBe(false);
476
+ });
477
+
478
+ /**
479
+ * What `chant graph --live` hands over: observed nodes. An address, a root
480
+ * and the provider's type; no `mode`, no `body`, no edge. Plus one the
481
+ * account holds that nothing declares.
482
+ */
483
+ const ORPHAN = key("aws_sqs_queue.nobody_declared");
484
+ function throughTheLiveGraph(): PredictBehaviourOptions {
485
+ const observed = new Map<string, { entityType: string; props: Record<string, unknown> }>();
486
+ for (const [name, entity] of entities) {
487
+ if (entity.entityType !== "Terraform::Resource") continue;
488
+ const address = String(entity.props.address);
489
+ observed.set(name, {
490
+ entityType: entity.entityType,
491
+ props: { address, root: ROOT, estate: ESTATE, resourceType: address.slice(0, address.indexOf(".")) },
492
+ });
493
+ }
494
+ observed.set(ORPHAN, {
495
+ entityType: "Terraform::Resource",
496
+ props: { address: "aws_sqs_queue.nobody_declared", root: ROOT, estate: ESTATE, resourceType: "aws_sqs_queue" },
497
+ });
498
+ return {
499
+ ...common,
500
+ from: "live",
501
+ entityNames: [...observed.keys()],
502
+ entities: observed,
503
+ edges: [],
504
+ edgeCoverage: { verdict: "unknown" },
505
+ };
506
+ }
507
+
508
+ it("declares a live graph's root again, so the account is read and the engine is sent what the build would send", async () => {
509
+ const direct = await sent({ ...common, from: "live", entityNames, entities });
510
+ const graphed = await sent(await terraformPredictOptionsFrom(throughTheLiveGraph(), async () => entities));
511
+ expect(graphed.entityNames).toEqual(direct.entityNames);
512
+ expect(graphed.edges).toEqual(direct.edges);
513
+ expect(graphed.edgeCoverage).toEqual(direct.edgeCoverage);
514
+ for (const name of direct.entityNames) {
515
+ expect(graphed.entities.get(name)?.props, name).toEqual(direct.entities.get(name)?.props);
516
+ }
517
+ });
518
+
519
+ it("read as it arrived, the same live graph reads no account at all", async () => {
520
+ // The hole the test above closes, pinned so it cannot close by accident:
521
+ // no observed entity says `mode: "live"`, so no root is live.
522
+ const asArrived = throughTheLiveGraph();
523
+ expect(liveRootsOf(asArrived.entityNames, asArrived.entities)).toEqual([]);
524
+ const read = await terraformPredictOptionsFrom(asArrived, async () => entities);
525
+ expect(liveRootsOf(read.entityNames, read.entities)).toEqual([ROOT]);
526
+ });
527
+
528
+ it("sets an observed orphan aside for the live read to list", async () => {
529
+ const read = await terraformPredictOptionsFrom(throughTheLiveGraph(), async () => entities);
530
+ expect(read.entityNames).not.toContain(ORPHAN);
531
+ expect(read.entities.has(ORPHAN)).toBe(false);
532
+ expect([...read.entityNames].sort()).toEqual(entityNames);
533
+ });
534
+
535
+ it("leaves a root the config does not render as it arrived", async () => {
536
+ const asArrived = throughTheLiveGraph();
537
+ const read = await terraformPredictOptionsFrom(asArrived, async () => new Map());
538
+ expect(read.entityNames).toEqual(asArrived.entityNames);
539
+ expect(read.entities.get(ORPHAN)).toEqual(asArrived.entities.get(ORPHAN));
540
+ });
541
+
542
+ it("reads the declaration again only for a live graph", async () => {
543
+ const declaredSide = { ...throughTheLiveGraph(), from: "declared" as const };
544
+ await expect(terraformPredictOptionsFrom(declaredSide, neverRedeclared)).resolves.toBeDefined();
545
+ await expect(terraformPredictOptionsFrom(throughTheLiveGraph(), neverRedeclared)).rejects.toThrow(
546
+ "the declaration was read again",
547
+ );
548
+ });
381
549
  });
@@ -60,8 +60,13 @@ import {
60
60
  type PredictBehaviourOptions,
61
61
  type UnpredictedEntity,
62
62
  } from "@intentius/chant/behaviour";
63
+ import { createBehaviourPredict } from "@intentius/chant/behaviour-predict";
64
+ import type { EntityReference } from "@intentius/chant/graph-ir";
63
65
  import { indexLivePlan, readLiveLs, type TerraformReadDeps } from "../describe-resources";
66
+ import type { TerraformConfig } from "../config";
64
67
  import { RESOURCE_TYPE } from "../hcl/parse";
68
+ import { choudoufuLiveLs, choudoufuLivePlanDocument } from "../op/activities/terraform";
69
+ import { terraformBehaviourKinds } from "./kinds";
65
70
  import {
66
71
  terraformBehaviourRequest,
67
72
  type TerraformBehaviourEntity,
@@ -105,19 +110,175 @@ export interface TerraformPredictOptions
105
110
  /** Directory the activities start the `chant.config.*` search from. Default: the process cwd. */
106
111
  cwd?: string;
107
112
  /**
108
- * Which estate to predict. `"live"`, the default, is this path's whole
109
- * point: the account as it stands, drift included. `"declared"` runs the
110
- * same producer over the declaration alone and reads no account, which is
111
- * the other half of the delta the epic asks for (#2355) — a caller wanting
112
- * that delta calls this twice and differences the two reports, and gets two
113
- * reports assembled by one producer rather than two shapes that have each
114
- * been through a different translation.
113
+ * Which estate to predict, as the contract's own `from` (#2494) and required
114
+ * for the same reason: a default is a guess about which estate somebody
115
+ * meant, and the report that comes back from a wrong guess is well-formed.
116
+ *
117
+ * `"live"` is this path's whole point: the account as it stands, drift
118
+ * included. `"declared"` runs the same producer over the declaration alone
119
+ * and reads no account, which is the other half of the delta the epic asks
120
+ * for (#2355) — a caller wanting that delta calls this twice and differences
121
+ * the two reports, and gets two reports assembled by one producer rather
122
+ * than two shapes that have each been through a different translation.
115
123
  *
116
124
  * `"declared"` on a project with no live root is the same request `"live"`
117
125
  * builds, because there is no account to read; the option exists so that a
118
126
  * caller can ask for the declaration of a root that *is* live.
119
127
  */
120
- from?: "live" | "declared";
128
+ from: "live" | "declared";
129
+ }
130
+
131
+ /**
132
+ * What the plugin method runs with, where a caller injects nothing (#2495).
133
+ *
134
+ * The two reads are the activities `describeResources` already drives. The
135
+ * engine front is core's, handed this lexicon's coverage rows and no other's:
136
+ * a plugin method takes options alone and cannot see which other lexicons the
137
+ * project configured, and every name this producer sends is a terraform block
138
+ * or a name it passed through untouched. A passed-through name of another
139
+ * lexicon's type comes back `unknown-type`, which is true of what this path
140
+ * knows about it.
141
+ */
142
+ const REAL_DEPS: TerraformBehaviourDeps = {
143
+ liveLs: choudoufuLiveLs,
144
+ livePlan: choudoufuLivePlanDocument,
145
+ predict: createBehaviourPredict({ kinds: [terraformBehaviourKinds] }),
146
+ };
147
+
148
+ /** Reads the project's declaration afresh. Injectable so a test parses nothing from disk. */
149
+ export type TerraformDeclarationReader = (cwd?: string) => Promise<ReadonlyMap<string, TerraformBehaviourEntity>>;
150
+
151
+ /**
152
+ * The build's own entities, read again from the project's config: what
153
+ * `buildRoots()` renders, with `mode`, `body` and `references` as the parse
154
+ * stamps them. The config is found the way `observeAmbient` and the
155
+ * activities find it, so `terraform.roots` means one thing to all of them.
156
+ */
157
+ export const readTerraformDeclaration: TerraformDeclarationReader = async (cwd) => {
158
+ const { loadChantConfigUpward } = await import("@intentius/chant/config");
159
+ const { renderTerraformRoots } = await import("../hcl/roots");
160
+ const { dirname, resolve } = await import("node:path");
161
+
162
+ const start = resolve(cwd ?? process.cwd());
163
+ const { config, configPath } = await loadChantConfigUpward(start);
164
+ const namespace = (config as { terraform?: TerraformConfig }).terraform;
165
+ const entities = new Map<string, TerraformBehaviourEntity>();
166
+ if (!namespace?.roots || Object.keys(namespace.roots).length === 0) return entities;
167
+ const rendered = await renderTerraformRoots({
168
+ projectRoot: configPath ? dirname(configPath) : start,
169
+ roots: namespace.roots,
170
+ binary: namespace.binary,
171
+ callModuleType: namespace.callModuleType,
172
+ });
173
+ for (const [name, entity] of rendered.entities) {
174
+ const e = entity as unknown as TerraformBehaviourEntity;
175
+ entities.set(name, {
176
+ entityType: e.entityType,
177
+ props: e.props ?? {},
178
+ ...(Array.isArray(e.references) ? { references: e.references } : {}),
179
+ });
180
+ }
181
+ return entities;
182
+ };
183
+
184
+ const rootOf = (entity: TerraformBehaviourEntity | undefined): string | undefined =>
185
+ typeof entity?.props.root === "string" ? entity.props.root : undefined;
186
+
187
+ /**
188
+ * Read the contract's options as this producer's (#2495): the plugin method's
189
+ * whole adaptation. Two things are missing from an entity by the time a graph
190
+ * has carried it here, and which one depends on the graph.
191
+ *
192
+ * ## A declared graph has lost its `references`
193
+ *
194
+ * `root` and `mode` need no work: they are on `props`, the graph projects
195
+ * `props` onto a node's `attrs` as they stand, and `behaviourRequestFromIr`
196
+ * hands `attrs` back as `props`. `references` are left off `attrs` on
197
+ * purpose, because an edge already says the same thing (`graph-ir.ts`'s
198
+ * `SKIP_KEYS`), and the contract's `edges` arrive in their place. An `IREdge`
199
+ * is an `EntityReference` plus the entity it leaves from, so grouping the
200
+ * edges by `from` gives every entity its references back, field for field.
201
+ * Without this a block whose body holds a `${…}` is counted as an unresolved
202
+ * kind on a graph that resolved it. An entity that still carries its own
203
+ * `references` keeps them: the edges are the copy.
204
+ *
205
+ * ## A live graph has lost the declaration
206
+ *
207
+ * `chant graph --live` hands over observed nodes: an address, a root, the
208
+ * provider's type, and no `mode`, no `body` and no edge. Read as they stand,
209
+ * no root is live, so the account is never read here; no block has a size,
210
+ * because `body.instance_type` went with the body; and containment is empty.
211
+ * The figures that come back are well-formed and differ from the declared
212
+ * side's for reasons that have nothing to do with the account, which is the
213
+ * delta this producer exists to keep honest.
214
+ *
215
+ * So under `from: "live"`, a root any of whose entities arrived with no
216
+ * `mode` is declared again from the project's own config, and the producer
217
+ * then reads the account itself, as it does for any other caller. The
218
+ * observed entities of that root are set aside, the undeclared ones
219
+ * included: an owned orphan is the live read's to list, under the key
220
+ * `observeAmbient` gave it, and one the account no longer holds is not in a
221
+ * prediction of the account.
222
+ *
223
+ * A root the config does not render is left as it arrived. That is a worse
224
+ * reading and the only one available.
225
+ *
226
+ * ## What stops here
227
+ *
228
+ * The caller's `edges` and `edgeCoverage`. Both are this producer's to
229
+ * compute, from whichever reads it ends up making (see
230
+ * {@link TerraformPredictOptions}).
231
+ */
232
+ export async function terraformPredictOptionsFrom(
233
+ options: PredictBehaviourOptions,
234
+ redeclare: TerraformDeclarationReader = readTerraformDeclaration,
235
+ ): Promise<TerraformPredictOptions> {
236
+ const { entities: given, edges, edgeCoverage: _edgeCoverage, ...rest } = options;
237
+ const leaving = new Map<string, EntityReference[]>();
238
+ for (const edge of edges ?? []) {
239
+ if (edge.kind !== "ref") continue;
240
+ const reference: EntityReference = {
241
+ to: edge.to,
242
+ ...(edge.viaAttr !== undefined ? { viaAttr: edge.viaAttr } : {}),
243
+ ...(edge.toAttr !== undefined ? { toAttr: edge.toAttr } : {}),
244
+ };
245
+ (leaving.get(edge.from) ?? leaving.set(edge.from, []).get(edge.from)!).push(reference);
246
+ }
247
+ const entities = new Map<string, TerraformBehaviourEntity>();
248
+ for (const [name, entity] of given) {
249
+ const own = (entity as TerraformBehaviourEntity).references;
250
+ const references = Array.isArray(own) ? own : leaving.get(name);
251
+ entities.set(name, references === undefined ? entity : { ...entity, references });
252
+ }
253
+ if (options.from !== "live") return { ...rest, entities };
254
+
255
+ const observed = new Set<string>();
256
+ for (const name of options.entityNames) {
257
+ const entity = entities.get(name);
258
+ const root = rootOf(entity);
259
+ if (root !== undefined && entity?.props.mode === undefined) observed.add(root);
260
+ }
261
+ if (observed.size === 0) return { ...rest, entities };
262
+
263
+ const declared = new Map<string, Map<string, TerraformBehaviourEntity>>();
264
+ for (const [name, entity] of await redeclare()) {
265
+ const root = rootOf(entity);
266
+ if (root === undefined || !observed.has(root)) continue;
267
+ (declared.get(root) ?? declared.set(root, new Map()).get(root)!).set(name, entity);
268
+ }
269
+ const entityNames = options.entityNames.filter((name) => {
270
+ const root = rootOf(entities.get(name));
271
+ if (root === undefined || !declared.has(root)) return true;
272
+ entities.delete(name);
273
+ return false;
274
+ });
275
+ for (const again of declared.values()) {
276
+ for (const [name, entity] of again) {
277
+ entities.set(name, entity);
278
+ entityNames.push(name);
279
+ }
280
+ }
281
+ return { ...rest, entityNames, entities };
121
282
  }
122
283
 
123
284
  /** Which roots the request would read live: those whose entities the parse stamped `mode: "live"`. */
@@ -146,7 +307,7 @@ function readsAsCredentials(text: string): boolean {
146
307
  */
147
308
  export async function predictTerraformBehaviour(
148
309
  options: TerraformPredictOptions,
149
- deps: TerraformBehaviourDeps,
310
+ deps: TerraformBehaviourDeps = REAL_DEPS,
150
311
  ): Promise<BehaviourResult> {
151
312
  const { cwd, from: _from, ...contract } = options;
152
313
  const where = cwd ? { cwd } : {};
@@ -191,6 +352,7 @@ export async function predictTerraformBehaviour(
191
352
  environment: contract.environment,
192
353
  buildOutput: contract.buildOutput,
193
354
  traffic: contract.traffic,
355
+ from: options.from,
194
356
  ...(contract.region !== undefined ? { region: contract.region } : {}),
195
357
  ...(contract.stack !== undefined ? { stack: contract.stack } : {}),
196
358
  ...(contract.owned !== undefined ? { owned: contract.owned } : {}),
@@ -65,11 +65,12 @@ beforeAll(async () => {
65
65
  const common = { environment: "prod", buildOutput: "/tmp/build", traffic: "100 rps, p50" };
66
66
 
67
67
  const declaredSide = (): TerraformBehaviourRequest =>
68
- terraformBehaviourRequest({ ...common, entityNames, entities });
68
+ terraformBehaviourRequest({ ...common, from: "declared", entityNames, entities });
69
69
 
70
70
  const liveSide = (): TerraformBehaviourRequest =>
71
71
  terraformBehaviourRequest({
72
72
  ...common,
73
+ from: "live",
73
74
  entityNames,
74
75
  entities,
75
76
  live: [{ root: ROOT, listing: LISTING, plan: PLAN }],
@@ -79,6 +80,11 @@ const factsOf = (built: TerraformBehaviourRequest, address: string): LiveResourc
79
80
  (built.request.entities.get(key(address))!.props as { live: LiveResourceFacts }).live;
80
81
 
81
82
  describe("the declared side", () => {
83
+ it("says which estate the request is about, on both sides (#2494)", () => {
84
+ expect(declaredSide().request.from).toBe("declared");
85
+ expect(liveSide().request.from).toBe("live");
86
+ });
87
+
82
88
  it("builds every declared block, from the file and nothing else", () => {
83
89
  const built = declaredSide();
84
90
  expect(built.request.entityNames).toEqual(entityNames);
@@ -179,6 +185,7 @@ describe("the live side", () => {
179
185
  it("withholds what is not this estate's when --owned was asked for", () => {
180
186
  const built = terraformBehaviourRequest({
181
187
  ...common,
188
+ from: "live",
182
189
  entityNames,
183
190
  entities,
184
191
  owned: true,
@@ -129,7 +129,7 @@ export interface LiveResourceFacts {
129
129
  }
130
130
 
131
131
  export interface TerraformBehaviourRequestOptions
132
- extends Pick<PredictBehaviourOptions, "environment" | "buildOutput" | "traffic" | "region" | "stack" | "owned"> {
132
+ extends Pick<PredictBehaviourOptions, "environment" | "buildOutput" | "traffic" | "from" | "region" | "stack" | "owned"> {
133
133
  entityNames: readonly string[];
134
134
  entities: ReadonlyMap<string, TerraformBehaviourEntity>;
135
135
  /**
@@ -613,6 +613,7 @@ export function terraformBehaviourRequest(options: TerraformBehaviourRequestOpti
613
613
  ...(options.stack !== undefined ? { stack: options.stack } : {}),
614
614
  ...(options.region !== undefined ? { region: options.region } : {}),
615
615
  ...(options.owned !== undefined ? { owned: options.owned } : {}),
616
+ from: options.from,
616
617
  traffic: options.traffic,
617
618
  edges: reconstructed.edges,
618
619
  edgeCoverage: edgeCoverageOf(reconstructed, [...unresolved]),
@@ -173,7 +173,7 @@ import {
173
173
  import type { OwnershipMarker } from "@intentius/chant/ownership";
174
174
  import {
175
175
  choudoufuLiveLs,
176
- choudoufuLivePlan,
176
+ choudoufuLivePlanDocument,
177
177
  terraformInit,
178
178
  terraformShow,
179
179
  } from "./op/activities/terraform";
@@ -1007,14 +1007,15 @@ async function liveRoots(options: LiveEstateOptions): Promise<Array<{ root: stri
1007
1007
  export interface TerraformReadDeps {
1008
1008
  init: typeof terraformInit;
1009
1009
  show: typeof terraformShow;
1010
- livePlan: typeof choudoufuLivePlan;
1010
+ /** The document alone (chant #2498): this reader indexes it and never reads the human render. */
1011
+ livePlan: typeof choudoufuLivePlanDocument;
1011
1012
  liveLs: typeof choudoufuLiveLs;
1012
1013
  }
1013
1014
 
1014
1015
  const REAL_DEPS: TerraformReadDeps = {
1015
1016
  init: terraformInit,
1016
1017
  show: terraformShow,
1017
- livePlan: choudoufuLivePlan,
1018
+ livePlan: choudoufuLivePlanDocument,
1018
1019
  liveLs: choudoufuLiveLs,
1019
1020
  };
1020
1021
 
@@ -100,7 +100,7 @@ export interface DescendOptions {
100
100
  projectRoot: string;
101
101
  /** Resolved mode. `"none"` returns immediately with nothing. */
102
102
  callModuleType?: "local" | "none";
103
- /** Injectable parser (tests); defaults to core's lazy-loaded `@cdktf/hcl2json`. */
103
+ /** Injectable parser (tests); defaults to core's lazy-loaded `@cdktn/hcl2json`. */
104
104
  hcl2json?: Hcl2Json;
105
105
  /** Mode facts stamped onto the child's entities, exactly as on the root's. */
106
106
  modeOptions?: TerraformRootModeOptions;
package/src/hcl/parse.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * Both funnel through `blocksToEntities`, so an entity the audit path sees is
10
10
  * the entity the build path sees.
11
11
  *
12
- * Core owns the parser glue: `loadHcl2json()` lazy-loads `@cdktf/hcl2json`
12
+ * Core owns the parser glue: `loadHcl2json()` lazy-loads `@cdktn/hcl2json`
13
13
  * (a ~1.8 MB wasm blob) and raises a one-line install hint when it is absent.
14
14
  * `parseTerraformDir()` next to it is NOT reused: it returns carve's `TfGraph`,
15
15
  * a scoring-and-excision shape with no room for the per-block bodies a
@@ -27,7 +27,7 @@ import type { SuppressionDirective } from "@intentius/chant/lint/suppressions";
27
27
  import type { TerraformDeleteMode } from "../config";
28
28
  import { scanSuppressions, directivesFor, type FileScan } from "./suppressions";
29
29
 
30
- /** A parsed HCL block body, as `@cdktf/hcl2json` encodes it. */
30
+ /** A parsed HCL block body, as `@cdktn/hcl2json` encodes it. */
31
31
  export type BlockBody = Record<string, unknown>;
32
32
 
33
33
  /**
package/src/hcl/roots.ts CHANGED
@@ -56,7 +56,7 @@ export interface RenderTerraformRootsOptions {
56
56
  * `../` is parsed as a child scope of the root that calls it.
57
57
  */
58
58
  callModuleType?: CallModuleType;
59
- /** Injectable parser (tests); defaults to core's lazy-loaded `@cdktf/hcl2json`. */
59
+ /** Injectable parser (tests); defaults to core's lazy-loaded `@cdktn/hcl2json`. */
60
60
  hcl2json?: Hcl2Json;
61
61
  }
62
62
 
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `hcl2json` drops comments entirely, and a probe of its output confirmed it
5
5
  * exposes no source ranges either, so a suppression comment has nothing to
6
- * attach to once a file goes through `@cdktf/hcl2json`. This module runs a
6
+ * attach to once a file goes through `@cdktn/hcl2json`. This module runs a
7
7
  * second, cheap pass over the SAME raw text `./parse.ts` hands the parser,
8
8
  * entirely with line-oriented regexes: a brace-depth counter to find each
9
9
  * top-level block's start line (`terraform`, `locals`, `provider "x"`,
package/src/hcl/value.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * A value envelope over a parsed block body (chant #2113).
3
3
  *
4
4
  * Every TF post-synth check reads `BlockBody` (`./parse.ts`), raw
5
- * `@cdktf/hcl2json` output, and re-derives, by hand, whether an attribute is
5
+ * `@cdktn/hcl2json` output, and re-derives, by hand, whether an attribute is
6
6
  * a plain literal, a reference/expression, or absent. tflint's OPA ruleset
7
7
  * hands a policy `{value, unknown, sensitive, ephemeral, range}` per
8
8
  * attribute so a check reads that uniformly instead of re-deriving it
package/src/index.ts CHANGED
@@ -99,10 +99,12 @@ export { TERRAFORM_STATE_OWNERSHIP_KEYS } from "./state-ownership";
99
99
  // path for a choudoufu root, reading `live-ls -json` and `live-plan -json`
100
100
  // through the same activities `describeResources` runs, and the same producer
101
101
  // over the declaration so the two sides of the delta are built alike. The
102
- // engine is injected: this lexicon dials none.
102
+ // engine is injected, and defaults to core's front (#2495): this lexicon
103
+ // dials none.
103
104
  export {
104
105
  predictTerraformBehaviour,
105
106
  terraformBehaviourRequest,
107
+ terraformPredictOptionsFrom,
106
108
  TERRAFORM_REFERENCE_CATALOG,
107
109
  type TerraformBehaviourDeps,
108
110
  type TerraformBehaviourEntity,
@@ -8,7 +8,7 @@
8
8
  * argument that takes a list or a bool.
9
9
  *
10
10
  * This is the one rule in the family that reads the block's SOURCE rather than
11
- * its parsed body, and it has to. `@cdktf/hcl2json` renders both `x = var.y`
11
+ * its parsed body, and it has to. `@cdktn/hcl2json` renders both `x = var.y`
12
12
  * and `x = "${var.y}"` as the string `"${var.y}"`, so a check over the parse
13
13
  * cannot tell the deprecated form from the idiomatic one and would report every
14
14
  * reference in the root. The quotes only survive in the text, so the text is
@@ -9,7 +9,7 @@
9
9
  * time and is dependency-light on purpose, since it shells out to the
10
10
  * configured binary and never touches the lexicon's HCL parse or serializer,
11
11
  * so it never pulls in
12
- * the ~1.8 MB `@cdktf/hcl2json` wasm parser just to decide whether an apply
12
+ * the ~1.8 MB `@cdktn/hcl2json` wasm parser just to decide whether an apply
13
13
  * should run `apply -auto-approve` with no plan file. Plain text and a
14
14
  * hand-rolled brace counter are enough for that narrower question, and the
15
15
  * literal sidecar filename below is kept in sync with `LIVE_SIDECAR_FILENAME`