@intentius/chant 0.19.1 → 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 (152) 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/package.json +2 -1
  81. package/src/build-params.test.ts +144 -0
  82. package/src/build-params.ts +207 -0
  83. package/src/build.test.ts +38 -0
  84. package/src/build.ts +144 -7
  85. package/src/cli/commands/build.test.ts +220 -2
  86. package/src/cli/commands/build.ts +111 -3
  87. package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
  88. package/src/cli/commands/check-lexicon-examples.ts +103 -0
  89. package/src/cli/commands/check-lexicon-intrinsics.test.ts +145 -0
  90. package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
  91. package/src/cli/commands/check-lexicon.test.ts +34 -0
  92. package/src/cli/commands/check-lexicon.ts +119 -1
  93. package/src/cli/commands/lint.ts +31 -3
  94. package/src/cli/commands/onboard.ts +1 -1
  95. package/src/cli/component-security-boundary.test.ts +170 -0
  96. package/src/cli/handlers/build.ts +24 -3
  97. package/src/cli/handlers/components.ts +9 -2
  98. package/src/cli/handlers/dev.ts +1 -1
  99. package/src/cli/handlers/graph.ts +7 -5
  100. package/src/cli/handlers/lifecycle.ts +2 -2
  101. package/src/cli/handlers/lint.ts +2 -0
  102. package/src/cli/handlers/misc.ts +2 -2
  103. package/src/cli/handlers/run-client.ts +1 -1
  104. package/src/cli/handlers/run.ts +20 -5
  105. package/src/cli/main.test.ts +22 -0
  106. package/src/cli/main.ts +39 -0
  107. package/src/cli/plugins.ts +20 -4
  108. package/src/cli/registry.ts +10 -0
  109. package/src/cli/security-boundary.test.ts +135 -0
  110. package/src/codegen/docs-sections.test.ts +61 -0
  111. package/src/codegen/docs-sections.ts +7 -3
  112. package/src/components/cli-support.ts +22 -10
  113. package/src/components/discover.ts +127 -25
  114. package/src/components/sandbox/driver.ts +114 -0
  115. package/src/components/sandbox/run.test.ts +185 -0
  116. package/src/components/sandbox/run.ts +177 -0
  117. package/src/composite.test.ts +21 -0
  118. package/src/composite.ts +20 -1
  119. package/src/config.ts +81 -0
  120. package/src/discovery/collect.ts +17 -3
  121. package/src/discovery/entity-wire-codec.ts +485 -0
  122. package/src/discovery/entity-wire.test.ts +240 -0
  123. package/src/discovery/entity-wire.ts +67 -0
  124. package/src/discovery/fold-import.test.ts +1026 -0
  125. package/src/discovery/fold-import.ts +1805 -0
  126. package/src/discovery/index.test.ts +191 -1
  127. package/src/discovery/index.ts +233 -1
  128. package/src/discovery/sandbox/bundle.ts +218 -0
  129. package/src/discovery/sandbox/child-errors.ts +65 -0
  130. package/src/discovery/sandbox/driver.ts +147 -0
  131. package/src/discovery/sandbox/run.test.ts +179 -0
  132. package/src/discovery/sandbox/run.ts +196 -0
  133. package/src/errors.ts +9 -1
  134. package/src/fold/fold.test.ts +535 -0
  135. package/src/fold/fold.ts +648 -0
  136. package/src/fold/subset-doc-parity.test.ts +183 -0
  137. package/src/fold/subset.test.ts +241 -0
  138. package/src/fold/subset.ts +302 -0
  139. package/src/index.ts +2 -0
  140. package/src/lexicon-output.ts +7 -2
  141. package/src/lexicon-schema.test.ts +14 -0
  142. package/src/lexicon-schema.ts +3 -1
  143. package/src/lexicon.ts +82 -1
  144. package/src/lifecycle/git.test.ts +10 -5
  145. package/src/lifecycle/release-ledger.test.ts +28 -0
  146. package/src/lifecycle/release-ledger.ts +11 -0
  147. package/src/lint/component-checks.ts +8 -1
  148. package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
  149. package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
  150. package/src/params.test.ts +22 -0
  151. package/src/params.ts +66 -0
  152. package/src/provenance.ts +22 -0
@@ -181,6 +181,27 @@ describe("expandComposite", () => {
181
181
  expect(expanded.get("sBucket")).toBe(bucket);
182
182
  });
183
183
 
184
+ test("is idempotent — repeated expansion does not duplicate shared array props (#1032)", () => {
185
+ const member = new MockResource({ type: "Bucket", tags: [{ key: "app", value: "x" }] });
186
+ const Comp = Composite<{}>(() => ({ bucket: member as unknown as Declarable }));
187
+ const instance = propagate(
188
+ Comp({}) as never,
189
+ { tags: [{ key: "env", value: "prod" }] },
190
+ );
191
+
192
+ const first = expandComposite("s", instance);
193
+ const firstTags = (first.get("sBucket") as unknown as MockResource).props.tags as unknown[];
194
+ // Expand the SAME singleton instance again (what building one tree twice in a
195
+ // process does) — must not re-merge onto the already-merged props.
196
+ const second = expandComposite("s", instance);
197
+ const secondTags = (second.get("sBucket") as unknown as MockResource).props.tags as unknown[];
198
+
199
+ // shared [env] + member [app] = 2 tags, both times — not 3 on the second pass.
200
+ expect(firstTags).toHaveLength(2);
201
+ expect(secondTags).toHaveLength(2);
202
+ expect(secondTags).toEqual(firstTags);
203
+ });
204
+
184
205
  test("handles empty composite", () => {
185
206
  const Empty = Composite<{}>(() => ({} as Record<string, Declarable>));
186
207
  const expanded = expandComposite("e", Empty({}));
package/src/composite.ts CHANGED
@@ -150,7 +150,20 @@ export function expandComposite(
150
150
  if (shared) {
151
151
  for (const entity of result.values()) {
152
152
  if ("props" in entity) {
153
- const existing = entity.props as Record<string, unknown>;
153
+ // Merge shared props onto each member's ORIGINAL props, not its current
154
+ // props. Members are module-level singletons, so without stashing the
155
+ // original a second expansion (e.g. building the same tree twice in one
156
+ // process) would re-merge shared arrays like tags onto the already-merged
157
+ // value, duplicating them on every rebuild (#1032). Stashing the original
158
+ // once makes expansion idempotent: same input -> same output, every time.
159
+ const store = entity as unknown as Record<symbol, unknown>;
160
+ let existing = store[ORIGINAL_PROPS] as Record<string, unknown> | undefined;
161
+ if (existing === undefined) {
162
+ existing = entity.props as Record<string, unknown>;
163
+ Object.defineProperty(entity, ORIGINAL_PROPS, {
164
+ value: existing, enumerable: false, configurable: true,
165
+ });
166
+ }
154
167
  const merged: Record<string, unknown> = {};
155
168
  for (const [k, v] of Object.entries(shared)) {
156
169
  if (v !== undefined) {
@@ -216,6 +229,12 @@ export function withDefaults<P, M extends CompositeMembers, D extends Partial<P>
216
229
  */
217
230
  export const SHARED_PROPS = Symbol.for("chant.composite.shared");
218
231
 
232
+ /**
233
+ * Symbol key stashing a member's original (pre-merge) props, so expandComposite()
234
+ * is idempotent across repeated expansions of the same singleton instance (#1032).
235
+ */
236
+ export const ORIGINAL_PROPS = Symbol.for("chant.composite.origProps");
237
+
219
238
  /**
220
239
  * Attaches shared properties to a composite instance.
221
240
  * During expandComposite(), shared props are merged into every member's props.
package/src/config.ts CHANGED
@@ -6,6 +6,7 @@ import type { OwnershipMarker } from "./ownership";
6
6
  import { DEFAULT_SBOM_FORMAT, type SbomFormat } from "./components/verbs/sbom-generator";
7
7
  import type { Severity } from "./components/verbs/vuln-scan";
8
8
  import type { VulnPolicy } from "./components/verbs/vuln-gate";
9
+ import type { BuildParamsConfig } from "./build-params";
9
10
 
10
11
  /**
11
12
  * Zod schema for ChantConfig validation.
@@ -21,6 +22,20 @@ export const ChantConfigSchema = z.object({
21
22
  env: z.string().min(1).optional(),
22
23
  enabled: z.boolean().optional(),
23
24
  }).optional(),
25
+ build: z.object({
26
+ fold: z.boolean().optional(),
27
+ }).optional(),
28
+ buildParams: z.record(
29
+ z.string(),
30
+ z.object({
31
+ type: z.enum(["string", "number", "boolean"]),
32
+ default: z.union([z.string(), z.number(), z.boolean()]).optional(),
33
+ enum: z.array(z.union([z.string(), z.number(), z.boolean()])).optional(),
34
+ env: z.string().min(1).optional(),
35
+ required: z.boolean().optional(),
36
+ description: z.string().optional(),
37
+ }),
38
+ ).optional(),
24
39
  release: z.object({
25
40
  autoRecord: z.boolean().optional(),
26
41
  }).optional(),
@@ -117,6 +132,45 @@ export interface ChantConfig {
117
132
  enabled?: boolean;
118
133
  };
119
134
 
135
+ /**
136
+ * `chant build` behavior toggles (#1022, epic #1019).
137
+ */
138
+ build?: {
139
+ /**
140
+ * Opt-in: fold source modules statically instead of importing/running
141
+ * them, falling back to run per-file for anything the folder can't
142
+ * represent (composite factory calls, non-`new` exports, …). Default
143
+ * `false`. The `--fold` CLI flag overrides this per-invocation (a flag
144
+ * of `true` always wins; the flag cannot force fold *off* when this is
145
+ * `true`). See {@link resolveFoldEnabled}.
146
+ */
147
+ fold?: boolean;
148
+
149
+ /**
150
+ * chant #1045 Phase 2 — opt-in: run-fallback source files (or, when
151
+ * `fold` above isn't set, every file) execute together, isolated, in one
152
+ * sandboxed child process instead of in-process. Default `false`. The
153
+ * `--sandbox` CLI flag overrides this per-invocation (a flag of `true`
154
+ * always wins; the flag cannot force sandboxing *off* when this is
155
+ * `true`). See {@link resolveSandboxEnabled}.
156
+ */
157
+ sandbox?: boolean;
158
+ };
159
+
160
+ /**
161
+ * Build-time parameters (#1064) — values supplied to `chant build` (a
162
+ * `--param name=value` flag, a `--params-file` JSON file, or a declared
163
+ * `env` var mapping) and bound to `params.<name>` (`@intentius/chant/params`)
164
+ * for source to reference, instead of reading `process.env` at module
165
+ * scope. Distinct from the deploy-time `Parameter` class
166
+ * (`lexicons/aws/src/parameter.ts`, a CloudFormation `Parameters:` entry
167
+ * that resolves at stack deploy) — a build-time parameter resolves before
168
+ * the template is even synthesized, so it can change which resources are
169
+ * produced at all (e.g. a tier selecting `light` vs `production`). See
170
+ * {@link resolveBuildParams} in `./build-params.ts`.
171
+ */
172
+ buildParams?: BuildParamsConfig;
173
+
120
174
  /**
121
175
  * Release-ledger recording behavior (#597, epic #551). Auto-emitting a
122
176
  * release record on a successful `chant run --components <name> --env
@@ -265,6 +319,33 @@ export function resolveAutoReleaseDisabled(config: ChantConfig, cliFlag?: boolea
265
319
  return config.release?.autoRecord === false;
266
320
  }
267
321
 
322
+ /**
323
+ * Whether `chant build` should use the fold path (#1022, epic #1019)
324
+ * instead of running each source module. Opt-in: off unless the CLI's
325
+ * `--fold` flag was passed (`cliFlag`) or the project config sets
326
+ * `build.fold: true` — the flag always wins for that one invocation,
327
+ * regardless of config.
328
+ */
329
+ export function resolveFoldEnabled(config: ChantConfig, cliFlag?: boolean): boolean {
330
+ if (cliFlag) return true;
331
+ return config.build?.fold === true;
332
+ }
333
+
334
+ /**
335
+ * Whether `chant build` should run its run-fallback files (or, without
336
+ * `--fold`, every file) in an isolated sandboxed child process rather than
337
+ * in-process (chant #1045 Phase 2). Opt-in: off unless the CLI's `--sandbox`
338
+ * flag was passed (`cliFlag`) or the project config sets `build.sandbox:
339
+ * true` — the flag always wins for that one invocation, regardless of
340
+ * config. Independent of {@link resolveFoldEnabled}: sandboxing without
341
+ * folding isolates every discovered file; sandboxing with folding isolates
342
+ * only the per-file run-fallback remainder.
343
+ */
344
+ export function resolveSandboxEnabled(config: ChantConfig, cliFlag?: boolean): boolean {
345
+ if (cliFlag) return true;
346
+ return config.build?.sandbox === true;
347
+ }
348
+
268
349
  /**
269
350
  * Resolve which SBOM format a `generate-sbom` step should request (#606),
270
351
  * given the project's `chant.config.ts` `sbom.format` and an optional
@@ -47,8 +47,21 @@ interface PendingEntry {
47
47
  /**
48
48
  * Flatten every module's exports into the ordered list of entities they
49
49
  * contribute — declarables directly, arrays element-by-element (indexed names),
50
- * composite instances expanded into members, and LexiconOutputs. Order is
51
- * preserved so downstream serializers emit resources in a stable order.
50
+ * composite instances expanded into members, and LexiconOutputs. `modules`
51
+ * order (one discovered file after another) is preserved so downstream
52
+ * serializers emit resources in a stable, file-discovery-order sequence.
53
+ *
54
+ * Within one module, exports are visited in ascending name order rather than
55
+ * `Object.entries()`'s own — a real ECMAScript Module namespace object
56
+ * already enumerates its (non-default) string keys this way per spec
57
+ * (`[[OwnPropertyKeys]]`, sorted), regardless of source declaration order;
58
+ * sorting here just makes that the case EXPLICITLY, so this doesn't quietly
59
+ * depend on whichever loader imported the file preserving (or not) that spec
60
+ * behavior — chant #1045 Phase 2 found `vite-node` (vitest's own in-process
61
+ * transform) does NOT sort, unlike plain Node, which made comparing an
62
+ * in-process build against a real-subprocess one (its differential's whole
63
+ * point) spuriously "drift" on multi-export-per-file modules whenever the
64
+ * in-process side ran under vitest.
52
65
  */
53
66
  function enumerateEntries(
54
67
  modules: Array<{ file: string; exports: Record<string, unknown> }>,
@@ -56,7 +69,8 @@ function enumerateEntries(
56
69
  const entries: PendingEntry[] = [];
57
70
 
58
71
  for (const { file, exports } of modules) {
59
- for (const [rawName, value] of Object.entries(exports)) {
72
+ const sortedExports = Object.entries(exports).sort(([a], [b]) => a.localeCompare(b));
73
+ for (const [rawName, value] of sortedExports) {
60
74
  const name = exportKey(rawName, file);
61
75
  if (isDeclarable(value)) {
62
76
  entries.push({ bareKey: name, value, file, provenance: { sourceFile: file } });