@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
@@ -0,0 +1,648 @@
1
+ import * as ts from "typescript";
2
+ import { intrinsicFolds, type IntrinsicDef } from "../lexicon";
3
+ import {
4
+ SUPPORTED_BINARY_OPERATORS,
5
+ SUPPORTED_UNARY_OPERATORS,
6
+ UNSUPPORTED_OBJECT_MEMBER_MESSAGE,
7
+ UNSUPPORTED_UNARY_MESSAGE,
8
+ callExpressionMessage,
9
+ computedPropertyNameMessage,
10
+ dynamicElementAccessMessage,
11
+ isLiteralElementKey,
12
+ isLiteralPropertyName,
13
+ resourceCtorArgMessage,
14
+ unsupportedBinaryMessage,
15
+ unsupportedExpressionMessage,
16
+ type SubsetRuleId,
17
+ } from "./subset";
18
+
19
+ /**
20
+ * fold — static AST value reducer (chant #1026/#1021/#1024, part of epic #1019)
21
+ *
22
+ * Reduces a single-file TypeScript expression AST to a value with NO
23
+ * module execution. The node-kind/operator/key subset it covers — literals,
24
+ * template interpolation, object/array literals (incl. spread), `const`
25
+ * identifier resolution, property and element access (incl. the
26
+ * cross-resource `{ __attrRef }` case, literal-key-only), unary `!`/`-`,
27
+ * the binary operators `+ - * / === !== > < >= <=`, short-circuit
28
+ * `&& || ??`, conditional expressions, `as`/`satisfies`/`!`/parenthesized
29
+ * unwrapping, a nested `new Type({...})` resource-as-value, and registered
30
+ * lexicon intrinsic tagged templates — is defined ONCE, in {@link "./subset"}
31
+ * ({@link findSubsetViolation}), and shared with EVL001/EVL003
32
+ * ({@link "../lint/rules/evl001-non-literal-expression"}), so the linted
33
+ * subset and the folded subset can never drift apart (#1024).
34
+ *
35
+ * A `CallExpression` has no case — a function call as a value is
36
+ * structurally unrepresentable, not merely linted against. Composite
37
+ * factory calls are out of scope here (epic Phase 5, #1023).
38
+ *
39
+ * Cross-file identifier resolution (chant #1020): `consts` alone is always
40
+ * this file's own top-level bindings — that part stays single-file, and
41
+ * nothing about the supported node-kind subset changes. But an identifier
42
+ * `fold()` can't find in `consts` isn't necessarily a dead end: the optional
43
+ * `externals` map (populated by ../discovery/fold-import.ts, which owns the
44
+ * module graph traversal) lets a caller pre-resolve an *imported* binding to
45
+ * its real value — a plain value for an imported `const`, or the REAL,
46
+ * already-constructed `Declarable`/`CompositeInstance` for a name bound to a
47
+ * resource/composite in the defining module — and `fold()` just returns it
48
+ * for the identifier, unchanged. This is why a bare object/array bracket
49
+ * index a few lines down (`obj[key]`) is enough to make `network.vpc.VpcId`
50
+ * fold correctly once `network` resolves via `externals` to the real,
51
+ * shared composite instance object: indexing a live class instance the same
52
+ * way as a plain object returns its real getter's real `AttrRef`, wired to
53
+ * the real shared parent — see fold-import.ts's module doc for why that
54
+ * shared identity is the entire hard part of #1020.
55
+ */
56
+
57
+ /**
58
+ * The result of folding an AST node: a plain value, a symbolic reference to
59
+ * a sibling resource's attribute, a folded intrinsic tagged template, an
60
+ * unresolved external symbol chain, or a folded resource spec.
61
+ */
62
+ export type FoldedValue =
63
+ | string
64
+ | number
65
+ | boolean
66
+ | null
67
+ | undefined
68
+ | FoldedValue[]
69
+ | { [key: string]: FoldedValue }
70
+ | AttrRefValue
71
+ | FoldedIntrinsic
72
+ | SymbolicValue
73
+ | FoldedResource;
74
+
75
+ /**
76
+ * Symbolic reference produced when a property/element access resolves to an
77
+ * attribute of another `const`-declared resource in the same file, e.g.
78
+ * `bucket.name` (or `bucket["name"]`) where `bucket` is
79
+ * `const bucket = new S3Bucket({...})`.
80
+ *
81
+ * This is the SAME envelope `AttrRef.prototype.toJSON()` produces at
82
+ * runtime ({@link "../attrref"}) — `serializer-walker.ts`'s `walkValue`
83
+ * already recognizes a plain `{ __attrRef }` object as an AttrRef envelope
84
+ * (it doesn't require a live `AttrRef` instance), so this is not an
85
+ * invented shape: it's the existing envelope, produced without running the
86
+ * module that would otherwise construct the real `AttrRef`.
87
+ */
88
+ export interface AttrRefValue {
89
+ __attrRef: { entity: string; attribute: string };
90
+ }
91
+
92
+ /**
93
+ * The result of folding a registered lexicon intrinsic tagged template
94
+ * (e.g. `Sub\`${AWS.StackName}-x\``) to its node form: tag name, cooked
95
+ * template string parts, and folded interpolated values (in order).
96
+ * Mirrors the runtime call shape `Tag(strings, ...values)` so a later
97
+ * build path can replay it into the real intrinsic object (#1022).
98
+ */
99
+ export interface FoldedIntrinsic {
100
+ __intrinsic: string;
101
+ strings: string[];
102
+ values: FoldedValue[];
103
+ }
104
+
105
+ /**
106
+ * A sub-expression inside a folded intrinsic that fold could not reduce to
107
+ * a value without resolving an identifier from outside this file — e.g. an
108
+ * imported pseudo-parameter namespace access like `AWS.StackName`.
109
+ * Cross-file import resolution is #1020; until then the raw source text is
110
+ * preserved verbatim (never stringified, never rejected) instead of being
111
+ * treated as an unresolved-identifier error. Only appears inside a folded
112
+ * intrinsic's `values` — see {@link foldIntrinsicValue}.
113
+ */
114
+ export interface SymbolicValue {
115
+ __symbol: string;
116
+ }
117
+
118
+ /**
119
+ * The result of folding a resource constructor: `new Type({ ...props })`.
120
+ */
121
+ export interface FoldedResource {
122
+ __resource: string;
123
+ props: { [key: string]: FoldedValue };
124
+ /**
125
+ * The constructor's optional second argument — CFN-style resource-level
126
+ * attributes (`DependsOn`, `Condition`, `DeletionPolicy`,
127
+ * `UpdateReplacePolicy`, `CreationPolicy`, `Metadata`, …) some lexicons
128
+ * accept alongside `props` (see `createResource`'s `attributes` param,
129
+ * ../runtime.ts). Present only when the source actually passed one.
130
+ */
131
+ attributes?: { [key: string]: FoldedValue };
132
+ }
133
+
134
+ /**
135
+ * One entry per exported `const` resource declaration in {@link foldModule}'s
136
+ * result. The `ok: false` case surfaces the same located, rule-id-tagged
137
+ * shape as {@link FoldError} (#1024) — `error` stays the formatted message
138
+ * string for backward-compat display, while `ruleId`/`line`/`column` let a
139
+ * caller cite the exact same rule id + position an EVL diagnostic for the
140
+ * same construct would.
141
+ */
142
+ export type FoldModuleEntry =
143
+ | { ok: true; spec: FoldedResource }
144
+ | { ok: false; error: string; ruleId: SubsetRuleId; line: number; column: number };
145
+
146
+ /**
147
+ * Error thrown when a node cannot be folded to a value without executing
148
+ * code. Carries the node's source position (1-based, matching `LintError`)
149
+ * so callers can report a located diagnostic, and the id of the EVL rule
150
+ * that flags the same construct (#1024) — "EVL001" (the general
151
+ * not-statically-evaluable umbrella) unless the rejection is specifically a
152
+ * dynamic element-access key, which is "EVL003"'s construct. A rejection
153
+ * with no EVL equivalent (unresolved identifier, unregistered intrinsic tag,
154
+ * spread of a value that turns out not to be an object/array — all
155
+ * environment/value-dependent, see {@link "./subset"}'s module doc) still
156
+ * defaults to "EVL001" since that's the closest umbrella rule, even though
157
+ * EVL can't actually detect it ahead of a real fold.
158
+ *
159
+ * chant #1020 hang fix — every `FoldError` is thrown for a routine, EXPECTED
160
+ * outcome (this node's shape isn't in the fold subset) and is ALWAYS caught
161
+ * a few frames up (`tryFoldFileCore`'s own top-level catch, ultimately),
162
+ * reduced to `.message`; `.stack` is never read anywhere on this path. V8
163
+ * still eagerly walks live JS frames to populate the (lazy) `.stack` getter's
164
+ * backing data at CONSTRUCTION time regardless of whether it's ever read —
165
+ * cheap for a shallow call stack, but expensive once the surrounding
166
+ * functions are hot enough for V8 to aggressively inline them (every corpus
167
+ * entry re-triggers the same call shapes across `foldFileMemoized` ->
168
+ * `buildExternals` -> `tryFoldFileCore` -> `resolveDeclaratorValue` ->
169
+ * `resolveLiveValue` -> `resolveCallExpression`/`fold`, chant #1020's
170
+ * cross-file resolution making that chain several layers deeper than the
171
+ * pre-#1020 single-file fold ever needed): capturing a stack from deep,
172
+ * optimized/inlined frames requires V8 to reconstruct them from deopt
173
+ * metadata, confirmed via `sample` to dominate CPU during the observed
174
+ * multi-minute stall (`Isolate::CaptureAndSetErrorStack` /
175
+ * `OptimizedJSFrame::Summarize` / `DeoptTranslationIterator`). Most files in
176
+ * the corpus (77/98 entries have at least one run-fallback file) throw one
177
+ * of these, so the cost compounds across a build. `Error.stackTraceLimit = 0`
178
+ * for the duration of `super()` makes V8 capture zero frames — free
179
+ * regardless of stack depth/optimization state — then the limit is restored
180
+ * immediately, so it doesn't suppress a real stack trace anywhere else in
181
+ * the process.
182
+ */
183
+ export class FoldError extends Error {
184
+ readonly line: number;
185
+ readonly column: number;
186
+ readonly ruleId: SubsetRuleId;
187
+
188
+ constructor(message: string, line: number, column: number, ruleId: SubsetRuleId = "EVL001") {
189
+ const prevStackTraceLimit = Error.stackTraceLimit;
190
+ Error.stackTraceLimit = 0;
191
+ super(`${line}:${column} - ${message}`);
192
+ Error.stackTraceLimit = prevStackTraceLimit;
193
+ this.name = "FoldError";
194
+ this.line = line;
195
+ this.column = column;
196
+ this.ruleId = ruleId;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Resolve a node's 1-based line/column via its owning `SourceFile`. Exported
202
+ * (chant #1020) so fold-import.ts can build its own located `FoldError`s
203
+ * (e.g. an import-cycle diagnostic pointing at the specific `import`
204
+ * statement that closes the cycle) using the exact same position math
205
+ * `foldError` uses here, rather than a second hand-rolled implementation.
206
+ */
207
+ export function locate(node: ts.Node): { line: number; column: number } {
208
+ const sourceFile = node.getSourceFile();
209
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
210
+ return { line: line + 1, column: character + 1 };
211
+ }
212
+
213
+ function foldError(node: ts.Node, message: string, ruleId: SubsetRuleId = "EVL001"): FoldError {
214
+ const { line, column } = locate(node);
215
+ return new FoldError(message, line, column, ruleId);
216
+ }
217
+
218
+ /**
219
+ * Collect every top-level `const x = <initializer>` in a source file into a
220
+ * name -> initializer map. Single-file only (cross-file is #1020).
221
+ */
222
+ export function collectConsts(sourceFile: ts.SourceFile): Map<string, ts.Expression> {
223
+ const consts = new Map<string, ts.Expression>();
224
+ for (const statement of sourceFile.statements) {
225
+ if (!ts.isVariableStatement(statement)) continue;
226
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
227
+ for (const decl of statement.declarationList.declarations) {
228
+ if (ts.isIdentifier(decl.name) && decl.initializer) {
229
+ consts.set(decl.name.text, decl.initializer);
230
+ }
231
+ }
232
+ }
233
+ return consts;
234
+ }
235
+
236
+ /** A property/element key foldable without execution: identifier, string, or numeric literal. */
237
+ function propName(node: ts.PropertyName): string {
238
+ if (isLiteralPropertyName(node)) return node.text;
239
+ throw foldError(node, computedPropertyNameMessage(node));
240
+ }
241
+
242
+ /**
243
+ * An element-access key foldable without execution: a string or numeric
244
+ * LITERAL only (EVL003 semantics — a variable or expression key is a
245
+ * dynamic key and is rejected).
246
+ */
247
+ function elementKey(node: ts.Expression): string {
248
+ if (isLiteralElementKey(node)) return node.text;
249
+ throw foldError(node, dynamicElementAccessMessage(node), "EVL003");
250
+ }
251
+
252
+ /** True when `ident` is a `const` bound to a `new Type(...)` resource constructor. */
253
+ function resolvesToResource(consts: Map<string, ts.Expression>, ident: ts.Identifier): boolean {
254
+ const init = consts.get(ident.text);
255
+ return init !== undefined && ts.isNewExpression(init);
256
+ }
257
+
258
+ /**
259
+ * True when `node` is an identifier, or a dotted/bracketed access chain
260
+ * rooted at an identifier, that isn't bound in `consts` — e.g.
261
+ * `AWS.StackName` from an imported pseudo-parameter namespace, or a bare
262
+ * imported identifier. Resolving what it actually refers to requires
263
+ * following an import (#1020), which is out of scope here.
264
+ */
265
+ function isUnresolvedSymbolChain(node: ts.Expression, consts: Map<string, ts.Expression>): boolean {
266
+ if (ts.isIdentifier(node)) return node.text !== "undefined" && !consts.has(node.text);
267
+ if (ts.isPropertyAccessExpression(node)) return isUnresolvedSymbolChain(node.expression, consts);
268
+ if (ts.isElementAccessExpression(node)) return isUnresolvedSymbolChain(node.expression, consts);
269
+ if (ts.isNonNullExpression(node)) return isUnresolvedSymbolChain(node.expression, consts);
270
+ return false;
271
+ }
272
+
273
+ /**
274
+ * Fold one interpolated sub-expression of a registered intrinsic tagged
275
+ * template. Identical to {@link fold}, except an unresolved external
276
+ * symbol chain (a pseudo-parameter-style access this file can't see the
277
+ * import for) folds to a {@link SymbolicValue} instead of throwing — the
278
+ * run path resolves it once the module actually imports and runs; fold
279
+ * preserves it symbolically rather than stringifying or rejecting it.
280
+ */
281
+ function foldIntrinsicValue(
282
+ node: ts.Expression,
283
+ consts: Map<string, ts.Expression>,
284
+ intrinsics: readonly IntrinsicDef[],
285
+ externals?: ReadonlyMap<string, unknown>,
286
+ ): FoldedValue {
287
+ // Deliberately checks `consts` only, not `externals` — an imported
288
+ // identifier here still folds to a symbolic `{__symbol}` node exactly as
289
+ // before #1020, revived later by fold-import.ts's own
290
+ // `resolveSymbolicValue` (a real, trusted import of the defining module —
291
+ // fine for a plain pseudo-parameter-style namespace, the only shape this
292
+ // path exists for). Wiring `externals` in here too would mean two
293
+ // different mechanisms resolving the same cross-file name inside an
294
+ // intrinsic interpolation; out of scope for #1020, which only needs
295
+ // identifiers/property access OUTSIDE tagged templates to resolve.
296
+ if (isUnresolvedSymbolChain(node, consts)) {
297
+ return { __symbol: node.getText() };
298
+ }
299
+ return fold(node, consts, intrinsics, externals);
300
+ }
301
+
302
+ /**
303
+ * Fold a `TaggedTemplateExpression` whose tag is a registered, foldable
304
+ * lexicon intrinsic ({@link intrinsicFolds}, `../lexicon.ts`) to its node
305
+ * form. An unregistered — or registered-but-not-foldable — tag throws a
306
+ * located {@link FoldError}.
307
+ */
308
+ function foldTaggedTemplate(
309
+ node: ts.TaggedTemplateExpression,
310
+ consts: Map<string, ts.Expression>,
311
+ intrinsics: readonly IntrinsicDef[],
312
+ externals?: ReadonlyMap<string, unknown>,
313
+ ): FoldedIntrinsic {
314
+ const tagName = node.tag.getText();
315
+ const isRegistered = intrinsics.some((i) => i.name === tagName && intrinsicFolds(i));
316
+ if (!isRegistered) {
317
+ throw foldError(node, `unregistered tagged template intrinsic: ${tagName}\`...\``);
318
+ }
319
+
320
+ const template = node.template;
321
+ if (ts.isNoSubstitutionTemplateLiteral(template)) {
322
+ return { __intrinsic: tagName, strings: [template.text], values: [] };
323
+ }
324
+
325
+ const strings = [template.head.text, ...template.templateSpans.map((span) => span.literal.text)];
326
+ const values = template.templateSpans.map((span) =>
327
+ foldIntrinsicValue(span.expression, consts, intrinsics, externals),
328
+ );
329
+ return { __intrinsic: tagName, strings, values };
330
+ }
331
+
332
+ /**
333
+ * Fold a single expression node to a value. Throws {@link FoldError} for
334
+ * anything outside the supported subset — including any `CallExpression`
335
+ * that isn't a registered intrinsic tagged template.
336
+ *
337
+ * @param intrinsics - Lexicon-registered intrinsics that fold
338
+ * ({@link intrinsicFolds}, e.g. `Sub`). A tagged template whose tag isn't
339
+ * in this list, or is registered but not foldable, is rejected. Defaults
340
+ * to none — pass the target lexicon's manifest `intrinsics` to recognize
341
+ * its tags.
342
+ * @param externals - chant #1020: pre-resolved imported bindings, consulted
343
+ * only when an identifier isn't in `consts`. See the module doc above.
344
+ * `undefined` (the default) preserves the exact pre-#1020 single-file
345
+ * behavior — every identifier not in `consts` is unresolved.
346
+ */
347
+ export function fold(
348
+ node: ts.Expression,
349
+ consts: Map<string, ts.Expression>,
350
+ intrinsics: readonly IntrinsicDef[] = [],
351
+ externals?: ReadonlyMap<string, unknown>,
352
+ ): FoldedValue {
353
+ if (
354
+ ts.isParenthesizedExpression(node) ||
355
+ ts.isAsExpression(node) ||
356
+ ts.isSatisfiesExpression(node) ||
357
+ ts.isNonNullExpression(node)
358
+ ) {
359
+ return fold(node.expression, consts, intrinsics, externals);
360
+ }
361
+
362
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
363
+ return node.text;
364
+ }
365
+
366
+ if (ts.isNumericLiteral(node)) {
367
+ return Number(node.text);
368
+ }
369
+
370
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
371
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
372
+ if (node.kind === ts.SyntaxKind.NullKeyword) return null;
373
+
374
+ if (ts.isIdentifier(node) && node.text === "undefined") return undefined;
375
+
376
+ if (ts.isTaggedTemplateExpression(node)) {
377
+ return foldTaggedTemplate(node, consts, intrinsics, externals);
378
+ }
379
+
380
+ if (ts.isTemplateExpression(node)) {
381
+ let out = node.head.text;
382
+ for (const span of node.templateSpans) {
383
+ out += String(fold(span.expression, consts, intrinsics, externals)) + span.literal.text;
384
+ }
385
+ return out;
386
+ }
387
+
388
+ if (ts.isObjectLiteralExpression(node)) {
389
+ const obj: { [key: string]: FoldedValue } = {};
390
+ for (const prop of node.properties) {
391
+ if (ts.isPropertyAssignment(prop)) {
392
+ obj[propName(prop.name)] = fold(prop.initializer, consts, intrinsics, externals);
393
+ } else if (ts.isShorthandPropertyAssignment(prop)) {
394
+ obj[prop.name.text] = fold(prop.name, consts, intrinsics, externals);
395
+ } else if (ts.isSpreadAssignment(prop)) {
396
+ const src = fold(prop.expression, consts, intrinsics, externals);
397
+ if (src === null || typeof src !== "object") {
398
+ throw foldError(prop, "spread source not an object");
399
+ }
400
+ Object.assign(obj, src);
401
+ } else {
402
+ throw foldError(prop, UNSUPPORTED_OBJECT_MEMBER_MESSAGE);
403
+ }
404
+ }
405
+ return obj;
406
+ }
407
+
408
+ if (ts.isArrayLiteralExpression(node)) {
409
+ const arr: FoldedValue[] = [];
410
+ for (const el of node.elements) {
411
+ if (ts.isSpreadElement(el)) {
412
+ const src = fold(el.expression, consts, intrinsics, externals);
413
+ if (!Array.isArray(src)) {
414
+ throw foldError(el, "spread source not an array");
415
+ }
416
+ arr.push(...src);
417
+ } else {
418
+ arr.push(fold(el, consts, intrinsics, externals));
419
+ }
420
+ }
421
+ return arr;
422
+ }
423
+
424
+ if (ts.isIdentifier(node)) {
425
+ if (!consts.has(node.text)) {
426
+ // chant #1020 — an imported binding fold-import.ts already resolved
427
+ // (to a plain value, or the real live Declarable/CompositeInstance a
428
+ // sibling file's own fold produced) is returned as-is: for a plain
429
+ // value this is exactly like resolving a local const; for a real live
430
+ // object, the property-access branches below just index it like any
431
+ // other object, which is what makes a real, correctly-identified
432
+ // `AttrRef` fall out of `network.vpc.VpcId` with zero special-casing
433
+ // here (see fold-import.ts's module doc).
434
+ if (externals?.has(node.text)) {
435
+ return externals.get(node.text) as FoldedValue;
436
+ }
437
+ // chant #1064 — a bare `process` reference is ALWAYS an ambient
438
+ // environment read (`process.env.X`, `process.argv`, …), never
439
+ // something a future resolution pass could fold: it's Node's global,
440
+ // not a local const or an importable module export. Point at the
441
+ // supported alternative instead of leaving the author to infer the fix
442
+ // from the generic "unresolved identifier" message — see
443
+ // ../build-params.ts/../params.ts.
444
+ if (node.text === "process") {
445
+ throw foldError(
446
+ node,
447
+ `ambient "process" read is not foldable — declare a build-time parameter instead ` +
448
+ `(chant.config.ts's buildParams + \`chant build --param name=value\`/\`--params-file\`) and reference ` +
449
+ `it via \`import { params } from "@intentius/chant/params"\`, rather than reading process.env directly`,
450
+ );
451
+ }
452
+ throw foldError(node, `unresolved identifier: ${node.text}`);
453
+ }
454
+ return fold(consts.get(node.text) as ts.Expression, consts, intrinsics, externals);
455
+ }
456
+
457
+ if (ts.isPropertyAccessExpression(node)) {
458
+ if (ts.isIdentifier(node.expression) && resolvesToResource(consts, node.expression)) {
459
+ return { __attrRef: { entity: node.expression.text, attribute: node.name.text } };
460
+ }
461
+ const obj = fold(node.expression, consts, intrinsics, externals);
462
+ if (obj === null || obj === undefined) return undefined;
463
+ return (obj as { [key: string]: FoldedValue })[node.name.text];
464
+ }
465
+
466
+ if (ts.isElementAccessExpression(node)) {
467
+ const key = elementKey(node.argumentExpression);
468
+ if (ts.isIdentifier(node.expression) && resolvesToResource(consts, node.expression)) {
469
+ return { __attrRef: { entity: node.expression.text, attribute: key } };
470
+ }
471
+ const obj = fold(node.expression, consts, intrinsics, externals);
472
+ if (obj === null || obj === undefined) return undefined;
473
+ return (obj as { [key: string]: FoldedValue })[key];
474
+ }
475
+
476
+ if (ts.isPrefixUnaryExpression(node)) {
477
+ if (!SUPPORTED_UNARY_OPERATORS.has(node.operator)) {
478
+ throw foldError(node, UNSUPPORTED_UNARY_MESSAGE);
479
+ }
480
+ const value = fold(node.operand, consts, intrinsics, externals);
481
+ if (node.operator === ts.SyntaxKind.ExclamationToken) return !value;
482
+ return -(value as unknown as number); // ts.SyntaxKind.MinusToken — the only other supported operator
483
+ }
484
+
485
+ if (ts.isBinaryExpression(node)) {
486
+ const opKind = node.operatorToken.kind;
487
+ const S = ts.SyntaxKind;
488
+
489
+ if (opKind === S.AmpersandAmpersandToken) {
490
+ const left = fold(node.left, consts, intrinsics, externals);
491
+ return left ? fold(node.right, consts, intrinsics, externals) : left;
492
+ }
493
+ if (opKind === S.BarBarToken) {
494
+ const left = fold(node.left, consts, intrinsics, externals);
495
+ return left ? left : fold(node.right, consts, intrinsics, externals);
496
+ }
497
+ if (opKind === S.QuestionQuestionToken) {
498
+ const left = fold(node.left, consts, intrinsics, externals);
499
+ return left === null || left === undefined ? fold(node.right, consts, intrinsics, externals) : left;
500
+ }
501
+
502
+ if (!SUPPORTED_BINARY_OPERATORS.has(opKind)) {
503
+ throw foldError(node, unsupportedBinaryMessage(opKind));
504
+ }
505
+
506
+ const left = fold(node.left, consts, intrinsics, externals);
507
+ const right = fold(node.right, consts, intrinsics, externals);
508
+ switch (opKind) {
509
+ case S.PlusToken:
510
+ return (left as unknown as string) + (right as unknown as string);
511
+ case S.MinusToken:
512
+ return (left as unknown as number) - (right as unknown as number);
513
+ case S.AsteriskToken:
514
+ return (left as unknown as number) * (right as unknown as number);
515
+ case S.SlashToken:
516
+ return (left as unknown as number) / (right as unknown as number);
517
+ case S.EqualsEqualsEqualsToken:
518
+ return left === right;
519
+ case S.ExclamationEqualsEqualsToken:
520
+ return left !== right;
521
+ case S.GreaterThanToken:
522
+ return (left as unknown as string) > (right as unknown as string);
523
+ case S.LessThanToken:
524
+ return (left as unknown as string) < (right as unknown as string);
525
+ case S.GreaterThanEqualsToken:
526
+ return (left as unknown as string) >= (right as unknown as string);
527
+ case S.LessThanEqualsToken:
528
+ return (left as unknown as string) <= (right as unknown as string);
529
+ default:
530
+ // Unreachable given the SUPPORTED_BINARY_OPERATORS guard above —
531
+ // kept as a defensive fallback in case that set and this switch
532
+ // ever fall out of sync.
533
+ throw foldError(node, unsupportedBinaryMessage(opKind));
534
+ }
535
+ }
536
+
537
+ if (ts.isConditionalExpression(node)) {
538
+ return fold(node.condition, consts, intrinsics, externals)
539
+ ? fold(node.whenTrue, consts, intrinsics, externals)
540
+ : fold(node.whenFalse, consts, intrinsics, externals);
541
+ }
542
+
543
+ if (ts.isNewExpression(node)) {
544
+ // A nested `new Type({...})` used as a property VALUE is not leaf-foldable.
545
+ // fold can only produce the {__resource, props} envelope, and — unlike a
546
+ // TOP-LEVEL resource, which fold-import constructs into a real Declarable —
547
+ // a nested one is never constructed, so the envelope leaks into serialization
548
+ // as the wrong value (real fold-vs-run drift; the #1025 differential caught
549
+ // this on gitlab/multi-stage-deploy, where `new Image({...})` as a job's
550
+ // `image:` must serialize as `{ name }`, not `{ __resource, props }`).
551
+ // Reject so the file falls back to run, which constructs and serializes it
552
+ // correctly. EVL permits this statically — it's a documented fold/EVL
553
+ // divergence, like identifier resolution and spread runtime type.
554
+ throw foldError(node, `nested \`new ${node.expression.getText()}(...)\` as a value is not foldable — falls back to run`);
555
+ }
556
+
557
+ if (ts.isCallExpression(node)) {
558
+ throw foldError(node, callExpressionMessage(node));
559
+ }
560
+
561
+ throw foldError(node, unsupportedExpressionMessage(node));
562
+ }
563
+
564
+ /**
565
+ * Fold a resource constructor call — `new Type({ ...props }, { ...attributes
566
+ * })` — to its spec. Each argument present must be an object literal
567
+ * (anything else is not statically evaluable and throws a located
568
+ * {@link FoldError}). The second argument (CFN-style resource attributes —
569
+ * `DependsOn`, `Condition`, `DeletionPolicy`, …) is optional, matching
570
+ * `createResource`'s runtime constructor signature (../runtime.ts).
571
+ */
572
+ export function foldResource(
573
+ node: ts.NewExpression,
574
+ consts: Map<string, ts.Expression>,
575
+ intrinsics: readonly IntrinsicDef[] = [],
576
+ externals?: ReadonlyMap<string, unknown>,
577
+ ): FoldedResource {
578
+ const typeName = node.expression.getText();
579
+ const [firstArg, secondArg] = node.arguments ?? [];
580
+
581
+ if (!firstArg) {
582
+ return { __resource: typeName, props: {} };
583
+ }
584
+ if (!ts.isObjectLiteralExpression(firstArg)) {
585
+ throw foldError(firstArg, resourceCtorArgMessage(typeName));
586
+ }
587
+ const props = fold(firstArg, consts, intrinsics, externals) as { [key: string]: FoldedValue };
588
+
589
+ if (!secondArg) {
590
+ return { __resource: typeName, props };
591
+ }
592
+ if (!ts.isObjectLiteralExpression(secondArg)) {
593
+ throw foldError(secondArg, `resource attributes argument must be an object literal: ${typeName}(...)`);
594
+ }
595
+ const attributes = fold(secondArg, consts, intrinsics, externals) as { [key: string]: FoldedValue };
596
+ return { __resource: typeName, props, attributes };
597
+ }
598
+
599
+ function hasExportModifier(statement: ts.VariableStatement): boolean {
600
+ return statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
601
+ }
602
+
603
+ /**
604
+ * Fold every exported `const X = new Type({...})` resource declaration in a
605
+ * source file to its spec, with no module execution.
606
+ *
607
+ * Non-resource `const` exports (anything whose initializer isn't a `new`
608
+ * expression) are left out of the result rather than folded or errored —
609
+ * `new`-less resource forms (composite factory calls) are epic Phase 5
610
+ * (#1023) and are not attempted here.
611
+ *
612
+ * @param intrinsics - Lexicon-registered intrinsic tags, forwarded to
613
+ * {@link fold} for every resource. See {@link fold}'s `intrinsics` param.
614
+ */
615
+ export function foldModule(
616
+ source: string,
617
+ fileName = "module.ts",
618
+ intrinsics: readonly IntrinsicDef[] = [],
619
+ ): Record<string, FoldModuleEntry> {
620
+ const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
621
+ const consts = collectConsts(sourceFile);
622
+ const result: Record<string, FoldModuleEntry> = {};
623
+
624
+ for (const statement of sourceFile.statements) {
625
+ if (!ts.isVariableStatement(statement)) continue;
626
+ if (!hasExportModifier(statement)) continue;
627
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
628
+
629
+ for (const decl of statement.declarationList.declarations) {
630
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
631
+ if (!ts.isNewExpression(decl.initializer)) continue;
632
+
633
+ const name = decl.name.text;
634
+ try {
635
+ const spec = foldResource(decl.initializer, consts, intrinsics);
636
+ result[name] = { ok: true, spec };
637
+ } catch (err) {
638
+ if (err instanceof FoldError) {
639
+ result[name] = { ok: false, error: err.message, ruleId: err.ruleId, line: err.line, column: err.column };
640
+ } else {
641
+ throw err;
642
+ }
643
+ }
644
+ }
645
+ }
646
+
647
+ return result;
648
+ }