@intentius/chant 0.21.0 → 0.23.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 (57) hide show
  1. package/dist/build.d.ts +7 -0
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/cli/build-params-cli.d.ts +55 -0
  4. package/dist/cli/build-params-cli.d.ts.map +1 -0
  5. package/dist/cli/commands/build.d.ts.map +1 -1
  6. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  7. package/dist/cli/commands/lint.d.ts.map +1 -1
  8. package/dist/cli/handlers/build.d.ts.map +1 -1
  9. package/dist/cli/handlers/run.d.ts +14 -1
  10. package/dist/cli/handlers/run.d.ts.map +1 -1
  11. package/dist/cli/lsp/server.d.ts.map +1 -1
  12. package/dist/components/cli-support.d.ts +33 -2
  13. package/dist/components/cli-support.d.ts.map +1 -1
  14. package/dist/components/discover.d.ts +28 -0
  15. package/dist/components/discover.d.ts.map +1 -1
  16. package/dist/discovery/fold-import.d.ts +71 -9
  17. package/dist/discovery/fold-import.d.ts.map +1 -1
  18. package/dist/discovery/index.d.ts +27 -4
  19. package/dist/discovery/index.d.ts.map +1 -1
  20. package/dist/fold/fold.d.ts.map +1 -1
  21. package/dist/fold/subset.d.ts +39 -2
  22. package/dist/fold/subset.d.ts.map +1 -1
  23. package/dist/lint/engine.d.ts +11 -1
  24. package/dist/lint/engine.d.ts.map +1 -1
  25. package/dist/lint/rule.d.ts +14 -0
  26. package/dist/lint/rule.d.ts.map +1 -1
  27. package/dist/serializer-walker.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/build.ts +9 -0
  30. package/src/cli/build-params-cli.test.ts +139 -0
  31. package/src/cli/build-params-cli.ts +107 -0
  32. package/src/cli/commands/build.ts +25 -36
  33. package/src/cli/commands/check-lexicon-examples.ts +16 -1
  34. package/src/cli/commands/lint.test.ts +74 -0
  35. package/src/cli/commands/lint.ts +33 -9
  36. package/src/cli/handlers/build.test.ts +147 -0
  37. package/src/cli/handlers/build.ts +23 -8
  38. package/src/cli/handlers/run.test.ts +160 -5
  39. package/src/cli/handlers/run.ts +46 -8
  40. package/src/cli/lsp/server.ts +7 -2
  41. package/src/components/cli-support.test.ts +221 -3
  42. package/src/components/cli-support.ts +37 -6
  43. package/src/components/discover.test.ts +63 -1
  44. package/src/components/discover.ts +42 -0
  45. package/src/discovery/fold-import.test.ts +328 -1
  46. package/src/discovery/fold-import.ts +414 -31
  47. package/src/discovery/index.test.ts +131 -0
  48. package/src/discovery/index.ts +53 -8
  49. package/src/discovery/sandbox/fold-boundary.test.ts +254 -0
  50. package/src/fold/fold.ts +6 -2
  51. package/src/fold/subset.test.ts +95 -15
  52. package/src/fold/subset.ts +46 -5
  53. package/src/lint/engine.ts +12 -0
  54. package/src/lint/rule.ts +14 -0
  55. package/src/lint/rules/evl001-non-literal-expression.test.ts +39 -0
  56. package/src/lint/rules/evl001-non-literal-expression.ts +1 -1
  57. package/src/serializer-walker.ts +14 -0
@@ -8,16 +8,45 @@
8
8
  * rather than going through the CLI arg-parsing/handler dispatch layer.
9
9
  */
10
10
 
11
- import { describe, test, expect, beforeEach, afterEach } from "vitest";
11
+ import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
12
12
  import { mkdir, writeFile, rm } from "node:fs/promises";
13
- import { join } from "node:path";
13
+ import { join, dirname, resolve as resolvePath } from "node:path";
14
+ import { fileURLToPath } from "node:url";
14
15
  import { tmpdir } from "node:os";
15
16
  import Ajv2020 from "ajv/dist/2020";
16
17
  import componentSchema from "./component.schema.json";
17
- import { listComponents, describeComponent, computeComponentGraph, runComponents, findComponentGate } from "./cli-support";
18
+ import {
19
+ listComponents,
20
+ describeComponent,
21
+ computeComponentGraph,
22
+ runComponents,
23
+ findComponentGate,
24
+ resolveComponentTargets,
25
+ generateComponentsPipeline,
26
+ } from "./cli-support";
18
27
  import { CapabilityRegistry, type DeployContext } from "./capability";
19
28
  import type { DriverComponent, RunProgressEvent } from "./driver";
20
29
 
30
+ // A minimal stand-in for the real gitlab lexicon plugin, satisfying
31
+ // `isLexiconPlugin` (../lexicon.ts) — used only by the `generateComponentsPipeline`
32
+ // buildParams test below, so that test doesn't depend on how `@intentius/
33
+ // chant-lexicon-gitlab`'s own real module resolves under the test runner.
34
+ vi.mock("@intentius/chant-lexicon-gitlab", () => ({
35
+ gitlab: {
36
+ name: "gitlab",
37
+ serializer: { name: "gitlab", rulePrefix: "GL", serialize: () => "" },
38
+ generate: () => {},
39
+ validate: () => [],
40
+ coverage: () => ({ total: 0, covered: 0 }),
41
+ package: () => "gitlab",
42
+ generateComponentPipeline: (components: DriverComponent[]) => ({
43
+ yaml: components.map((c) => c.name).join(","),
44
+ stages: ["wave-1"],
45
+ jobs: components.map((c) => ({ jobName: c.name, component: c.name, stage: "wave-1", needs: [] })),
46
+ }),
47
+ },
48
+ }));
49
+
21
50
  describe("listComponents", () => {
22
51
  let testDir: string;
23
52
 
@@ -578,6 +607,195 @@ describe("runComponents", () => {
578
607
  });
579
608
  });
580
609
 
610
+ // ── build-time parameters (chant #1108) ──────────────────────────────────────
611
+ //
612
+ // Before #1108, `discoverComponents`'s callers here never resolved
613
+ // `chant.config.ts`'s declared `buildParams` at all, so a `*.component.ts`
614
+ // file reading `params.<name>` (`@intentius/chant/params`) always saw `{}` —
615
+ // the exact probe from chant#1108's reproduction. These tests lock in that
616
+ // `runComponents`/`resolveComponentTargets`/`generateComponentsPipeline` now
617
+ // forward an already-resolved `buildParams` into discovery, so a component's
618
+ // `params.<name>` read reflects it before any step dispatches.
619
+
620
+ const thisDir = dirname(fileURLToPath(import.meta.url));
621
+ const paramsModulePath = resolvePath(thisDir, "../params");
622
+
623
+ describe("runComponents — buildParams (chant #1108)", () => {
624
+ let testDir: string;
625
+
626
+ beforeEach(async () => {
627
+ testDir = join(tmpdir(), `chant-run-components-buildparams-test-${Date.now()}-${Math.random()}`);
628
+ await mkdir(testDir, { recursive: true });
629
+ });
630
+
631
+ afterEach(async () => {
632
+ await rm(testDir, { recursive: true, force: true });
633
+ });
634
+
635
+ test("the previously-{} case: a component's params.<name> read resolves when buildParams is passed", async () => {
636
+ await writeFile(
637
+ join(testDir, "svc.component.ts"),
638
+ `
639
+ import { params } from ${JSON.stringify(paramsModulePath)};
640
+ export const svc = {
641
+ name: "svc",
642
+ dependsOn: [],
643
+ deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing", tier: params.tier }] }],
644
+ };
645
+ `,
646
+ );
647
+
648
+ const capability = fakeCapability("deploy-thing");
649
+ const registry = new CapabilityRegistry();
650
+ registry.register(capability);
651
+
652
+ const result = await runComponents(testDir, "svc", {
653
+ registry,
654
+ buildParams: [{ name: "tier", value: "production", source: "cli" }],
655
+ });
656
+
657
+ expect(result.success).toBe(true);
658
+ expect(capability.calls[0]?.input).toMatchObject({ tier: "production" });
659
+ });
660
+
661
+ test("without buildParams, params.<name> is undefined (the bug this issue fixes, still true for a caller that resolves none)", async () => {
662
+ await writeFile(
663
+ join(testDir, "svc.component.ts"),
664
+ `
665
+ import { params } from ${JSON.stringify(paramsModulePath)};
666
+ export const svc = {
667
+ name: "svc",
668
+ dependsOn: [],
669
+ deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing", tier: params.tier ?? "unset" }] }],
670
+ };
671
+ `,
672
+ );
673
+
674
+ const capability = fakeCapability("deploy-thing");
675
+ const registry = new CapabilityRegistry();
676
+ registry.register(capability);
677
+
678
+ const result = await runComponents(testDir, "svc", { registry });
679
+
680
+ expect(result.success).toBe(true);
681
+ expect(capability.calls[0]?.input).toMatchObject({ tier: "unset" });
682
+ });
683
+
684
+ test("RunComponentsResult.buildParams echoes back the resolved provenance that was passed in", async () => {
685
+ await writeFile(
686
+ join(testDir, "svc.component.ts"),
687
+ `export const svc = { name: "svc", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing" }] }] };`,
688
+ );
689
+
690
+ const registry = fakeRegistry();
691
+ const result = await runComponents(testDir, "svc", {
692
+ registry,
693
+ buildParams: [{ name: "tier", value: "production", source: "cli" }],
694
+ });
695
+
696
+ expect(result.buildParams).toEqual([{ name: "tier", value: "production", source: "cli" }]);
697
+ });
698
+
699
+ test("selector 'all' also forwards buildParams into discovery", async () => {
700
+ await writeFile(
701
+ join(testDir, "svc.component.ts"),
702
+ `
703
+ import { params } from ${JSON.stringify(paramsModulePath)};
704
+ export const svc = { name: "svc", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing", tier: params.tier }] }] };
705
+ `,
706
+ );
707
+
708
+ const capability = fakeCapability("deploy-thing");
709
+ const registry = new CapabilityRegistry();
710
+ registry.register(capability);
711
+
712
+ const result = await runComponents(testDir, "all", {
713
+ registry,
714
+ buildParams: [{ name: "tier", value: "staging", source: "env" }],
715
+ });
716
+
717
+ expect(result.success).toBe(true);
718
+ expect(capability.calls[0]?.input).toMatchObject({ tier: "staging" });
719
+ });
720
+ });
721
+
722
+ describe("resolveComponentTargets — buildParams (chant #1108)", () => {
723
+ let testDir: string;
724
+
725
+ beforeEach(async () => {
726
+ testDir = join(tmpdir(), `chant-resolve-targets-buildparams-test-${Date.now()}-${Math.random()}`);
727
+ await mkdir(testDir, { recursive: true });
728
+ });
729
+
730
+ afterEach(async () => {
731
+ await rm(testDir, { recursive: true, force: true });
732
+ });
733
+
734
+ test("forwards buildParams into discovery — the durable (--temporal) path's entrypoint", async () => {
735
+ await writeFile(
736
+ join(testDir, "svc.component.ts"),
737
+ `
738
+ import { params } from ${JSON.stringify(paramsModulePath)};
739
+ export const svc = {
740
+ name: "svc",
741
+ dependsOn: [],
742
+ deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", stack: params.env }] }],
743
+ };
744
+ `,
745
+ );
746
+
747
+ const result = await resolveComponentTargets(testDir, "svc", undefined, [
748
+ { name: "env", value: "prod-a", source: "env" },
749
+ ]);
750
+
751
+ expect(result.success).toBe(true);
752
+ expect((result.targets[0].deploy[0].steps[0] as { stack?: unknown }).stack).toBe("prod-a");
753
+ });
754
+ });
755
+
756
+ describe("generateComponentsPipeline — buildParams (chant #1108)", () => {
757
+ let testDir: string;
758
+
759
+ beforeEach(async () => {
760
+ testDir = join(tmpdir(), `chant-generate-components-buildparams-test-${Date.now()}-${Math.random()}`);
761
+ await mkdir(testDir, { recursive: true });
762
+ });
763
+
764
+ afterEach(async () => {
765
+ await rm(testDir, { recursive: true, force: true });
766
+ });
767
+
768
+ test("forwards buildParams into discovery before synthesizing the pipeline", async () => {
769
+ await writeFile(
770
+ join(testDir, "svc.component.ts"),
771
+ `
772
+ import { params } from ${JSON.stringify(paramsModulePath)};
773
+ export const svc = {
774
+ name: String(params.name),
775
+ dependsOn: [],
776
+ deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
777
+ };
778
+ `,
779
+ );
780
+
781
+ // Uses the mocked "gitlab" plugin above (see the `vi.mock` at the top of
782
+ // this file) — proves the component name `discoverComponents` resolved
783
+ // (via `params.name`) made it all the way through to the synthesized
784
+ // pipeline, not just to `discover()`'s own return value.
785
+ const result = await generateComponentsPipeline(
786
+ testDir,
787
+ "gitlab",
788
+ undefined,
789
+ undefined,
790
+ [{ name: "name", value: "named-from-params", source: "cli" }],
791
+ );
792
+
793
+ expect(result.success).toBe(true);
794
+ expect(result.jobs?.map((j) => j.component)).toEqual(["named-from-params"]);
795
+ expect(result.buildParams).toEqual([{ name: "name", value: "named-from-params", source: "cli" }]);
796
+ });
797
+ });
798
+
581
799
  describe("runComponents — onProgress (--progress-json wiring, M3)", () => {
582
800
  let testDir: string;
583
801
 
@@ -23,6 +23,7 @@
23
23
  */
24
24
 
25
25
  import { discoverComponents } from "./discover";
26
+ import type { BuildParamProvenance } from "../provenance";
26
27
  import { projectToJson, type Archetype } from "./component";
27
28
  import {
28
29
  resolveComponentGraph,
@@ -185,6 +186,8 @@ export interface GenerateComponentsResult {
185
186
  /** Every generated job, for a machine-readable view (`--format json`). */
186
187
  jobs?: Array<{ jobName: string; component: string; stage: string; needs: string[] }>;
187
188
  error?: string;
189
+ /** This invocation's resolved build-time parameters (chant #1108) — the generate-mode counterpart of `../cli/commands/build.ts`'s `BuildResult.buildParams`. Empty when the project declares/supplies none. */
190
+ buildParams?: BuildParamProvenance[];
188
191
  }
189
192
 
190
193
  /**
@@ -221,6 +224,15 @@ export async function generateComponentsPipeline(
221
224
  lexicon: GenerateLexicon,
222
225
  options?: ComponentPipelineOptions,
223
226
  sandbox?: boolean,
227
+ /**
228
+ * chant #1108 — this invocation's resolved build-time parameter values
229
+ * (../cli/handlers/build.ts resolves them, the same sequence `chant build`
230
+ * runs, BEFORE calling this function), forwarded into discovery so a
231
+ * `params.<name>` reference inside a discovered `*.component.ts` file
232
+ * resolves instead of reading `{}`. See `discoverComponents`'s
233
+ * `buildParams` option (./discover.ts) for the full doc.
234
+ */
235
+ buildParams?: BuildParamProvenance[],
224
236
  ): Promise<GenerateComponentsResult> {
225
237
  const plugin = await loadLexiconPlugin(lexicon);
226
238
  if (!plugin?.generateComponentPipeline) {
@@ -230,7 +242,7 @@ export async function generateComponentsPipeline(
230
242
  };
231
243
  }
232
244
 
233
- const result = await discoverComponents(path, { sandbox });
245
+ const result = await discoverComponents(path, { sandbox, buildParams });
234
246
  if (result.errors.length > 0) {
235
247
  return { success: false, error: result.errors.map((e) => e.message).join("\n") };
236
248
  }
@@ -243,7 +255,7 @@ export async function generateComponentsPipeline(
243
255
 
244
256
  try {
245
257
  const { yaml, stages, jobs } = plugin.generateComponentPipeline(driverComponents, options);
246
- return { success: true, yaml, stages, jobs };
258
+ return { success: true, yaml, stages, jobs, buildParams };
247
259
  } catch (err) {
248
260
  if (err instanceof UnknownDependencyError || err instanceof DependencyCycleError) {
249
261
  return { success: false, error: err.message };
@@ -344,6 +356,21 @@ export interface RunComponentsOptions {
344
356
  * `--progress-json`, in which case nothing changes.
345
357
  */
346
358
  onProgress?: (event: RunProgressEvent) => void;
359
+ /**
360
+ * chant #1108 — this run's resolved build-time parameter values, resolved
361
+ * the exact same way `chant build` resolves them
362
+ * (../cli/build-params-cli.ts's `resolveCliBuildParams`, driven by
363
+ * `--param`/`--params-file`/a declared `env` mapping/`chant.config.ts`'s
364
+ * `buildParams` defaults). The CLI handler (../cli/handlers/run.ts)
365
+ * resolves + logs these BEFORE calling `runComponents`, the same
366
+ * sequencing `chant build` uses — this function only forwards the
367
+ * already-resolved values into discovery (`resolveComponentTargets` below,
368
+ * then `discoverComponents`); it does not resolve them itself, so
369
+ * `runComponents` stays free of CLI-flag-parsing/formatting concerns.
370
+ * Default: none — `params.*` stays `{}`, matching every caller (including
371
+ * every test in `cli-support.test.ts`) that doesn't supply this.
372
+ */
373
+ buildParams?: BuildParamProvenance[];
347
374
  }
348
375
 
349
376
  /** Result of `chant run --components <name|all>`. */
@@ -356,6 +383,8 @@ export interface RunComponentsResult {
356
383
  error?: string;
357
384
  /** Set when a selected component (or one of its `deploy`/`rollback` phases) contains a `gate` the local executor cannot run. */
358
385
  gateUnsupported?: { component: string; signalName: string };
386
+ /** This run's resolved build-time parameters (chant #1108) — the component-driver counterpart of `../cli/commands/build.ts`'s `BuildResult.buildParams`. Present only once the run actually reached dispatch (mirrors `BuildResult.buildParams`, which is likewise absent on an early-error return). */
387
+ buildParams?: BuildParamProvenance[];
359
388
  }
360
389
 
361
390
  /**
@@ -404,8 +433,10 @@ export async function resolveComponentTargets(
404
433
  path: string,
405
434
  selector: string,
406
435
  sandbox?: boolean,
436
+ /** chant #1108 — this invocation's resolved build-time parameter values, forwarded into `discoverComponents` so a `params.<name>` reference inside a discovered `*.component.ts` file resolves instead of reading `{}`. See `discoverComponents`'s `buildParams` option (./discover.ts). */
437
+ buildParams?: BuildParamProvenance[],
407
438
  ): Promise<ResolvedComponentTargets> {
408
- const result = await discoverComponents(path, { sandbox });
439
+ const result = await discoverComponents(path, { sandbox, buildParams });
409
440
  if (result.errors.length > 0) {
410
441
  return { success: false, targets: [], error: result.errors.map((e) => e.message).join("\n") };
411
442
  }
@@ -433,7 +464,7 @@ export async function runComponents(
433
464
  selector: string,
434
465
  options: RunComponentsOptions = {},
435
466
  ): Promise<RunComponentsResult> {
436
- const resolved = await resolveComponentTargets(path, selector, options.sandbox);
467
+ const resolved = await resolveComponentTargets(path, selector, options.sandbox, options.buildParams);
437
468
  if (!resolved.success) {
438
469
  return { success: false, selected: [], error: resolved.error };
439
470
  }
@@ -479,7 +510,7 @@ export async function runComponents(
479
510
  try {
480
511
  if (selector === "all") {
481
512
  const run = await runInterpretDriver(resolvedTargets, registry, { env, componentOutputs: seedOutputs, onProgress });
482
- return { success: true, run, selected };
513
+ return { success: true, run, selected, buildParams: options.buildParams };
483
514
  }
484
515
 
485
516
  // Single-component invocation: run just this component, bypassing
@@ -518,7 +549,7 @@ export async function runComponents(
518
549
  failedComponent: componentResult.ok ? undefined : componentResult.component,
519
550
  componentOutputs,
520
551
  };
521
- return { success: componentResult.ok, run, selected };
552
+ return { success: componentResult.ok, run, selected, buildParams: options.buildParams };
522
553
  } catch (err) {
523
554
  if (err instanceof DriverRunFailure) {
524
555
  return { success: false, run: err.result, selected, error: err.message };
@@ -11,9 +11,11 @@
11
11
 
12
12
  import { describe, test, expect, beforeEach, afterEach } from "vitest";
13
13
  import { mkdir, writeFile, rm } from "node:fs/promises";
14
- import { join } from "node:path";
14
+ import { join, dirname, resolve as resolvePath } from "node:path";
15
+ import { fileURLToPath } from "node:url";
15
16
  import { tmpdir } from "node:os";
16
17
  import { discoverComponents } from "./discover";
18
+ import { params } from "../params";
17
19
 
18
20
  describe("discoverComponents", () => {
19
21
  let testDir: string;
@@ -349,4 +351,64 @@ describe("discoverComponents", () => {
349
351
  expect(result.components.has("first-svc")).toBe(true);
350
352
  expect(result.components.has("nested-svc")).toBe(false);
351
353
  });
354
+
355
+ // ── chant #1108 — build-time parameters populated before import ────────────
356
+
357
+ describe("buildParams (chant #1108)", () => {
358
+ const thisDir = dirname(fileURLToPath(import.meta.url));
359
+ const paramsPath = resolvePath(thisDir, "../params");
360
+
361
+ test("with no buildParams option, params.* stays empty (matches every non-run/generate caller today)", async () => {
362
+ await writeFile(
363
+ join(testDir, "svc.component.ts"),
364
+ `
365
+ export const svc = {
366
+ name: "svc",
367
+ dependsOn: [],
368
+ deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
369
+ };
370
+ `,
371
+ );
372
+
373
+ const result = await discoverComponents(testDir);
374
+
375
+ expect(result.components.has("svc")).toBe(true);
376
+ expect(params).toEqual({});
377
+ });
378
+
379
+ test("populates params.* BEFORE importing *.component.ts, so a live import observes the resolved value", async () => {
380
+ await writeFile(
381
+ join(testDir, "svc.component.ts"),
382
+ `
383
+ import { params } from ${JSON.stringify(paramsPath)};
384
+ export const svc = {
385
+ name: "svc",
386
+ dependsOn: [],
387
+ deploy: [{ phase: "Apply", steps: [{ kind: "shell", command: String(params.tier) }] }],
388
+ };
389
+ `,
390
+ );
391
+
392
+ const result = await discoverComponents(testDir, {
393
+ buildParams: [{ name: "tier", value: "production", source: "cli" }],
394
+ });
395
+
396
+ expect(result.errors).toEqual([]);
397
+ const svc = result.components.get("svc");
398
+ expect((svc?.component.deploy[0].steps[0] as { command?: unknown }).command).toBe("production");
399
+ });
400
+
401
+ test("a second call with no buildParams resets params.* — no stale leak from a prior call in the same process", async () => {
402
+ await writeFile(
403
+ join(testDir, "a.component.ts"),
404
+ `export const a = { name: "a", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }] };`,
405
+ );
406
+
407
+ await discoverComponents(testDir, { buildParams: [{ name: "tier", value: "production", source: "cli" }] });
408
+ expect(params).toEqual({ tier: "production" });
409
+
410
+ await discoverComponents(testDir);
411
+ expect(params).toEqual({});
412
+ });
413
+ });
352
414
  });
@@ -46,6 +46,9 @@ import { pathToFileURL } from "node:url";
46
46
  import { existsSync } from "node:fs";
47
47
  import { DiscoveryError } from "../errors";
48
48
  import { isComponent, type Component } from "./component";
49
+ import type { BuildParamProvenance } from "../provenance";
50
+ import { buildParamValues } from "../build-params";
51
+ import { setBuildParams } from "../params";
49
52
 
50
53
  /** One discovered component, paired with the file it was exported from. */
51
54
  export interface DiscoveredComponent {
@@ -75,6 +78,34 @@ export interface ComponentDiscoveryOptions {
75
78
  * requested.
76
79
  */
77
80
  sandbox?: boolean;
81
+
82
+ /**
83
+ * chant #1108 — this invocation's resolved build-time parameter values
84
+ * (../build-params.ts's `resolveBuildParams`, driven by the CLI's
85
+ * `--param`/`--params-file`/a declared `env` mapping/`chant.config.ts`'s
86
+ * `buildParams` defaults — see ../cli/build-params-cli.ts's
87
+ * `resolveCliBuildParams`, the exact resolution `chant build` runs).
88
+ * Populated into ../params.ts's shared `params` object (`setBuildParams`,
89
+ * below) before any `*.component.ts` file is scanned/imported, mirroring
90
+ * ../discovery/index.ts's `discover()` — the lexicon-resource counterpart
91
+ * of this function, and the thing chant #1108 exists to bring this one to
92
+ * parity with. Before #1108, no caller populated this, so a live `import {
93
+ * params } from "@intentius/chant/params"` inside a component file (e.g. a
94
+ * naming helper deriving a stack name) always saw `{}`.
95
+ *
96
+ * Default: none — `params` stays `{}`, matching every caller that doesn't
97
+ * resolve build-time parameters today (`chant list/describe/graph/lint
98
+ * --components`, `chant components status`); only the run and generate
99
+ * call sites (../cli/handlers/run.ts, ../cli/handlers/build.ts) pass a
100
+ * resolved value.
101
+ *
102
+ * Only takes effect for the in-process (non-sandboxed) import path below —
103
+ * `{ sandbox: true }`'s child process gets its own, unpopulated `params`
104
+ * module instance, the same pre-existing gap `discover({ sandbox: true })`
105
+ * has for lexicon resources (chant #1045 Phase 2 never threaded
106
+ * `buildParams` into its sandboxed child either; out of scope here too).
107
+ */
108
+ buildParams?: BuildParamProvenance[];
78
109
  }
79
110
 
80
111
  /** One already-imported `*.component.ts` module — the input to {@link collectComponents}. */
@@ -240,6 +271,17 @@ export async function discoverComponents(
240
271
  path: string,
241
272
  options?: ComponentDiscoveryOptions,
242
273
  ): Promise<ComponentDiscoveryResult> {
274
+ // chant #1108 — populate the shared build-time-parameters object BEFORE
275
+ // scanning/importing any *.component.ts file below, so a live `import {
276
+ // params } from "@intentius/chant/params"` inside a component file
277
+ // observes this invocation's resolved values instead of an empty object.
278
+ // Unconditional (not just when `buildParams` is set) so a stale value from
279
+ // a PRIOR discoverComponents() call in the same process (tests, several
280
+ // `--components` subcommands run back-to-back) never leaks into a call
281
+ // that supplied none — mirrors ../discovery/index.ts's `discover()`,
282
+ // identical rationale.
283
+ setBuildParams(buildParamValues(options?.buildParams ?? []));
284
+
243
285
  const sourceFiles = await findComponentFiles(path);
244
286
 
245
287
  if (options?.sandbox && sourceFiles.length > 0) {