@intentius/chant 0.37.2 → 0.39.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 (130) hide show
  1. package/dist/build.d.ts +21 -0
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/cli/commands/build.d.ts.map +1 -1
  4. package/dist/cli/commands/check-lexicon-mcp.d.ts +44 -0
  5. package/dist/cli/commands/check-lexicon-mcp.d.ts.map +1 -0
  6. package/dist/cli/commands/check-lexicon-plugin.d.ts +57 -0
  7. package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -0
  8. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  9. package/dist/cli/handlers/emulator.d.ts.map +1 -1
  10. package/dist/cli/handlers/graph.d.ts.map +1 -1
  11. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  12. package/dist/cli/handlers/run-client.d.ts.map +1 -1
  13. package/dist/cli/handlers/search.d.ts +30 -1
  14. package/dist/cli/handlers/search.d.ts.map +1 -1
  15. package/dist/cli/main.d.ts.map +1 -1
  16. package/dist/cli/mcp/server.d.ts +26 -2
  17. package/dist/cli/mcp/server.d.ts.map +1 -1
  18. package/dist/cli/plugins.d.ts +20 -0
  19. package/dist/cli/plugins.d.ts.map +1 -1
  20. package/dist/codegen/registry.d.ts +23 -0
  21. package/dist/codegen/registry.d.ts.map +1 -0
  22. package/dist/components/sandbox/driver.d.ts.map +1 -1
  23. package/dist/composite.d.ts +23 -4
  24. package/dist/composite.d.ts.map +1 -1
  25. package/dist/deep-observation.d.ts +11 -0
  26. package/dist/deep-observation.d.ts.map +1 -1
  27. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  28. package/dist/graph-declared.d.ts.map +1 -1
  29. package/dist/graph-ir.d.ts +17 -3
  30. package/dist/graph-ir.d.ts.map +1 -1
  31. package/dist/graph-refs.d.ts +24 -0
  32. package/dist/graph-refs.d.ts.map +1 -1
  33. package/dist/kubectl-context.d.ts.map +1 -1
  34. package/dist/lexicon-config.d.ts +61 -0
  35. package/dist/lexicon-config.d.ts.map +1 -0
  36. package/dist/lexicon.d.ts +85 -37
  37. package/dist/lexicon.d.ts.map +1 -1
  38. package/dist/lifecycle/deep-diff.d.ts +11 -0
  39. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  40. package/dist/lifecycle/digest.d.ts.map +1 -1
  41. package/dist/lifecycle/identity.d.ts +52 -0
  42. package/dist/lifecycle/identity.d.ts.map +1 -0
  43. package/dist/lifecycle/observe.d.ts +5 -0
  44. package/dist/lifecycle/observe.d.ts.map +1 -1
  45. package/dist/lifecycle/replay.d.ts.map +1 -1
  46. package/dist/lifecycle/types.d.ts +30 -0
  47. package/dist/lifecycle/types.d.ts.map +1 -1
  48. package/dist/live-endpoint.d.ts +21 -22
  49. package/dist/live-endpoint.d.ts.map +1 -1
  50. package/dist/managed-fields.d.ts +11 -0
  51. package/dist/managed-fields.d.ts.map +1 -1
  52. package/dist/op/emulator-freshness.d.ts +44 -0
  53. package/dist/op/emulator-freshness.d.ts.map +1 -0
  54. package/dist/op/emulator-lifecycle.d.ts +36 -0
  55. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  56. package/dist/op/index.d.ts +4 -2
  57. package/dist/op/index.d.ts.map +1 -1
  58. package/dist/ownership.d.ts +33 -0
  59. package/dist/ownership.d.ts.map +1 -1
  60. package/dist/serializer.d.ts +15 -0
  61. package/dist/serializer.d.ts.map +1 -1
  62. package/package.json +1 -1
  63. package/src/audit/catalog.test.ts +58 -6
  64. package/src/build.ts +24 -0
  65. package/src/cli/commands/build.ts +10 -0
  66. package/src/cli/commands/check-lexicon-doc-drift.test.ts +73 -0
  67. package/src/cli/commands/check-lexicon-mcp.test.ts +93 -0
  68. package/src/cli/commands/check-lexicon-mcp.ts +103 -0
  69. package/src/cli/commands/check-lexicon-plugin.test.ts +149 -0
  70. package/src/cli/commands/check-lexicon-plugin.ts +115 -0
  71. package/src/cli/commands/check-lexicon.ts +176 -26
  72. package/src/cli/handlers/components.test.ts +17 -0
  73. package/src/cli/handlers/components.ts +1 -1
  74. package/src/cli/handlers/emulator.ts +12 -8
  75. package/src/cli/handlers/graph.test.ts +71 -12
  76. package/src/cli/handlers/graph.ts +46 -5
  77. package/src/cli/handlers/lifecycle.test.ts +25 -4
  78. package/src/cli/handlers/lifecycle.ts +19 -3
  79. package/src/cli/handlers/run-client.ts +3 -1
  80. package/src/cli/handlers/search-kind.test.ts +45 -0
  81. package/src/cli/handlers/search.ts +102 -4
  82. package/src/cli/main.ts +32 -10
  83. package/src/cli/mcp/server.test.ts +82 -0
  84. package/src/cli/mcp/server.ts +40 -5
  85. package/src/cli/param-flag-scope.test.ts +69 -0
  86. package/src/cli/plugins.test.ts +33 -1
  87. package/src/cli/plugins.ts +55 -0
  88. package/src/codegen/registry.test.ts +56 -0
  89. package/src/codegen/registry.ts +69 -0
  90. package/src/components/SPRAWL-VALIDATION.md +5 -5
  91. package/src/components/sandbox/driver.test.ts +27 -0
  92. package/src/components/sandbox/driver.ts +12 -0
  93. package/src/composite.ts +33 -4
  94. package/src/deep-observation.ts +11 -0
  95. package/src/discovery/sandbox/driver.test.ts +34 -0
  96. package/src/discovery/sandbox/driver.ts +19 -0
  97. package/src/graph-declared.test.ts +86 -0
  98. package/src/graph-declared.ts +14 -2
  99. package/src/graph-ir.ts +32 -8
  100. package/src/graph-refs.test.ts +56 -0
  101. package/src/graph-refs.ts +37 -1
  102. package/src/kubectl-context.ts +4 -1
  103. package/src/lexicon-config.test.ts +111 -0
  104. package/src/lexicon-config.ts +92 -0
  105. package/src/lexicon-doc-coverage.test.ts +128 -0
  106. package/src/lexicon-seams.test.ts +113 -0
  107. package/src/lexicon.ts +88 -38
  108. package/src/lifecycle/deep-diff.test.ts +48 -1
  109. package/src/lifecycle/deep-diff.ts +16 -0
  110. package/src/lifecycle/digest.test.ts +81 -0
  111. package/src/lifecycle/digest.ts +34 -3
  112. package/src/lifecycle/identity.test.ts +39 -0
  113. package/src/lifecycle/identity.ts +61 -0
  114. package/src/lifecycle/observe.test.ts +75 -1
  115. package/src/lifecycle/observe.ts +28 -2
  116. package/src/lifecycle/replay.test.ts +251 -0
  117. package/src/lifecycle/replay.ts +67 -19
  118. package/src/lifecycle/types.ts +26 -0
  119. package/src/live-endpoint.test.ts +51 -12
  120. package/src/live-endpoint.ts +32 -33
  121. package/src/managed-fields.test.ts +50 -0
  122. package/src/managed-fields.ts +25 -6
  123. package/src/meta/peer-deps.test.ts +111 -14
  124. package/src/op/emulator-declaration.test.ts +63 -0
  125. package/src/op/emulator-freshness.test.ts +135 -0
  126. package/src/op/emulator-freshness.ts +102 -0
  127. package/src/op/emulator-lifecycle.ts +49 -0
  128. package/src/op/index.ts +4 -2
  129. package/src/ownership.ts +41 -0
  130. package/src/serializer.ts +16 -0
@@ -350,7 +350,7 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
350
350
  const endpointResult = applyLiveEndpoint(
351
351
  config.environments,
352
352
  environment,
353
- plugins.filter((p) => p.describeResources || p.describeStackStatus).map((p) => p.name),
353
+ plugins.filter((p) => p.describeResources || p.describeStackStatus),
354
354
  );
355
355
  if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
356
356
  try {
@@ -1,6 +1,6 @@
1
1
  import { exec } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { emulatorLifecycle } from "../../op";
3
+ import { emulatorLifecycle, emulatorsOf } from "../../op";
4
4
  import { formatError, formatWarning, formatSuccess } from "../format";
5
5
  import type { CommandContext } from "../registry";
6
6
 
@@ -48,8 +48,13 @@ export async function runEmulator(ctx: CommandContext): Promise<number> {
48
48
  return 1;
49
49
  }
50
50
 
51
- const withEmulator = plugins.filter((p) => p.emulator && (!args.lexicon || p.name === args.lexicon));
52
- if (withEmulator.length === 0) {
51
+ // One entry per emulator, not per lexicon (#1345): fly ships two — mudflaps
52
+ // for the Machines API and spritzer for Sprites — and reporting a lexicon
53
+ // would have to pick one of them.
54
+ const targets = plugins
55
+ .filter((p) => !args.lexicon || p.name === args.lexicon)
56
+ .flatMap((p) => emulatorsOf(p.emulator).map((cap) => ({ lexicon: p.name, cap })));
57
+ if (targets.length === 0) {
53
58
  if (args.json) {
54
59
  console.log(JSON.stringify({ emulators: [] }));
55
60
  return 0;
@@ -63,19 +68,18 @@ export async function runEmulator(ctx: CommandContext): Promise<number> {
63
68
  }
64
69
 
65
70
  const reports: EmulatorReport[] = [];
66
- for (const p of withEmulator) {
67
- const cap = p.emulator!;
71
+ for (const { lexicon, cap } of targets) {
68
72
  const lc = emulatorLifecycle(cap.spec);
69
73
  if (action === "up") {
70
74
  const { endpoint } = await lc.up();
71
- reports.push({ lexicon: p.name, name: cap.spec.name, endpoint, env: cap.env(endpoint) });
75
+ reports.push({ lexicon, name: cap.spec.name, endpoint, env: cap.env(endpoint) });
72
76
  } else if (action === "down") {
73
77
  await lc.down();
74
- reports.push({ lexicon: p.name, name: cap.spec.name, endpoint: "", env: {} });
78
+ reports.push({ lexicon, name: cap.spec.name, endpoint: "", env: {} });
75
79
  } else {
76
80
  const running = await isRunning(cap.spec.name);
77
81
  const endpoint = running ? lc.endpoint(cap.spec.containerPort) : "";
78
- reports.push({ lexicon: p.name, name: cap.spec.name, endpoint, env: endpoint ? cap.env(endpoint) : {} });
82
+ reports.push({ lexicon, name: cap.spec.name, endpoint, env: endpoint ? cap.env(endpoint) : {} });
79
83
  }
80
84
  }
81
85
 
@@ -3,6 +3,18 @@ import type { ParsedArgs } from "../registry";
3
3
  import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
4
4
  import { AttrRef } from "../../attrref";
5
5
 
6
+ /**
7
+ * The aws emulator capability, as the real plugin declares it. `--live`
8
+ * endpoint injection reads the endpoint var off this rather than off a map
9
+ * keyed by lexicon name (#1345), so a mock that omits it gets no injection —
10
+ * which is the same thing that would happen in production.
11
+ */
12
+ const awsEmulatorStub = {
13
+ spec: { name: "chant-floci", image: "floci/floci:1.5.34", containerPort: 4566, healthPath: "/_localstack/health" },
14
+ env: (endpoint: string) => ({ AWS_ENDPOINT_URL: endpoint, AWS_ACCESS_KEY_ID: "test" }),
15
+ };
16
+
17
+
6
18
  const discoverOpsMock = vi.fn();
7
19
  vi.mock("../../op/discover", () => ({
8
20
  discoverOps: () => discoverOpsMock(),
@@ -10,7 +22,9 @@ vi.mock("../../op/discover", () => ({
10
22
 
11
23
  const discoverMock = vi.fn();
12
24
  vi.mock("../../discovery/index", () => ({
13
- discover: () => discoverMock(),
25
+ // Forwards its arguments so a test can assert what the handler passed —
26
+ // `buildParams` in particular (#1359).
27
+ discover: (...a: unknown[]) => discoverMock(...a),
14
28
  }));
15
29
 
16
30
  const lintMock = vi.fn();
@@ -62,11 +76,13 @@ vi.mock("../../components/discover", () => ({
62
76
  discoverComponents: (...a: unknown[]) => discoverComponentsMock(...a),
63
77
  }));
64
78
  const loadChantConfigMock = vi.fn();
79
+ const loadChantConfigUpwardMock = vi.fn();
65
80
  vi.mock("../../config", async () => {
66
81
  const actual = await vi.importActual<typeof import("../../config")>("../../config");
67
82
  return {
68
83
  ...actual,
69
84
  loadChantConfig: (...a: unknown[]) => loadChantConfigMock(...a),
85
+ loadChantConfigUpward: (...a: unknown[]) => loadChantConfigUpwardMock(...a),
70
86
  };
71
87
  });
72
88
  vi.mock("../../build", () => ({
@@ -124,6 +140,8 @@ describe("runGraph", () => {
124
140
  resolveLexMock.mockReset();
125
141
  loadChantConfigMock.mockReset();
126
142
  loadChantConfigMock.mockResolvedValue({ config: {} });
143
+ loadChantConfigUpwardMock.mockReset();
144
+ loadChantConfigUpwardMock.mockResolvedValue({ config: {} });
127
145
  });
128
146
 
129
147
  describe("Op graph (default)", () => {
@@ -186,6 +204,47 @@ describe("runGraph", () => {
186
204
  expect(ir.edges).toContainEqual({ from: "subnet", to: "vpc", kind: "ref", viaAttr: "network" });
187
205
  });
188
206
 
207
+ // #1339 — `chant graph` discovered source with `params.*` empty, so a
208
+ // declaration conditioned on a build parameter always took its default:
209
+ // the graph showed one shape whatever `--param` or a declared `env:`
210
+ // mapping said, while `chant build` on the same source showed another.
211
+ describe("build-time parameters reach discovery (#1359)", () => {
212
+ const withTierParam = (): void => {
213
+ loadChantConfigUpwardMock.mockResolvedValue({
214
+ config: { buildParams: { tier: { type: "string", enum: ["light", "prod"], default: "light" } } },
215
+ });
216
+ };
217
+
218
+ test("--param is resolved and passed to discover", async () => {
219
+ lintClean(); discovered(); withTierParam();
220
+ const exit = await runGraph({
221
+ args: makeArgs({ format: "ir", param: ["tier=prod"] }),
222
+ plugins: [],
223
+ serializers: [],
224
+ });
225
+ expect(exit).toBe(0);
226
+ const [, options] = discoverMock.mock.calls[0] as [string, { buildParams?: Array<{ name: string; value: unknown }> }];
227
+ expect(options.buildParams).toContainEqual(expect.objectContaining({ name: "tier", value: "prod" }));
228
+ });
229
+
230
+ test("a declared default is passed too, so discovery never sees an empty params object", async () => {
231
+ lintClean(); discovered(); withTierParam();
232
+ await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
233
+ const [, options] = discoverMock.mock.calls[0] as [string, { buildParams?: Array<{ name: string; value: unknown }> }];
234
+ expect(options.buildParams).toContainEqual(expect.objectContaining({ name: "tier", value: "light" }));
235
+ });
236
+
237
+ test("an unresolvable parameter stops the graph rather than emitting a default one", async () => {
238
+ lintClean(); discovered();
239
+ loadChantConfigUpwardMock.mockResolvedValue({
240
+ config: { buildParams: { tier: { type: "string", required: true } } },
241
+ });
242
+ const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
243
+ expect(exit).toBe(1);
244
+ expect(stdoutBuf.join("\n")).toBe("");
245
+ });
246
+ });
247
+
189
248
  test("lint gate: refuses to emit when source has lint errors", async () => {
190
249
  lintMock.mockResolvedValue({ success: false });
191
250
  const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
@@ -427,7 +486,7 @@ describe("runGraph", () => {
427
486
  test("loads plugins for --live when ctx.plugins is empty", async () => {
428
487
  resolveLexMock.mockResolvedValue(["aws"]);
429
488
  loadPluginsMock.mockResolvedValue([
430
- { name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
489
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
431
490
  ]);
432
491
  observeMock.mockResolvedValue({
433
492
  observations: [{ lexicon: "aws", resources: { "web-vpc": { type: "AWS::EC2::VPC", status: "OK" } } }],
@@ -448,7 +507,7 @@ describe("runGraph", () => {
448
507
  test("--at graphs the recorded snapshot without reading the estate", async () => {
449
508
  resolveLexMock.mockResolvedValue(["aws"]);
450
509
  loadPluginsMock.mockResolvedValue([
451
- { name: "aws", serializer: {}, describeResources: () => Promise.resolve({}),
510
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}),
452
511
  enrichLiveAttrs: () => Promise.reject(new Error("must not be called on a replay")) },
453
512
  ]);
454
513
  replayMock.mockResolvedValue({
@@ -473,7 +532,7 @@ describe("runGraph", () => {
473
532
  hasSnapshotMock.mockResolvedValue(true);
474
533
  resolveLexMock.mockResolvedValue(["aws"]);
475
534
  loadPluginsMock.mockResolvedValue([
476
- { name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
535
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
477
536
  ]);
478
537
  observeMock.mockResolvedValue({ observations: [], errors: ["could not connect"], warnings: [] });
479
538
  const errs: string[] = [];
@@ -489,7 +548,7 @@ describe("runGraph", () => {
489
548
  hasSnapshotMock.mockResolvedValue(false);
490
549
  resolveLexMock.mockResolvedValue(["aws"]);
491
550
  loadPluginsMock.mockResolvedValue([
492
- { name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
551
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
493
552
  ]);
494
553
  observeMock.mockResolvedValue({ observations: [], errors: ["could not connect"], warnings: [] });
495
554
  const errs: string[] = [];
@@ -515,7 +574,7 @@ describe("runGraph", () => {
515
574
  test("single-stack project (no components): observeResources gets an empty stacks list", async () => {
516
575
  resolveLexMock.mockResolvedValue(["aws"]);
517
576
  loadPluginsMock.mockResolvedValue([
518
- { name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
577
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
519
578
  ]);
520
579
  observeMock.mockResolvedValue({ observations: [], errors: [], warnings: [] });
521
580
  const exit = await runGraph({ args: makeArgs({ format: "ir", live: true, env: "prod" }), plugins: [], serializers: [] });
@@ -533,7 +592,7 @@ describe("runGraph", () => {
533
592
  test("multi-stack component project: resolves each component's cfn-deploy stack(s) and passes them to observeResources", async () => {
534
593
  resolveLexMock.mockResolvedValue(["aws"]);
535
594
  loadPluginsMock.mockResolvedValue([
536
- { name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
595
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
537
596
  ]);
538
597
  discoverComponentsMock.mockResolvedValue({
539
598
  errors: [],
@@ -580,7 +639,7 @@ describe("runGraph", () => {
580
639
  test("ChantConfig.stacks: observeResources gets every declared stack, with its region and src", async () => {
581
640
  resolveLexMock.mockResolvedValue(["aws"]);
582
641
  loadPluginsMock.mockResolvedValue([
583
- { name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
642
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
584
643
  ]);
585
644
  loadChantConfigMock.mockResolvedValue({
586
645
  config: {
@@ -609,7 +668,7 @@ describe("runGraph", () => {
609
668
  test("ChantConfig.stacks: a stack also derived from a component is not observed twice", async () => {
610
669
  resolveLexMock.mockResolvedValue(["aws"]);
611
670
  loadPluginsMock.mockResolvedValue([
612
- { name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
671
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
613
672
  ]);
614
673
  discoverComponentsMock.mockResolvedValue({
615
674
  errors: [],
@@ -636,7 +695,7 @@ describe("runGraph", () => {
636
695
  test("component discovery errors: falls back to the single-stack path with a warning", async () => {
637
696
  resolveLexMock.mockResolvedValue(["aws"]);
638
697
  loadPluginsMock.mockResolvedValue([
639
- { name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
698
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
640
699
  ]);
641
700
  discoverComponentsMock.mockResolvedValue({ errors: [{ message: "bad component" }], sourceFiles: [], components: new Map() });
642
701
  observeMock.mockResolvedValue({ observations: [], errors: [], warnings: [] });
@@ -667,7 +726,7 @@ describe("runGraph", () => {
667
726
  });
668
727
  resolveLexMock.mockResolvedValue(["aws"]);
669
728
  loadPluginsMock.mockResolvedValue([
670
- { name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
729
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
671
730
  ]);
672
731
  let seenDuringObserve: string | undefined;
673
732
  observeMock.mockImplementation(async () => {
@@ -688,7 +747,7 @@ describe("runGraph", () => {
688
747
  });
689
748
  resolveLexMock.mockResolvedValue(["aws"]);
690
749
  loadPluginsMock.mockResolvedValue([
691
- { name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
750
+ { name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
692
751
  ]);
693
752
  let seenDuringObserve: string | undefined;
694
753
  observeMock.mockImplementation(async () => {
@@ -7,7 +7,7 @@ import { buildDeclaredPerStack } from "../../graph-declared";
7
7
  import { reconstructEdges, mergeCatalogs, containmentGroups, type ReferenceCatalog, type ContainmentPair } from "../../graph-refs";
8
8
  import { observeResources } from "../../lifecycle/observe";
9
9
  import { replaySnapshots, hasSnapshot } from "../../lifecycle/replay";
10
- import { loadChantConfig, environmentNames } from "../../config";
10
+ import { loadChantConfig, environmentNames, loadChantConfigUpward, type ChantConfig } from "../../config";
11
11
  import { applyLiveEndpoint } from "../../live-endpoint";
12
12
  import { applyDetail, type DetailLevel } from "../../graph-detail";
13
13
  import { applyLens, parseLens } from "../../graph-lens";
@@ -20,9 +20,42 @@ import { readFileSync } from "node:fs";
20
20
  import { formatError, formatWarning, formatBold } from "../format";
21
21
  import type { CommandContext } from "../registry";
22
22
  import { computeComponentGraph, generateComponentsPipeline } from "../../components/cli-support";
23
+ import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
24
+ import type { BuildParamProvenance } from "../../provenance";
23
25
  import { discoverComponents } from "../../components/discover";
24
26
  import { cfnDeployStacks } from "./components";
25
27
 
28
+
29
+ /**
30
+ * Resolve this invocation's declared build-time parameters, the same way
31
+ * `chant build` does, so `discover()` sees the values the source will read.
32
+ *
33
+ * Without this `chant graph` discovered source with `params.*` empty, so every
34
+ * declaration conditioned on a parameter took its default — a project whose
35
+ * tier is a `buildParams` entry graphed as one tier whatever `--param tier=`
36
+ * or the declared `env:` mapping said, while `chant build` on the same source
37
+ * and the same environment emitted a different resource set. Two commands
38
+ * disagreeing about what the source says.
39
+ *
40
+ * Returns `undefined` when resolution failed, having printed why — the caller
41
+ * should stop rather than graph something that will not build.
42
+ */
43
+ async function graphBuildParams(
44
+ ctx: CommandContext,
45
+ projectPath: string,
46
+ ): Promise<BuildParamProvenance[] | undefined> {
47
+ const { config } = await loadChantConfigUpward(projectPath).catch(() => ({ config: {} as ChantConfig }));
48
+ const resolution = resolveCliBuildParams(config.buildParams, {
49
+ cli: parseParamFlags(ctx.args.param),
50
+ paramsFile: ctx.args.paramsFile,
51
+ });
52
+ if (!resolution.success) {
53
+ for (const message of resolution.errors) console.error(message);
54
+ return undefined;
55
+ }
56
+ return resolution.provenance;
57
+ }
58
+
26
59
  /**
27
60
  * `chant graph` — the Op dependency graph by default; `--stacks` renders the
28
61
  * cross-stack apply-ordering graph (edges, order, waves) chant computes from
@@ -158,7 +191,7 @@ async function runGraphLive(
158
191
  // exported e.g. AWS_ENDPOINT_URL. Ambient always wins when it's already set.
159
192
  // Scoped to just this describe/enrich pass — restored in `finally` so it
160
193
  // never leaks into a later invocation in the same process.
161
- const endpointResult = applyLiveEndpoint(config.environments, environment, observing.map((p) => p.name));
194
+ const endpointResult = applyLiveEndpoint(config.environments, environment, observing);
162
195
  if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
163
196
 
164
197
  let ir: GraphIR;
@@ -271,7 +304,10 @@ async function runGraphLive(
271
304
  ? overlayGraphs(ir, declaredIr, overlayOpts)
272
305
  : sourceOverlayGraphs(declaredIr, ir, overlayOpts);
273
306
  } else {
274
- const declared = await discover(resolve(args.src ?? config.sourceDir ?? "."));
307
+ const declaredParams = await graphBuildParams(ctx, projectPath);
308
+ const declared = await discover(resolve(args.src ?? config.sourceDir ?? "."), {
309
+ ...(declaredParams ? { buildParams: declaredParams } : {}),
310
+ });
275
311
  if (declared.errors.length === 0) {
276
312
  const declaredIr = buildGraphIr(declared.entities, projectPath);
277
313
  ir =
@@ -487,7 +523,10 @@ async function runGraphView(
487
523
  return 1;
488
524
  }
489
525
 
490
- const result = await discover(projectPath);
526
+ const buildParams = await graphBuildParams(ctx, projectPath);
527
+ if (!buildParams) return 1;
528
+
529
+ const result = await discover(projectPath, { buildParams });
491
530
  if (result.errors.length > 0) {
492
531
  for (const e of result.errors) console.error(formatError({ message: e.message }));
493
532
  return 1;
@@ -596,7 +635,9 @@ async function runOpGraph(): Promise<number> {
596
635
 
597
636
  async function runStackGraph(ctx: CommandContext): Promise<number> {
598
637
  const projectPath = resolve(ctx.args.path === "." ? "." : ctx.args.path);
599
- const result = await discover(projectPath);
638
+ const buildParams = await graphBuildParams(ctx, projectPath);
639
+ if (!buildParams) return 1;
640
+ const result = await discover(projectPath, { buildParams });
600
641
  if (result.errors.length > 0) {
601
642
  for (const e of result.errors) console.error(formatError({ message: e.message }));
602
643
  return 1;
@@ -5,6 +5,18 @@ import type { LexiconPlugin, ResourceMetadata } from "../../lexicon";
5
5
  import type { BuildResult } from "../../build";
6
6
  import type { ParsedArgs } from "../registry";
7
7
 
8
+ /**
9
+ * The aws emulator capability, as the real plugin declares it. `--live`
10
+ * endpoint injection reads the endpoint var off this rather than off a map
11
+ * keyed by lexicon name (#1345), so a mock that omits it gets no injection —
12
+ * the same thing that would happen in production.
13
+ */
14
+ const awsEmulatorStub = {
15
+ spec: { name: "chant-floci", image: "floci/floci:1.5.34", containerPort: 4566, healthPath: "/_localstack/health" },
16
+ env: (endpoint: string) => ({ AWS_ENDPOINT_URL: endpoint, AWS_ACCESS_KEY_ID: "test" }),
17
+ };
18
+
19
+
8
20
  const buildMock = vi.fn();
9
21
  const fetchLifecycleMock = vi.fn();
10
22
  const readSnapshotMock = vi.fn();
@@ -123,6 +135,7 @@ describe("runLifecycleDiff --live", () => {
123
135
  const plugins: LexiconPlugin[] = [
124
136
  createMockPlugin({
125
137
  name: "aws",
138
+ emulator: awsEmulatorStub,
126
139
  describeResources: staticDescribeResources({
127
140
  bucket: meta({ status: "UPDATE_COMPLETE" }),
128
141
  }),
@@ -186,6 +199,7 @@ describe("runLifecycleDiff --live", () => {
186
199
  const plugins: LexiconPlugin[] = [
187
200
  createMockPlugin({
188
201
  name: "aws",
202
+ emulator: awsEmulatorStub,
189
203
  describeResources: async () => { throw new Error("Unable to locate credentials"); },
190
204
  }),
191
205
  ];
@@ -211,7 +225,7 @@ describe("runLifecycleDiff --live", () => {
211
225
  loadChantConfigMock.mockResolvedValue({ config: { sourceDir: "src" } });
212
226
 
213
227
  const plugins: LexiconPlugin[] = [
214
- createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
228
+ createMockPlugin({ name: "aws", emulator: awsEmulatorStub, describeResources: staticDescribeResources({}) }),
215
229
  ];
216
230
  const exit = await runLifecycleDiff({
217
231
  args: makeArgs({ path: "diff", extraPositional: "prod", extraPositional2: "aws", live: true }),
@@ -231,7 +245,7 @@ describe("runLifecycleDiff --live", () => {
231
245
  loadChantConfigMock.mockResolvedValue({ config: { sourceDir: "src" } });
232
246
 
233
247
  const plugins: LexiconPlugin[] = [
234
- createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
248
+ createMockPlugin({ name: "aws", emulator: awsEmulatorStub, describeResources: staticDescribeResources({}) }),
235
249
  ];
236
250
  const exit = await runLifecycleDiff({
237
251
  args: makeArgs({ path: "diff", extraPositional: "prod", extraPositional2: "aws", live: true, src: "infra" }),
@@ -259,6 +273,7 @@ describe("runLifecycleDiff --live", () => {
259
273
  const plugins: LexiconPlugin[] = [
260
274
  createMockPlugin({
261
275
  name: "aws",
276
+ emulator: awsEmulatorStub,
262
277
  describeResources: async (options: { stack?: string }) => {
263
278
  observedStacks.push(options.stack);
264
279
  return {};
@@ -371,6 +386,7 @@ describe("runLifecycleDiff --live", () => {
371
386
  const withDeep = (over: Parameters<typeof createMockPlugin>[0] = {}) =>
372
387
  createMockPlugin({
373
388
  name: "aws",
389
+ emulator: awsEmulatorStub,
374
390
  describeResources: staticObservation({ bucket: meta() }),
375
391
  observeResourcesDeep: staticDeepObservation({
376
392
  bucket: {
@@ -501,6 +517,7 @@ describe("runLifecycleDiff --live", () => {
501
517
  const plugins: LexiconPlugin[] = [
502
518
  createMockPlugin({
503
519
  name: "aws",
520
+ emulator: awsEmulatorStub,
504
521
  describeResources: async () => {
505
522
  seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
506
523
  return {};
@@ -533,6 +550,7 @@ describe("runLifecycleDiff --live", () => {
533
550
  const plugins: LexiconPlugin[] = [
534
551
  createMockPlugin({
535
552
  name: "aws",
553
+ emulator: awsEmulatorStub,
536
554
  describeResources: async () => {
537
555
  seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
538
556
  return {};
@@ -575,7 +593,7 @@ describe("runLifecyclePlan", () => {
575
593
  test("happy path: proposes a create for a declared, unobserved-nowhere-else entity", async () => {
576
594
  buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
577
595
  const plugins: LexiconPlugin[] = [
578
- createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
596
+ createMockPlugin({ name: "aws", emulator: awsEmulatorStub, describeResources: staticDescribeResources({}) }),
579
597
  ];
580
598
  const exit = await runLifecyclePlan({
581
599
  args: makeArgs({ path: "plan", extraPositional: "prod" }),
@@ -608,6 +626,7 @@ describe("runLifecyclePlan", () => {
608
626
  const plugins: LexiconPlugin[] = [
609
627
  createMockPlugin({
610
628
  name: "aws",
629
+ emulator: awsEmulatorStub,
611
630
  describeResources: async () => {
612
631
  seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
613
632
  return {};
@@ -638,6 +657,7 @@ describe("runLifecyclePlan", () => {
638
657
  const plugins: LexiconPlugin[] = [
639
658
  createMockPlugin({
640
659
  name: "aws",
660
+ emulator: awsEmulatorStub,
641
661
  describeResources: async () => {
642
662
  seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
643
663
  return {};
@@ -720,6 +740,7 @@ describe("runLifecycleSnapshot", () => {
720
740
  const plugins: LexiconPlugin[] = [
721
741
  createMockPlugin({
722
742
  name: "aws",
743
+ emulator: awsEmulatorStub,
723
744
  describeResources: staticDescribeResources({ bucket: meta() }),
724
745
  }),
725
746
  ];
@@ -842,7 +863,7 @@ describe("runLifecycleSnapshot", () => {
842
863
  return { snapshots: [], commit: "sha", warnings: [], errors: [] };
843
864
  });
844
865
  const plugins: LexiconPlugin[] = [
845
- createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
866
+ createMockPlugin({ name: "aws", emulator: awsEmulatorStub, describeResources: staticDescribeResources({}) }),
846
867
  ];
847
868
  const exit = await runLifecycleSnapshot({
848
869
  args: makeArgs({ command: "state", path: "snapshot", extraPositional: "floci" }),
@@ -135,7 +135,7 @@ export async function runLifecycleSnapshot(ctx: CommandContext): Promise<number>
135
135
  // #1166 — same self-sufficiency as `chant graph --live`: a snapshot is
136
136
  // always a live read, so an environment's declared endpoint applies here
137
137
  // too, unless the ambient shell already set it.
138
- const endpointResult = applyLiveEndpoint(config.environments, environment, observingPlugins.map((p) => p.name));
138
+ const endpointResult = applyLiveEndpoint(config.environments, environment, observingPlugins);
139
139
  if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
140
140
 
141
141
  // Build every stack first, so the ambient scan (#1278) can be bounded by the
@@ -349,7 +349,7 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
349
349
  // Floci), so `--live` is self-sufficient even when the ambient shell never
350
350
  // exported e.g. AWS_ENDPOINT_URL. Ambient always wins when it's already set.
351
351
  // Scoped to just the live reads below — restored in `finally`.
352
- const liveLexicons = args.live ? plugins.filter((p) => p.describeResources || p.listArtifacts).map((p) => p.name) : [];
352
+ const liveLexicons = args.live ? plugins.filter((p) => p.describeResources || p.listArtifacts) : [];
353
353
  const endpointResult = applyLiveEndpoint(config.environments, environment, liveLexicons);
354
354
  if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
355
355
 
@@ -597,6 +597,16 @@ async function runLifecycleDiffDigest(args: DigestDiffArgs): Promise<number> {
597
597
  for (const name of diff.unchanged) {
598
598
  console.log(name.padEnd(20) + "unchanged".padEnd(12) + (currentDigest.resources[name]?.type ?? ""));
599
599
  }
600
+
601
+ // chant #1442 — a lexicon bump can change emitted output with no source
602
+ // change, so every resource above can read "unchanged" while the build is
603
+ // not the same build. Printed under the table rather than as a row: no
604
+ // resource's declaration moved, the thing that interpreted them did.
605
+ for (const change of diff.lexiconVersionChanges) {
606
+ const from = change.previous ?? "(absent)";
607
+ const to = change.current ?? "(absent)";
608
+ console.log(`\nlexicon ${change.lexicon}: ${from} → ${to}`);
609
+ }
600
610
  }
601
611
 
602
612
  return 0;
@@ -1043,7 +1053,13 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
1043
1053
  // declare its own endpoint, applied here unless the ambient shell already
1044
1054
  // set it. `chant lifecycle plan` is always a live read (no `--live` flag of
1045
1055
  // its own), so this applies unconditionally.
1046
- const endpointResult = applyLiveEndpoint(config.environments, environment, lexicons);
1056
+ // Names here, plugins there: the endpoint vars come from each lexicon's own
1057
+ // emulator capability (#1345), so this needs the loaded plugin, not the name.
1058
+ const endpointResult = applyLiveEndpoint(
1059
+ config.environments,
1060
+ environment,
1061
+ lexicons.map((name) => plugins.find((p) => p.name === name)).filter((p) => p !== undefined),
1062
+ );
1047
1063
  if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
1048
1064
 
1049
1065
  try {
@@ -116,7 +116,9 @@ export function resolveProfile(
116
116
  config: Record<string, unknown>,
117
117
  profileName?: string,
118
118
  ): WorkerProfile {
119
- const temporal = config.temporal as Record<string, unknown> | undefined;
119
+ // #1344 the temporal lexicon declares this namespace and core validates it
120
+ // at load, so this reads a checked value rather than asserting one.
121
+ const temporal = (config as { temporal?: Record<string, unknown> }).temporal;
120
122
  if (!temporal?.profiles) {
121
123
  throw new Error(
122
124
  'No temporal.profiles found in chant.config.ts. Add a profile to use `chant run`.',
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveKindTerms, type Term } from "./search";
3
+ import type { IRNode } from "../../graph-ir";
4
+
5
+ const nodes = [
6
+ { id: "vpc", kind: "AWS::EC2::VPC", lexicon: "aws", attrs: {} },
7
+ { id: "att", kind: "AWS::EC2::VPCGatewayAttachment", lexicon: "aws", attrs: {} },
8
+ { id: "sub", kind: "AWS::EC2::Subnet", lexicon: "aws", attrs: {} },
9
+ { id: "assoc", kind: "AWS::EC2::SubnetRouteTableAssociation", lexicon: "aws", attrs: {} },
10
+ { id: "igw", kind: "AWS::EC2::InternetGateway", lexicon: "aws", attrs: {} },
11
+ ] as IRNode[];
12
+
13
+ const resolve = (a: string) => {
14
+ const t: Term = { kind: "kind", a };
15
+ resolveKindTerms([t], nodes);
16
+ return t.kinds ? [...t.kinds].sort() : undefined;
17
+ };
18
+
19
+ describe("a kind term means the kind, not any type containing its letters", () => {
20
+ it("EC2::VPC is the VPC, not the gateway attachment", () => {
21
+ // The estate has 6 VPCs and answered 9. VPCGatewayAttachment contains the
22
+ // characters "EC2::VPC" and is not a VPC.
23
+ expect(resolve("EC2::VPC")).toEqual(["AWS::EC2::VPC"]);
24
+ });
25
+
26
+ it("Subnet is the subnet, not the route table association", () => {
27
+ expect(resolve("Subnet")).toEqual(["AWS::EC2::Subnet"]);
28
+ });
29
+
30
+ it("a full type still resolves to itself", () => {
31
+ expect(resolve("AWS::EC2::Subnet")).toEqual(["AWS::EC2::Subnet"]);
32
+ });
33
+
34
+ it("a genuine substring search still works", () => {
35
+ // Nothing lines up with a `::` boundary here, so the documented substring
36
+ // rule applies and finds both gateway-ish kinds.
37
+ expect(resolve("Gateway")).toBeUndefined();
38
+ });
39
+
40
+ it("resolves inside an edge sub-term, where the negation lives", () => {
41
+ const t: Term = { kind: "edge", a: "", dir: "in", sub: { kind: "kind", a: "EC2::VPC" } };
42
+ resolveKindTerms([t], nodes);
43
+ expect(t.sub?.kinds && [...t.sub.kinds]).toEqual(["AWS::EC2::VPC"]);
44
+ });
45
+ });