@intentius/chant 0.44.14 → 0.45.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 (170) hide show
  1. package/dist/audit/discover.d.ts +26 -2
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-model.d.ts +10 -0
  4. package/dist/audit/report-model.d.ts.map +1 -1
  5. package/dist/audit/rules-doc.d.ts.map +1 -1
  6. package/dist/cli/build-params-cli.d.ts +16 -7
  7. package/dist/cli/build-params-cli.d.ts.map +1 -1
  8. package/dist/cli/commands/audit.d.ts +20 -0
  9. package/dist/cli/commands/audit.d.ts.map +1 -1
  10. package/dist/cli/commands/build.d.ts +9 -0
  11. package/dist/cli/commands/build.d.ts.map +1 -1
  12. package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
  13. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
  15. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  18. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
  19. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  20. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  21. package/dist/cli/commands/onboard.d.ts +28 -0
  22. package/dist/cli/commands/onboard.d.ts.map +1 -1
  23. package/dist/cli/handlers/build.d.ts.map +1 -1
  24. package/dist/cli/handlers/graph.d.ts.map +1 -1
  25. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  26. package/dist/cli/handlers/run.d.ts.map +1 -1
  27. package/dist/cli/handlers/search.d.ts +27 -0
  28. package/dist/cli/handlers/search.d.ts.map +1 -1
  29. package/dist/cli/main.d.ts.map +1 -1
  30. package/dist/codegen/docs-pages.d.ts +39 -0
  31. package/dist/codegen/docs-pages.d.ts.map +1 -0
  32. package/dist/codegen/docs-sections.d.ts.map +1 -1
  33. package/dist/codegen/docs-sidebar.d.ts +13 -2
  34. package/dist/codegen/docs-sidebar.d.ts.map +1 -1
  35. package/dist/codegen/docs-types.d.ts +30 -10
  36. package/dist/codegen/docs-types.d.ts.map +1 -1
  37. package/dist/codegen/docs.d.ts +14 -2
  38. package/dist/codegen/docs.d.ts.map +1 -1
  39. package/dist/codegen/surface-snapshot.d.ts +27 -3
  40. package/dist/codegen/surface-snapshot.d.ts.map +1 -1
  41. package/dist/codegen/validate.d.ts +11 -3
  42. package/dist/codegen/validate.d.ts.map +1 -1
  43. package/dist/components/capability-plugin.d.ts +13 -2
  44. package/dist/components/capability-plugin.d.ts.map +1 -1
  45. package/dist/components/driver.d.ts +38 -0
  46. package/dist/components/driver.d.ts.map +1 -1
  47. package/dist/components/index.d.ts +1 -1
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/components/starter-plugin.d.ts.map +1 -1
  50. package/dist/config.d.ts +57 -5
  51. package/dist/config.d.ts.map +1 -1
  52. package/dist/discovery/fold-import.d.ts.map +1 -1
  53. package/dist/fold/fold.d.ts +100 -1
  54. package/dist/fold/fold.d.ts.map +1 -1
  55. package/dist/graph-ir.d.ts +12 -0
  56. package/dist/graph-ir.d.ts.map +1 -1
  57. package/dist/graph-ops.d.ts +17 -0
  58. package/dist/graph-ops.d.ts.map +1 -0
  59. package/dist/lifecycle/change-set.d.ts +23 -2
  60. package/dist/lifecycle/change-set.d.ts.map +1 -1
  61. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  62. package/dist/lifecycle/observe.d.ts +8 -0
  63. package/dist/lifecycle/observe.d.ts.map +1 -1
  64. package/dist/lifecycle/replay.d.ts.map +1 -1
  65. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  66. package/dist/lifecycle/types.d.ts +7 -0
  67. package/dist/lifecycle/types.d.ts.map +1 -1
  68. package/dist/lint/policy.d.ts.map +1 -1
  69. package/dist/managed-fields.d.ts +33 -21
  70. package/dist/managed-fields.d.ts.map +1 -1
  71. package/dist/observation.d.ts +23 -1
  72. package/dist/observation.d.ts.map +1 -1
  73. package/dist/yaml.d.ts +0 -8
  74. package/dist/yaml.d.ts.map +1 -1
  75. package/package.json +1 -1
  76. package/src/__snapshots__/okf.test.ts.snap +20 -0
  77. package/src/audit/discover.test.ts +55 -1
  78. package/src/audit/discover.ts +70 -2
  79. package/src/audit/edge-init-safety.test.ts +149 -18
  80. package/src/audit/report-model.ts +12 -1
  81. package/src/audit/rules-doc.ts +1 -0
  82. package/src/build-params.test.ts +52 -0
  83. package/src/cli/build-params-cli.test.ts +25 -8
  84. package/src/cli/build-params-cli.ts +37 -23
  85. package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
  86. package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
  87. package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
  88. package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
  89. package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
  90. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
  91. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
  92. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
  93. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
  94. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
  95. package/src/cli/commands/audit.test.ts +83 -1
  96. package/src/cli/commands/audit.ts +121 -28
  97. package/src/cli/commands/build.test.ts +144 -5
  98. package/src/cli/commands/build.ts +66 -14
  99. package/src/cli/commands/check-lexicon-docs.ts +30 -0
  100. package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
  101. package/src/cli/commands/check-lexicon-examples.ts +66 -17
  102. package/src/cli/commands/check-lexicon.test.ts +1 -1
  103. package/src/cli/commands/check-lexicon.ts +19 -4
  104. package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
  105. package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
  106. package/src/cli/commands/init-lexicon.test.ts +42 -1
  107. package/src/cli/commands/init-lexicon.ts +3 -1
  108. package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
  109. package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
  110. package/src/cli/commands/lexicon-surface-diff.ts +1 -1
  111. package/src/cli/commands/onboard.test.ts +319 -225
  112. package/src/cli/commands/onboard.ts +144 -58
  113. package/src/cli/handlers/build.test.ts +3 -3
  114. package/src/cli/handlers/build.ts +2 -0
  115. package/src/cli/handlers/components.ts +2 -2
  116. package/src/cli/handlers/graph.test.ts +44 -0
  117. package/src/cli/handlers/graph.ts +22 -2
  118. package/src/cli/handlers/lifecycle.test.ts +117 -0
  119. package/src/cli/handlers/lifecycle.ts +16 -0
  120. package/src/cli/handlers/run.test.ts +5 -5
  121. package/src/cli/handlers/run.ts +1 -0
  122. package/src/cli/handlers/search-live.test.ts +217 -0
  123. package/src/cli/handlers/search.test.ts +41 -1
  124. package/src/cli/handlers/search.ts +55 -6
  125. package/src/cli/main.ts +5 -3
  126. package/src/cli/security-boundary.test.ts +2 -1
  127. package/src/codegen/docs-pages.test.ts +194 -0
  128. package/src/codegen/docs-pages.ts +138 -0
  129. package/src/codegen/docs-sections.ts +0 -8
  130. package/src/codegen/docs-sidebar.ts +50 -37
  131. package/src/codegen/docs-types.ts +32 -4
  132. package/src/codegen/docs.ts +78 -40
  133. package/src/codegen/lexicon-regen.ts +3 -1
  134. package/src/codegen/rolling-upgrade.test.ts +1 -1
  135. package/src/codegen/rolling-upgrade.ts +1 -1
  136. package/src/codegen/surface-snapshot.test.ts +139 -0
  137. package/src/codegen/surface-snapshot.ts +297 -122
  138. package/src/codegen/validate.test.ts +16 -3
  139. package/src/codegen/validate.ts +17 -7
  140. package/src/components/auto-release.ts +1 -1
  141. package/src/components/capability-plugin.ts +23 -2
  142. package/src/components/driver.test.ts +45 -0
  143. package/src/components/driver.ts +64 -27
  144. package/src/components/index.ts +2 -0
  145. package/src/components/starter-plugin.ts +5 -2
  146. package/src/config.ts +101 -7
  147. package/src/discovery/fold-import.test.ts +427 -2
  148. package/src/discovery/fold-import.ts +163 -7
  149. package/src/discovery/sandbox/driver.test.ts +11 -0
  150. package/src/fold/fold.test.ts +41 -0
  151. package/src/fold/fold.ts +372 -2
  152. package/src/graph-ir-live.test.ts +42 -0
  153. package/src/graph-ir.ts +42 -2
  154. package/src/graph-ops.test.ts +69 -0
  155. package/src/graph-ops.ts +42 -0
  156. package/src/lifecycle/change-set.test.ts +41 -0
  157. package/src/lifecycle/change-set.ts +30 -2
  158. package/src/lifecycle/deep-observe.ts +4 -0
  159. package/src/lifecycle/observe.ts +29 -2
  160. package/src/lifecycle/replay.ts +3 -0
  161. package/src/lifecycle/snapshot.ts +3 -0
  162. package/src/lifecycle/types.ts +7 -0
  163. package/src/lint/policy.ts +14 -3
  164. package/src/managed-fields.test.ts +15 -42
  165. package/src/managed-fields.ts +43 -26
  166. package/src/observation.test.ts +24 -8
  167. package/src/observation.ts +48 -4
  168. package/src/ownership.test.ts +63 -1
  169. package/src/yaml.test.ts +15 -0
  170. package/src/yaml.ts +19 -2
@@ -14,6 +14,8 @@ import {
14
14
  fold,
15
15
  propName,
16
16
  FoldError,
17
+ FoldableFunction,
18
+ isFoldableFunction,
17
19
  locate,
18
20
  type FoldedResource,
19
21
  type FoldedValue,
@@ -545,8 +547,15 @@ interface ReExportDeclarator {
545
547
  elements: Array<{ imported: string; exportedName: string }>;
546
548
  }
547
549
 
550
+ /** `export function name(...) {...}` (chant #1373) — exported as a {@link FoldableFunction} marker so an importer can fold a call to it. The body's own foldability is judged at the CALL, never here: an unfoldable helper nobody calls from foldable source costs its module nothing. */
551
+ interface FunctionDeclarator {
552
+ kind: "function";
553
+ name: string;
554
+ }
555
+
548
556
  type ScanDeclarator =
549
557
  | ResourceDeclarator
558
+ | FunctionDeclarator
550
559
  | SingleDeclarator
551
560
  | DestructureDeclarator
552
561
  | NamedExportDeclarator
@@ -563,8 +572,9 @@ interface ScanResult {
563
572
  * `export const X = <expr>` / `export const { a, b } = <expr>` (composite
564
573
  * calls and member access on them, #1023), `export { a, b }` (a local
565
574
  * named-export list), and `export { a, b } from "./other"` (a re-export
566
- * chain, #1020). Any OTHER export construct (`export default`, `export *
567
- * from`, an exported function/class, `let`/`var`, a destructured export with
575
+ * chain, #1020), and `export function name(...) {...}` (a foldable-function
576
+ * export, #1373). Any OTHER export construct (`export default`, `export *
577
+ * from`, an exported class, `let`/`var`, a destructured export with
568
578
  * a rest/nested/defaulted element) makes the whole file ineligible: the
569
579
  * module must run so that construct is actually evaluated. This mirrors the
570
580
  * epic's hybrid design — fallback is per-module, not per-declaration,
@@ -629,10 +639,17 @@ function scanExports(sourceFile: ts.SourceFile): ScanResult {
629
639
  }
630
640
 
631
641
  if (ts.isFunctionDeclaration(statement) && hasExportModifier(statement)) {
632
- return {
633
- declarators,
634
- unfoldableReason: `exported function declaration "${statement.name?.text ?? "<anonymous>"}" is not foldable`,
635
- };
642
+ // chant #1373 — a named, bodied exported function is a foldable-function
643
+ // export (see {@link FunctionDeclarator}). An overload signature has no
644
+ // body and no value of its own skipped, the implementation that
645
+ // follows it is the export. `export default function` has no name an
646
+ // importer could bind and stays a disqualifier, like `export default`.
647
+ if (statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) || !statement.name) {
648
+ return { declarators, unfoldableReason: "`export default` is not foldable" };
649
+ }
650
+ if (!statement.body) continue;
651
+ declarators.push({ kind: "function", name: statement.name.text });
652
+ continue;
636
653
  }
637
654
  if (ts.isClassDeclaration(statement) && hasExportModifier(statement)) {
638
655
  return {
@@ -727,6 +744,54 @@ function collectLocalBindings(sourceFile: ts.SourceFile): Map<string, LocalBindi
727
744
  return bindings;
728
745
  }
729
746
 
747
+ /** `(x) => …` / `function (x) {…}`, possibly wrapped in parens/`as`/`satisfies` — the initializer shapes that make a `const` a function binding. */
748
+ function unwrapFunctionInitializer(node: ts.Expression): ts.ArrowFunction | ts.FunctionExpression | undefined {
749
+ let current = node;
750
+ while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current)) {
751
+ current = current.expression;
752
+ }
753
+ return ts.isArrowFunction(current) || ts.isFunctionExpression(current) ? current : undefined;
754
+ }
755
+
756
+ /**
757
+ * chant #1373 — every top-level function a file declares, exported or not, as
758
+ * a {@link FoldableFunction}: `function f(...) {...}` and `const f = (...) =>
759
+ * …`. Placed in the file's own `externals` (so a same-file call folds, and a
760
+ * sibling function can call it) and exported under its name (so an importer's
761
+ * `buildExternals` can). The marker carries the file's scope BY REFERENCE:
762
+ * `ctx.externals` is still being filled (pre-built resources land in it after
763
+ * this runs) and a body reads it only when called.
764
+ *
765
+ * The marker's `consts` drops every `new`-bound const (transitively, through
766
+ * aliases) — see {@link FoldableFunction.consts} for why a body must reach a
767
+ * module-level resource through `externals` only.
768
+ */
769
+ function collectLocalFunctions(sourceFile: ts.SourceFile, ctx: ResolveCtx): Map<string, FoldableFunction> {
770
+ const consts = new Map(ctx.consts);
771
+ for (const [name] of [...consts]) {
772
+ if (constResolvesToResource(ctx.consts, name, new Set())) consts.delete(name);
773
+ }
774
+ const functions = new Map<string, FoldableFunction>();
775
+ const add = (name: string, fn: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression): void => {
776
+ functions.set(name, new FoldableFunction(name, fn, ctx.file, consts, ctx.externals, ctx.crossFileFailures));
777
+ };
778
+
779
+ for (const statement of sourceFile.statements) {
780
+ if (ts.isFunctionDeclaration(statement)) {
781
+ if (statement.name && statement.body) add(statement.name.text, statement);
782
+ continue;
783
+ }
784
+ if (!ts.isVariableStatement(statement)) continue;
785
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
786
+ for (const decl of statement.declarationList.declarations) {
787
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
788
+ const fn = unwrapFunctionInitializer(decl.initializer);
789
+ if (fn) add(decl.name.text, fn);
790
+ }
791
+ }
792
+ return functions;
793
+ }
794
+
730
795
  // ─────────────────────────────────────────────────────────────────────────
731
796
  // Import resolution — unchanged from #1022, shared by the resource and
732
797
  // composite-call resolution paths.
@@ -1208,7 +1273,14 @@ async function resolveLiveValue(node: ts.Expression, ctx: ResolveCtx): Promise<L
1208
1273
  // exports object). Absent from `externals` falls through to
1209
1274
  // `undefined`, exactly the pre-#1020 behavior for any unbound name.
1210
1275
  if (ctx.externals.has(node.text)) {
1211
- return { value: ctx.externals.get(node.text) };
1276
+ const external = ctx.externals.get(node.text);
1277
+ // chant #1373 — a function marker is callable, never a value. The
1278
+ // export-by-name cases (`export const g = f`, `export { f }`) are
1279
+ // handled by the declarator loop before it gets here.
1280
+ if (isFoldableFunction(external)) {
1281
+ throw cheapError(`function "${node.text}" used as a value is not foldable`);
1282
+ }
1283
+ return { value: external };
1212
1284
  }
1213
1285
  return undefined;
1214
1286
  }
@@ -1278,10 +1350,48 @@ async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx):
1278
1350
  }
1279
1351
  const calleeName = node.expression.text;
1280
1352
  const binding = ctx.imports.get(calleeName);
1353
+
1354
+ // chant #1373 — a project-local function with a foldable body (declared in
1355
+ // this file, or imported from a sibling project file) is evaluated
1356
+ // statically, through the same `fold()` branch a NESTED call to it takes.
1357
+ // When its body turns out not to fold, an IMPORTED callee still has the
1358
+ // pre-#1373 path below (interpret as a composite, else import and invoke) —
1359
+ // a helper that wraps a composite call, say, keeps folding by invocation
1360
+ // exactly as it did. A same-file callee has nothing to invoke without
1361
+ // running the file, so the fold diagnostic is the verdict.
1362
+ const local = ctx.externals.get(calleeName);
1363
+ if (isFoldableFunction(local)) {
1364
+ let foldFailure: Error | undefined;
1365
+ try {
1366
+ const folded = fold(node, ctx.consts, ctx.intrinsics, ctx.externals);
1367
+ return await reviveFoldedValue(folded, ctx, false);
1368
+ } catch (err) {
1369
+ if (!(err instanceof Error)) throw err;
1370
+ foldFailure = err;
1371
+ }
1372
+ if (!binding) throw foldFailure;
1373
+ try {
1374
+ return await resolveImportedCall(node, calleeName, binding, ctx);
1375
+ } catch (err) {
1376
+ throw cheapError(
1377
+ `${foldFailure.message}; invoking it instead failed: ${err instanceof Error ? err.message : String(err)}`,
1378
+ );
1379
+ }
1380
+ }
1381
+
1281
1382
  if (!binding) {
1282
1383
  throw cheapError(callExpressionMessage(node));
1283
1384
  }
1385
+ return resolveImportedCall(node, calleeName, binding, ctx);
1386
+ }
1284
1387
 
1388
+ /** The pre-#1373 two-arm resolution of an IMPORTED callee — see {@link resolveCallExpression}. */
1389
+ async function resolveImportedCall(
1390
+ node: ts.CallExpression,
1391
+ calleeName: string,
1392
+ binding: ImportBinding,
1393
+ ctx: ResolveCtx,
1394
+ ): Promise<unknown> {
1285
1395
  // chant #1023 — is this callee a composite whose body can be INTERPRETED
1286
1396
  // rather than run? Purely a static question (does the defining module parse,
1287
1397
  // does it declare this export as `Composite(<fn>, "<name>")`, is `<fn>`'s
@@ -3108,6 +3218,10 @@ async function buildExternals(
3108
3218
  * falls back. See {@link FoldFileResult.liveSources}.
3109
3219
  */
3110
3220
  function hasObjectIdentity(value: unknown): boolean {
3221
+ // chant #1373 — a foldable-function marker is a description of source, not
3222
+ // an object the run path could hold a different copy of: a call to it
3223
+ // produces the same value folded or run. No identity to disagree about.
3224
+ if (isFoldableFunction(value)) return false;
3111
3225
  return value !== null && (typeof value === "object" || typeof value === "function");
3112
3226
  }
3113
3227
 
@@ -3143,6 +3257,19 @@ async function resolveDeclaratorValue(node: ts.Expression, ctx: ResolveCtx): Pro
3143
3257
  * entry point below.
3144
3258
  */
3145
3259
  async function tryFoldFileCore(file: string, session: FoldSession): Promise<FoldFileResult> {
3260
+ // chant #1373 — chant-core's own modules are library code, not project
3261
+ // source: their exports are reached by importing them (a helper, a
3262
+ // constructor, `params`), never by folding their bodies. Before #1373 every
3263
+ // one of them happened to disqualify itself at the scan (each exports a
3264
+ // function); now that an exported function is a foldable shape, the
3265
+ // exclusion has to be explicit, or a fixture/in-repo import by path would
3266
+ // walk fold into chant's own tree and fold `params` to an empty object.
3267
+ {
3268
+ const root = chantCoreRoot();
3269
+ if (file === root || file.startsWith(root + sep)) {
3270
+ return { ok: false, reason: "chant's own module is not project source" };
3271
+ }
3272
+ }
3146
3273
  try {
3147
3274
  const source = await readFile(file, "utf-8");
3148
3275
  const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
@@ -3177,10 +3304,24 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
3177
3304
  // {@link preresolveResourceConsts}.
3178
3305
  const prebuiltResources = await preresolveResourceConsts(ctx);
3179
3306
 
3307
+ // chant #1373 — this file's own functions, callable from its own folds
3308
+ // (and, once exported, from an importer's). Registered after the
3309
+ // resource pre-pass so a marker never shadows a pre-built instance; a
3310
+ // function and a const cannot share a name in valid TypeScript anyway.
3311
+ const localFunctions = collectLocalFunctions(sourceFile, ctx);
3312
+ for (const [name, marker] of localFunctions) {
3313
+ if (!ctx.externals.has(name)) ctx.externals.set(name, marker);
3314
+ }
3315
+
3180
3316
  const entities: FoldedEntity[] = [];
3181
3317
  const exportedValues = new Map<string, unknown>();
3182
3318
 
3183
3319
  for (const decl of scan.declarators) {
3320
+ if (decl.kind === "function") {
3321
+ applyResolvedValue(decl.name, localFunctions.get(decl.name), entities, exportedValues);
3322
+ continue;
3323
+ }
3324
+
3184
3325
  if (decl.kind === "resource") {
3185
3326
  // chant #1169 — the pre-pass already built this exact node. Reuse that
3186
3327
  // instance rather than constructing a second one: a sibling prop that
@@ -3199,6 +3340,15 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
3199
3340
  }
3200
3341
 
3201
3342
  if (decl.kind === "single") {
3343
+ // chant #1373 — `export const f = (…) => …` exports the function
3344
+ // marker, exactly like `export function f`; so does an alias of one
3345
+ // (`export const g = f`, `f` declared here or imported).
3346
+ const aliased = ts.isIdentifier(decl.node) ? ctx.externals.get(decl.node.text) : undefined;
3347
+ const marker = localFunctions.get(decl.name) ?? (isFoldableFunction(aliased) ? aliased : undefined);
3348
+ if (marker) {
3349
+ applyResolvedValue(decl.name, marker, entities, exportedValues);
3350
+ continue;
3351
+ }
3202
3352
  let value: unknown;
3203
3353
  try {
3204
3354
  value = (await resolveDeclaratorValue(decl.node, ctx)).value;
@@ -3244,6 +3394,12 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
3244
3394
  // (ctx.locals/ctx.externals lookup + memoized destructured-member
3245
3395
  // extraction) rather than duplicating it here.
3246
3396
  for (const { localNameNode, exportedName } of decl.elements) {
3397
+ // chant #1373 — `function f() {…}; export { f }`.
3398
+ const marker = localFunctions.get(localNameNode.text);
3399
+ if (marker) {
3400
+ applyResolvedValue(exportedName, marker, entities, exportedValues);
3401
+ continue;
3402
+ }
3247
3403
  let value: unknown;
3248
3404
  try {
3249
3405
  value = (await resolveDeclaratorValue(localNameNode, ctx)).value;
@@ -27,6 +27,17 @@ describe("generateDriverSource — build-time parameters (#1108)", () => {
27
27
  expect(bindAt).toBeLessThan(firstImportAt);
28
28
  });
29
29
 
30
+ test("an unset optional parameter is absent from the snapshot, not carried as null (#1371)", () => {
31
+ // The snapshot crosses into the child as JSON. JSON has no `undefined`,
32
+ // so "unset" must be expressed by omitting the key — a `null` here would
33
+ // bind `params.<name>` to a value in the child while the parent's fold
34
+ // path reads `undefined`.
35
+ setBuildParams({ tier: "light" });
36
+ const source = generateDriverSource({ files: ["/tmp/project/a.ts"], buildRoot: "/tmp/project" });
37
+ expect(source).toContain(`setBuildParams(${JSON.stringify({ tier: "light" })});`);
38
+ expect(source).not.toContain("null");
39
+ });
40
+
30
41
  test("with no params resolved, binds an empty snapshot (explicit, not absent)", () => {
31
42
  const source = generateDriverSource({ files: ["/tmp/project/a.ts"], buildRoot: "/tmp/project" });
32
43
  expect(source).toContain("setBuildParams({});");
@@ -151,6 +151,47 @@ describe("fold — cross-resource attribute refs", () => {
151
151
  expect(foldResult).toEqual(runResult);
152
152
  expect(foldResult).toEqual({ __attrRef: { entity: "bucket", attribute: "arn" } });
153
153
  });
154
+
155
+ // chant #1535 — the const is not a bare `new` but a conditional that yields
156
+ // one. Indexing the folded `{__resource}` envelope by the attribute name
157
+ // returned `undefined`, so `Principal: { Federated: provider.Arn }` emitted
158
+ // `Principal: {}` with no diagnostic.
159
+ test("property access on a const bound to a conditional `new` becomes the same symbolic ref", () => {
160
+ const src = `
161
+ const flag = true;
162
+ const provider = flag ? new OIDCProvider({ Url: "https://x" }) : undefined;
163
+ const ref = provider.Arn;
164
+ const nested = { Principal: { Federated: provider ? provider.Arn : "fallback" } };
165
+ `;
166
+ expect(foldConst(src, "ref")).toEqual({ __attrRef: { entity: "provider", attribute: "Arn" } });
167
+ expect(foldConst(src, "nested")).toEqual({
168
+ Principal: { Federated: { __attrRef: { entity: "provider", attribute: "Arn" } } },
169
+ });
170
+ });
171
+
172
+ test("element access on a const bound to a conditional `new` becomes the same symbolic ref", () => {
173
+ const src = `
174
+ const provider = true ? new OIDCProvider({}) : undefined;
175
+ const ref = provider["Arn"];
176
+ `;
177
+ expect(foldConst(src, "ref")).toEqual({ __attrRef: { entity: "provider", attribute: "Arn" } });
178
+ });
179
+
180
+ test("the untaken branch still folds to undefined, as the run path would see it", () => {
181
+ const src = `
182
+ const provider = false ? new OIDCProvider({}) : undefined;
183
+ const ref = provider ? provider.Arn : "fallback";
184
+ `;
185
+ expect(foldConst(src, "ref")).toBe("fallback");
186
+ });
187
+
188
+ test("attribute read on an inline resource expression throws a located FoldError rather than folding to undefined", () => {
189
+ const src = `
190
+ const ref = (true ? new OIDCProvider({}) : undefined).Arn;
191
+ `;
192
+ expect(() => foldConst(src, "ref")).toThrow(FoldError);
193
+ expect(() => foldConst(src, "ref")).toThrow(/attribute "Arn" read on an inline resource expression is not foldable/);
194
+ });
154
195
  });
155
196
 
156
197
  describe("fold — element access", () => {