@intentius/chant 0.19.0 → 0.20.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 (156) hide show
  1. package/dist/build-params.d.ts +108 -0
  2. package/dist/build-params.d.ts.map +1 -0
  3. package/dist/build.d.ts +79 -1
  4. package/dist/build.d.ts.map +1 -1
  5. package/dist/cli/commands/build.d.ts +31 -0
  6. package/dist/cli/commands/build.d.ts.map +1 -1
  7. package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
  8. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
  9. package/dist/cli/commands/check-lexicon-intrinsics.d.ts +54 -0
  10. package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
  11. package/dist/cli/commands/check-lexicon.d.ts +1 -1
  12. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  13. package/dist/cli/commands/lint.d.ts +27 -0
  14. package/dist/cli/commands/lint.d.ts.map +1 -1
  15. package/dist/cli/handlers/build.d.ts.map +1 -1
  16. package/dist/cli/handlers/components.d.ts.map +1 -1
  17. package/dist/cli/handlers/lint.d.ts.map +1 -1
  18. package/dist/cli/handlers/run-client.d.ts +1 -1
  19. package/dist/cli/handlers/run-client.d.ts.map +1 -1
  20. package/dist/cli/handlers/run.d.ts.map +1 -1
  21. package/dist/cli/main.d.ts.map +1 -1
  22. package/dist/cli/plugins.d.ts +16 -0
  23. package/dist/cli/plugins.d.ts.map +1 -1
  24. package/dist/cli/registry.d.ts +10 -0
  25. package/dist/cli/registry.d.ts.map +1 -1
  26. package/dist/codegen/docs-sections.d.ts.map +1 -1
  27. package/dist/components/cli-support.d.ts +12 -5
  28. package/dist/components/cli-support.d.ts.map +1 -1
  29. package/dist/components/discover.d.ts +62 -7
  30. package/dist/components/discover.d.ts.map +1 -1
  31. package/dist/components/sandbox/driver.d.ts +12 -0
  32. package/dist/components/sandbox/driver.d.ts.map +1 -0
  33. package/dist/components/sandbox/run.d.ts +42 -0
  34. package/dist/components/sandbox/run.d.ts.map +1 -0
  35. package/dist/composite.d.ts +5 -0
  36. package/dist/composite.d.ts.map +1 -1
  37. package/dist/config.d.ts +71 -0
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/discovery/collect.d.ts.map +1 -1
  40. package/dist/discovery/entity-wire-codec.d.ts +166 -0
  41. package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
  42. package/dist/discovery/entity-wire.d.ts +50 -0
  43. package/dist/discovery/entity-wire.d.ts.map +1 -0
  44. package/dist/discovery/fold-import.d.ts +210 -0
  45. package/dist/discovery/fold-import.d.ts.map +1 -0
  46. package/dist/discovery/index.d.ts +74 -1
  47. package/dist/discovery/index.d.ts.map +1 -1
  48. package/dist/discovery/sandbox/bundle.d.ts +18 -0
  49. package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
  50. package/dist/discovery/sandbox/child-errors.d.ts +15 -0
  51. package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
  52. package/dist/discovery/sandbox/driver.d.ts +13 -0
  53. package/dist/discovery/sandbox/driver.d.ts.map +1 -0
  54. package/dist/discovery/sandbox/run.d.ts +69 -0
  55. package/dist/discovery/sandbox/run.d.ts.map +1 -0
  56. package/dist/errors.d.ts +9 -1
  57. package/dist/errors.d.ts.map +1 -1
  58. package/dist/fold/fold.d.ts +226 -0
  59. package/dist/fold/fold.d.ts.map +1 -0
  60. package/dist/fold/subset.d.ts +102 -0
  61. package/dist/fold/subset.d.ts.map +1 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/lexicon-output.d.ts +7 -2
  65. package/dist/lexicon-output.d.ts.map +1 -1
  66. package/dist/lexicon-schema.d.ts +2 -2
  67. package/dist/lexicon-schema.d.ts.map +1 -1
  68. package/dist/lexicon.d.ts +81 -1
  69. package/dist/lexicon.d.ts.map +1 -1
  70. package/dist/lifecycle/release-ledger.d.ts +11 -0
  71. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  72. package/dist/lint/component-checks.d.ts +7 -1
  73. package/dist/lint/component-checks.d.ts.map +1 -1
  74. package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
  75. package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
  76. package/dist/params.d.ts +60 -0
  77. package/dist/params.d.ts.map +1 -0
  78. package/dist/provenance.d.ts +21 -0
  79. package/dist/provenance.d.ts.map +1 -1
  80. package/dist/terraform/aws-resources.d.ts.map +1 -1
  81. package/package.json +2 -1
  82. package/src/build-params.test.ts +144 -0
  83. package/src/build-params.ts +207 -0
  84. package/src/build.test.ts +38 -0
  85. package/src/build.ts +144 -7
  86. package/src/cli/commands/build.test.ts +220 -2
  87. package/src/cli/commands/build.ts +111 -3
  88. package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
  89. package/src/cli/commands/check-lexicon-examples.ts +103 -0
  90. package/src/cli/commands/check-lexicon-intrinsics.test.ts +145 -0
  91. package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
  92. package/src/cli/commands/check-lexicon.test.ts +34 -0
  93. package/src/cli/commands/check-lexicon.ts +119 -1
  94. package/src/cli/commands/lint.ts +31 -3
  95. package/src/cli/commands/onboard.ts +1 -1
  96. package/src/cli/component-security-boundary.test.ts +170 -0
  97. package/src/cli/handlers/build.ts +24 -3
  98. package/src/cli/handlers/components.ts +9 -2
  99. package/src/cli/handlers/dev.ts +1 -1
  100. package/src/cli/handlers/graph.ts +7 -5
  101. package/src/cli/handlers/lifecycle.ts +2 -2
  102. package/src/cli/handlers/lint.ts +2 -0
  103. package/src/cli/handlers/misc.ts +2 -2
  104. package/src/cli/handlers/run-client.ts +1 -1
  105. package/src/cli/handlers/run.ts +20 -5
  106. package/src/cli/main.test.ts +22 -0
  107. package/src/cli/main.ts +39 -0
  108. package/src/cli/plugins.ts +20 -4
  109. package/src/cli/registry.ts +10 -0
  110. package/src/cli/security-boundary.test.ts +135 -0
  111. package/src/codegen/docs-sections.test.ts +61 -0
  112. package/src/codegen/docs-sections.ts +7 -3
  113. package/src/components/cli-support.ts +22 -10
  114. package/src/components/discover.ts +127 -25
  115. package/src/components/sandbox/driver.ts +114 -0
  116. package/src/components/sandbox/run.test.ts +185 -0
  117. package/src/components/sandbox/run.ts +177 -0
  118. package/src/composite.test.ts +21 -0
  119. package/src/composite.ts +20 -1
  120. package/src/config.ts +81 -0
  121. package/src/discovery/collect.ts +17 -3
  122. package/src/discovery/entity-wire-codec.ts +485 -0
  123. package/src/discovery/entity-wire.test.ts +240 -0
  124. package/src/discovery/entity-wire.ts +67 -0
  125. package/src/discovery/fold-import.test.ts +1026 -0
  126. package/src/discovery/fold-import.ts +1805 -0
  127. package/src/discovery/index.test.ts +191 -1
  128. package/src/discovery/index.ts +233 -1
  129. package/src/discovery/sandbox/bundle.ts +218 -0
  130. package/src/discovery/sandbox/child-errors.ts +65 -0
  131. package/src/discovery/sandbox/driver.ts +147 -0
  132. package/src/discovery/sandbox/run.test.ts +179 -0
  133. package/src/discovery/sandbox/run.ts +196 -0
  134. package/src/errors.ts +9 -1
  135. package/src/fold/fold.test.ts +535 -0
  136. package/src/fold/fold.ts +648 -0
  137. package/src/fold/subset-doc-parity.test.ts +183 -0
  138. package/src/fold/subset.test.ts +241 -0
  139. package/src/fold/subset.ts +302 -0
  140. package/src/index.ts +2 -0
  141. package/src/lexicon-output.ts +7 -2
  142. package/src/lexicon-schema.test.ts +14 -0
  143. package/src/lexicon-schema.ts +3 -1
  144. package/src/lexicon.ts +82 -1
  145. package/src/lifecycle/git.test.ts +10 -5
  146. package/src/lifecycle/release-ledger.test.ts +28 -0
  147. package/src/lifecycle/release-ledger.ts +11 -0
  148. package/src/lint/component-checks.ts +8 -1
  149. package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
  150. package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
  151. package/src/params.test.ts +22 -0
  152. package/src/params.ts +66 -0
  153. package/src/provenance.ts +22 -0
  154. package/src/terraform/adopt-state.test.ts +1 -1
  155. package/src/terraform/aws-resources.test.ts +11 -4
  156. package/src/terraform/aws-resources.ts +60 -0
@@ -1 +1 @@
1
- {"version":3,"file":"evl003-dynamic-property-access.d.ts","sourceRoot":"","sources":["../../../src/lint/rules/evl003-dynamic-property-access.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAA+B,MAAM,SAAS,CAAC;AA8BrE,eAAO,MAAM,+BAA+B,EAAE,QAU7C,CAAC"}
1
+ {"version":3,"file":"evl003-dynamic-property-access.d.ts","sourceRoot":"","sources":["../../../src/lint/rules/evl003-dynamic-property-access.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAA+B,MAAM,SAAS,CAAC;AAuCrE,eAAO,MAAM,+BAA+B,EAAE,QAU7C,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Build-time parameters — the runtime binding source references (chant #1064,
3
+ * follow-up to epic #1019's fold work).
4
+ *
5
+ * This is deliberately NOT `Parameter` (`lexicons/aws/src/parameter.ts`), which
6
+ * is a deploy-time CloudFormation parameter: it emits a `Parameters:` block and
7
+ * resolves when the STACK deploys. A build-time parameter resolves before the
8
+ * template is even synthesized — its value can change WHICH resources are
9
+ * produced at all (loomster's `LOOM_TIER` selecting `light` vs `production` vs
10
+ * `production-ha`), which a deploy-time `Parameter` structurally cannot do.
11
+ *
12
+ * Project source never reads `process.env` directly to vary a build — that
13
+ * reads ambient state at module-evaluation time, which `fold()` correctly
14
+ * cannot reduce to a value (see ../fold/fold.ts). Instead it imports this
15
+ * module's `params` object:
16
+ *
17
+ * ```ts
18
+ * import { params } from "@intentius/chant/params";
19
+ * export const tier = params.tier as Tier;
20
+ * ```
21
+ *
22
+ * `params` is declared, validated (type + optional `enum`), and resolved by
23
+ * the CLI BEFORE discovery ever touches a project file (../build-params.ts's
24
+ * `resolveBuildParams`, driven by `chant build --param`/`--params-file`/a
25
+ * declared `env` mapping/`chant.config.ts`'s `buildParams` defaults). Two
26
+ * consumers read the resolved values, and both see the identical object:
27
+ *
28
+ * - The FOLD path (../discovery/fold-import.ts's `buildExternals`) recognizes
29
+ * a named `params` import resolving to *this* module and substitutes the
30
+ * already-resolved values directly, with zero import performed — so
31
+ * `params.tier` folds to a LITERAL, not a symbolic node. This is the entire
32
+ * point: the value is known at build invocation, so there is nothing left
33
+ * to defer.
34
+ * - The RUN path (a run-fallback file that imports this module for real)
35
+ * gets the exact same values, because `setBuildParams` below is called
36
+ * once, in-process, before ANY project file is imported or folded —
37
+ * mutating this shared object in place rather than replacing the binding,
38
+ * so a live-imported reference always observes the current build's values.
39
+ */
40
+ import type { BuildParamValue } from "./build-params.js";
41
+ export type { BuildParamValue };
42
+ /**
43
+ * The current build's resolved parameter values, keyed by declared name.
44
+ * Empty until a build populates it via {@link setBuildParams}. Mutated IN
45
+ * PLACE (never reassigned) so a real `import { params }` in a run-fallback
46
+ * file — which captures this exact object reference at import time — always
47
+ * observes whatever the current build resolved, even though the import
48
+ * happens after this module was first loaded.
49
+ */
50
+ export declare const params: Record<string, BuildParamValue>;
51
+ /**
52
+ * Replace the shared {@link params} object's contents in place. Called once
53
+ * per build (../discovery/index.ts's `discover()`), before any project file
54
+ * is imported or folded, with the CLI/config-resolved values
55
+ * (../build-params.ts's `resolveBuildParams`). Never called from project
56
+ * source — this is chant's own build-invocation plumbing, not a public
57
+ * authoring API.
58
+ */
59
+ export declare function setBuildParams(values: Readonly<Record<string, BuildParamValue>>): void;
60
+ //# sourceMappingURL=params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../src/params.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAM,CAAC;AAE1D;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,GAAG,IAAI,CAGtF"}
@@ -8,6 +8,12 @@
8
8
  * This is entity-level provenance (which file declared it, and which composite
9
9
  * expanded it), not a YAML-line source map. It answers "where did this resource
10
10
  * come from?", which is the question an agent asks before changing it.
11
+ *
12
+ * {@link BuildParamProvenance} (chant #1064) is the other half: not "where did
13
+ * this ENTITY come from" but "what INPUTS was this whole BUILD invoked with" —
14
+ * the question ambient `process.env` reads used to answer invisibly. See
15
+ * ../build-params.ts for declaration/resolution and ../build.ts's
16
+ * `BuildResult.buildParams` for where a build surfaces it.
11
17
  */
12
18
  export interface EntityProvenance {
13
19
  /** Absolute path of the source file that declared (or exported) the entity. */
@@ -30,4 +36,19 @@ export interface EntityProvenance {
30
36
  export declare function setProvenance(entity: object, prov: EntityProvenance): void;
31
37
  /** Read an entity's build provenance, if any was stamped. */
32
38
  export declare function getProvenance(entity: object): EntityProvenance | undefined;
39
+ /**
40
+ * One resolved build-time parameter (chant #1064, see ../build-params.ts): its
41
+ * final value and which source won it, so a build's parameter inputs are
42
+ * auditable rather than inferred. `source` records precedence, most to least
43
+ * specific: an explicit `--param`/`--params-file` value beats a declared `env`
44
+ * mapping, which beats `chant.config.ts`'s `default`.
45
+ */
46
+ export interface BuildParamProvenance {
47
+ /** The declared parameter name (a key of `chant.config.ts`'s `buildParams`). */
48
+ name: string;
49
+ /** The resolved, type-coerced value actually bound to `params.<name>` for this build. */
50
+ value: string | number | boolean;
51
+ /** Which input supplied the value. */
52
+ source: "cli" | "params-file" | "env" | "default";
53
+ }
33
54
  //# sourceMappingURL=provenance.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provenance.d.ts","sourceRoot":"","sources":["../src/provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAe1E;AAED,6DAA6D;AAC7D,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAE1E"}
1
+ {"version":3,"file":"provenance.d.ts","sourceRoot":"","sources":["../src/provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAe1E;AAED,6DAA6D;AAC7D,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAE1E;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACjC,sCAAsC;IACtC,MAAM,EAAE,KAAK,GAAG,aAAa,GAAG,KAAK,GAAG,SAAS,CAAC;CACnD"}
@@ -1 +1 @@
1
- {"version":3,"file":"aws-resources.d.ts","sourceRoot":"","sources":["../../src/terraform/aws-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,+DAA+D;AAC/D,KAAK,SAAS,GAAG,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAA;CAAE,CAAC;AAE/E,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChB,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,yFAAyF;IACzF,IAAI,EAAE,MAAM,CAAC;IACb,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAClC,kEAAkE;IAClE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,eAAO,MAAM,kBAAkB,iCAAiC,CAAC;AAajE,eAAO,MAAM,eAAe,EAAE,YAAY,EA8DzC,CAAC;AAIF,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAErE;AASD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,CAoB1D"}
1
+ {"version":3,"file":"aws-resources.d.ts","sourceRoot":"","sources":["../../src/terraform/aws-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,+DAA+D;AAC/D,KAAK,SAAS,GAAG,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAA;CAAE,CAAC;AAE/E,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChB,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,yFAAyF;IACzF,IAAI,EAAE,MAAM,CAAC;IACb,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAClC,kEAAkE;IAClE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,eAAO,MAAM,kBAAkB,iCAAiC,CAAC;AAajE,eAAO,MAAM,eAAe,EAAE,YAAY,EA0HzC,CAAC;AAIF,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAErE;AASD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,CAoB1D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -76,6 +76,7 @@
76
76
  },
77
77
  "dependencies": {
78
78
  "@dagrejs/dagre": "^3.0.0",
79
+ "esbuild": "^0.27.0",
79
80
  "fflate": "^0.8.2",
80
81
  "picomatch": "^4.0.3",
81
82
  "tsx": "^4.0.0",
@@ -0,0 +1,144 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { resolveBuildParams, buildParamValues, type BuildParamsConfig } from "./build-params";
3
+
4
+ const TIER_DEF: BuildParamsConfig = {
5
+ tier: { type: "string", default: "light", enum: ["light", "production", "production-ha"] },
6
+ };
7
+
8
+ describe("resolveBuildParams — precedence", () => {
9
+ test("cli beats params-file beats env beats default", () => {
10
+ const defs: BuildParamsConfig = { name: { type: "string", default: "d", env: "NAME_ENV" } };
11
+
12
+ const cliOnly = resolveBuildParams(defs, { cli: { name: "from-cli" } });
13
+ expect(cliOnly.errors).toEqual([]);
14
+ expect(cliOnly.provenance).toEqual([{ name: "name", value: "from-cli", source: "cli" }]);
15
+
16
+ const cliOverFile = resolveBuildParams(defs, { cli: { name: "from-cli" }, fromFile: { name: "from-file" } });
17
+ expect(cliOverFile.provenance).toEqual([{ name: "name", value: "from-cli", source: "cli" }]);
18
+
19
+ const fileOverEnv = resolveBuildParams(defs, { fromFile: { name: "from-file" }, env: { NAME_ENV: "from-env" } });
20
+ expect(fileOverEnv.provenance).toEqual([{ name: "name", value: "from-file", source: "params-file" }]);
21
+
22
+ const envOverDefault = resolveBuildParams(defs, { env: { NAME_ENV: "from-env" } });
23
+ expect(envOverDefault.provenance).toEqual([{ name: "name", value: "from-env", source: "env" }]);
24
+
25
+ const defaultOnly = resolveBuildParams(defs, {});
26
+ expect(defaultOnly.provenance).toEqual([{ name: "name", value: "d", source: "default" }]);
27
+ });
28
+
29
+ test("a parameter with no env mapping ignores an unrelated env var of the same name", () => {
30
+ const defs: BuildParamsConfig = { name: { type: "string", default: "d" } };
31
+ const result = resolveBuildParams(defs, { env: { name: "should-not-be-used" } });
32
+ expect(result.provenance).toEqual([{ name: "name", value: "d", source: "default" }]);
33
+ });
34
+ });
35
+
36
+ describe("resolveBuildParams — validation", () => {
37
+ test("missing value with no default is a build error naming the parameter", () => {
38
+ const defs: BuildParamsConfig = { tier: { type: "string" } };
39
+ const result = resolveBuildParams(defs, {});
40
+ expect(result.provenance).toEqual([]);
41
+ expect(result.errors).toHaveLength(1);
42
+ expect(result.errors[0]).toContain('"tier"');
43
+ expect(result.errors[0]).toContain("--param tier=");
44
+ });
45
+
46
+ test("enum violation names the parameter and the allowed values", () => {
47
+ const result = resolveBuildParams(TIER_DEF, { cli: { tier: "bogus" } });
48
+ expect(result.provenance).toEqual([]);
49
+ expect(result.errors).toHaveLength(1);
50
+ expect(result.errors[0]).toContain('"tier"');
51
+ expect(result.errors[0]).toContain("light");
52
+ expect(result.errors[0]).toContain("bogus");
53
+ });
54
+
55
+ test("enum success resolves normally", () => {
56
+ const result = resolveBuildParams(TIER_DEF, { cli: { tier: "production" } });
57
+ expect(result.errors).toEqual([]);
58
+ expect(result.provenance).toEqual([{ name: "tier", value: "production", source: "cli" }]);
59
+ });
60
+
61
+ test("unknown --param key (not declared) is a build error", () => {
62
+ const result = resolveBuildParams(TIER_DEF, { cli: { bogusName: "x" } });
63
+ expect(result.errors.some((e) => e.includes('"bogusName"') && e.includes("--param"))).toBe(true);
64
+ });
65
+
66
+ test("unknown --params-file key (not declared) is a build error", () => {
67
+ const result = resolveBuildParams(TIER_DEF, { fromFile: { bogusName: "x" } });
68
+ expect(result.errors.some((e) => e.includes('"bogusName"') && e.includes("--params-file"))).toBe(true);
69
+ });
70
+
71
+ test("number type coercion, including a non-numeric failure", () => {
72
+ const defs: BuildParamsConfig = { count: { type: "number" } };
73
+ const ok = resolveBuildParams(defs, { cli: { count: "42" } });
74
+ expect(ok.errors).toEqual([]);
75
+ expect(ok.provenance).toEqual([{ name: "count", value: 42, source: "cli" }]);
76
+
77
+ const bad = resolveBuildParams(defs, { cli: { count: "not-a-number" } });
78
+ expect(bad.provenance).toEqual([]);
79
+ expect(bad.errors[0]).toContain('"count"');
80
+ expect(bad.errors[0]).toContain("number");
81
+ });
82
+
83
+ test("boolean type coercion, including an invalid string", () => {
84
+ const defs: BuildParamsConfig = { flag: { type: "boolean" } };
85
+ const trueVal = resolveBuildParams(defs, { cli: { flag: "true" } });
86
+ expect(trueVal.provenance).toEqual([{ name: "flag", value: true, source: "cli" }]);
87
+
88
+ const falseVal = resolveBuildParams(defs, { cli: { flag: "false" } });
89
+ expect(falseVal.provenance).toEqual([{ name: "flag", value: false, source: "cli" }]);
90
+
91
+ const bad = resolveBuildParams(defs, { cli: { flag: "nope" } });
92
+ expect(bad.provenance).toEqual([]);
93
+ expect(bad.errors[0]).toContain("boolean");
94
+ });
95
+
96
+ test("params-file value must be a scalar, not an object/array", () => {
97
+ const defs: BuildParamsConfig = { thing: { type: "string" } };
98
+ const result = resolveBuildParams(defs, { fromFile: { thing: { nested: true } as unknown as string } });
99
+ expect(result.provenance).toEqual([]);
100
+ expect(result.errors[0]).toContain('"thing"');
101
+ });
102
+
103
+ test("every failure across multiple parameters is reported at once", () => {
104
+ const defs: BuildParamsConfig = {
105
+ a: { type: "string" },
106
+ b: { type: "number" },
107
+ };
108
+ const result = resolveBuildParams(defs, { cli: { b: "nan" } });
109
+ expect(result.errors).toHaveLength(2);
110
+ });
111
+
112
+ test("no declared params at all — empty resolution, no errors", () => {
113
+ const result = resolveBuildParams(undefined, {});
114
+ expect(result).toEqual({ provenance: [], errors: [] });
115
+ });
116
+
117
+ test("required: false — an unresolved optional parameter is simply omitted, not an error", () => {
118
+ const defs: BuildParamsConfig = { hostedZoneId: { type: "string", required: false } };
119
+ const result = resolveBuildParams(defs, {});
120
+ expect(result.errors).toEqual([]);
121
+ expect(result.provenance).toEqual([]);
122
+ });
123
+
124
+ test("required: false — a supplied value still resolves normally", () => {
125
+ const defs: BuildParamsConfig = { hostedZoneId: { type: "string", required: false } };
126
+ const result = resolveBuildParams(defs, { cli: { hostedZoneId: "Z123" } });
127
+ expect(result.errors).toEqual([]);
128
+ expect(result.provenance).toEqual([{ name: "hostedZoneId", value: "Z123", source: "cli" }]);
129
+ });
130
+ });
131
+
132
+ describe("buildParamValues", () => {
133
+ test("projects provenance records down to a plain value map", () => {
134
+ const provenance = [
135
+ { name: "tier", value: "light" as const, source: "default" as const },
136
+ { name: "count", value: 3 as const, source: "cli" as const },
137
+ ];
138
+ expect(buildParamValues(provenance)).toEqual({ tier: "light", count: 3 });
139
+ });
140
+
141
+ test("empty provenance yields an empty map", () => {
142
+ expect(buildParamValues([])).toEqual({});
143
+ });
144
+ });
@@ -0,0 +1,207 @@
1
+ import type { BuildParamProvenance } from "./provenance";
2
+
3
+ /**
4
+ * Build-time parameter declaration + resolution (chant #1064, follow-up to
5
+ * epic #1019's fold work — see issue #1064's "DECISION: option 1" comment).
6
+ *
7
+ * A build-time parameter is declared in `chant.config.ts`'s `buildParams`
8
+ * (name, type, optional `default`/`enum`/`env` mapping), supplied to `chant
9
+ * build` (a `--param name=value` flag, a `--params-file` JSON file, or a
10
+ * declared `env` var), and referenced from source as `params.<name>` (see
11
+ * ./params.ts) — never as an ambient `process.env` read. This module owns
12
+ * declaration + precedence + validation; ./params.ts is the plain runtime
13
+ * object source references; ../discovery/fold-import.ts is what makes a
14
+ * `params.<name>` reference fold to a literal instead of a symbolic node.
15
+ *
16
+ * This is NOT the deploy-time `Parameter` class (`lexicons/aws/src/parameter.ts`)
17
+ * — that resolves when a CloudFormation stack deploys; this resolves before
18
+ * the template is even synthesized, so its value can change which resources
19
+ * are produced at all. See ./params.ts's module doc for the full distinction.
20
+ */
21
+
22
+ /** A build-time parameter's resolved (and declared-default/enum) value — always a scalar. */
23
+ export type BuildParamValue = string | number | boolean;
24
+
25
+ /**
26
+ * One declared build-time parameter (`chant.config.ts`'s `buildParams.<name>`).
27
+ */
28
+ export interface BuildParamDef {
29
+ /** The value's declared type — supplied strings (CLI flags, env vars, JSON-file strings) are coerced to it. */
30
+ type: "string" | "number" | "boolean";
31
+ /** Value used when no `--param`/`--params-file`/declared `env` var supplies one. Omit to require an explicit value every build. */
32
+ default?: BuildParamValue;
33
+ /**
34
+ * Allowed values — a resolved value outside this list is a build error
35
+ * naming the parameter (never a thrown error from user source). Replaces
36
+ * the hand-written `if (!VALID.includes(raw)) throw ...` pattern loomster's
37
+ * `params.ts` files used before migrating to this mechanism.
38
+ */
39
+ enum?: readonly BuildParamValue[];
40
+ /**
41
+ * Opt-in, EXPLICITLY declared environment-variable fallback — e.g. `env:
42
+ * "LOOM_TIER"`. This is the only place an env var may feed a build-time
43
+ * parameter: reading `process.env` directly from project source is never
44
+ * supported (see ./params.ts's module doc and ../fold/fold.ts's pointed
45
+ * error for a bare `process` reference). Consulted only when no
46
+ * `--param`/`--params-file` value was supplied for this parameter.
47
+ */
48
+ env?: string;
49
+ /**
50
+ * Set `false` to make an unresolved value NOT a build error: `params.<name>`
51
+ * is simply omitted (reads as plain JS `undefined` — a normal, un-erroring
52
+ * property access on an object missing that key), instead of the default
53
+ * behavior of requiring every declared parameter to resolve to something.
54
+ * For a value that is genuinely optional with no meaningful default (an ARN
55
+ * that references an existing resource only on some deploys, a CIDR
56
+ * override, a JSON blob) — the same "unset means the composite decides"
57
+ * shape a hand-written `process.env.X || undefined` used to express.
58
+ * Default `true` (a declared parameter must resolve to a value).
59
+ */
60
+ required?: boolean;
61
+ /** Human-readable description, surfaced in error messages and docs generation. */
62
+ description?: string;
63
+ }
64
+
65
+ /** A project's full set of declared build-time parameters, keyed by name. */
66
+ export type BuildParamsConfig = Record<string, BuildParamDef>;
67
+
68
+ /** Raw, not-yet-validated inputs {@link resolveBuildParams} resolves against a project's declared {@link BuildParamsConfig}. */
69
+ export interface BuildParamsInput {
70
+ /** `--param name=value` flags, repeated — highest precedence. */
71
+ cli?: Record<string, string>;
72
+ /** Parsed contents of a `--params-file` JSON file — second precedence. */
73
+ fromFile?: Record<string, unknown>;
74
+ /** The process environment, consulted only for a parameter that declares an `env` mapping, and only once `cli`/`fromFile` have no value for it. */
75
+ env?: Record<string, string | undefined>;
76
+ }
77
+
78
+ /** Result of resolving a project's declared parameters against one build invocation's inputs. */
79
+ export interface BuildParamsResolution {
80
+ /** Every successfully resolved parameter — see {@link BuildParamProvenance}. Empty when the project declares none. */
81
+ provenance: BuildParamProvenance[];
82
+ /**
83
+ * Validation failures, each naming the offending parameter — an unknown
84
+ * `--param`/`--params-file` key, a missing required value, a type mismatch,
85
+ * or a value outside a declared `enum`. Reported by the CLI as a build
86
+ * error (chant #1064's acceptance criterion: "not a thrown error inside
87
+ * user source"); never thrown from here.
88
+ */
89
+ errors: string[];
90
+ }
91
+
92
+ function formatValue(value: unknown): string {
93
+ return typeof value === "string" ? `"${value}"` : String(value);
94
+ }
95
+
96
+ /** Coerce `raw` to `def.type`, appending a located error and returning `undefined` on failure. */
97
+ function coerce(name: string, raw: BuildParamValue, def: BuildParamDef, errors: string[]): BuildParamValue | undefined {
98
+ if (def.type === "string") return String(raw);
99
+
100
+ if (def.type === "number") {
101
+ const n = typeof raw === "number" ? raw : Number(raw);
102
+ if (Number.isNaN(n)) {
103
+ errors.push(`build parameter "${name}" must be a number, got ${formatValue(raw)}`);
104
+ return undefined;
105
+ }
106
+ return n;
107
+ }
108
+
109
+ // def.type === "boolean"
110
+ if (typeof raw === "boolean") return raw;
111
+ if (raw === "true") return true;
112
+ if (raw === "false") return false;
113
+ errors.push(`build parameter "${name}" must be a boolean ("true" or "false"), got ${formatValue(raw)}`);
114
+ return undefined;
115
+ }
116
+
117
+ /**
118
+ * Resolve a project's declared build-time parameters against one build's
119
+ * supplied inputs. Precedence per parameter, most to least specific:
120
+ * `cli` (`--param name=value`) > `fromFile` (`--params-file`) > the
121
+ * parameter's own declared `env` mapping (only if set) > `def.default`.
122
+ *
123
+ * A parameter with no declared `default` and no value from any source is a
124
+ * build error, not a silently-`undefined` value — a build-time parameter
125
+ * exists specifically so a project never has an invisible dependency on
126
+ * ambient state; leaving one unresolved would just reintroduce that under a
127
+ * different name. Every failure is collected (not thrown), each naming the
128
+ * offending parameter, so a single invocation reports every problem at once.
129
+ *
130
+ * The one opt-out is `def.required: false` — for a parameter that is
131
+ * genuinely optional with no meaningful default (an ARN that only applies to
132
+ * a reference-existing deploy, a CIDR override), an unresolved value is
133
+ * simply omitted from `provenance`/`params` rather than an error; source
134
+ * reads it as plain `undefined`, same as before migrating off
135
+ * `process.env.X || undefined`.
136
+ */
137
+ export function resolveBuildParams(defs: BuildParamsConfig | undefined, input: BuildParamsInput): BuildParamsResolution {
138
+ const declared = defs ?? {};
139
+ const cli = input.cli ?? {};
140
+ const fromFile = input.fromFile ?? {};
141
+ const env = input.env ?? {};
142
+ const errors: string[] = [];
143
+ const provenance: BuildParamProvenance[] = [];
144
+
145
+ for (const key of Object.keys(cli)) {
146
+ if (!(key in declared)) {
147
+ errors.push(`unknown build parameter "${key}" (from --param) — not declared in chant.config.ts's buildParams`);
148
+ }
149
+ }
150
+ for (const key of Object.keys(fromFile)) {
151
+ if (!(key in declared)) {
152
+ errors.push(`unknown build parameter "${key}" (from --params-file) — not declared in chant.config.ts's buildParams`);
153
+ }
154
+ }
155
+
156
+ for (const [name, def] of Object.entries(declared)) {
157
+ let raw: BuildParamValue | undefined;
158
+ let source: BuildParamProvenance["source"] | undefined;
159
+
160
+ if (name in cli) {
161
+ raw = cli[name];
162
+ source = "cli";
163
+ } else if (name in fromFile) {
164
+ const fileValue = fromFile[name];
165
+ if (typeof fileValue !== "string" && typeof fileValue !== "number" && typeof fileValue !== "boolean") {
166
+ errors.push(`build parameter "${name}" (from --params-file) must be a string, number, or boolean`);
167
+ continue;
168
+ }
169
+ raw = fileValue;
170
+ source = "params-file";
171
+ } else if (def.env && env[def.env] !== undefined) {
172
+ raw = env[def.env];
173
+ source = "env";
174
+ } else if (def.default !== undefined) {
175
+ raw = def.default;
176
+ source = "default";
177
+ }
178
+
179
+ if (raw === undefined || source === undefined) {
180
+ if (def.required === false) continue; // omitted entirely — params.<name> reads as undefined, not an error
181
+ const envHint = def.env ? `, set ${def.env}` : "";
182
+ errors.push(
183
+ `build parameter "${name}" has no value — pass --param ${name}=<value>, use --params-file${envHint}, or add a default in chant.config.ts's buildParams`,
184
+ );
185
+ continue;
186
+ }
187
+
188
+ const value = coerce(name, raw, def, errors);
189
+ if (value === undefined) continue;
190
+
191
+ if (def.enum && !def.enum.includes(value)) {
192
+ errors.push(
193
+ `build parameter "${name}" must be one of ${def.enum.map(formatValue).join(", ")}, got ${formatValue(value)}`,
194
+ );
195
+ continue;
196
+ }
197
+
198
+ provenance.push({ name, value, source });
199
+ }
200
+
201
+ return { provenance, errors };
202
+ }
203
+
204
+ /** Project the resolved provenance records down to a plain `{ name: value }` map — what actually gets bound to `params.<name>` (see ./params.ts). */
205
+ export function buildParamValues(provenance: readonly BuildParamProvenance[]): Record<string, BuildParamValue> {
206
+ return Object.fromEntries(provenance.map((p) => [p.name, p.value]));
207
+ }
package/src/build.test.ts CHANGED
@@ -64,6 +64,44 @@ export const testEntity = {
64
64
  expect(result.entities.size).toBe(1);
65
65
  expect(result.entities.has("testEntity")).toBe(true);
66
66
  expect(result.errors.length).toBe(0);
67
+ expect(result.foldDecisions).toEqual([]);
68
+ });
69
+
70
+ test("#1022 — --fold folds a leaf-only module and surfaces foldDecisions", async () => {
71
+ const { dirname, resolve: resolvePath } = await import("node:path");
72
+ const { fileURLToPath } = await import("node:url");
73
+ const thisDir = dirname(fileURLToPath(import.meta.url));
74
+ const runtimePath = resolvePath(thisDir, "runtime");
75
+
76
+ await writeFile(
77
+ join(testDir, "resources.ts"),
78
+ `
79
+ import { createResource } from ${JSON.stringify(runtimePath)};
80
+ export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
81
+ `
82
+ );
83
+ await writeFile(
84
+ join(testDir, "main.ts"),
85
+ `
86
+ import { Bucket } from "./resources";
87
+ throw new Error("must never execute — sentinel for #1022 fold verification");
88
+ export const bucket = new Bucket({ name: "my-bucket" });
89
+ `
90
+ );
91
+
92
+ const mockSerializer: Serializer = {
93
+ name: "aws",
94
+ rulePrefix: "TEST",
95
+ serialize: (entities) => JSON.stringify([...entities.keys()]),
96
+ };
97
+
98
+ const result = await build(testDir, [mockSerializer], undefined, { fold: true });
99
+
100
+ expect(result.errors).toEqual([]);
101
+ expect(result.entities.size).toBe(1);
102
+ expect(result.entities.has("bucket")).toBe(true);
103
+ const mainDecision = result.foldDecisions.find((d) => d.file.endsWith("main.ts"));
104
+ expect(mainDecision?.mode).toBe("fold");
67
105
  });
68
106
 
69
107
  test("handles discovery errors", async () => {