@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
@@ -197,7 +197,8 @@ describe("tryFoldFile", () => {
197
197
  file,
198
198
  `
199
199
  import { Bucket } from "./resources";
200
- function computeName(): string { return "dynamic"; }
200
+ import { hostname } from "node:os";
201
+ function computeName(): string { return hostname(); }
201
202
  export const bucket = new Bucket({ name: computeName() });
202
203
  `,
203
204
  );
@@ -206,7 +207,12 @@ describe("tryFoldFile", () => {
206
207
 
207
208
  expect(result.ok).toBe(false);
208
209
  if (result.ok) return;
209
- expect(result.reason).toContain("computeName");
210
+ // chant #1373 — a same-file function with a foldable body now folds
211
+ // (see the "project-local function calls" block below), so the
212
+ // unfoldable thing here is what the body DOES, and the reason names
213
+ // both the callee and that.
214
+ expect(result.reason).toContain('call to "computeName"');
215
+ expect(result.reason).toContain("hostname(...)");
210
216
  });
211
217
 
212
218
  test("falls back when the resource constructor is not a resolvable import", async () => {
@@ -1021,6 +1027,37 @@ describe("tryFoldFile — build-time parameters (chant #1064)", () => {
1021
1027
  expect(result.exportedValues.get("project")).toBe("loom");
1022
1028
  });
1023
1029
 
1030
+ test("an unset optional parameter folds to undefined, not null — defaults and truthiness behave (#1371)", async () => {
1031
+ const file = join(testDir, "main.ts");
1032
+ await writeFile(
1033
+ file,
1034
+ `
1035
+ import { params } from ${JSON.stringify(paramsPath)};
1036
+ export const region = "us-east-1";
1037
+ export const raw = params.baseImageArn;
1038
+ export const withDefault =
1039
+ (params.baseImageArn as string | undefined) ?? \`arn:aws:lambda:\${region}:aws:microvm-image:al2023-1\`;
1040
+ export const conditional = { ...(params.baseImageArn ? { baseImageArn: params.baseImageArn } : {}) };
1041
+ export const direct = { baseImageArn: params.baseImageArn };
1042
+ `,
1043
+ );
1044
+
1045
+ // Declared `required: false` with no value: the resolved map has no key at all.
1046
+ const session = createFoldSession([], {});
1047
+ const result = await tryFoldFile(file, [], session);
1048
+
1049
+ expect(result.ok).toBe(true);
1050
+ if (!result.ok) return;
1051
+ expect(result.exportedValues.get("raw")).toBeUndefined();
1052
+ expect(result.exportedValues.get("raw")).not.toBeNull();
1053
+ expect(result.exportedValues.get("withDefault")).toBe("arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1");
1054
+ expect(result.exportedValues.get("conditional")).toEqual({});
1055
+ // The key is present but undefined — what the serializers drop (see yaml.ts).
1056
+ const direct = result.exportedValues.get("direct") as { baseImageArn?: unknown };
1057
+ expect(direct.baseImageArn).toBeUndefined();
1058
+ expect(JSON.stringify(direct)).toBe("{}");
1059
+ });
1060
+
1024
1061
  test("an unrelated import also named `params` (not resolving to ../params.ts) is unaffected", async () => {
1025
1062
  await writeFile(
1026
1063
  join(testDir, "local-config.ts"),
@@ -2150,6 +2187,59 @@ describe("tryFoldFile — constructions as values (chant #1169)", () => {
2150
2187
  });
2151
2188
  });
2152
2189
 
2190
+ // chant #1535 — the referenced resource is declared through a conditional
2191
+ // (`flag ? new T(...) : undefined`), the kubemicrovm-ops shape that shipped
2192
+ // a role whose trust policy had `Principal: {}`. The attribute read used to
2193
+ // index the folded `{__resource}` envelope and come back `undefined`, which
2194
+ // the props walk then dropped without a diagnostic. It must now fold to the
2195
+ // same by-name envelope a bare `new` const produces, at any nesting depth.
2196
+ test("an attribute reference to a CONDITIONALLY declared same-file resource folds to the `{__attrRef}` envelope, even nested inside a document", async () => {
2197
+ await writeDefs();
2198
+ const file = join(testDir, "main.ts");
2199
+ await writeFile(
2200
+ file,
2201
+ `
2202
+ import { Job } from "./resources";
2203
+ const declared = true;
2204
+ export const first = declared ? new Job({ stage: "first" }) : undefined;
2205
+ const firstId = first ? first.Id : "fallback";
2206
+ export const second = new Job({
2207
+ upstream: first.Id,
2208
+ policy: { Statement: [{ Principal: { Federated: firstId } }] },
2209
+ });
2210
+ `,
2211
+ );
2212
+
2213
+ const result = await tryFoldFile(file);
2214
+
2215
+ expect(result.ok).toBe(true);
2216
+ if (!result.ok) return;
2217
+ expect(result.entities.map(([name]) => name)).toEqual(["first", "second"]);
2218
+ const props = propsOf(result.exportedValues.get("second"));
2219
+ expect(props.upstream).toEqual({ __attrRef: { entity: "first", attribute: "Id" } });
2220
+ expect(props.policy).toEqual({
2221
+ Statement: [{ Principal: { Federated: { __attrRef: { entity: "first", attribute: "Id" } } } }],
2222
+ });
2223
+ });
2224
+
2225
+ test("an attribute read on an inline resource expression falls the file back to run instead of folding to nothing", async () => {
2226
+ await writeDefs();
2227
+ const file = join(testDir, "main.ts");
2228
+ await writeFile(
2229
+ file,
2230
+ `
2231
+ import { Job } from "./resources";
2232
+ export const second = new Job({ upstream: (true ? new Job({}) : undefined).Id });
2233
+ `,
2234
+ );
2235
+
2236
+ const result = await tryFoldFile(file);
2237
+
2238
+ expect(result.ok).toBe(false);
2239
+ if (result.ok) return;
2240
+ expect(result.reason).toMatch(/attribute "Id" read on an inline resource expression is not foldable/);
2241
+ });
2242
+
2153
2243
  test("a nested construction whose class isn't a resolvable import falls the file back to run", async () => {
2154
2244
  await writeDefs();
2155
2245
  const file = join(testDir, "main.ts");
@@ -2294,3 +2384,338 @@ describe("tryFoldFile — constructions as values (chant #1169)", () => {
2294
2384
  expect(tainted.has(sharedPath)).toBe(true);
2295
2385
  });
2296
2386
  });
2387
+
2388
+ // chant #1373 — a call to a PROJECT-LOCAL function (imported from a sibling
2389
+ // project file, or declared in the file itself) folds when the callee's body
2390
+ // is itself foldable: arguments fold in the caller, the body folds in the
2391
+ // defining module's scope, nothing is imported or run. Before this, the
2392
+ // recommended "read build parameters through one helper" shape was exactly
2393
+ // the shape that demoted a parameter file — and every file importing it —
2394
+ // to run.
2395
+ describe("tryFoldFile — project-local function calls (chant #1373)", () => {
2396
+ let testDir: string;
2397
+
2398
+ beforeEach(async () => {
2399
+ testDir = join(tmpdir(), `chant-fold-import-1373-test-${Date.now()}-${Math.random()}`);
2400
+ await mkdir(testDir, { recursive: true });
2401
+ await mkdir(join(testDir, "lib"), { recursive: true });
2402
+ await writeFile(
2403
+ join(testDir, "resources.ts"),
2404
+ `
2405
+ import { createResource } from ${JSON.stringify(runtimePath)};
2406
+ export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
2407
+ `,
2408
+ );
2409
+ });
2410
+
2411
+ afterEach(async () => {
2412
+ await rm(testDir, { recursive: true, force: true });
2413
+ });
2414
+
2415
+ /** The issue's own shape: a `lib/target.ts` helper, a `params.ts` that reads parameters through it, and a `plane.ts` that imports the result. */
2416
+ async function writeIssueShape(targetBody: string): Promise<{ params: string; plane: string; target: string }> {
2417
+ const target = join(testDir, "lib", "target.ts");
2418
+ await writeFile(
2419
+ target,
2420
+ `
2421
+ throw new Error("must never execute — sentinel for #1373 fold verification (target.ts)");
2422
+ ${targetBody}
2423
+ `,
2424
+ );
2425
+ const params = join(testDir, "params.ts");
2426
+ await writeFile(
2427
+ params,
2428
+ `
2429
+ import { params } from ${JSON.stringify(paramsPath)};
2430
+ import { optionalAccountId } from "./lib/target";
2431
+ throw new Error("must never execute — sentinel for #1373 fold verification (params.ts)");
2432
+ export const namingParams = {
2433
+ prefix: "kmv",
2434
+ accountId: optionalAccountId(params.accountId),
2435
+ };
2436
+ export const bucketMode = namingParams.accountId === undefined ? "shared" : "per-account";
2437
+ `,
2438
+ );
2439
+ const plane = join(testDir, "plane.ts");
2440
+ await writeFile(
2441
+ plane,
2442
+ `
2443
+ import { Bucket } from "./resources";
2444
+ import { namingParams, bucketMode } from "./params";
2445
+ throw new Error("must never execute — sentinel for #1373 fold verification (plane.ts)");
2446
+ export const artifactBucket = new Bucket({
2447
+ name: \`\${namingParams.prefix}-artifacts-\${namingParams.accountId ?? "shared"}\`,
2448
+ mode: bucketMode,
2449
+ });
2450
+ `,
2451
+ );
2452
+ return { params, plane, target };
2453
+ }
2454
+
2455
+ const OPTIONAL_ACCOUNT_ID = `
2456
+ export function optionalAccountId(raw: string | undefined): string | undefined {
2457
+ const trimmed = raw ?? "";
2458
+ return trimmed === "" ? undefined : trimmed;
2459
+ }
2460
+ `;
2461
+
2462
+ test("the issue's shape folds end to end: params.ts through an imported helper, plane.ts through params.ts", async () => {
2463
+ const { params, plane } = await writeIssueShape(OPTIONAL_ACCOUNT_ID);
2464
+ const session = createFoldSession([], { accountId: "123456789012" });
2465
+
2466
+ const paramsResult = await tryFoldFile(params, [], session);
2467
+ expect(paramsResult.ok, !paramsResult.ok ? paramsResult.reason : "").toBe(true);
2468
+ if (!paramsResult.ok) return;
2469
+ expect(paramsResult.exportedValues.get("namingParams")).toEqual({ prefix: "kmv", accountId: "123456789012" });
2470
+ expect(paramsResult.exportedValues.get("bucketMode")).toBe("per-account");
2471
+
2472
+ const planeResult = await tryFoldFile(plane, [], session);
2473
+ expect(planeResult.ok, !planeResult.ok ? planeResult.reason : "").toBe(true);
2474
+ if (!planeResult.ok) return;
2475
+ const [name, entity] = planeResult.entities[0];
2476
+ expect(name).toBe("artifactBucket");
2477
+ expect((entity as unknown as { props: unknown }).props).toEqual({
2478
+ name: "kmv-artifacts-123456789012",
2479
+ mode: "per-account",
2480
+ });
2481
+ });
2482
+
2483
+ test("an absent parameter takes the helper's own defaulting path", async () => {
2484
+ const { params } = await writeIssueShape(OPTIONAL_ACCOUNT_ID);
2485
+ const result = await tryFoldFile(params, [], createFoldSession([], {}));
2486
+
2487
+ expect(result.ok, !result.ok ? result.reason : "").toBe(true);
2488
+ if (!result.ok) return;
2489
+ expect(result.exportedValues.get("namingParams")).toEqual({ prefix: "kmv", accountId: undefined });
2490
+ expect(result.exportedValues.get("bucketMode")).toBe("shared");
2491
+ });
2492
+
2493
+ test("the taint chain is broken: nothing in the chain falls back, and the helper module is not a live-identity source", async () => {
2494
+ const { params, plane, target } = await writeIssueShape(OPTIONAL_ACCOUNT_ID);
2495
+ const session = createFoldSession([], { accountId: "123456789012" });
2496
+ const files = [target, params, plane];
2497
+ // Sequential, as `discover()`'s own per-file loop is.
2498
+ const results = new Map<string, Awaited<ReturnType<typeof tryFoldFile>>>();
2499
+ for (const f of files) results.set(f, await tryFoldFile(f, [], session));
2500
+
2501
+ for (const [file, result] of results) {
2502
+ expect(result.ok, `${file}: ${!result.ok ? result.reason : ""}`).toBe(true);
2503
+ }
2504
+ const wouldFold = new Map(files.map((f) => [f, results.get(f)!.ok]));
2505
+ const liveSources = new Map(
2506
+ files.map((f) => {
2507
+ const r = results.get(f)!;
2508
+ return [f, r.ok ? r.liveSources : new Set<string>()] as const;
2509
+ }),
2510
+ );
2511
+ const tainted = await planFoldTaint(files, wouldFold, liveSources);
2512
+ expect([...tainted]).toEqual([]);
2513
+ // A function marker is a description of source, not an object the run
2514
+ // path could hold a different copy of — no identity edge to target.ts.
2515
+ expect(liveSources.get(params)!.has(target)).toBe(false);
2516
+ });
2517
+
2518
+ test("a body that reads process.env is not foldable, and the reason names the callee, its file, and the read", async () => {
2519
+ const { params, target } = await writeIssueShape(`
2520
+ export function optionalAccountId(raw: string | undefined): string | undefined {
2521
+ return raw ?? process.env.AWS_ACCOUNT_ID;
2522
+ }
2523
+ `);
2524
+ const result = await tryFoldFile(params, [], createFoldSession([], {}));
2525
+
2526
+ expect(result.ok).toBe(false);
2527
+ if (result.ok) return;
2528
+ expect(result.reason).toContain('"namingParams" is not foldable');
2529
+ expect(result.reason).toContain('call to "optionalAccountId"');
2530
+ expect(result.reason).toContain(target);
2531
+ expect(result.reason).toContain('ambient "process" read is not foldable');
2532
+ // The old, unactionable wording is gone.
2533
+ expect(result.reason).not.toContain("function call as a value");
2534
+ });
2535
+
2536
+ test("a body that does I/O is not foldable, and the reason names the callee and the call it makes", async () => {
2537
+ const { params } = await writeIssueShape(`
2538
+ import { readFileSync } from "node:fs";
2539
+ export function optionalAccountId(raw: string | undefined): string | undefined {
2540
+ return raw ?? readFileSync("/etc/account-id", "utf-8");
2541
+ }
2542
+ `);
2543
+ const result = await tryFoldFile(params, [], createFoldSession([], {}));
2544
+
2545
+ expect(result.ok).toBe(false);
2546
+ if (result.ok) return;
2547
+ expect(result.reason).toContain('call to "optionalAccountId"');
2548
+ expect(result.reason).toContain("function call as a value is not foldable: readFileSync(...)");
2549
+ });
2550
+
2551
+ test("a body outside the statement subset is refused with the construct named", async () => {
2552
+ const { params } = await writeIssueShape(`
2553
+ export function optionalAccountId(raw: string | undefined): string | undefined {
2554
+ if (raw === "") return undefined;
2555
+ return raw;
2556
+ }
2557
+ `);
2558
+ const result = await tryFoldFile(params, [], createFoldSession([], {}));
2559
+
2560
+ expect(result.ok).toBe(false);
2561
+ if (result.ok) return;
2562
+ expect(result.reason).toContain('call to "optionalAccountId"');
2563
+ expect(result.reason).toContain("`IfStatement` in a function body is not foldable");
2564
+ });
2565
+
2566
+ test("an unresolved name inside the body is reported as such", async () => {
2567
+ const { params } = await writeIssueShape(`
2568
+ import { DEFAULT_ACCOUNT } from "./missing";
2569
+ export function optionalAccountId(raw: string | undefined): string | undefined {
2570
+ return raw ?? DEFAULT_ACCOUNT;
2571
+ }
2572
+ `);
2573
+ const result = await tryFoldFile(params, [], createFoldSession([], {}));
2574
+
2575
+ expect(result.ok).toBe(false);
2576
+ if (result.ok) return;
2577
+ expect(result.reason).toContain('call to "optionalAccountId"');
2578
+ expect(result.reason).toContain("unresolved identifier: DEFAULT_ACCOUNT");
2579
+ });
2580
+
2581
+ test("an arrow-function const, a destructured parameter, a default, and a sibling-function call all fold", async () => {
2582
+ await writeFile(
2583
+ join(testDir, "lib", "naming.ts"),
2584
+ `
2585
+ throw new Error("must never execute — sentinel for #1373 fold verification");
2586
+ const SEPARATOR = "-";
2587
+ function joinParts(parts: string[], sep = SEPARATOR) {
2588
+ return parts[0] + sep + parts[1] + sep + parts[2];
2589
+ }
2590
+ export const loomName = ({ env, app }: { env: string; app: string }, suffix = "svc") =>
2591
+ joinParts([env, app, suffix]);
2592
+ export const nameOf = loomName;
2593
+ `,
2594
+ );
2595
+ const file = join(testDir, "main.ts");
2596
+ await writeFile(
2597
+ file,
2598
+ `
2599
+ import { Bucket } from "./resources";
2600
+ import { loomName, nameOf } from "./lib/naming";
2601
+ export const bucket = new Bucket({
2602
+ name: loomName({ env: "prod", app: "api" }),
2603
+ alt: nameOf({ env: "dev", app: "web" }, "job"),
2604
+ });
2605
+ `,
2606
+ );
2607
+
2608
+ const result = await tryFoldFile(file);
2609
+
2610
+ expect(result.ok, !result.ok ? result.reason : "").toBe(true);
2611
+ if (!result.ok) return;
2612
+ expect((result.entities[0][1] as unknown as { props: unknown }).props).toEqual({
2613
+ name: "prod-api-svc",
2614
+ alt: "dev-web-job",
2615
+ });
2616
+ });
2617
+
2618
+ test("a same-file function folds too, and a TOP-LEVEL call to a project function folds without importing its module", async () => {
2619
+ await writeFile(
2620
+ join(testDir, "lib", "target.ts"),
2621
+ `
2622
+ throw new Error("must never execute — sentinel for #1373 fold verification");
2623
+ export function region(raw?: string) { return raw ?? "us-east-1"; }
2624
+ `,
2625
+ );
2626
+ const file = join(testDir, "main.ts");
2627
+ await writeFile(
2628
+ file,
2629
+ `
2630
+ import { region } from "./lib/target";
2631
+ const upper = (s: string) => s + "!";
2632
+ export const REGION = region();
2633
+ export const LOUD = upper(REGION);
2634
+ `,
2635
+ );
2636
+
2637
+ const result = await tryFoldFile(file);
2638
+
2639
+ expect(result.ok, !result.ok ? result.reason : "").toBe(true);
2640
+ if (!result.ok) return;
2641
+ expect(result.exportedValues.get("REGION")).toBe("us-east-1");
2642
+ expect(result.exportedValues.get("LOUD")).toBe("us-east-1!");
2643
+ });
2644
+
2645
+ test("a function used as a VALUE is refused — only a call to it folds", async () => {
2646
+ await writeFile(
2647
+ join(testDir, "lib", "target.ts"),
2648
+ `export function region(raw?: string) { return raw ?? "us-east-1"; }`,
2649
+ );
2650
+ const file = join(testDir, "main.ts");
2651
+ await writeFile(
2652
+ file,
2653
+ `
2654
+ import { region } from "./lib/target";
2655
+ export const config = { resolver: region };
2656
+ `,
2657
+ );
2658
+
2659
+ const result = await tryFoldFile(file);
2660
+
2661
+ expect(result.ok).toBe(false);
2662
+ if (result.ok) return;
2663
+ expect(result.reason).toContain('function "region" used as a value is not foldable');
2664
+ });
2665
+
2666
+ test("a `new` inside a function body is refused rather than revived against the wrong file's imports", async () => {
2667
+ await writeFile(
2668
+ join(testDir, "lib", "target.ts"),
2669
+ `
2670
+ import { Bucket } from "../resources";
2671
+ export function makeBucket(name: string) { return new Bucket({ name }); }
2672
+ `,
2673
+ );
2674
+ const file = join(testDir, "main.ts");
2675
+ await writeFile(
2676
+ file,
2677
+ `
2678
+ import { makeBucket } from "./lib/target";
2679
+ export const config = { bucket: makeBucket("x") };
2680
+ `,
2681
+ );
2682
+
2683
+ const result = await tryFoldFile(file);
2684
+
2685
+ expect(result.ok).toBe(false);
2686
+ if (result.ok) return;
2687
+ expect(result.reason).toContain("`new Bucket(...)` inside a folded function body is not foldable");
2688
+ });
2689
+
2690
+ test("a recursive function terminates with a depth diagnostic instead of hanging", async () => {
2691
+ await writeFile(
2692
+ join(testDir, "lib", "target.ts"),
2693
+ `export function loop(n: number): number { return n === 0 ? 0 : loop(n - 1); }`,
2694
+ );
2695
+ const file = join(testDir, "main.ts");
2696
+ await writeFile(file, `import { loop } from "./lib/target"; export const x = { n: loop(1000) };`);
2697
+
2698
+ const result = await tryFoldFile(file);
2699
+
2700
+ expect(result.ok).toBe(false);
2701
+ if (result.ok) return;
2702
+ expect(result.reason).toContain("call depth exceeded");
2703
+ });
2704
+
2705
+ test("a package (node_modules) function is still not folded — only project files produce a callable", async () => {
2706
+ const file = join(testDir, "main.ts");
2707
+ await writeFile(
2708
+ file,
2709
+ `
2710
+ import { basename } from "node:path";
2711
+ export const x = { n: basename("/a/b") };
2712
+ `,
2713
+ );
2714
+
2715
+ const result = await tryFoldFile(file);
2716
+
2717
+ expect(result.ok).toBe(false);
2718
+ if (result.ok) return;
2719
+ expect(result.reason).toContain("function call as a value is not foldable: basename(...)");
2720
+ });
2721
+ });