@nudojs/service 1.0.1 → 3.0.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.
package/dist/index.js CHANGED
@@ -1,17 +1,21 @@
1
1
  import {
2
+ DEFAULT_ANALYSIS_MODE,
3
+ analysisConfig,
2
4
  builtinProtoMember,
3
5
  builtinProtoMemberNames,
6
+ clearPathEnvCaches,
4
7
  describeAbsMember,
8
+ diskCacheRoot,
5
9
  findProjectConfig,
6
10
  interfaceConfig,
7
11
  loadEnvs,
8
12
  matchesEmitAllowlist,
9
13
  preloadPathEnvs
10
- } from "./chunk-2VHITMTM.js";
14
+ } from "./chunk-ADFCZP72.js";
11
15
 
12
16
  // src/analyzer.ts
13
- import { readFileSync as readFileSync5, existsSync as existsSync4, statSync as statSync4, realpathSync } from "fs";
14
- import { resolve as resolve5, dirname as dirname6 } from "path";
17
+ import { readFileSync as readFileSync5, existsSync as existsSync5, statSync as statSync5, realpathSync } from "fs";
18
+ import { resolve as resolve6, dirname as dirname7 } from "path";
15
19
  import traverse from "@babel/traverse";
16
20
  import {
17
21
  createEnvironment as createEnvironment2,
@@ -34,6 +38,9 @@ import {
34
38
  strLit,
35
39
  boolLit,
36
40
  abs as makeAbsVal,
41
+ formalParamsFromNodes,
42
+ formalParamDisplayNames,
43
+ runWithEvalMissingSlot,
37
44
  stableAnalyzeKeySource as stableAnalyzeKeySource2,
38
45
  fnFingerprints
39
46
  } from "@nudojs/core";
@@ -383,9 +390,70 @@ function defaultLoadModule(spec, fromFile) {
383
390
  }
384
391
  }
385
392
 
393
+ // src/env-path-deps.ts
394
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
395
+ import { dirname as dirname2, resolve as resolve2, join as join2 } from "path";
396
+ import { extractAllLoadSpecs } from "@nudojs/core";
397
+ var envDependents = /* @__PURE__ */ new Map();
398
+ function norm(p) {
399
+ return p.replace(/\\/g, "/");
400
+ }
401
+ function resolveLoadSpecPath(spec, fromFile) {
402
+ if (!spec.startsWith(".") && !spec.startsWith("/")) return void 0;
403
+ try {
404
+ const base = dirname2(resolve2(fromFile));
405
+ const p = resolve2(base, spec);
406
+ for (const cand of [
407
+ p,
408
+ `${p}.js`,
409
+ `${p}.mjs`,
410
+ `${p}.ts`,
411
+ join2(p, "index.js"),
412
+ join2(p, "index.mjs"),
413
+ join2(p, "index.ts")
414
+ ]) {
415
+ if (existsSync2(cand) && !statSync2(cand).isDirectory()) return cand;
416
+ }
417
+ return void 0;
418
+ } catch {
419
+ return void 0;
420
+ }
421
+ }
422
+ function noteEnvPathDeps(sourcePath, source) {
423
+ const src = norm(sourcePath);
424
+ for (const set of envDependents.values()) set.delete(src);
425
+ let specs = [];
426
+ try {
427
+ specs = extractAllLoadSpecs(source);
428
+ } catch {
429
+ return;
430
+ }
431
+ for (const spec of specs) {
432
+ if (!spec.startsWith(".") && !spec.startsWith("/")) continue;
433
+ const abs = resolveLoadSpecPath(spec, sourcePath);
434
+ if (!abs) continue;
435
+ const key = norm(abs);
436
+ if (!envDependents.has(key)) envDependents.set(key, /* @__PURE__ */ new Set());
437
+ envDependents.get(key).add(src);
438
+ }
439
+ }
440
+ function envPathDependents(envPath) {
441
+ return [...envDependents.get(norm(envPath)) ?? []];
442
+ }
443
+ function clearEnvPathDeps() {
444
+ envDependents.clear();
445
+ }
446
+ function isEnvTemplatePath(path) {
447
+ const lower = norm(path).toLowerCase();
448
+ return lower.endsWith(".env.js") || lower.endsWith(".env.mjs") || lower.endsWith(".env.ts") || lower.endsWith(".env.cjs") || lower.endsWith(".env.cts") || lower.endsWith(".env.mts") || /\/mock-[^/]+\.(js|mjs|ts)$/.test(lower) || /\.mock\.(js|mjs|ts)$/.test(lower);
449
+ }
450
+
451
+ // src/analyzer.ts
452
+ import { loadModuleDepsFingerprint as loadModuleDepsFingerprint2, hashSource as hashSource2 } from "@nudojs/core";
453
+
386
454
  // src/abs-modules-graph.ts
387
- import { readFileSync as readFileSync4, statSync as statSync3 } from "fs";
388
- import { dirname as dirname5, resolve as resolve4 } from "path";
455
+ import { readFileSync as readFileSync4, statSync as statSync4 } from "fs";
456
+ import { dirname as dirname6, resolve as resolve5 } from "path";
389
457
  import { parse as parse3 } from "@nudojs/parser";
390
458
  import {
391
459
  evalProgramAbs,
@@ -398,19 +466,19 @@ import {
398
466
  confJoin as confJoin2,
399
467
  absFunction as absFunction2
400
468
  } from "@nudojs/core";
401
- import { dirname as dirname4 } from "path";
469
+ import { dirname as dirname5 } from "path";
402
470
 
403
471
  // src/harvest-auto.ts
404
472
  import { parse as parse2 } from "@nudojs/parser";
405
473
 
406
474
  // src/static-imports.ts
407
475
  import { parse } from "@nudojs/parser";
408
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
409
- import { resolve as resolve2, dirname as dirname2, join as join2 } from "path";
476
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
477
+ import { resolve as resolve3, dirname as dirname3, join as join3 } from "path";
410
478
  import { generalizeFromAst } from "@nudojs/core";
411
479
  function resolveRelative(fromFile, spec) {
412
480
  if (!spec.startsWith(".") && !spec.startsWith("/")) return void 0;
413
- const base = resolve2(dirname2(fromFile), spec);
481
+ const base = resolve3(dirname3(fromFile), spec);
414
482
  for (const cand of [
415
483
  base,
416
484
  base + ".js",
@@ -419,12 +487,12 @@ function resolveRelative(fromFile, spec) {
419
487
  base + ".ts",
420
488
  base + ".mts",
421
489
  base + ".cts",
422
- join2(base, "index.js"),
423
- join2(base, "index.cjs"),
424
- join2(base, "index.mjs"),
425
- join2(base, "index.ts")
490
+ join3(base, "index.js"),
491
+ join3(base, "index.cjs"),
492
+ join3(base, "index.mjs"),
493
+ join3(base, "index.ts")
426
494
  ]) {
427
- if (existsSync2(cand)) return cand;
495
+ if (existsSync3(cand)) return cand;
428
496
  }
429
497
  return void 0;
430
498
  }
@@ -543,14 +611,14 @@ function collectCjsExports(stmt, named, setDefault) {
543
611
  function collectStaticImports(entryFile, maxDepth = 8) {
544
612
  const graph = /* @__PURE__ */ new Map();
545
613
  const queue = [
546
- { file: resolve2(entryFile), depth: 0 }
614
+ { file: resolve3(entryFile), depth: 0 }
547
615
  ];
548
616
  const seen = /* @__PURE__ */ new Set();
549
617
  while (queue.length > 0) {
550
618
  const { file, depth } = queue.shift();
551
619
  if (seen.has(file) || depth > maxDepth) continue;
552
620
  seen.add(file);
553
- if (!existsSync2(file)) continue;
621
+ if (!existsSync3(file)) continue;
554
622
  const source = readFileSync2(file, "utf8");
555
623
  const mod = analyzeExportsFromSource(file, source);
556
624
  graph.set(file, mod);
@@ -564,19 +632,19 @@ function collectStaticImports(entryFile, maxDepth = 8) {
564
632
  }
565
633
 
566
634
  // src/harvest-package.ts
567
- import { existsSync as existsSync3, readdirSync, statSync as statSync2, readFileSync as readFileSync3 } from "fs";
568
- import { join as join3, resolve as resolve3, dirname as dirname3, basename } from "path";
635
+ import { existsSync as existsSync4, readdirSync, statSync as statSync3, readFileSync as readFileSync3 } from "fs";
636
+ import { join as join4, resolve as resolve4, dirname as dirname4, basename } from "path";
569
637
  import { harvestDts } from "@nudojs/harvester";
570
638
  import { formatShape } from "@nudojs/core";
571
639
  function resolvePackageRoot(pkg, fromDir = process.cwd()) {
572
- let dir = resolve3(fromDir);
640
+ let dir = resolve4(fromDir);
573
641
  const bare = pkg.replace(/^@/, "").replace(/\//g, "__");
574
642
  for (let i = 0; i < 8; i++) {
575
- const typesPath = join3(dir, "node_modules", "@types", bare);
576
- if (existsSync3(typesPath)) return typesPath;
577
- const pkgPath = join3(dir, "node_modules", pkg);
578
- if (existsSync3(pkgPath)) return pkgPath;
579
- const parent = resolve3(dir, "..");
643
+ const typesPath = join4(dir, "node_modules", "@types", bare);
644
+ if (existsSync4(typesPath)) return typesPath;
645
+ const pkgPath = join4(dir, "node_modules", pkg);
646
+ if (existsSync4(pkgPath)) return pkgPath;
647
+ const parent = resolve4(dir, "..");
580
648
  if (parent === dir) break;
581
649
  dir = parent;
582
650
  }
@@ -606,10 +674,10 @@ function collectDtsFiles(root, maxFiles = 8) {
606
674
  for (const name of entries) {
607
675
  if (out.length >= maxFiles * 3) return;
608
676
  if (SKIP_DIRS.has(name)) continue;
609
- const p = join3(dir, name);
677
+ const p = join4(dir, name);
610
678
  let st;
611
679
  try {
612
- st = statSync2(p);
680
+ st = statSync3(p);
613
681
  } catch {
614
682
  continue;
615
683
  }
@@ -625,15 +693,15 @@ function collectDtsFiles(root, maxFiles = 8) {
625
693
  return out.slice(0, maxFiles);
626
694
  }
627
695
  function entryDtsFromPackageJson(root) {
628
- const pj = join3(root, "package.json");
629
- if (!existsSync3(pj)) return void 0;
696
+ const pj = join4(root, "package.json");
697
+ if (!existsSync4(pj)) return void 0;
630
698
  try {
631
699
  const raw = JSON.parse(readFileSync3(pj, "utf8"));
632
700
  const entry = raw.types ?? raw.typings;
633
701
  if (typeof entry === "string" && entry.endsWith(".d.ts")) {
634
- const p = resolve3(root, entry);
635
- if (existsSync3(p)) {
636
- const st = statSync2(p);
702
+ const p = resolve4(root, entry);
703
+ if (existsSync4(p)) {
704
+ const st = statSync3(p);
637
705
  if (st.size <= MAX_DTS_BYTES) return p;
638
706
  }
639
707
  }
@@ -649,7 +717,7 @@ function collectDtsFromEntry(entry, maxFiles = 200) {
649
717
  const RELATIVE_FROM_REGEX = /\bfrom\s+["'](\.[^"']+)["']/g;
650
718
  while (queue.length > 0 && files.length < maxFiles) {
651
719
  const current = queue.shift();
652
- if (seen.has(current) || !existsSync3(current)) continue;
720
+ if (seen.has(current) || !existsSync4(current)) continue;
653
721
  seen.add(current);
654
722
  if (!current.endsWith(".d.ts")) continue;
655
723
  files.push(current);
@@ -659,14 +727,14 @@ function collectDtsFromEntry(entry, maxFiles = 200) {
659
727
  } catch {
660
728
  continue;
661
729
  }
662
- const dir = dirname3(current);
730
+ const dir = dirname4(current);
663
731
  for (const match of text.matchAll(REFERENCE_PATH_REGEX)) {
664
- queue.push(resolve3(dir, match[1]));
732
+ queue.push(resolve4(dir, match[1]));
665
733
  }
666
734
  for (const match of text.matchAll(RELATIVE_FROM_REGEX)) {
667
- const base = resolve3(dir, match[1]);
668
- for (const candidate of [`${base}.d.ts`, join3(base, "index.d.ts")]) {
669
- if (existsSync3(candidate)) {
735
+ const base = resolve4(dir, match[1]);
736
+ for (const candidate of [`${base}.d.ts`, join4(base, "index.d.ts")]) {
737
+ if (existsSync4(candidate)) {
670
738
  queue.push(candidate);
671
739
  break;
672
740
  }
@@ -678,11 +746,11 @@ function collectDtsFromEntry(entry, maxFiles = 200) {
678
746
  function harvestPackage(pkg, fromDir, maxFiles = 8) {
679
747
  const root = resolvePackageRoot(pkg, fromDir);
680
748
  if (!root) return { error: `package not found: ${pkg}` };
681
- const entry = entryDtsFromPackageJson(root) ?? join3(root, "index.d.ts");
682
- let dtsFiles = existsSync3(entry) ? collectDtsFromEntry(entry, maxFiles) : [];
749
+ const entry = entryDtsFromPackageJson(root) ?? join4(root, "index.d.ts");
750
+ let dtsFiles = existsSync4(entry) ? collectDtsFromEntry(entry, maxFiles) : [];
683
751
  if (dtsFiles.length === 0) {
684
752
  const collected = collectDtsFiles(root, maxFiles);
685
- dtsFiles = entry && existsSync3(entry) && !collected.includes(entry) ? [entry, ...collected.filter((f) => f !== entry)].slice(0, maxFiles) : collected;
753
+ dtsFiles = entry && existsSync4(entry) && !collected.includes(entry) ? [entry, ...collected.filter((f) => f !== entry)].slice(0, maxFiles) : collected;
686
754
  }
687
755
  if (dtsFiles.length === 0) {
688
756
  return { error: `no .d.ts under ${root}` };
@@ -710,6 +778,49 @@ function lookupHarvested(h, moduleName, exportName) {
710
778
  }
711
779
 
712
780
  // src/harvest-auto.ts
781
+ var NODE_BUILTINS = /* @__PURE__ */ new Set([
782
+ "assert",
783
+ "async_hooks",
784
+ "buffer",
785
+ "child_process",
786
+ "cluster",
787
+ "console",
788
+ "constants",
789
+ "crypto",
790
+ "dgram",
791
+ "diagnostics_channel",
792
+ "dns",
793
+ "domain",
794
+ "events",
795
+ "fs",
796
+ "http",
797
+ "http2",
798
+ "https",
799
+ "inspector",
800
+ "module",
801
+ "net",
802
+ "os",
803
+ "path",
804
+ "perf_hooks",
805
+ "process",
806
+ "punycode",
807
+ "querystring",
808
+ "readline",
809
+ "repl",
810
+ "stream",
811
+ "string_decoder",
812
+ "timers",
813
+ "tls",
814
+ "trace_events",
815
+ "tty",
816
+ "url",
817
+ "util",
818
+ "v8",
819
+ "vm",
820
+ "wasi",
821
+ "worker_threads",
822
+ "zlib"
823
+ ]);
713
824
  function barePackageName(spec) {
714
825
  if (!spec) return void 0;
715
826
  if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
@@ -717,7 +828,9 @@ function barePackageName(spec) {
717
828
  if (spec.startsWith("@")) {
718
829
  return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : void 0;
719
830
  }
720
- return parts[0];
831
+ const name = parts[0];
832
+ if (NODE_BUILTINS.has(name)) return void 0;
833
+ return name;
721
834
  }
722
835
  function collectBarePackages(source) {
723
836
  try {
@@ -840,7 +953,7 @@ function bareSpecToAbsModules(spec, fromFile) {
840
953
  const parts = spec.split("/");
841
954
  const pkg = spec.startsWith("@") ? parts.length >= 2 ? `${parts[0]}/${parts[1]}` : void 0 : parts[0];
842
955
  if (!pkg) return void 0;
843
- const table = harvestToAbsModules(pkg, dirname4(fromFile));
956
+ const table = harvestToAbsModules(pkg, dirname5(fromFile));
844
957
  return table[spec] ?? table[pkg];
845
958
  }
846
959
 
@@ -848,8 +961,8 @@ function bareSpecToAbsModules(spec, fromFile) {
848
961
  function defaultAbsLoadModule(spec, fromFile) {
849
962
  if (!spec.startsWith(".") && !spec.startsWith("/")) return void 0;
850
963
  try {
851
- const p = resolve4(dirname5(resolve4(fromFile)), spec);
852
- for (const cand of [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve4(p, "index.js")]) {
964
+ const p = resolve5(dirname6(resolve5(fromFile)), spec);
965
+ for (const cand of [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve5(p, "index.js")]) {
853
966
  try {
854
967
  return readFileSync4(cand, "utf-8");
855
968
  } catch {
@@ -861,8 +974,8 @@ function defaultAbsLoadModule(spec, fromFile) {
861
974
  }
862
975
  }
863
976
  function relCandidates(spec, fromFile) {
864
- const p = resolve4(dirname5(resolve4(fromFile)), spec);
865
- return [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve4(p, "index.js")];
977
+ const p = resolve5(dirname6(resolve5(fromFile)), spec);
978
+ return [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve5(p, "index.js")];
866
979
  }
867
980
  function resolveRel(spec, fromFile) {
868
981
  if (!spec.startsWith(".") && !spec.startsWith("/")) return null;
@@ -966,7 +1079,7 @@ function evalAbsModuleGraph(entrySource, entryFile, opts = {}) {
966
1079
  const shared = absModuleCache.get(absPath);
967
1080
  if (shared) {
968
1081
  try {
969
- const st = statSync3(absPath);
1082
+ const st = statSync4(absPath);
970
1083
  if (st.mtimeMs === shared.mtimeMs && st.size === shared.size) {
971
1084
  for (const iss of shared.issues) pushIssue(iss.kind, iss.label, iss.reason);
972
1085
  cache.set(absPath, shared.exports);
@@ -1031,7 +1144,7 @@ function evalAbsModuleGraph(entrySource, entryFile, opts = {}) {
1031
1144
  cache.set(absPath, exports);
1032
1145
  let fingerprint;
1033
1146
  try {
1034
- const st = statSync3(absPath);
1147
+ const st = statSync4(absPath);
1035
1148
  fingerprint = { mtimeMs: st.mtimeMs, size: st.size };
1036
1149
  } catch {
1037
1150
  }
@@ -1102,7 +1215,8 @@ import {
1102
1215
  stableAnalyzeKeySource,
1103
1216
  formatAbs as formatAbs3,
1104
1217
  hashSource,
1105
- getFnImpl as getFnImpl2
1218
+ getFnImpl as getFnImpl2,
1219
+ loadModuleDepsFingerprint
1106
1220
  } from "@nudojs/core";
1107
1221
  import { parse as parse4, extractInlineDirectives } from "@nudojs/parser";
1108
1222
 
@@ -1245,6 +1359,50 @@ function collectEnvModules(envNames) {
1245
1359
  }
1246
1360
  return out;
1247
1361
  }
1362
+ var envHarvestConflictCollector = null;
1363
+ function setEnvHarvestConflictCollector(collector) {
1364
+ const prev = envHarvestConflictCollector;
1365
+ envHarvestConflictCollector = collector;
1366
+ return prev;
1367
+ }
1368
+ function getEnvHarvestConflictCollector() {
1369
+ return envHarvestConflictCollector;
1370
+ }
1371
+ function mergeHarvestUnderEnv(harvestModules, envModules, opts) {
1372
+ const out = {};
1373
+ for (const [mod, exports] of Object.entries(harvestModules)) {
1374
+ const named = { ...exports.named };
1375
+ out[mod] = exports.default !== void 0 ? { named, default: exports.default } : { named };
1376
+ }
1377
+ const notify = opts?.onConflict ?? envHarvestConflictCollector;
1378
+ for (const [mod, envExports] of Object.entries(envModules)) {
1379
+ const existing = out[mod];
1380
+ if (!existing) {
1381
+ const named2 = { ...envExports.named };
1382
+ out[mod] = envExports.default !== void 0 ? { named: named2, default: envExports.default } : { named: named2 };
1383
+ continue;
1384
+ }
1385
+ const overwritten = [];
1386
+ for (const key of Object.keys(envExports.named)) {
1387
+ if (existing.named[key] !== void 0) overwritten.push(key);
1388
+ }
1389
+ const defaultOverwritten = envExports.default !== void 0 && existing.default !== void 0;
1390
+ if (notify && (overwritten.length > 0 || defaultOverwritten)) {
1391
+ try {
1392
+ notify({
1393
+ module: mod,
1394
+ exports: overwritten,
1395
+ defaultOverwritten
1396
+ });
1397
+ } catch {
1398
+ }
1399
+ }
1400
+ const named = { ...existing.named, ...envExports.named };
1401
+ const defaultAbs = envExports.default !== void 0 ? envExports.default : existing.default;
1402
+ out[mod] = defaultAbs !== void 0 ? { named, default: defaultAbs } : { named };
1403
+ }
1404
+ return out;
1405
+ }
1248
1406
  function collectBPathReplacements(source) {
1249
1407
  const targets = [];
1250
1408
  const values = {};
@@ -1304,6 +1462,15 @@ function isBPathCapable(source, envNames = []) {
1304
1462
  }
1305
1463
  var bRunByFile = /* @__PURE__ */ new Map();
1306
1464
  var MAX_B_RUN_CACHE = 32;
1465
+ function bPathDepKey(source, filePath) {
1466
+ try {
1467
+ const fp = loadModuleDepsFingerprint(source, defaultLoadModule, filePath);
1468
+ if (fp.truncated) return null;
1469
+ return hashSource(fp.fp);
1470
+ } catch {
1471
+ return null;
1472
+ }
1473
+ }
1307
1474
  function clearBPathCache() {
1308
1475
  bRunByFile.clear();
1309
1476
  clearAnalysisFileCache();
@@ -1316,12 +1483,12 @@ function evictBPathCacheForFiles(files) {
1316
1483
  }
1317
1484
  return n;
1318
1485
  }
1319
- function bPathCacheSet(filePath, stableSource, mode, envKey, mockKey, value) {
1486
+ function bPathCacheSet(filePath, stableSource, mode, envKey, mockKey, depKey, value) {
1320
1487
  if (bRunByFile.size >= MAX_B_RUN_CACHE && !bRunByFile.has(filePath)) {
1321
1488
  const oldest = bRunByFile.keys().next().value;
1322
1489
  if (oldest !== void 0) bRunByFile.delete(oldest);
1323
1490
  }
1324
- bRunByFile.set(filePath, { stableSource, mode, envKey, mockKey, value });
1491
+ bRunByFile.set(filePath, { stableSource, mode, envKey, mockKey, depKey, value });
1325
1492
  }
1326
1493
  function tryRunBPath(source, filePath, opts = {}) {
1327
1494
  if (!isBPathCapable(source, opts.envNames ?? [])) return void 0;
@@ -1329,11 +1496,15 @@ function tryRunBPath(source, filePath, opts = {}) {
1329
1496
  const envKey = (opts.envNames ?? []).join(",");
1330
1497
  const mockKey = mockSeedFingerprint(opts.mocks);
1331
1498
  const stable = stableAnalyzeKeySource(source);
1332
- const cached = bRunByFile.get(filePath);
1333
- if (cached && cached.stableSource === stable && cached.mode === mode && cached.envKey === envKey && cached.mockKey === mockKey) {
1334
- bRunByFile.delete(filePath);
1335
- bRunByFile.set(filePath, cached);
1336
- return cached.value ?? void 0;
1499
+ const depKey = bPathDepKey(source, filePath);
1500
+ const canCache = depKey !== null;
1501
+ if (canCache) {
1502
+ const cached = bRunByFile.get(filePath);
1503
+ if (cached && cached.stableSource === stable && cached.mode === mode && cached.envKey === envKey && cached.mockKey === mockKey && cached.depKey === depKey) {
1504
+ bRunByFile.delete(filePath);
1505
+ bRunByFile.set(filePath, cached);
1506
+ return cached.value ?? void 0;
1507
+ }
1337
1508
  }
1338
1509
  let out = null;
1339
1510
  try {
@@ -1346,7 +1517,7 @@ function tryRunBPath(source, filePath, opts = {}) {
1346
1517
  try {
1347
1518
  const { modules: graphMods, issues } = evalAbsModuleGraph(source, filePath);
1348
1519
  const envMods = collectEnvModules(opts.envNames ?? []);
1349
- const modules = { ...envMods, ...graphMods };
1520
+ const modules = mergeHarvestUnderEnv(graphMods, envMods);
1350
1521
  const { targets, values, asTargets, asValues } = collectBPathReplacements(source);
1351
1522
  const envGlobals = {
1352
1523
  ...collectEnvGlobals(opts.envNames ?? []),
@@ -1378,7 +1549,9 @@ function tryRunBPath(source, filePath, opts = {}) {
1378
1549
  } catch {
1379
1550
  out = null;
1380
1551
  }
1381
- bPathCacheSet(filePath, stable, mode, envKey, mockKey, out);
1552
+ if (canCache && depKey !== null) {
1553
+ bPathCacheSet(filePath, stable, mode, envKey, mockKey, depKey, out);
1554
+ }
1382
1555
  return out ?? void 0;
1383
1556
  }
1384
1557
  function tryBPathCallFull(source, filePath, fnName, args, opts = {}) {
@@ -1520,6 +1693,9 @@ function collectDeclared(file) {
1520
1693
  case "ImportNamespaceSpecifier":
1521
1694
  addId(o.local);
1522
1695
  break;
1696
+ case "CatchClause":
1697
+ addId(o.param);
1698
+ break;
1523
1699
  default:
1524
1700
  break;
1525
1701
  }
@@ -1621,10 +1797,10 @@ function collectBPathDiagnostics(source, extraKnown) {
1621
1797
  // src/analyzer.ts
1622
1798
  import { setAbsTruncationCollector as setAbsTruncationCollector2 } from "@nudojs/core";
1623
1799
  function resolveImportPath(specifier, fromDir) {
1624
- const basePath = resolve5(fromDir, specifier);
1800
+ const basePath = resolve6(fromDir, specifier);
1625
1801
  for (const ext of ["", ".js", ".ts", ".mjs"]) {
1626
1802
  const candidate = basePath + ext;
1627
- if (existsSync4(candidate)) return candidate;
1803
+ if (existsSync5(candidate)) return candidate;
1628
1804
  }
1629
1805
  return null;
1630
1806
  }
@@ -1644,7 +1820,7 @@ function buildModuleGraph(files, cache) {
1644
1820
  if (cache) {
1645
1821
  let stat = null;
1646
1822
  try {
1647
- stat = statSync4(file);
1823
+ stat = statSync5(file);
1648
1824
  } catch {
1649
1825
  }
1650
1826
  const cached = stat ? cache.get(file) : void 0;
@@ -1673,7 +1849,7 @@ function extractImportEdges(file) {
1673
1849
  if (stmt.type !== "ImportDeclaration") continue;
1674
1850
  const specifier = stmt.source.value;
1675
1851
  if (!specifier.startsWith(".") && !specifier.startsWith("/")) continue;
1676
- const resolved = resolveImportPath(specifier, dirname6(file));
1852
+ const resolved = resolveImportPath(specifier, dirname7(file));
1677
1853
  if (resolved) edges.push(resolved);
1678
1854
  }
1679
1855
  return edges;
@@ -1748,23 +1924,13 @@ function locFromNode(node) {
1748
1924
  }
1749
1925
  function extractParamNames(node) {
1750
1926
  const fn = node.type === "ExportDefaultDeclaration" ? node.declaration : node;
1751
- if (fn.type === "FunctionDeclaration" || fn.type === "FunctionExpression" || fn.type === "ArrowFunctionExpression") {
1752
- return fn.params.map((p) => {
1753
- if (p.type === "Identifier") return p.name;
1754
- if (p.type === "AssignmentPattern" && p.left.type === "Identifier") return p.left.name;
1755
- if (p.type === "RestElement" && p.argument.type === "Identifier") return `...${p.argument.name}`;
1756
- return "_";
1757
- });
1927
+ if (fn.type === "FunctionDeclaration" || fn.type === "FunctionExpression" || fn.type === "ArrowFunctionExpression" || fn.type === "ClassMethod" || fn.type === "ObjectMethod" || fn.type === "TSDeclareMethod") {
1928
+ return formalParamDisplayNames(formalParamsFromNodes(fn.params));
1758
1929
  }
1759
1930
  if (fn.type === "VariableDeclaration") {
1760
1931
  const decl = fn.declarations[0];
1761
1932
  if (decl.init?.type === "FunctionExpression" || decl.init?.type === "ArrowFunctionExpression") {
1762
- return decl.init.params.map((p) => {
1763
- if (p.type === "Identifier") return p.name;
1764
- if (p.type === "AssignmentPattern" && p.left.type === "Identifier") return p.left.name;
1765
- if (p.type === "RestElement" && p.argument.type === "Identifier") return `...${p.argument.name}`;
1766
- return "_";
1767
- });
1933
+ return formalParamDisplayNames(formalParamsFromNodes(decl.init.params));
1768
1934
  }
1769
1935
  }
1770
1936
  return [];
@@ -1828,12 +1994,103 @@ function assignmentChainName(expr) {
1828
1994
  function collectTopLevelFunctions(ast) {
1829
1995
  const results = [];
1830
1996
  if (ast.type !== "File") return results;
1831
- for (const stmt of ast.program.body) {
1997
+ const body = ast.program.body;
1998
+ const classDecls = /* @__PURE__ */ new Map();
1999
+ const exportedClassNames = /* @__PURE__ */ new Set();
2000
+ const collectClassMethods = (cname, decl, stmt) => {
2001
+ const members = decl?.body?.body ?? [];
2002
+ for (const m of members) {
2003
+ const mem = m;
2004
+ const isMethod = mem.type === "MethodDefinition" || mem.type === "ClassMethod" || mem.type === "TSDeclareMethod";
2005
+ if (!isMethod) continue;
2006
+ if (mem.kind && mem.kind !== "method") continue;
2007
+ if (mem.static) continue;
2008
+ const keyName = mem.key?.type === "Identifier" ? mem.key.name : void 0;
2009
+ if (!keyName) continue;
2010
+ const methodNode = mem.type === "MethodDefinition" ? mem.value : mem;
2011
+ if (!methodNode) continue;
2012
+ results.push({
2013
+ name: `${cname}.${keyName}`,
2014
+ node: methodNode,
2015
+ stmt,
2016
+ noDeclaration: true
2017
+ });
2018
+ }
2019
+ };
2020
+ for (const stmt of body) {
2021
+ if (stmt.type === "ClassDeclaration" && stmt.id?.name) {
2022
+ const n = stmt.id.name;
2023
+ classDecls.set(n, { node: stmt, stmt });
2024
+ continue;
2025
+ }
2026
+ if (stmt.type === "ExportNamedDeclaration") {
2027
+ const d = stmt.declaration;
2028
+ if (d?.type === "ClassDeclaration" && d.id?.name) {
2029
+ const n = d.id.name;
2030
+ classDecls.set(n, { node: d, stmt });
2031
+ exportedClassNames.add(n);
2032
+ continue;
2033
+ }
2034
+ if (!d && stmt.specifiers) {
2035
+ for (const spec of stmt.specifiers) {
2036
+ if (spec?.type !== "ExportSpecifier") continue;
2037
+ const local = spec.local?.name;
2038
+ const exported = spec.exported?.name ?? spec.exported?.value;
2039
+ if (!local) continue;
2040
+ if (classDecls.has(local) || body.some(
2041
+ (s) => s.type === "ClassDeclaration" && s.id?.name === local
2042
+ )) {
2043
+ exportedClassNames.add(local);
2044
+ }
2045
+ if (exported === "default") exportedClassNames.add(local);
2046
+ }
2047
+ continue;
2048
+ }
2049
+ }
2050
+ if (stmt.type === "ExportDefaultDeclaration") {
2051
+ const d = stmt.declaration;
2052
+ if (d?.type === "ClassDeclaration" && d.id?.name) {
2053
+ const n = d.id.name;
2054
+ classDecls.set(n, { node: d, stmt });
2055
+ exportedClassNames.add(n);
2056
+ } else if (d?.type === "Identifier" && typeof d.name === "string") {
2057
+ exportedClassNames.add(d.name);
2058
+ }
2059
+ continue;
2060
+ }
2061
+ }
2062
+ for (const stmt of body) {
1832
2063
  const decl = resolveFunctionNode(stmt);
1833
2064
  if (decl.type === "FunctionDeclaration" && decl.id) {
1834
2065
  results.push({ name: decl.id.name, node: decl, stmt, noDeclaration: false });
1835
2066
  continue;
1836
2067
  }
2068
+ if (decl.type === "ClassDeclaration" && decl.id?.name) {
2069
+ continue;
2070
+ }
2071
+ if (stmt.type === "ExportNamedDeclaration") {
2072
+ const d = resolveFunctionNode(stmt);
2073
+ if (d.type === "FunctionDeclaration" && d.id) {
2074
+ results.push({ name: d.id.name, node: d, stmt, noDeclaration: false });
2075
+ }
2076
+ if (d.type === "ClassDeclaration" && d.id?.name) {
2077
+ }
2078
+ continue;
2079
+ }
2080
+ if (stmt.type === "ExportDefaultDeclaration") {
2081
+ const d = stmt.declaration;
2082
+ if (d?.type === "FunctionDeclaration" && d.id) {
2083
+ results.push({ name: d.id.name, node: d, stmt, noDeclaration: false });
2084
+ } else if (d && (isFnExprValue(d) || d.type === "FunctionDeclaration" && !d.id)) {
2085
+ results.push({
2086
+ name: "default",
2087
+ node: d,
2088
+ stmt,
2089
+ noDeclaration: true
2090
+ });
2091
+ }
2092
+ continue;
2093
+ }
1837
2094
  if (stmt.type === "VariableDeclaration") {
1838
2095
  for (const declarator of stmt.declarations) {
1839
2096
  if (declarator.id?.type === "Identifier" && isFnExprValue(declarator.init)) {
@@ -1851,6 +2108,11 @@ function collectTopLevelFunctions(ast) {
1851
2108
  }
1852
2109
  }
1853
2110
  }
2111
+ for (const cname of exportedClassNames) {
2112
+ const hit = classDecls.get(cname);
2113
+ if (!hit) continue;
2114
+ collectClassMethods(cname, hit.node, hit.stmt);
2115
+ }
1854
2116
  return results;
1855
2117
  }
1856
2118
  function findSingleModuleExportsFunction(ast) {
@@ -1877,7 +2139,7 @@ function findSingleModuleExportsFunction(ast) {
1877
2139
  }
1878
2140
  return found;
1879
2141
  }
1880
- var MAX_PRECISE_CALLSITE_CASES = 3;
2142
+ var DEFAULT_CALLSITE_BUDGET = 3;
1881
2143
  var COLLAPSE_LITERAL_THRESHOLD = 4;
1882
2144
  function dedupeCallRecords(records) {
1883
2145
  const seen = /* @__PURE__ */ new Set();
@@ -1895,7 +2157,7 @@ function locFromCallLoc(loc) {
1895
2157
  const p = loc ?? { line: 0, column: 0 };
1896
2158
  return { start: { line: p.line, column: p.column }, end: { line: p.line, column: p.column } };
1897
2159
  }
1898
- function synthesizeExternalFunctions(records, currentFile) {
2160
+ function synthesizeExternalFunctions(records, currentFile, callSiteBudget = DEFAULT_CALLSITE_BUDGET) {
1899
2161
  const groups = /* @__PURE__ */ new Map();
1900
2162
  for (const rec of records) {
1901
2163
  if (!rec.targetModule || !rec.targetExport) continue;
@@ -1919,7 +2181,7 @@ function synthesizeExternalFunctions(records, currentFile) {
1919
2181
  cases: [],
1920
2182
  fromModule: module
1921
2183
  };
1922
- const precise = deduped.slice(0, MAX_PRECISE_CALLSITE_CASES);
2184
+ const precise = deduped.slice(0, callSiteBudget);
1923
2185
  for (const rec of precise) {
1924
2186
  analysis.cases.push({
1925
2187
  name: `call@L${rec.callLoc?.line ?? 0}`,
@@ -1929,7 +2191,7 @@ function synthesizeExternalFunctions(records, currentFile) {
1929
2191
  source: "callsite"
1930
2192
  });
1931
2193
  }
1932
- const remaining = deduped.slice(MAX_PRECISE_CALLSITE_CASES);
2194
+ const remaining = deduped.slice(callSiteBudget);
1933
2195
  if (remaining.length > 0) {
1934
2196
  const symArgsAbs = Array.from(
1935
2197
  { length: arity },
@@ -1970,16 +2232,17 @@ function collectEnvNames(filePath, source, includeProject) {
1970
2232
  const fileDirectives = extractFileDirectives(ast);
1971
2233
  const fileEnvNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
1972
2234
  if (!includeProject) return fileEnvNames;
1973
- const projectConfig = findProjectConfig(dirname6(filePath));
2235
+ const projectConfig = findProjectConfig(dirname7(filePath));
1974
2236
  const projectEnvNames = projectConfig?.config.env ?? [];
1975
2237
  return [.../* @__PURE__ */ new Set([...projectEnvNames, ...fileEnvNames])];
1976
2238
  }
1977
- async function analyzeFileAsync(filePath, source, activeCases, externalCallRecords) {
2239
+ async function analyzeFileAsync(filePath, source, activeCases, externalCallRecords, loadModule) {
1978
2240
  const envNames = collectEnvNames(filePath, source, true);
2241
+ noteEnvPathDeps(filePath, source);
1979
2242
  if (envNames.length > 0) {
1980
- await preloadPathEnvs(envNames, dirname6(filePath));
2243
+ await preloadPathEnvs(envNames, dirname7(filePath));
1981
2244
  }
1982
- return analyzeFile(filePath, source, activeCases, externalCallRecords);
2245
+ return analyzeFile(filePath, source, activeCases, externalCallRecords, loadModule);
1983
2246
  }
1984
2247
  function collectCallRecords(filePath, source) {
1985
2248
  if (/\brequire\s*\(/.test(source) && filePath) {
@@ -2050,7 +2313,7 @@ function collectCallRecords(filePath, source) {
2050
2313
  var TEST_CALLBACK_NAMES = /* @__PURE__ */ new Set(["it", "test", "describe"]);
2051
2314
  var externalRecordIds = /* @__PURE__ */ new WeakMap();
2052
2315
  var nextExternalRecordId = 1;
2053
- function analysisFileCacheKey(filePath, source, activeCases, externalCallRecords) {
2316
+ function analysisFileCacheKey(filePath, source, activeCases, externalCallRecords, analysisCfg, loadModule, projectEnvNames, autoBind) {
2054
2317
  let cases = "-";
2055
2318
  if (activeCases && activeCases.size > 0) {
2056
2319
  cases = [...activeCases.entries()].sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0).map(([k, v]) => `${k}=${v}`).join(",");
@@ -2064,11 +2327,31 @@ function analysisFileCacheKey(filePath, source, activeCases, externalCallRecords
2064
2327
  }
2065
2328
  ext = `n${externalCallRecords.length}#id${id}`;
2066
2329
  }
2330
+ const cfg = analysisCfg ? `m=${analysisCfg.mode}|e=${analysisCfg.evalMissingSlot}|b=${analysisCfg.callSiteBudget}|d=${analysisCfg.diagnostics}` : "-";
2331
+ const lm = loadModule !== void 0 && loadModule !== defaultLoadModule ? "lm1" : "lm0";
2332
+ const envSeg = projectEnvNames && projectEnvNames.length > 0 ? projectEnvNames.join(",") : "-";
2333
+ const abSeg = autoBind === false ? "ab0" : autoBind === true ? "ab1" : "ab?";
2334
+ const effectiveLoader = loadModule ?? defaultLoadModule;
2335
+ let depSeg = "-";
2336
+ let noCache = false;
2337
+ try {
2338
+ const fp = loadModuleDepsFingerprint2(source, effectiveLoader, filePath);
2339
+ if (fp.truncated) {
2340
+ noCache = true;
2341
+ depSeg = `trunc:${fp.paths.length}`;
2342
+ } else {
2343
+ depSeg = hashSource2(fp.fp);
2344
+ }
2345
+ } catch {
2346
+ noCache = true;
2347
+ depSeg = "fperr";
2348
+ }
2067
2349
  return {
2068
2350
  filePath,
2069
2351
  // 尾部无 @nudo 注释/空行不进键:comment-only 编辑命中 AnalysisResult
2070
2352
  source: stableAnalyzeKeySource2(source),
2071
- auxKey: `${cases}\0${ext}`
2353
+ auxKey: `${cases}\0${ext}\0${cfg}\0${lm}\0${envSeg}\0${abSeg}\0${depSeg}`,
2354
+ noCache
2072
2355
  };
2073
2356
  }
2074
2357
  function cloneAnalysisResult(r) {
@@ -2086,12 +2369,20 @@ function cloneFunctionAnalysis(a) {
2086
2369
  return {
2087
2370
  ...a,
2088
2371
  paramNames: [...a.paramNames],
2372
+ ...a.formals ? { formals: a.formals.map((f) => ({ ...f })) } : {},
2089
2373
  cases: a.cases.map((c) => ({
2090
2374
  ...c,
2091
2375
  argAbs: [...c.argAbs],
2092
2376
  ...c.intension ? { intension: { ...c.intension } } : {}
2093
2377
  })),
2094
- loc: { start: { ...a.loc.start }, end: { ...a.loc.end } }
2378
+ loc: { start: { ...a.loc.start }, end: { ...a.loc.end } },
2379
+ ...a.hof ? {
2380
+ hof: {
2381
+ ...a.hof.fnRels ? { fnRels: a.hof.fnRels.map((r) => ({ ...r })) } : {},
2382
+ ...a.hof.entryShapes ? { entryShapes: a.hof.entryShapes.map((s) => ({ ...s })) } : {},
2383
+ ...a.hof.symbolic ? { symbolic: a.hof.symbolic } : {}
2384
+ }
2385
+ } : {}
2095
2386
  };
2096
2387
  }
2097
2388
  function shiftSourceLoc(loc, lineDelta) {
@@ -2115,17 +2406,60 @@ function shiftCallRecordLines(r, lineDelta) {
2115
2406
  callLoc: { line: r.callLoc.line + lineDelta, column: r.callLoc.column }
2116
2407
  };
2117
2408
  }
2118
- function analyzeFile(filePath, source, activeCases, externalCallRecords) {
2119
- const k = analysisFileCacheKey(filePath, source, activeCases, externalCallRecords);
2120
- const hit = analysisCacheGet(k.filePath, k.source, k.auxKey);
2121
- if (hit !== void 0) {
2122
- return cloneAnalysisResult(hit);
2409
+ function analyzeFile(filePath, source, activeCases, externalCallRecords, loadModule) {
2410
+ const projectConfig = findProjectConfig(dirname7(filePath));
2411
+ const cfg = analysisConfig(projectConfig?.config);
2412
+ const projectEnvNames = projectConfig?.config.env ?? [];
2413
+ const autoBind = interfaceConfig(projectConfig?.config).autoBind;
2414
+ const k = analysisFileCacheKey(
2415
+ filePath,
2416
+ source,
2417
+ activeCases,
2418
+ externalCallRecords,
2419
+ cfg,
2420
+ loadModule,
2421
+ projectEnvNames,
2422
+ autoBind !== false
2423
+ );
2424
+ if (!k.noCache) {
2425
+ const hit = analysisCacheGet(k.filePath, k.source, k.auxKey);
2426
+ if (hit !== void 0) {
2427
+ return cloneAnalysisResult(hit);
2428
+ }
2429
+ }
2430
+ const result = analyzeFileUncached(filePath, source, activeCases, externalCallRecords, loadModule);
2431
+ if (!k.noCache) {
2432
+ analysisCacheSet(k.filePath, k.source, k.auxKey, result);
2123
2433
  }
2124
- const result = analyzeFileUncached(filePath, source, activeCases, externalCallRecords);
2125
- analysisCacheSet(k.filePath, k.source, k.auxKey, result);
2126
2434
  return cloneAnalysisResult(result);
2127
2435
  }
2128
- function analyzeFileUncached(filePath, source, activeCases, externalCallRecords) {
2436
+ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords, loadModule) {
2437
+ const projectConfig = findProjectConfig(dirname7(filePath));
2438
+ const missingSlotOn = analysisConfig(projectConfig?.config).evalMissingSlot === "warning";
2439
+ return runWithEvalMissingSlot(
2440
+ missingSlotOn,
2441
+ () => analyzeFileUncachedInner(filePath, source, activeCases, externalCallRecords, loadModule)
2442
+ );
2443
+ }
2444
+ function findModuleImportLoc(source, module) {
2445
+ const bare = module.startsWith("node:") ? module.slice("node:".length) : module;
2446
+ const alts = [.../* @__PURE__ */ new Set([module, bare, `node:${bare}`])];
2447
+ const lines = source.split(/\r?\n/);
2448
+ for (let i = 0; i < lines.length; i++) {
2449
+ const line = lines[i];
2450
+ for (const alt of alts) {
2451
+ const re = new RegExp(
2452
+ `["'\`]${alt.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["'\`]`
2453
+ );
2454
+ const m = re.exec(line);
2455
+ if (m && m.index !== void 0) {
2456
+ return { line: i, column: m.index, length: m[0].length };
2457
+ }
2458
+ }
2459
+ }
2460
+ return null;
2461
+ }
2462
+ function analyzeFileUncachedInner(filePath, source, activeCases, externalCallRecords, loadModule) {
2129
2463
  const ast = parse5(source);
2130
2464
  const functions = extractDirectives(ast);
2131
2465
  const diagnostics = [];
@@ -2133,20 +2467,47 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2133
2467
  const nodeAbsMap = /* @__PURE__ */ new Map();
2134
2468
  const functionResults = [];
2135
2469
  const caseHints = [];
2470
+ const envHarvestConflicts = [];
2471
+ const prevHarvestConflictCollector = setEnvHarvestConflictCollector((c) => {
2472
+ if (!envHarvestConflicts.some((x) => x.module === c.module)) {
2473
+ envHarvestConflicts.push(c);
2474
+ } else {
2475
+ const prev = envHarvestConflicts.find((x) => x.module === c.module);
2476
+ for (const e of c.exports) {
2477
+ if (!prev.exports.includes(e)) prev.exports.push(e);
2478
+ }
2479
+ prev.defaultOverwritten ||= c.defaultOverwritten;
2480
+ }
2481
+ });
2136
2482
  const fileDirectives = extractFileDirectives(ast);
2137
2483
  const fileEnvNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
2138
- const projectConfig = findProjectConfig(dirname6(filePath));
2484
+ const projectConfig = findProjectConfig(dirname7(filePath));
2139
2485
  const projectEnvNames = projectConfig?.config.env ?? [];
2140
2486
  const envNames = [.../* @__PURE__ */ new Set([...projectEnvNames, ...fileEnvNames])];
2487
+ const analysisCfg = analysisConfig(projectConfig?.config);
2488
+ const callSiteBudget = analysisCfg.callSiteBudget;
2141
2489
  const callRecords = [];
2142
2490
  const globalEnv = createEnvironment2();
2143
2491
  const bMemberDiagNames = /* @__PURE__ */ new Set();
2144
2492
  const bMemberDiagSeen = /* @__PURE__ */ new Set();
2145
2493
  const pushBMemberDiag = (d, fallbackLine) => {
2146
2494
  bMemberDiagNames.add(d.name);
2147
- const key = `${d.kind}:${d.name}:${d.receiver}:${d.line ?? fallbackLine}:${d.column ?? 0}`;
2495
+ const key = `${d.kind}:${d.name}:${d.receiver}:${d.line ?? fallbackLine}:${d.column ?? 0}:${d.code ?? ""}`;
2148
2496
  if (bMemberDiagSeen.has(key)) return;
2149
2497
  bMemberDiagSeen.add(key);
2498
+ if (d.code === "nudo:missing-slot") {
2499
+ diagnostics.push({
2500
+ range: {
2501
+ start: { line: d.line ?? fallbackLine, column: d.column ?? 0 },
2502
+ end: { line: d.line ?? fallbackLine, column: (d.column ?? 0) + d.name.length }
2503
+ },
2504
+ severity: "warning",
2505
+ message: `Field '${d.name}' is missing on the evaluated object shape`,
2506
+ code: "nudo:missing-slot",
2507
+ ...d.origin ? { origin: d.origin } : {}
2508
+ });
2509
+ return;
2510
+ }
2150
2511
  if (d.receiver === "unknown") {
2151
2512
  diagnostics.push({
2152
2513
  range: {
@@ -2204,9 +2565,10 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2204
2565
  try {
2205
2566
  const g = evalAbsModuleGraph(source, filePath, {
2206
2567
  seedVars: seeds.seedVars,
2207
- seedFns: seeds.seedFns
2568
+ seedFns: seeds.seedFns,
2569
+ ...loadModule ? { loadModule } : {}
2208
2570
  });
2209
- absGraphModules = { ...collectEnvModules(envNames), ...g.modules };
2571
+ absGraphModules = mergeHarvestUnderEnv(g.modules, collectEnvModules(envNames));
2210
2572
  pushBModuleIssues(g.issues);
2211
2573
  } catch {
2212
2574
  }
@@ -2235,7 +2597,7 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2235
2597
  if (selfContained || canAbsModules) {
2236
2598
  setAbsTruncationCollector2((label) => bTruncatedFns.add(label));
2237
2599
  try {
2238
- absCallRecords = collectAbsCallRecords(source, seeds, filePath, absGraphModules, envNames);
2600
+ absCallRecords = collectAbsCallRecords(source, seeds, filePath, absGraphModules, envNames, loadModule);
2239
2601
  } finally {
2240
2602
  setAbsTruncationCollector2(null);
2241
2603
  }
@@ -2257,7 +2619,10 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2257
2619
  } catch {
2258
2620
  }
2259
2621
  }
2260
- if ((selfContained || canAbsModules) && absCallRecords.length > 0) {
2622
+ if (bHostedEval && bTopCallRecords.length > 0) {
2623
+ callRecords.length = 0;
2624
+ callRecords.push(...bTopCallRecords);
2625
+ } else if ((selfContained || canAbsModules) && absCallRecords.length > 0) {
2261
2626
  callRecords.length = 0;
2262
2627
  callRecords.push(...absCallRecords);
2263
2628
  } else if (bTopCallRecords.length > 0) {
@@ -2345,7 +2710,16 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2345
2710
  const skipDirective = fn.directives.find((d) => d.kind === "skip");
2346
2711
  const fnLoc = locFromNode(fn.node);
2347
2712
  const paramNames = extractParamNames(fn.node);
2348
- const analysis = { name: fn.name, loc: fnLoc, paramNames, cases: [] };
2713
+ const formals = formalParamsFromNodes(
2714
+ fn.node.params ?? []
2715
+ );
2716
+ const analysis = {
2717
+ name: fn.name,
2718
+ loc: fnLoc,
2719
+ paramNames,
2720
+ ...formals.length > 0 ? { formals } : {},
2721
+ cases: []
2722
+ };
2349
2723
  if (skipDirective && skipDirective.kind === "skip") {
2350
2724
  analysis.skipped = true;
2351
2725
  if (skipDirective.returns) {
@@ -2357,14 +2731,31 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2357
2731
  const caseDirectives = fn.directives.filter((d) => d.kind === "case");
2358
2732
  const activeCaseIdx = activeCases?.get(fn.name) ?? 0;
2359
2733
  const fp = fnFpMap?.get(fn.name);
2360
- const fnCacheKey = fp && caseDirectives.length > 0 ? [
2734
+ const analysisFnKey = `m=${analysisCfg.mode}|e=${analysisCfg.evalMissingSlot}|b=${analysisCfg.callSiteBudget}`;
2735
+ let fnDepSeg = "-";
2736
+ let fnDepFailClosed = false;
2737
+ try {
2738
+ const dfp = loadModuleDepsFingerprint2(source, loadModule ?? defaultLoadModule, filePath);
2739
+ if (dfp.truncated) {
2740
+ fnDepFailClosed = true;
2741
+ fnDepSeg = null;
2742
+ } else {
2743
+ fnDepSeg = hashSource2(dfp.fp);
2744
+ }
2745
+ } catch {
2746
+ fnDepFailClosed = true;
2747
+ fnDepSeg = null;
2748
+ }
2749
+ const fnCacheKey = !fnDepFailClosed && fp && caseDirectives.length > 0 ? [
2361
2750
  filePath,
2362
2751
  fp.own,
2363
2752
  fp.deps,
2364
2753
  String(activeCaseIdx),
2365
2754
  caseDirectiveKey(caseDirectives, formatAbs4),
2366
2755
  envKeyFn,
2367
- mockKeyFn
2756
+ mockKeyFn,
2757
+ analysisFnKey,
2758
+ fnDepSeg ?? "-"
2368
2759
  ].join("\0") : void 0;
2369
2760
  const dLen0 = diagnostics.length;
2370
2761
  const hLen0 = caseHints.length;
@@ -2538,6 +2929,7 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2538
2929
  COLLAPSE_LITERAL_THRESHOLD
2539
2930
  );
2540
2931
  }
2932
+ attachHofSnapshot(analysis, source);
2541
2933
  if (fnCacheKey) {
2542
2934
  fnAnalysisCacheSet(fnCacheKey, {
2543
2935
  analysis: cloneFunctionAnalysis(analysis),
@@ -2571,7 +2963,7 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2571
2963
  }
2572
2964
  return n;
2573
2965
  };
2574
- const currentModulePath = normalizeModulePath(resolve5(filePath));
2966
+ const currentModulePath = normalizeModulePath(resolve6(filePath));
2575
2967
  const singleExportFn = findSingleModuleExportsFunction(ast);
2576
2968
  const reportInjectedDomainExceeds = (name, node, fallbackLoc) => {
2577
2969
  if (!externalCallRecords || externalCallRecords.length === 0) return;
@@ -2586,10 +2978,10 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2586
2978
  if (injected.length === 0) return;
2587
2979
  const nameLoc = fnNameLoc(node, fallbackLoc);
2588
2980
  const fnNode = resolveFunctionNode(node);
2589
- const autoBind = interfaceConfig(findProjectConfig(dirname6(filePath))?.config).autoBind;
2981
+ const autoBind = interfaceConfig(findProjectConfig(dirname7(filePath))?.config).autoBind;
2590
2982
  const domainIssues = checkInjectedDomainEvidence(name, source, injected, {
2591
2983
  paramNames: extractParamNames(fnNode),
2592
- loadModule: defaultLoadModule,
2984
+ loadModule: loadModule ?? defaultLoadModule,
2593
2985
  fromFile: filePath,
2594
2986
  loc: { line: nameLoc.start.line, column: nameLoc.start.column },
2595
2987
  ...autoBind === false ? { autoBind: false } : {}
@@ -2639,7 +3031,7 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2639
3031
  return 0;
2640
3032
  };
2641
3033
  const ordered = records.map((r, i) => ({ r, i })).sort((a, b) => informativeness(a.r) - informativeness(b.r) || a.i - b.i).map(({ r }) => r);
2642
- const precise = ordered.slice(0, MAX_PRECISE_CALLSITE_CASES);
3034
+ const precise = ordered.slice(0, callSiteBudget);
2643
3035
  for (const rec of precise) {
2644
3036
  let absRaw;
2645
3037
  let absResult;
@@ -2666,7 +3058,7 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2666
3058
  if (absRaw) attachAbsToIntension(caseResult2, absRaw, candidate.name);
2667
3059
  candidate.analysis.cases.push(caseResult2);
2668
3060
  }
2669
- const remaining = ordered.slice(MAX_PRECISE_CALLSITE_CASES).filter((rec) => !rec.argAbs.some((a) => a.shape.k === "unknown" && !a.term));
3061
+ const remaining = ordered.slice(callSiteBudget).filter((rec) => !rec.argAbs.some((a) => a.shape.k === "unknown" && !a.term));
2670
3062
  if (remaining.length > 0) {
2671
3063
  const fnNode2 = resolveFunctionNode(candidate.node);
2672
3064
  const paramCount = extractParamNames(fnNode2).length;
@@ -2707,7 +3099,8 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2707
3099
  const symCase = {
2708
3100
  name: "call@symbolic",
2709
3101
  argAbs: widenedArgsAbs,
2710
- abs: symAbs ?? absUnknown2,
3102
+ // B4:超预算聚合必须 #widened(可解释降级)
3103
+ abs: symAbs ? { ...symAbs, conf: symAbs.conf === "exact" ? "widened" : symAbs.conf } : absUnknown2,
2711
3104
  throwsAbs: neverAbs,
2712
3105
  source: "callsite",
2713
3106
  aggregatedFrom: remaining.length
@@ -2771,11 +3164,34 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords)
2771
3164
  candidate.analysis.cases.push(caseResult);
2772
3165
  candidate.analysis.entryOnly = true;
2773
3166
  candidate.analysis.combinedAbs = entryAbs;
3167
+ attachHofSnapshot(candidate.analysis, source);
2774
3168
  }
2775
3169
  if (!bHostedEval) {
2776
3170
  buildNodeTypeMap(ast, globalEnv, nodeAbsMap);
2777
3171
  }
2778
- const externalFunctions = synthesizeExternalFunctions(callRecords, filePath);
3172
+ const externalFunctions = synthesizeExternalFunctions(
3173
+ callRecords,
3174
+ filePath,
3175
+ analysisConfig(projectConfig?.config).callSiteBudget
3176
+ );
3177
+ setEnvHarvestConflictCollector(prevHarvestConflictCollector);
3178
+ for (const c of envHarvestConflicts) {
3179
+ const parts = [];
3180
+ if (c.exports.length > 0) parts.push(`export(s) ${c.exports.join(", ")}`);
3181
+ if (c.defaultOverwritten) parts.push("default");
3182
+ const detail = parts.length > 0 ? ` \u2014 ${parts.join("; ")}` : "";
3183
+ const loc = findModuleImportLoc(source, c.module);
3184
+ const range = loc ? {
3185
+ start: { line: loc.line, column: loc.column },
3186
+ end: { line: loc.line, column: loc.column + loc.length }
3187
+ } : { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } };
3188
+ diagnostics.push({
3189
+ range,
3190
+ severity: "warning",
3191
+ message: `handwritten @nudo:env wins over harvest on module "${c.module}"${detail}; harvest only fills missing slots (B8). code=nudo:env-harvest-conflict`,
3192
+ code: "nudo:env-harvest-conflict"
3193
+ });
3194
+ }
2779
3195
  return {
2780
3196
  functions: functionResults,
2781
3197
  diagnostics,
@@ -2870,7 +3286,7 @@ function absModulesOk(source, envNames) {
2870
3286
  void envNames;
2871
3287
  return true;
2872
3288
  }
2873
- function collectAbsCallRecords(source, seeds, filePath, precomputedModules, envNames = []) {
3289
+ function collectAbsCallRecords(source, seeds, filePath, precomputedModules, envNames = [], loadModule) {
2874
3290
  const absCalls = [];
2875
3291
  let modules = precomputedModules;
2876
3292
  let importLocals = /* @__PURE__ */ new Map();
@@ -2879,7 +3295,8 @@ function collectAbsCallRecords(source, seeds, filePath, precomputedModules, envN
2879
3295
  try {
2880
3296
  const graph = evalAbsModuleGraph(source, filePath, {
2881
3297
  seedVars: seeds?.seedVars,
2882
- seedFns: seeds?.seedFns
3298
+ seedFns: seeds?.seedFns,
3299
+ ...loadModule ? { loadModule } : {}
2883
3300
  });
2884
3301
  modules = graph.modules;
2885
3302
  } catch {
@@ -2887,7 +3304,7 @@ function collectAbsCallRecords(source, seeds, filePath, precomputedModules, envN
2887
3304
  }
2888
3305
  }
2889
3306
  if (envNames.length > 0) {
2890
- modules = { ...collectEnvModules(envNames), ...modules ?? {} };
3307
+ modules = mergeHarvestUnderEnv(modules ?? {}, collectEnvModules(envNames));
2891
3308
  }
2892
3309
  try {
2893
3310
  importLocals = buildAbsImportLocalMap(source, filePath);
@@ -2986,10 +3403,10 @@ function buildAbsImportLocalMap(source, fromFile) {
2986
3403
  return out;
2987
3404
  }
2988
3405
  function resolveImportAbs(spec, fromFile) {
2989
- const base = dirname6(resolve5(fromFile));
2990
- const p = resolve5(base, spec);
2991
- for (const cand of [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve5(p, "index.js")]) {
2992
- if (existsSync4(cand) && !statSync4(cand).isDirectory()) return cand;
3406
+ const base = dirname7(resolve6(fromFile));
3407
+ const p = resolve6(base, spec);
3408
+ for (const cand of [p, `${p}.js`, `${p}.mjs`, `${p}.ts`, resolve6(p, "index.js")]) {
3409
+ if (existsSync5(cand) && !statSync5(cand).isDirectory()) return cand;
2993
3410
  }
2994
3411
  return null;
2995
3412
  }
@@ -3063,6 +3480,22 @@ function tryAttachIntension(caseResult, source, fnName) {
3063
3480
  } catch {
3064
3481
  }
3065
3482
  }
3483
+ function attachHofSnapshot(analysis, source) {
3484
+ if (analysis.hof) return;
3485
+ try {
3486
+ const g = generalizeFromAst2(analysis.name, source);
3487
+ if (!g) return;
3488
+ const fnRels = g.fnRels ? [...g.fnRels.entries()].map(([param, rec]) => ({ param, abs: rec.abs })) : void 0;
3489
+ const entryShapes = g.entryShapes ? [...g.entryShapes.entries()].map(([param, rec]) => ({ param, abs: rec.abs })) : void 0;
3490
+ if (!fnRels?.length && !entryShapes?.length) return;
3491
+ analysis.hof = {
3492
+ ...fnRels?.length ? { fnRels } : {},
3493
+ ...entryShapes?.length ? { entryShapes } : {},
3494
+ symbolic: g.symbolic
3495
+ };
3496
+ } catch {
3497
+ }
3498
+ }
3066
3499
  function attachAbsToIntension(caseResult, absVal, label) {
3067
3500
  const prev = caseResult.intension ?? {};
3068
3501
  caseResult.abs = absVal;
@@ -3074,10 +3507,27 @@ function attachAbsToIntension(caseResult, absVal, label) {
3074
3507
  };
3075
3508
  }
3076
3509
  function safeAbsOrUnknown(a) {
3077
- if (!a || typeof a !== "object" || !("shape" in a) || !a.shape) {
3078
- return { shape: { k: "unknown" }, conf: "opaque" };
3510
+ if (a && typeof a === "object" && "shape" in a && "conf" in a) {
3511
+ return a;
3512
+ }
3513
+ if (typeof a === "function") {
3514
+ const fn = a;
3515
+ const n = Math.max(0, fn.length);
3516
+ const params = Array.from({ length: n }, (_, i) => `arg${i}`);
3517
+ return absFunction4(params, {
3518
+ body: { type: "BlockStatement", body: [], directives: [] },
3519
+ apply: (args) => {
3520
+ try {
3521
+ const r = fn(...args);
3522
+ if (r && typeof r === "object" && "shape" in r) return r;
3523
+ return absUnknown2;
3524
+ } catch {
3525
+ return absUnknown2;
3526
+ }
3527
+ }
3528
+ });
3079
3529
  }
3080
- return a;
3530
+ return absUnknown2;
3081
3531
  }
3082
3532
  function callRecordFromAbsCall(r, impMap) {
3083
3533
  const argAbs = r.args.map(safeAbsOrUnknown);
@@ -3099,7 +3549,7 @@ function callRecordFromAbsCall(r, impMap) {
3099
3549
  }
3100
3550
 
3101
3551
  // src/lsp-surface.ts
3102
- import { dirname as dirname7 } from "path";
3552
+ import { dirname as dirname8 } from "path";
3103
3553
  import traverse2 from "@babel/traverse";
3104
3554
  import {
3105
3555
  generalizeFromAst as generalizeFromAst3,
@@ -3109,7 +3559,8 @@ import {
3109
3559
  collectAbsNodeTypes,
3110
3560
  findAbsAtPosition,
3111
3561
  evalSource,
3112
- setAbsNodeCollector as setAbsNodeCollector2
3562
+ setAbsNodeCollector as setAbsNodeCollector2,
3563
+ interfaceTierOf
3113
3564
  } from "@nudojs/core";
3114
3565
  import { parse as parse6, extractDirectives as extractDirectives2, extractFileDirectives as extractFileDirectives2 } from "@nudojs/parser";
3115
3566
  function getCasesForFile(filePath, source) {
@@ -3123,14 +3574,14 @@ function getCasesForFile(filePath, source) {
3123
3574
  async function getTypeAtPositionAsync(filePath, source, line, column, activeCases) {
3124
3575
  const envNames = collectEnvNames(filePath, source, false);
3125
3576
  if (envNames.length > 0) {
3126
- await preloadPathEnvs(envNames, dirname7(filePath));
3577
+ await preloadPathEnvs(envNames, dirname8(filePath));
3127
3578
  }
3128
3579
  return getTypeAtPosition(filePath, source, line, column, activeCases);
3129
3580
  }
3130
3581
  async function getAbsAtPositionAsync(filePath, source, line, column, activeCases) {
3131
3582
  const envNames = collectEnvNames(filePath, source, false);
3132
3583
  if (envNames.length > 0) {
3133
- await preloadPathEnvs(envNames, dirname7(filePath));
3584
+ await preloadPathEnvs(envNames, dirname8(filePath));
3134
3585
  }
3135
3586
  return getAbsAtPosition(filePath, source, line, column, activeCases);
3136
3587
  }
@@ -3218,7 +3669,7 @@ function getAbsAtPosition(filePath, source, line, column, activeCases) {
3218
3669
  function getTypeAtPosition(filePath, source, line, column, activeCases) {
3219
3670
  return getAbsAtPosition(filePath, source, line, column, activeCases);
3220
3671
  }
3221
- function getHoverAtPosition(filePath, source, line, column, activeCases) {
3672
+ function getHoverAtPosition(filePath, source, line, column, activeCases, opts) {
3222
3673
  let file;
3223
3674
  try {
3224
3675
  file = parse6(source);
@@ -3227,6 +3678,7 @@ function getHoverAtPosition(filePath, source, line, column, activeCases) {
3227
3678
  }
3228
3679
  const envNames = collectEnvNames(filePath, source, false);
3229
3680
  const fnName = findFunctionNameAtPosition(source, line, column, file);
3681
+ const tier = fnName !== void 0 ? interfaceTierOf(source, fnName, filePath, opts ?? {}) : void 0;
3230
3682
  let gDisplay;
3231
3683
  let gAbs;
3232
3684
  let gMulti;
@@ -3241,18 +3693,26 @@ function getHoverAtPosition(filePath, source, line, column, activeCases) {
3241
3693
  } catch {
3242
3694
  }
3243
3695
  }
3696
+ const withTier = (info) => {
3697
+ if (!info || !tier) return info;
3698
+ return {
3699
+ ...info,
3700
+ interfaceSource: tier.source,
3701
+ ...tier.display !== void 0 ? { interfaceDisplay: tier.display } : {}
3702
+ };
3703
+ };
3244
3704
  const attachIntension = (info) => {
3245
- if (!gDisplay) return info;
3705
+ if (!gDisplay) return withTier(info);
3246
3706
  if (!info) {
3247
- return { typeText: gDisplay, intension: gDisplay, abs: gAbs, absMultiline: gMulti };
3707
+ return withTier({ typeText: gDisplay, intension: gDisplay, abs: gAbs, absMultiline: gMulti });
3248
3708
  }
3249
- return {
3709
+ return withTier({
3250
3710
  ...info,
3251
3711
  intension: gDisplay,
3252
3712
  // 外延侧已有更准 Abs 时保留;否则用 symbolic 兜底
3253
3713
  abs: info.abs ?? gAbs,
3254
3714
  absMultiline: info.absMultiline ?? gMulti
3255
- };
3715
+ });
3256
3716
  };
3257
3717
  const insideCaseFn = positionInsideCaseFunction(
3258
3718
  source,
@@ -3333,6 +3793,14 @@ function getHoverAtPosition(filePath, source, line, column, activeCases) {
3333
3793
  }
3334
3794
  } catch {
3335
3795
  }
3796
+ if (tier && gDisplay) {
3797
+ return withTier({
3798
+ typeText: gDisplay,
3799
+ intension: gDisplay,
3800
+ abs: gAbs,
3801
+ absMultiline: gMulti
3802
+ });
3803
+ }
3336
3804
  return null;
3337
3805
  }
3338
3806
  function findIdentNameAtPosition(source, line, column, fileAst) {
@@ -3657,10 +4125,111 @@ function serializeInferJson(result, file) {
3657
4125
  function isNudoTargetPath(path) {
3658
4126
  const lower = path.toLowerCase();
3659
4127
  if (lower.endsWith(".d.ts")) return false;
3660
- if (lower.endsWith(".nudo.js") || lower.endsWith(".nudo.ts")) return false;
4128
+ if (lower.endsWith(".nudo.js") || lower.endsWith(".nudo.mjs") || lower.endsWith(".nudo.ts") || lower.endsWith(".nudo.draft.js") || lower.endsWith(".nudo.draft.mjs") || lower.endsWith(".nudo.draft.ts")) {
4129
+ return false;
4130
+ }
3661
4131
  return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".ts");
3662
4132
  }
3663
4133
 
4134
+ // src/watch-paths.ts
4135
+ var PROJECT_CONFIG_BASENAMES = /* @__PURE__ */ new Set([
4136
+ "package.json",
4137
+ "nudo.json",
4138
+ "nudo.config.js",
4139
+ "nudo.config.mjs",
4140
+ "nudo.config.ts",
4141
+ ".nudorc",
4142
+ ".nudorc.json"
4143
+ ]);
4144
+ function isSidecarPath(path) {
4145
+ const lower = path.toLowerCase().replace(/\\/g, "/");
4146
+ if (lower.includes(".nudo.draft.")) return false;
4147
+ return lower.endsWith(".nudo.js") || lower.endsWith(".nudo.mjs") || lower.endsWith(".nudo.ts");
4148
+ }
4149
+ function isProjectConfigPath(path) {
4150
+ const norm2 = path.replace(/\\/g, "/");
4151
+ if (/\/node_modules\//.test(`/${norm2}`)) return false;
4152
+ const base = norm2.split("/").pop() ?? path;
4153
+ return PROJECT_CONFIG_BASENAMES.has(base.toLowerCase());
4154
+ }
4155
+ function isWatchRelevantPath(path) {
4156
+ return isNudoTargetPath(path) || isSidecarPath(path) || isProjectConfigPath(path) || isEnvTemplatePath(path);
4157
+ }
4158
+ function ambientSourcesOfSidecar(sidecarPath) {
4159
+ const norm2 = sidecarPath.replace(/\\/g, "/");
4160
+ const m = norm2.match(/^(.*)\.nudo\.(js|ts)$/i);
4161
+ if (!m || norm2.toLowerCase().includes(".nudo.draft.")) return [];
4162
+ const base = m[1];
4163
+ const ext = m[2].toLowerCase();
4164
+ return ext === "js" ? [`${base}.js`, `${base}.mjs`] : [`${base}.ts`, `${base}.mts`];
4165
+ }
4166
+
4167
+ // src/analysis-scope.ts
4168
+ import { dirname as dirname9 } from "path";
4169
+ import { existsSync as existsSync6 } from "fs";
4170
+ import { sidecarPathOf } from "@nudojs/core";
4171
+ var NOISY_WARNING_CODES = /* @__PURE__ */ new Set([
4172
+ "nudo:unknown-recv",
4173
+ "nudo:builtin-unknown",
4174
+ "nudo:no-signature"
4175
+ ]);
4176
+ function filterDiagnosticsByLevel(diags, level) {
4177
+ if (level === "verbose") return diags;
4178
+ if (level === "off") return [];
4179
+ if (level === "errors") return diags.filter((d) => d.severity === "error");
4180
+ return diags.filter((d) => {
4181
+ if (d.severity === "error") return true;
4182
+ if (d.severity === "info") return false;
4183
+ if (d.severity === "warning" && d.code && NOISY_WARNING_CODES.has(d.code)) return false;
4184
+ return d.severity === "warning";
4185
+ });
4186
+ }
4187
+ function diagnosticsLevelForFile(filePath) {
4188
+ return analysisConfig(findProjectConfig(dirname9(filePath))?.config).diagnostics;
4189
+ }
4190
+ function hasNudoDirectives(source) {
4191
+ return /@nudo:(case|mock|pure|skip|sample|refine|interface|import|env|mock-module|as|replace)\b/.test(source);
4192
+ }
4193
+ function stripCommentsAndStrings(source) {
4194
+ return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ").replace(/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`/g, '""');
4195
+ }
4196
+ function hasExport(source) {
4197
+ const s = stripCommentsAndStrings(source);
4198
+ return /(^|[\s;}])export\b/.test(s) || /\bmodule\.exports\b/.test(s) || /\bexports\s*[.[]/.test(s) || /Object\.assign\s*\(\s*(module\.)?exports\b/.test(s);
4199
+ }
4200
+ function shouldAnalyzeFile(filePath, source, config) {
4201
+ if (!isNudoTargetPath(filePath)) return false;
4202
+ const proj = findProjectConfig(dirname9(filePath));
4203
+ const cfg = config ?? analysisConfig(proj?.config);
4204
+ const projectDir = proj?.projectDir;
4205
+ if (cfg.exclude.length > 0 && projectDir) {
4206
+ if (matchesEmitAllowlist(filePath, projectDir, cfg.exclude)) return false;
4207
+ } else if (cfg.exclude.length > 0 && !projectDir) {
4208
+ const norm2 = filePath.replace(/\\/g, "/");
4209
+ if (cfg.exclude.some((p) => simpleExcludeHit(p, norm2))) return false;
4210
+ }
4211
+ if (cfg.include.length > 0 && projectDir) {
4212
+ if (!matchesEmitAllowlist(filePath, projectDir, cfg.include)) return false;
4213
+ }
4214
+ if (cfg.mode === "all") return true;
4215
+ const text = source;
4216
+ if (text !== void 0 && hasNudoDirectives(text)) return true;
4217
+ if (cfg.mode === "directives") return false;
4218
+ if (text !== void 0 && hasExport(text)) return true;
4219
+ try {
4220
+ if (existsSync6(sidecarPathOf(filePath))) return true;
4221
+ } catch {
4222
+ }
4223
+ return false;
4224
+ }
4225
+ function simpleExcludeHit(pattern, path) {
4226
+ const bare = pattern.replace(/^\*\*/, "").replace(/^\//, "").replace(/\/\*\*$/, "").replace(/^\*\//, "");
4227
+ if (!bare || bare.includes("*")) {
4228
+ return /\/(node_modules|dist|coverage)\//.test("/" + path + "/");
4229
+ }
4230
+ return path.includes(`/${bare}/`) || path.endsWith(`/${bare}`);
4231
+ }
4232
+
3664
4233
  // src/session-cache.ts
3665
4234
  import {
3666
4235
  resetGeneralizeMemo,
@@ -3674,10 +4243,12 @@ function evictAnalysisCachesForFiles(files) {
3674
4243
  evictAnalysisFileCacheForFiles(files);
3675
4244
  evictFnAnalysisCacheForFiles(files);
3676
4245
  evictAbsModuleCacheFiles(files);
4246
+ clearPathEnvCaches();
3677
4247
  }
3678
4248
  function clearAnalysisSessionCaches() {
3679
4249
  clearBPathCache();
3680
4250
  clearAbsModuleCache();
4251
+ clearPathEnvCaches();
3681
4252
  resetGeneralizeMemo();
3682
4253
  resetCheckSourceMemo();
3683
4254
  resetNudoModuleExecCache();
@@ -3687,20 +4258,269 @@ function resetAllAnalysisCaches() {
3687
4258
  resetParseSourceCache();
3688
4259
  }
3689
4260
 
4261
+ // src/analysis-session.ts
4262
+ function createDefaultSession() {
4263
+ return {
4264
+ evictForDependents(files) {
4265
+ evictAnalysisCachesForFiles(files);
4266
+ },
4267
+ clear() {
4268
+ clearAnalysisSessionCaches();
4269
+ },
4270
+ reset() {
4271
+ resetAllAnalysisCaches();
4272
+ },
4273
+ analyze(filePath, source, activeCases, externalCallRecords) {
4274
+ return analyzeFile(
4275
+ filePath,
4276
+ source,
4277
+ activeCases,
4278
+ externalCallRecords
4279
+ );
4280
+ }
4281
+ };
4282
+ }
4283
+ var defaultSession = void 0;
4284
+ function getAnalysisSession() {
4285
+ if (!defaultSession) defaultSession = createDefaultSession();
4286
+ return defaultSession;
4287
+ }
4288
+ function setAnalysisSession(session) {
4289
+ const prev = defaultSession;
4290
+ defaultSession = session;
4291
+ return prev;
4292
+ }
4293
+
4294
+ // src/disk-cache.ts
4295
+ import { createHash } from "crypto";
4296
+ import { createRequire } from "module";
4297
+ import { mkdirSync, readFileSync as readFileSync6, writeFileSync, existsSync as existsSync7, rmSync } from "fs";
4298
+ import { join as join5, dirname as dirname10, relative, sep, isAbsolute } from "path";
4299
+ function readServiceVersion() {
4300
+ try {
4301
+ const require2 = createRequire(import.meta.url);
4302
+ for (const p of ["../package.json", "./package.json", "../../package.json"]) {
4303
+ try {
4304
+ const pkg = require2(p);
4305
+ if (pkg?.name === "@nudojs/service" && pkg.version) return pkg.version;
4306
+ if (pkg?.version && p.includes("service")) return pkg.version;
4307
+ } catch {
4308
+ }
4309
+ }
4310
+ } catch {
4311
+ }
4312
+ return "0";
4313
+ }
4314
+ var ANALYSIS_ABI = `nudo-check-cache-v2+${readServiceVersion()}`;
4315
+ function sha256Hex(data) {
4316
+ return createHash("sha256").update(data).digest("hex");
4317
+ }
4318
+ function relativizePath(p, root) {
4319
+ const norm2 = p.split(sep).join("/");
4320
+ if (!root) return norm2;
4321
+ const r = relative(root, p).split(sep).join("/");
4322
+ if (!r.startsWith("..") && !isAbsolute(r)) return r;
4323
+ return `ext:${createHash("sha256").update(norm2).digest("hex").slice(0, 16)}`;
4324
+ }
4325
+ function sanitizeCacheNamespace(ns) {
4326
+ return ns.replace(/[^a-z0-9_-]/gi, "_").replace(/^_+|_+$/g, "") || "cache";
4327
+ }
4328
+ var DiskCache = class {
4329
+ root;
4330
+ ns;
4331
+ enabled = false;
4332
+ constructor(opts) {
4333
+ this.root = opts.root;
4334
+ this.ns = sanitizeCacheNamespace(opts.namespace);
4335
+ this.enabled = !!opts.root;
4336
+ }
4337
+ pathFor(key) {
4338
+ if (!/^[a-f0-9]{16,128}$/i.test(key)) {
4339
+ throw new Error("DiskCache key must be a hex digest");
4340
+ }
4341
+ return join5(this.root, this.ns, key.slice(0, 2), `${key}.json`);
4342
+ }
4343
+ get(key) {
4344
+ if (!this.enabled || !this.root) return void 0;
4345
+ try {
4346
+ const p = this.pathFor(key);
4347
+ if (!existsSync7(p)) return void 0;
4348
+ const raw = readFileSync6(p, "utf8");
4349
+ const parsed = JSON.parse(raw);
4350
+ if (parsed?.abi !== ANALYSIS_ABI) return void 0;
4351
+ return parsed.value;
4352
+ } catch {
4353
+ return void 0;
4354
+ }
4355
+ }
4356
+ set(key, value) {
4357
+ if (!this.enabled || !this.root) return;
4358
+ try {
4359
+ const p = this.pathFor(key);
4360
+ mkdirSync(dirname10(p), { recursive: true });
4361
+ writeFileSync(p, JSON.stringify({ abi: ANALYSIS_ABI, value }), "utf8");
4362
+ } catch {
4363
+ }
4364
+ }
4365
+ clearNamespace() {
4366
+ if (!this.enabled || !this.root) return;
4367
+ try {
4368
+ rmSync(join5(this.root, this.ns), { recursive: true, force: true });
4369
+ } catch {
4370
+ }
4371
+ }
4372
+ };
4373
+ function checkCacheKey(filePath, source, opts) {
4374
+ const rel = relativizePath(filePath, opts.projectDir);
4375
+ const sidecarSha = opts.sidecarContent != null ? sha256Hex(opts.sidecarContent) : "nosidecar";
4376
+ const depSeg = (opts.depContents ?? []).map(
4377
+ (d) => `${relativizePath(d.path, opts.projectDir)}\0${d.content != null ? sha256Hex(d.content) : "miss"}`
4378
+ ).join("\n");
4379
+ const envSeg = (opts.projectEnvNames ?? []).length > 0 ? [...opts.projectEnvNames ?? []].sort().join(",") : "-";
4380
+ const cfgSeg = opts.analysisCfg ? `${opts.analysisCfg.mode ?? "-"}|${opts.analysisCfg.evalMissingSlot ?? "-"}|${opts.analysisCfg.callSiteBudget ?? "-"}` : "-";
4381
+ return sha256Hex(
4382
+ [
4383
+ ANALYSIS_ABI,
4384
+ rel,
4385
+ opts.autoBind ? "ab1" : "ab0",
4386
+ sha256Hex(source),
4387
+ sidecarSha,
4388
+ depSeg,
4389
+ envSeg,
4390
+ cfgSeg
4391
+ ].join("\0")
4392
+ );
4393
+ }
4394
+ function ifaceCacheKey(filePath, source, opts) {
4395
+ const rel = relativizePath(filePath, opts.projectDir);
4396
+ const sidecarSeg = opts.autoBind && opts.sidecarSource !== void 0 ? `sc:${sha256Hex(opts.sidecarSource)}` : "sc0";
4397
+ const depSeg = (opts.depContents ?? []).map(
4398
+ (d) => `${relativizePath(d.path, opts.projectDir)}\0${d.content != null ? sha256Hex(d.content) : "miss"}`
4399
+ ).join("\n");
4400
+ const envSeg = (opts.projectEnvNames ?? []).length > 0 ? [...opts.projectEnvNames ?? []].sort().join(",") : "-";
4401
+ return sha256Hex(
4402
+ [
4403
+ ANALYSIS_ABI,
4404
+ "iface",
4405
+ rel,
4406
+ opts.autoBind ? "ab1" : "ab0",
4407
+ sha256Hex(source),
4408
+ sidecarSeg,
4409
+ depSeg,
4410
+ envSeg
4411
+ ].join("\0")
4412
+ );
4413
+ }
4414
+ function extractNudoImportSpecs(source) {
4415
+ const specs = /* @__PURE__ */ new Set();
4416
+ const named = /@nudo:import\s*\{[^}]*\}\s*from\s*["']([^"']+)["']/g;
4417
+ const ns = /@nudo:import\s+\*\s+as\s+\w+\s+from\s*["']([^"']+)["']/g;
4418
+ let m;
4419
+ while (m = named.exec(source)) specs.add(m[1]);
4420
+ while (m = ns.exec(source)) specs.add(m[1]);
4421
+ return [...specs];
4422
+ }
4423
+
4424
+ // src/dep-contents.ts
4425
+ import { loadModuleDepsFingerprint as loadModuleDepsFingerprint3 } from "@nudojs/core";
4426
+ function collectLoadDepContents(filePath, source, loadModule) {
4427
+ const fp = loadModuleDepsFingerprint3(source, loadModule, filePath);
4428
+ const depContents = fp.contents.length > 0 ? fp.contents : fp.paths.map((path) => ({ path, content: null }));
4429
+ return { depContents, truncated: fp.truncated };
4430
+ }
4431
+
3690
4432
  // src/harvest-node.ts
3691
- import { existsSync as existsSync5 } from "fs";
4433
+ import { existsSync as existsSync8, statSync as statSync6 } from "fs";
4434
+ import { join as join6 } from "path";
3692
4435
  import { harvestDts as harvestDts2 } from "@nudojs/harvester";
3693
- function harvestNodeTypes(fromDir, maxFiles = 12, maxMs = 2500) {
4436
+ var HARVEST_NODE_DEFAULT_MAX_FILES = 12;
4437
+ var HARVEST_NODE_DEFAULT_MAX_MS = 2500;
4438
+ var nodeHarvestCache = /* @__PURE__ */ new Map();
4439
+ function notFoundCacheKey(fromDir) {
4440
+ return `not-found|@types/node|${fromDir ?? ""}`;
4441
+ }
4442
+ function nodeHarvestCacheKey(root, maxFiles, maxMs) {
4443
+ let sig = "nostat";
4444
+ try {
4445
+ const st = statSync6(join6(root, "package.json"));
4446
+ sig = `${st.size}:${Math.floor(st.mtimeMs)}`;
4447
+ } catch {
4448
+ }
4449
+ return `${root}|${maxFiles}|${maxMs}|${sig}`;
4450
+ }
4451
+ function clearNodeHarvestCache() {
4452
+ nodeHarvestCache.clear();
4453
+ }
4454
+ function getNodeHarvestCacheSize() {
4455
+ return nodeHarvestCache.size;
4456
+ }
4457
+ function isHarvestNodeDisabled(env = process.env) {
4458
+ return env.NUDO_HARVEST_NODE === "off";
4459
+ }
4460
+ function harvestNodeTypes(fromDir, maxFiles = HARVEST_NODE_DEFAULT_MAX_FILES, maxMs = HARVEST_NODE_DEFAULT_MAX_MS) {
4461
+ if (isHarvestNodeDisabled()) {
4462
+ return {
4463
+ ok: false,
4464
+ error: "harvest disabled via NUDO_HARVEST_NODE=off",
4465
+ reason: "disabled"
4466
+ };
4467
+ }
3694
4468
  const root = resolvePackageRoot("@types/node", fromDir) ?? resolvePackageRoot("node", fromDir);
3695
- if (!root || !existsSync5(root)) {
3696
- return { ok: false, error: "@types/node not found" };
4469
+ if (!root || !existsSync8(root)) {
4470
+ const missKey = notFoundCacheKey(fromDir);
4471
+ const miss = nodeHarvestCache.get(missKey);
4472
+ if (miss && !miss.ok && miss.reason === "not-found") {
4473
+ return { ...miss, cached: true };
4474
+ }
4475
+ const result = {
4476
+ ok: false,
4477
+ error: "@types/node not found",
4478
+ reason: "not-found"
4479
+ };
4480
+ nodeHarvestCache.set(missKey, result);
4481
+ return result;
4482
+ }
4483
+ const key = nodeHarvestCacheKey(root, maxFiles, maxMs);
4484
+ const hit = nodeHarvestCache.get(key);
4485
+ if (hit) {
4486
+ return hit.ok ? { ...hit, cached: true } : { ...hit, cached: true };
3697
4487
  }
3698
4488
  const dts = collectDtsFiles(root, maxFiles);
3699
4489
  if (dts.length === 0) {
3700
- return { ok: false, error: `no .d.ts under ${root}` };
4490
+ const result = {
4491
+ ok: false,
4492
+ error: `no .d.ts under ${root}`,
4493
+ reason: "no-dts"
4494
+ };
4495
+ nodeHarvestCache.set(key, result);
4496
+ return result;
4497
+ }
4498
+ try {
4499
+ const env = harvestDts2(dts, { maxMs });
4500
+ const stats = {
4501
+ files: env.stats.files,
4502
+ symbols: env.stats.symbols,
4503
+ skipped: env.stats.skipped
4504
+ };
4505
+ const result = {
4506
+ ok: true,
4507
+ env,
4508
+ root,
4509
+ files: stats.files,
4510
+ stats,
4511
+ cached: false
4512
+ };
4513
+ nodeHarvestCache.set(key, result);
4514
+ return result;
4515
+ } catch (e) {
4516
+ const result = {
4517
+ ok: false,
4518
+ error: `harvest failed: ${e.message}`,
4519
+ reason: "failed"
4520
+ };
4521
+ nodeHarvestCache.set(key, result);
4522
+ return result;
3701
4523
  }
3702
- const env = harvestDts2(dts, { maxMs });
3703
- return { ok: true, env, root, files: env.stats.files };
3704
4524
  }
3705
4525
  function summarizeNodeEnv(env) {
3706
4526
  return {
@@ -3712,6 +4532,9 @@ function summarizeNodeEnv(env) {
3712
4532
 
3713
4533
  // src/semantic-tokens.ts
3714
4534
  import { parse as parse7 } from "@nudojs/parser";
4535
+ import {
4536
+ interfaceTierOf as interfaceTierOf2
4537
+ } from "@nudojs/core";
3715
4538
  var SEMANTIC_TOKEN_TYPES = [
3716
4539
  "function",
3717
4540
  "variable",
@@ -3729,7 +4552,13 @@ var SEMANTIC_TOKEN_MODIFIERS = [
3729
4552
  "declaration",
3730
4553
  "readonly",
3731
4554
  "deprecated",
3732
- "unreachable"
4555
+ "unreachable",
4556
+ /** handwritten:显式契约(侧车手写 / @nudo:refine) */
4557
+ "contract",
4558
+ /** generated:侧车 @generated 段 */
4559
+ "generated",
4560
+ /** derived:implicit 展示档(非义务契约) */
4561
+ "derived"
3733
4562
  ];
3734
4563
  function encodeSemanticTokens(tokens) {
3735
4564
  const result = [];
@@ -3750,6 +4579,12 @@ var TYPE_PARAMETER = SEMANTIC_TOKEN_TYPES.indexOf("parameter");
3750
4579
  var TYPE_PROPERTY = SEMANTIC_TOKEN_TYPES.indexOf("property");
3751
4580
  var TYPE_METHOD = SEMANTIC_TOKEN_TYPES.indexOf("method");
3752
4581
  var MOD_DECLARATION = 1 << SEMANTIC_TOKEN_MODIFIERS.indexOf("declaration");
4582
+ var MOD_CONTRACT = 1 << SEMANTIC_TOKEN_MODIFIERS.indexOf("contract");
4583
+ var MOD_GENERATED = 1 << SEMANTIC_TOKEN_MODIFIERS.indexOf("generated");
4584
+ var MOD_DERIVED = 1 << SEMANTIC_TOKEN_MODIFIERS.indexOf("derived");
4585
+ function interfaceTierModifierBit(src) {
4586
+ return src === "handwritten" ? MOD_CONTRACT : src === "generated" ? MOD_GENERATED : MOD_DERIVED;
4587
+ }
3753
4588
  function collectFunctionBindingNames(filePath, source, _ast) {
3754
4589
  try {
3755
4590
  const binds = collectAbsBindingsFromGraph(source, filePath);
@@ -3764,7 +4599,7 @@ function collectFunctionBindingNames(filePath, source, _ast) {
3764
4599
  }
3765
4600
  return /* @__PURE__ */ new Set();
3766
4601
  }
3767
- function buildSemanticTokens(filePath, source) {
4602
+ function buildSemanticTokens(filePath, source, opts) {
3768
4603
  let ast;
3769
4604
  try {
3770
4605
  ast = parse7(source);
@@ -3774,6 +4609,7 @@ function buildSemanticTokens(filePath, source) {
3774
4609
  const functionNames = collectFunctionBindingNames(filePath, source, ast);
3775
4610
  const program = ast.program ?? ast;
3776
4611
  const topLevelDeclarators = /* @__PURE__ */ new Set();
4612
+ const topLevelFnDeclNodes = /* @__PURE__ */ new Set();
3777
4613
  for (const stmt of program.body ?? []) {
3778
4614
  const decl = stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportDefaultDeclaration" ? stmt.declaration ?? null : stmt;
3779
4615
  if (decl && decl.type === "VariableDeclaration") {
@@ -3781,9 +4617,30 @@ function buildSemanticTokens(filePath, source) {
3781
4617
  topLevelDeclarators.add(d);
3782
4618
  }
3783
4619
  }
4620
+ if (decl && decl.type === "FunctionDeclaration" && decl.id) {
4621
+ topLevelFnDeclNodes.add(decl);
4622
+ }
3784
4623
  }
4624
+ const tierOpts = {
4625
+ ...opts?.loadModule ? { loadModule: opts.loadModule } : {},
4626
+ ...opts?.autoBind !== void 0 ? { autoBind: opts.autoBind } : {}
4627
+ };
4628
+ const tierModCache = /* @__PURE__ */ new Map();
4629
+ const tierModFor = (name) => {
4630
+ const cached = tierModCache.get(name);
4631
+ if (cached !== void 0) return cached;
4632
+ let bit = 0;
4633
+ try {
4634
+ const tier = interfaceTierOf2(source, name, filePath, tierOpts);
4635
+ if (tier) bit = interfaceTierModifierBit(tier.source);
4636
+ } catch {
4637
+ bit = 0;
4638
+ }
4639
+ tierModCache.set(name, bit);
4640
+ return bit;
4641
+ };
3785
4642
  const tokens = [];
3786
- const pushIdentifier = (id, typeIndex) => {
4643
+ const pushIdentifier = (id, typeIndex, extraMod = 0) => {
3787
4644
  const loc = id.loc;
3788
4645
  const name = id.name;
3789
4646
  if (!loc || typeof name !== "string") return;
@@ -3792,7 +4649,7 @@ function buildSemanticTokens(filePath, source) {
3792
4649
  char: loc.start.column,
3793
4650
  length: name.length,
3794
4651
  typeIndex,
3795
- modifierBitmask: MOD_DECLARATION
4652
+ modifierBitmask: MOD_DECLARATION | extraMod
3796
4653
  });
3797
4654
  };
3798
4655
  const collectParams = (params) => {
@@ -3815,15 +4672,23 @@ function buildSemanticTokens(filePath, source) {
3815
4672
  const id = n.id;
3816
4673
  if (id?.type === "Identifier") {
3817
4674
  const isTopLevel = topLevelDeclarators.has(node);
3818
- const typeIndex = isTopLevel && isFunctionValue(id.name) ? TYPE_FUNCTION : TYPE_VARIABLE;
3819
- pushIdentifier(id, typeIndex);
4675
+ const name = id.name;
4676
+ const isFn = isTopLevel && isFunctionValue(name);
4677
+ const typeIndex = isFn ? TYPE_FUNCTION : TYPE_VARIABLE;
4678
+ const extraMod = isFn ? tierModFor(name) : 0;
4679
+ pushIdentifier(id, typeIndex, extraMod);
3820
4680
  }
3821
4681
  break;
3822
4682
  }
3823
4683
  case "FunctionDeclaration":
3824
4684
  case "FunctionExpression": {
3825
4685
  const id = n.id;
3826
- if (id?.type === "Identifier") pushIdentifier(id, TYPE_FUNCTION);
4686
+ if (id?.type === "Identifier") {
4687
+ const name = id.name;
4688
+ const isTopLevelFnDecl = n.type === "FunctionDeclaration" && topLevelFnDeclNodes.has(node);
4689
+ const extraMod = isTopLevelFnDecl ? tierModFor(name) : 0;
4690
+ pushIdentifier(id, TYPE_FUNCTION, extraMod);
4691
+ }
3827
4692
  collectParams(n.params ?? []);
3828
4693
  break;
3829
4694
  }
@@ -3871,14 +4736,102 @@ import {
3871
4736
  litValue as litValue3,
3872
4737
  abs as makeAbs2,
3873
4738
  isTemplateLike,
3874
- templatePartsOf
4739
+ templatePartsOf,
4740
+ collectAbsFreeVars
3875
4741
  } from "@nudojs/core";
3876
- function wrapComplexAbs(a) {
3877
- const ts = absToTSType(a);
3878
- if (a.shape.k === "sum") return `(${ts})`;
4742
+ function tsTypeParamName(id) {
4743
+ let n = id.replace(/[^A-Za-z0-9_$]/g, "_");
4744
+ if (!/^[A-Za-z_$]/.test(n)) n = `T_${n}`;
4745
+ if (n.length === 0) n = "T";
4746
+ return n;
4747
+ }
4748
+ function wrapComplexAbs(a, typeVars) {
4749
+ const ts = absToTSType(a, typeVars);
4750
+ if (a.shape.k === "sum" || a.shape.k === "fn") return `(${ts})`;
3879
4751
  return ts;
3880
4752
  }
3881
- function absToTSType(a) {
4753
+ function wrapUnionMember(a, typeVars) {
4754
+ const ts = absToTSType(a, typeVars);
4755
+ if (a.shape.k === "fn") return `(${ts})`;
4756
+ return ts;
4757
+ }
4758
+ var TS_PARAM_RESERVED = /* @__PURE__ */ new Set([
4759
+ "break",
4760
+ "case",
4761
+ "catch",
4762
+ "class",
4763
+ "const",
4764
+ "continue",
4765
+ "debugger",
4766
+ "default",
4767
+ "delete",
4768
+ "do",
4769
+ "else",
4770
+ "enum",
4771
+ "export",
4772
+ "extends",
4773
+ "false",
4774
+ "finally",
4775
+ "for",
4776
+ "function",
4777
+ "if",
4778
+ "import",
4779
+ "in",
4780
+ "instanceof",
4781
+ "new",
4782
+ "null",
4783
+ "return",
4784
+ "super",
4785
+ "switch",
4786
+ "this",
4787
+ "throw",
4788
+ "true",
4789
+ "try",
4790
+ "typeof",
4791
+ "var",
4792
+ "void",
4793
+ "while",
4794
+ "with",
4795
+ "yield",
4796
+ "let",
4797
+ "static",
4798
+ "await",
4799
+ "implements",
4800
+ "interface",
4801
+ "package",
4802
+ "private",
4803
+ "protected",
4804
+ "public",
4805
+ "arguments",
4806
+ "eval",
4807
+ "constructor"
4808
+ ]);
4809
+ function isTsIdent(name) {
4810
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !TS_PARAM_RESERVED.has(name);
4811
+ }
4812
+ function sanitizeParamName(name, index) {
4813
+ if (name.startsWith("...")) {
4814
+ const rest = name.slice(3);
4815
+ if (isTsIdent(rest)) return name;
4816
+ return `...arg${index}`;
4817
+ }
4818
+ if (isTsIdent(name)) return name;
4819
+ return `arg${index}`;
4820
+ }
4821
+ function formatPropKey(k) {
4822
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k)) return k;
4823
+ if (/^\d+$/.test(k)) return k;
4824
+ return JSON.stringify(k);
4825
+ }
4826
+ function absToTSType(a, typeVars) {
4827
+ if (a.shape.k === "any" && a.term?.op === "var" && typeVars) {
4828
+ const mapped = typeVars.get(a.term.id);
4829
+ if (mapped) return mapped;
4830
+ }
4831
+ if (a.shape.k === "arr" && a.shape.element.shape.k === "any" && a.shape.element.term?.op === "var" && typeVars) {
4832
+ const mapped = typeVars.get(a.shape.element.term.id);
4833
+ if (mapped) return `${mapped}[]`;
4834
+ }
3882
4835
  if (a.term?.op === "lit") {
3883
4836
  const v = a.term.value;
3884
4837
  if (v === null) return "null";
@@ -3894,7 +4847,7 @@ function absToTSType(a) {
3894
4847
  const inner = parts.map((p) => {
3895
4848
  const lv = litValue3(p);
3896
4849
  if (typeof lv === "string") return lv;
3897
- return `\${${absToTSType(p)}}`;
4850
+ return `\${${absToTSType(p, typeVars)}}`;
3898
4851
  }).join("");
3899
4852
  return `\`${inner}\``;
3900
4853
  }
@@ -3908,34 +4861,50 @@ function absToTSType(a) {
3908
4861
  return a.shape.type;
3909
4862
  case "obj": {
3910
4863
  const entries = Object.entries(a.shape.slots).map(([k, slot]) => {
3911
- const inner = absToTSType(slot.value);
4864
+ const inner = absToTSType(slot.value, typeVars);
3912
4865
  if (slot.optional) {
3913
- const t = slot.value.shape.k === "sum" ? `(${inner})` : inner;
3914
- return `${k}: ${t} | undefined`;
4866
+ const t = wrapComplexAbs(slot.value, typeVars);
4867
+ return `${formatPropKey(k)}: ${t} | undefined`;
3915
4868
  }
3916
- return `${k}: ${inner}`;
4869
+ return `${formatPropKey(k)}: ${inner}`;
3917
4870
  });
3918
4871
  if (entries.length === 0) return "{}";
3919
4872
  return `{ ${entries.join("; ")} }`;
3920
4873
  }
3921
4874
  case "arr":
3922
- return `${wrapComplexAbs(a.shape.element)}[]`;
4875
+ return `${wrapComplexAbs(a.shape.element, typeVars)}[]`;
3923
4876
  case "tuple": {
3924
- const inner = a.shape.elements.map(absToTSType).join(", ");
3925
- return `[${inner}]`;
4877
+ const parts = a.shape.elements.map((e) => absToTSType(e, typeVars));
4878
+ if (a.shape.rest) {
4879
+ const rest = a.shape.rest;
4880
+ const restTs = rest.shape.k === "arr" ? absToTSType(rest, typeVars) : `${absToTSType(rest, typeVars)}[]`;
4881
+ parts.push(`...${restTs}`);
4882
+ }
4883
+ return `[${parts.join(", ")}]`;
3926
4884
  }
3927
4885
  case "fn": {
3928
- const params = a.shape.params.map((p) => `${p}: unknown`).join(", ");
3929
- const ret = a.shape.returnType ? absToTSType(a.shape.returnType) : "unknown";
4886
+ const paramTypes = a.shape.paramTypes;
4887
+ const params = a.shape.params.map((p, i) => {
4888
+ const isRest = p.startsWith("...");
4889
+ const name = sanitizeParamName(p, i);
4890
+ const pt = paramTypes?.[i];
4891
+ let typeStr;
4892
+ if (pt) typeStr = absToTSType(pt, typeVars);
4893
+ else if (isRest) typeStr = "unknown[]";
4894
+ else typeStr = "unknown";
4895
+ return `${name}: ${typeStr}`;
4896
+ }).join(", ");
4897
+ const ret = a.shape.returnType ? absToTSType(a.shape.returnType, typeVars) : "unknown";
3930
4898
  return `(${params}) => ${ret}`;
3931
4899
  }
3932
4900
  case "brand":
3933
- return a.shape.name;
4901
+ return isTsIdent(a.shape.name) || /^[A-Z][A-Za-z0-9_$]*$/.test(a.shape.name) ? a.shape.name : "unknown";
3934
4902
  case "eff":
3935
- if (a.shape.eff === "promise") return `Promise<${absToTSType(a.shape.inner)}>`;
3936
- return absToTSType(a.shape.inner);
4903
+ if (a.shape.eff === "promise")
4904
+ return `Promise<${absToTSType(a.shape.inner, typeVars)}>`;
4905
+ return absToTSType(a.shape.inner, typeVars);
3937
4906
  case "sum": {
3938
- const parts = a.shape.members.map(absToTSType).filter((p) => p !== "never");
4907
+ const parts = a.shape.members.map((m) => wrapUnionMember(m, typeVars)).filter((p) => p !== "never");
3939
4908
  const uniq = [...new Set(parts)];
3940
4909
  if (uniq.length === 0) return "never";
3941
4910
  if (uniq.length === 1) return uniq[0];
@@ -4067,13 +5036,18 @@ function computeMainSignature(fn) {
4067
5036
  }
4068
5037
  const typeStr = paramTypeFromAbs(members);
4069
5038
  let name = getParamName(fn, i);
5039
+ const isRest = name.startsWith("...");
5040
+ const bare = isRest ? name.slice(3) : name;
5041
+ if (!isTsIdent(bare)) {
5042
+ name = isRest ? `...arg${i}` : `arg${i}`;
5043
+ }
4070
5044
  if (usedNames.has(name)) {
4071
5045
  let n = 2;
4072
5046
  while (usedNames.has(`${name}${n}`)) n++;
4073
5047
  name = `${name}${n}`;
4074
5048
  }
4075
5049
  usedNames.add(name);
4076
- const optional = i >= minArity && !name.startsWith("...");
5050
+ const optional = i >= minArity && !isRest;
4077
5051
  params.push(optional ? `${name}?: ${typeStr}` : `${name}: ${typeStr}`);
4078
5052
  paramNames.push(name);
4079
5053
  paramTypes.push(typeStr);
@@ -4088,7 +5062,7 @@ function generateJSDoc(fn, sig) {
4088
5062
  for (const c of fn.cases) {
4089
5063
  const preciseDiffers = c.argAbs.length !== sig.paramTypes.length || c.argAbs.some((a, i) => absToTSType(a) !== sig.paramTypes[i]) || absToTSType(c.abs) !== sig.returnType;
4090
5064
  if (!preciseDiffers) continue;
4091
- const argsStr = c.argAbs.map(absToTSType).join(", ");
5065
+ const argsStr = c.argAbs.map((a) => absToTSType(a)).join(", ");
4092
5066
  lines.push(` * Case: ${c.name} (${argsStr}) => ${absToTSType(c.abs)}`);
4093
5067
  }
4094
5068
  for (let i = 0; i < sig.paramTypes.length; i++) {
@@ -4098,8 +5072,103 @@ function generateJSDoc(fn, sig) {
4098
5072
  lines.push(" */");
4099
5073
  return lines.join("\n");
4100
5074
  }
5075
+ function computeHofSignature(fn) {
5076
+ const hof = fn.hof;
5077
+ if (!hof) return void 0;
5078
+ const byParam = /* @__PURE__ */ new Map();
5079
+ for (const s of hof.entryShapes ?? []) byParam.set(s.param, s.abs);
5080
+ for (const r of hof.fnRels ?? []) byParam.set(r.param, r.abs);
5081
+ if (byParam.size === 0 && !hof.symbolic) return void 0;
5082
+ if (fn.cases.length > 0) {
5083
+ for (const [param] of byParam) {
5084
+ const idx = fn.paramNames.indexOf(param);
5085
+ if (idx < 0) continue;
5086
+ const caseType = paramTypeFromAbs(
5087
+ fn.cases.map((c) => c.argAbs[idx]).filter((a) => !!a)
5088
+ );
5089
+ if (caseType !== "unknown" && caseType !== "unknown[]") {
5090
+ return void 0;
5091
+ }
5092
+ }
5093
+ }
5094
+ const arity = Math.max(
5095
+ fn.paramNames.length,
5096
+ ...[...byParam.keys()].map((p) => fn.paramNames.indexOf(p) + 1),
5097
+ 0
5098
+ );
5099
+ if (arity === 0 && !hof.symbolic) return void 0;
5100
+ const free = /* @__PURE__ */ new Set();
5101
+ for (const a of byParam.values()) {
5102
+ for (const id of collectAbsFreeVars(a)) free.add(id);
5103
+ }
5104
+ const retAbs = hof.symbolic ?? fn.combinedAbs;
5105
+ if (retAbs) {
5106
+ for (const id of collectAbsFreeVars(retAbs)) free.add(id);
5107
+ }
5108
+ if (free.size === 0) return void 0;
5109
+ const typeVars = /* @__PURE__ */ new Map();
5110
+ const used = /* @__PURE__ */ new Set();
5111
+ const typeParams = [];
5112
+ for (const id of [...free].sort()) {
5113
+ let n = tsTypeParamName(id);
5114
+ if (used.has(n) || TS_PARAM_RESERVED.has(n)) {
5115
+ let i = 2;
5116
+ while (used.has(`${n}${i}`)) i++;
5117
+ n = `${n}${i}`;
5118
+ }
5119
+ used.add(n);
5120
+ typeVars.set(id, n);
5121
+ typeParams.push(n);
5122
+ }
5123
+ const params = [];
5124
+ const paramNames = [];
5125
+ const paramTypes = [];
5126
+ const usedNames = /* @__PURE__ */ new Set();
5127
+ for (let i = 0; i < arity; i++) {
5128
+ const rawName = getParamName(fn, i);
5129
+ const isRest = rawName.startsWith("...");
5130
+ let name = sanitizeParamName(rawName, i);
5131
+ if (usedNames.has(name)) {
5132
+ let n = 2;
5133
+ while (usedNames.has(`${name}${n}`)) n++;
5134
+ name = isRest && name.startsWith("...") ? `...${name.slice(3)}${n}` : `${name}${n}`;
5135
+ }
5136
+ usedNames.add(name);
5137
+ const promoted = byParam.get(fn.paramNames[i] ?? rawName) ?? byParam.get(rawName);
5138
+ const typeStr = promoted ? absToTSType(promoted, typeVars) : isRest ? "unknown[]" : "unknown";
5139
+ params.push(`${name}: ${typeStr}`);
5140
+ paramNames.push(name);
5141
+ paramTypes.push(typeStr);
5142
+ }
5143
+ const returnType = retAbs ? absToTSType(retAbs, typeVars) : "unknown";
5144
+ return { typeParams, params, paramNames, paramTypes, returnType };
5145
+ }
4101
5146
  function generateFunctionDtsLines(fn) {
4102
- if (fn.noDeclaration) return [];
5147
+ let emitFn = fn;
5148
+ if (fn.noDeclaration) {
5149
+ const m = /^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/.exec(fn.name);
5150
+ if (!m) return [];
5151
+ emitFn = { ...fn, name: `${m[1]}_${m[2]}`, noDeclaration: false };
5152
+ }
5153
+ fn = emitFn;
5154
+ const hofSig = computeHofSignature(fn);
5155
+ if (hofSig) {
5156
+ const lines2 = [];
5157
+ if (fn.cases.length > 0) {
5158
+ const jsdoc2 = generateJSDoc(fn, {
5159
+ params: hofSig.params,
5160
+ paramNames: hofSig.paramNames,
5161
+ paramTypes: hofSig.paramTypes,
5162
+ returnType: hofSig.returnType
5163
+ });
5164
+ if (jsdoc2) lines2.push(jsdoc2);
5165
+ }
5166
+ const tparams = hofSig.typeParams.length > 0 ? `<${hofSig.typeParams.join(", ")}>` : "";
5167
+ lines2.push(
5168
+ `export declare function ${fn.name}${tparams}(${hofSig.params.join(", ")}): ${hofSig.returnType};`
5169
+ );
5170
+ return lines2;
5171
+ }
4103
5172
  if (fn.cases.length === 0) {
4104
5173
  const retAbs = fn.combinedAbs;
4105
5174
  if (retAbs) {
@@ -4108,7 +5177,6 @@ function generateFunctionDtsLines(fn) {
4108
5177
  ];
4109
5178
  }
4110
5179
  return [];
4111
- return [];
4112
5180
  }
4113
5181
  const sig = computeMainSignature(fn);
4114
5182
  const jsdoc = generateJSDoc(fn, sig);
@@ -4539,8 +5607,8 @@ function unifiedDiff(a, b, path) {
4539
5607
  }
4540
5608
 
4541
5609
  // src/interface-surface.ts
4542
- import { readFileSync as readFileSync6 } from "fs";
4543
- import { dirname as dirname8, resolve as resolve6 } from "path";
5610
+ import { readFileSync as readFileSync7, existsSync as existsSync9 } from "fs";
5611
+ import { dirname as dirname11, resolve as resolve7 } from "path";
4544
5612
  import {
4545
5613
  effectiveInterface,
4546
5614
  formatConstraint,
@@ -4548,9 +5616,13 @@ import {
4548
5616
  interfaceDiagCount,
4549
5617
  localNamedExports,
4550
5618
  refineDiagCount,
5619
+ sidecarPathOf as sidecarPathOf2,
4551
5620
  takeInterfaceDiagsSince,
4552
5621
  takeRefineDiagsSince
4553
5622
  } from "@nudojs/core";
5623
+ function collectDepContents(filePath, source, loadModule) {
5624
+ return collectLoadDepContents(filePath, source, loadModule ?? defaultLoadModule);
5625
+ }
4554
5626
  function formatInterfaceSurfaceLine(e) {
4555
5627
  const params = `(${e.params.map((p) => `${p.name}: ${p.display}`).join(", ")})`;
4556
5628
  let line = ` ${e.fn} [${e.source}] ${params}`;
@@ -4565,19 +5637,92 @@ function absToImplicitDisplay(a) {
4565
5637
  return "unknown";
4566
5638
  }
4567
5639
  }
5640
+ function constraintToJson(c) {
5641
+ return JSON.parse(JSON.stringify(c));
5642
+ }
5643
+ function cachedToEffective(e) {
5644
+ return {
5645
+ fnName: e.fnName,
5646
+ params: e.params.map((p) => ({
5647
+ param: p.param,
5648
+ constraint: p.constraint
5649
+ })),
5650
+ ...e.returns ? { returns: { constraint: e.returns.constraint } } : {},
5651
+ source: e.source,
5652
+ ...e.conflict ? { conflict: e.conflict } : {}
5653
+ };
5654
+ }
4568
5655
  async function interfaceSurface(filePath, opts = {}) {
4569
- const abs = resolve6(filePath);
4570
- const source = readFileSync6(abs, "utf-8");
5656
+ const abs = resolve7(filePath);
5657
+ const source = opts.source ?? readFileSync7(abs, "utf-8");
5658
+ const fromBuffer = opts.source !== void 0;
4571
5659
  const ifaceSince = interfaceDiagCount();
4572
5660
  const refineSince = refineDiagCount();
4573
- const autoBind = opts.autoBind ?? interfaceConfig(findProjectConfig(dirname8(abs))?.config).autoBind;
5661
+ const proj = findProjectConfig(dirname11(abs));
5662
+ const autoBind = opts.autoBind ?? interfaceConfig(proj?.config).autoBind;
4574
5663
  const loadModule = opts.loadModule ?? defaultLoadModule;
4575
5664
  const exported = localNamedExports(source);
4576
5665
  const kindOf = (fnName) => exported.has(fnName) ? "export" : "local";
4577
- const analysis = await analyzeFileAsync(abs, source, void 0, opts.records);
5666
+ const analysis = await analyzeFileAsync(abs, source, void 0, opts.records, loadModule);
5667
+ let disk;
5668
+ let ifaceKey;
5669
+ let cachedTable;
5670
+ if (!opts.loadModule && !opts.records && !fromBuffer) {
5671
+ const cacheRoot = diskCacheRoot(proj?.config, proj?.projectDir);
5672
+ disk = new DiskCache({ root: cacheRoot, namespace: "iface" });
5673
+ if (disk.enabled) {
5674
+ let sidecarSource;
5675
+ try {
5676
+ const sc = sidecarPathOf2(abs);
5677
+ if (autoBind !== false) {
5678
+ const openSc = loadModule(`./${sc.slice(sc.lastIndexOf("/") + 1)}`, abs);
5679
+ if (openSc !== void 0) sidecarSource = openSc;
5680
+ else if (existsSync9(sc)) sidecarSource = readFileSync7(sc, "utf-8");
5681
+ }
5682
+ } catch {
5683
+ sidecarSource = void 0;
5684
+ }
5685
+ const dep = collectDepContents(abs, source, loadModule);
5686
+ const hasBareMiss = (dep.depContents ?? []).some((d) => d.content == null);
5687
+ if (dep.truncated || hasBareMiss) {
5688
+ ifaceKey = void 0;
5689
+ cachedTable = void 0;
5690
+ } else {
5691
+ ifaceKey = ifaceCacheKey(abs, source, {
5692
+ autoBind: autoBind !== false,
5693
+ projectDir: proj?.projectDir,
5694
+ sidecarSource,
5695
+ depContents: dep.depContents,
5696
+ projectEnvNames: proj?.config.env ?? []
5697
+ });
5698
+ cachedTable = disk.get(ifaceKey);
5699
+ }
5700
+ }
5701
+ }
4578
5702
  const entries = [];
5703
+ const freshTable = { fns: {} };
5704
+ const useCache = cachedTable !== void 0;
4579
5705
  for (const fn of analysis.functions) {
4580
- const eff = effectiveInterface(source, fn.name, { loadModule, fromFile: abs, autoBind });
5706
+ let eff;
5707
+ if (useCache) {
5708
+ const hit = cachedTable.fns[fn.name];
5709
+ eff = hit === null ? void 0 : hit ? cachedToEffective(hit) : void 0;
5710
+ } else {
5711
+ eff = effectiveInterface(source, fn.name, { loadModule, fromFile: abs, autoBind });
5712
+ if (freshTable) {
5713
+ freshTable.fns[fn.name] = eff ? {
5714
+ fnName: eff.fnName,
5715
+ params: eff.params.map((p) => ({
5716
+ param: p.param,
5717
+ constraint: constraintToJson(p.constraint)
5718
+ })),
5719
+ ...eff.returns ? { returns: { constraint: constraintToJson(eff.returns.constraint) } } : {},
5720
+ // 磁盘表保留真实分档;implicit 也可序列化(展示层用)
5721
+ source: eff.source,
5722
+ ...eff.conflict ? { conflict: eff.conflict } : {}
5723
+ } : null;
5724
+ }
5725
+ }
4581
5726
  if (eff) {
4582
5727
  entries.push({
4583
5728
  fn: fn.name,
@@ -4608,14 +5753,20 @@ async function interfaceSurface(filePath, opts = {}) {
4608
5753
  }
4609
5754
  entries.push({ fn: fn.name, kind: kindOf(fn.name), source: "implicit", params, returns: ret });
4610
5755
  }
5756
+ if (disk?.enabled && ifaceKey && !useCache && analysis.functions.length > 0) {
5757
+ try {
5758
+ disk.set(ifaceKey, freshTable);
5759
+ } catch {
5760
+ }
5761
+ }
4611
5762
  takeInterfaceDiagsSince(ifaceSince);
4612
5763
  takeRefineDiagsSince(refineSince);
4613
5764
  return entries;
4614
5765
  }
4615
5766
 
4616
5767
  // src/interface-emitter.ts
4617
- import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync, renameSync, unlinkSync } from "fs";
4618
- import { basename as basename2, dirname as dirname9, relative, resolve as resolve7 } from "path";
5768
+ import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "fs";
5769
+ import { basename as basename2, dirname as dirname12, relative as relative2, resolve as resolve8 } from "path";
4619
5770
  import {
4620
5771
  execNudoModule,
4621
5772
  formatConstraint as formatConstraint2,
@@ -4626,20 +5777,20 @@ import {
4626
5777
  localNamedExports as localNamedExports2,
4627
5778
  parseSource as parseSource2,
4628
5779
  refineDiagCount as refineDiagCount2,
4629
- sidecarPathOf,
5780
+ sidecarPathOf as sidecarPathOf3,
4630
5781
  takeInterfaceDiagsSince as takeInterfaceDiagsSince2,
4631
5782
  takeRefineDiagsSince as takeRefineDiagsSince2
4632
5783
  } from "@nudojs/core";
4633
5784
  import { randomBytes } from "crypto";
4634
5785
  var GENERATED_HEADER = "// @generated by nudo \u2014 do not edit; regenerate with `nudo interface --emit`";
4635
5786
  async function emitInterface(filePath, opts) {
4636
- const abs = resolve7(filePath);
4637
- if (isNodeModulesPath(abs) || isNodeModulesPath(sidecarPathOf(abs))) {
5787
+ const abs = resolve8(filePath);
5788
+ if (isNodeModulesPath(abs) || isNodeModulesPath(sidecarPathOf3(abs))) {
4638
5789
  throw new Error(
4639
5790
  `emit target '${abs}' is inside node_modules; contract sidecars are never written there`
4640
5791
  );
4641
5792
  }
4642
- const proj = findProjectConfig(dirname9(abs));
5793
+ const proj = findProjectConfig(dirname12(abs));
4643
5794
  const allow = interfaceConfig(proj?.config).emit;
4644
5795
  if (!matchesEmitAllowlist(abs, proj?.projectDir, allow)) {
4645
5796
  return {
@@ -4650,28 +5801,34 @@ async function emitInterface(filePath, opts) {
4650
5801
  {
4651
5802
  code: "nudo:interface-emit-denied",
4652
5803
  severity: "warning",
4653
- message: `emit target '${relative(process.cwd(), abs) || abs}' is outside package.json#nudo.interface.emit allowlist`
5804
+ message: `emit target '${relative2(process.cwd(), abs) || abs}' is outside package.json#nudo.interface.emit allowlist`
4654
5805
  }
4655
5806
  ],
4656
- sidecarPath: sidecarPathOf(abs)
5807
+ sidecarPath: sidecarPathOf3(abs)
4657
5808
  };
4658
5809
  }
4659
- const source = readFileSync7(abs, "utf-8");
5810
+ const source = opts.source ?? readFileSync8(abs, "utf-8");
4660
5811
  const ifaceSince = interfaceDiagCount2();
4661
5812
  const refineSince = refineDiagCount2();
4662
- const sidecarPath = sidecarPathOf(abs);
4663
- const sidecarSrc = existsSync6(sidecarPath) ? readFileSync7(sidecarPath, "utf-8") : "";
4664
- const srcRel = relative(dirname9(sidecarPath), abs) || basename2(abs);
5813
+ const sidecarPath = sidecarPathOf3(abs);
5814
+ const sidecarSrc = existsSync10(sidecarPath) ? readFileSync8(sidecarPath, "utf-8") : "";
5815
+ const srcRel = relative2(dirname12(sidecarPath), abs) || basename2(abs);
4665
5816
  const sections = collectGeneratedSections(sidecarSrc);
4666
5817
  const generatedNames = new Set(sections.flatMap((s) => s.names));
4667
5818
  const declared = topLevelDeclaredNames(sidecarSrc);
4668
5819
  if (declared === void 0) {
4669
5820
  throw new Error(
4670
- `sidecar '${relative(process.cwd(), sidecarPath) || sidecarPath}' is not parseable; fix it before emitting`
5821
+ `sidecar '${relative2(process.cwd(), sidecarPath) || sidecarPath}' is not parseable; fix it before emitting`
4671
5822
  );
4672
5823
  }
4673
5824
  for (const n of generatedNames) declared.delete(n);
4674
- const analysis = await analyzeFileAsync(abs, source, void 0, opts.records);
5825
+ const analysis = await analyzeFileAsync(
5826
+ abs,
5827
+ source,
5828
+ void 0,
5829
+ opts.records,
5830
+ opts.loadModule
5831
+ );
4675
5832
  const exported = localNamedExports2(source);
4676
5833
  const fnByName = /* @__PURE__ */ new Map();
4677
5834
  for (const f of analysis.functions) {
@@ -4774,15 +5931,15 @@ async function emitInterface(filePath, opts) {
4774
5931
  finalContent = joinSections(base, [...preserved, ...accepted.map((a) => a.text)]);
4775
5932
  }
4776
5933
  const changed = finalContent !== sidecarSrc;
4777
- const diff = changed ? unifiedDiff(sidecarSrc, finalContent, relative(process.cwd(), sidecarPath) || sidecarPath) : void 0;
5934
+ const diff = changed ? unifiedDiff(sidecarSrc, finalContent, relative2(process.cwd(), sidecarPath) || sidecarPath) : void 0;
4778
5935
  if (changed && !opts.dryRun) {
4779
5936
  const tmp = `${sidecarPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
4780
5937
  try {
4781
- writeFileSync(tmp, finalContent, "utf-8");
5938
+ writeFileSync2(tmp, finalContent, "utf-8");
4782
5939
  renameSync(tmp, sidecarPath);
4783
5940
  } catch (e) {
4784
5941
  try {
4785
- if (existsSync6(tmp)) unlinkSync(tmp);
5942
+ if (existsSync10(tmp)) unlinkSync(tmp);
4786
5943
  } catch {
4787
5944
  }
4788
5945
  throw e;
@@ -4972,29 +6129,638 @@ function joinSections(base, sectionTexts) {
4972
6129
  return lead + normalized.join("\n");
4973
6130
  }
4974
6131
 
4975
- // src/interface-derivation.ts
4976
- import { existsSync as existsSync7, readFileSync as readFileSync8, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
4977
- import { basename as basename3, dirname as dirname10, relative as relative2, resolve as resolve8 } from "path";
6132
+ // src/interface-draft.ts
6133
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync3, realpathSync as realpathSync2 } from "fs";
6134
+ import { basename as basename3, dirname as dirname13, isAbsolute as isAbsolute2, join as join7, relative as relative3 } from "path";
6135
+ import {
6136
+ effectiveInterface as effectiveInterface2,
6137
+ formatConstraint as formatConstraint3,
6138
+ formatShape as formatShape6,
6139
+ generalizeFromAst as generalizeFromAst4,
6140
+ isIntFlag,
6141
+ joinThenProject as joinThenProject2,
6142
+ localNamedExports as localNamedExports3,
6143
+ sidecarPathOf as sidecarPathOf4
6144
+ } from "@nudojs/core";
4978
6145
  import { parse as parse9 } from "@nudojs/parser";
6146
+ function collectParamBodyAccesses(source) {
6147
+ const out = /* @__PURE__ */ new Map();
6148
+ let ast;
6149
+ try {
6150
+ ast = parse9(source);
6151
+ } catch {
6152
+ return out;
6153
+ }
6154
+ const keyOf = (node) => {
6155
+ if (node.type === "Identifier") return node.name;
6156
+ if (node.type === "StringLiteral") return node.value;
6157
+ return void 0;
6158
+ };
6159
+ const visitFn = (fnName, fnNode, paramNames) => {
6160
+ if (paramNames.size === 0) return;
6161
+ const byParam = /* @__PURE__ */ new Map();
6162
+ const walk = (node, shadowed) => {
6163
+ if (!node || typeof node !== "object") return;
6164
+ const n = node;
6165
+ if ((n.type === "VariableDeclarator" || n.type === "FunctionDeclaration") && n.id?.type === "Identifier") {
6166
+ const id = n.id.name;
6167
+ if (paramNames.has(id)) {
6168
+ shadowed = new Set(shadowed).add(id);
6169
+ }
6170
+ }
6171
+ if (n.type === "MemberExpression" || n.type === "OptionalMemberExpression") {
6172
+ const obj = n.object;
6173
+ const prop = n.property;
6174
+ const computed = n.computed === true;
6175
+ if (obj?.type === "Identifier" && paramNames.has(obj.name) && !shadowed.has(obj.name) && prop && !computed) {
6176
+ const key = keyOf(prop);
6177
+ const pname = obj.name;
6178
+ if (key !== void 0) {
6179
+ if (!byParam.has(pname)) byParam.set(pname, /* @__PURE__ */ new Set());
6180
+ byParam.get(pname).add(key);
6181
+ }
6182
+ }
6183
+ }
6184
+ for (const k of Object.keys(n)) {
6185
+ if (k === "loc" || k === "start" || k === "end") continue;
6186
+ const child = n[k];
6187
+ if (Array.isArray(child)) {
6188
+ for (const item of child) walk(item, shadowed);
6189
+ } else if (child && typeof child === "object") {
6190
+ walk(child, shadowed);
6191
+ }
6192
+ }
6193
+ };
6194
+ walk(fnNode, /* @__PURE__ */ new Set());
6195
+ if (byParam.size > 0) out.set(fnName, byParam);
6196
+ };
6197
+ const paramSet = (fnNode) => {
6198
+ const names = /* @__PURE__ */ new Set();
6199
+ const params = fnNode.params ?? [];
6200
+ for (const p of params) {
6201
+ if (!p) continue;
6202
+ if (p.type === "Identifier") names.add(p.name);
6203
+ else if (p.type === "AssignmentPattern" && p.left?.type === "Identifier") {
6204
+ names.add(p.left.name);
6205
+ } else if (p.type === "RestElement" && p.argument?.type === "Identifier") {
6206
+ names.add(p.argument.name);
6207
+ } else if (p.type === "ObjectPattern") {
6208
+ for (const prop of p.properties ?? []) {
6209
+ if (prop.type === "ObjectProperty") {
6210
+ const v = prop.value;
6211
+ if (v.type === "Identifier") names.add(v.name);
6212
+ else if (v.type === "AssignmentPattern" && v.left?.type === "Identifier") {
6213
+ names.add(v.left.name);
6214
+ }
6215
+ }
6216
+ }
6217
+ }
6218
+ }
6219
+ return names;
6220
+ };
6221
+ const visitClassMethods = (className, classNode) => {
6222
+ const body = classNode.body?.body ?? [];
6223
+ for (const m of body) {
6224
+ const mem = m;
6225
+ const isMethod = mem.type === "MethodDefinition" || mem.type === "ClassMethod" || mem.type === "TSDeclareMethod";
6226
+ if (!isMethod || mem.static) continue;
6227
+ if (mem.kind && mem.kind !== "method") continue;
6228
+ const keyName = mem.key?.type === "Identifier" ? mem.key.name : void 0;
6229
+ if (!keyName) continue;
6230
+ const methodNode = mem.type === "MethodDefinition" ? mem.value : mem;
6231
+ if (!methodNode) continue;
6232
+ visitFn(`${className}.${keyName}`, methodNode, paramSet(methodNode));
6233
+ }
6234
+ };
6235
+ const considerDecl = (decl, exported) => {
6236
+ if (!decl) return;
6237
+ if (decl.type === "FunctionDeclaration" && decl.id) {
6238
+ const id = decl.id;
6239
+ visitFn(id.name, decl, paramSet(decl));
6240
+ return;
6241
+ }
6242
+ if (decl.type === "ClassDeclaration" && decl.id?.name) {
6243
+ visitClassMethods(decl.id.name, decl);
6244
+ return;
6245
+ }
6246
+ if (decl.type === "VariableDeclaration") {
6247
+ for (const d of decl.declarations ?? []) {
6248
+ const id = d.id;
6249
+ const init = d.init;
6250
+ if (exported && id?.type === "Identifier" && init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
6251
+ visitFn(id.name, init, paramSet(init));
6252
+ }
6253
+ }
6254
+ }
6255
+ };
6256
+ const program = ast.program;
6257
+ const bodyStmts = program?.body ?? [];
6258
+ for (const stmt of bodyStmts) {
6259
+ if (stmt.type === "ExportNamedDeclaration") {
6260
+ considerDecl(stmt.declaration, true);
6261
+ for (const spec of stmt.specifiers ?? []) {
6262
+ const local = spec.local;
6263
+ if (local?.type !== "Identifier") continue;
6264
+ for (const s2 of bodyStmts) {
6265
+ if (s2.type === "ClassDeclaration" && s2.id?.name === local.name) {
6266
+ visitClassMethods(local.name, s2);
6267
+ }
6268
+ }
6269
+ }
6270
+ } else if (stmt.type === "ExportDefaultDeclaration") {
6271
+ considerDecl(stmt.declaration, true);
6272
+ } else if (stmt.type === "ClassDeclaration") {
6273
+ const id = stmt.id;
6274
+ if (id?.name) visitClassMethods(id.name, stmt);
6275
+ } else if (stmt.type === "FunctionDeclaration") {
6276
+ }
6277
+ }
6278
+ return out;
6279
+ }
6280
+ function caseEvidence(fn) {
6281
+ const callsite = fn.cases.filter((c) => c.source === "callsite");
6282
+ const directive = fn.cases.filter((c) => c.source === "directive");
6283
+ return {
6284
+ paramCases: callsite.length > 0 ? callsite : directive,
6285
+ returnCases: callsite.length > 0 ? callsite : directive.length > 0 ? directive : fn.cases,
6286
+ paramEvidence: callsite.length > 0 ? "callsite" : directive.length > 0 ? "directive" : "none",
6287
+ rawReturnEvidence: callsite.length > 0 ? "callsite" : directive.length > 0 ? "directive" : "none"
6288
+ };
6289
+ }
6290
+ function widenDraftConstraint(c) {
6291
+ const stripEqLits = (preds2) => preds2.filter((p) => !(p.op === "eq" && p.b?.op === "lit"));
6292
+ if (c.members && c.members.length > 0) {
6293
+ const widenedMembers = c.members.map(widenDraftConstraint).filter((m) => m !== void 0);
6294
+ if (widenedMembers.length === 0) {
6295
+ const prim = litPrimOf(c.members[0]);
6296
+ if (!prim) return void 0;
6297
+ return { __nudoConstraint: true, prim, preds: [] };
6298
+ }
6299
+ const prims = new Set(widenedMembers.map((m) => m.prim).filter(Boolean));
6300
+ if (prims.size === 1 && widenedMembers.every((m) => !m.fields && !m.element && !m.members)) {
6301
+ return { __nudoConstraint: true, prim: [...prims][0], preds: [] };
6302
+ }
6303
+ return {
6304
+ __nudoConstraint: true,
6305
+ ...c.prim ? { prim: c.prim } : {},
6306
+ preds: stripEqLits(c.preds ?? []),
6307
+ members: widenedMembers
6308
+ };
6309
+ }
6310
+ if (c.fields) {
6311
+ const fields = {};
6312
+ for (const [k, f] of Object.entries(c.fields)) {
6313
+ const w = widenDraftConstraint(f.constraint);
6314
+ if (!w) continue;
6315
+ fields[k] = { constraint: w, ...f.optional ? { optional: true } : {} };
6316
+ }
6317
+ return {
6318
+ __nudoConstraint: true,
6319
+ fields,
6320
+ preds: stripEqLits(c.preds ?? [])
6321
+ };
6322
+ }
6323
+ if (c.element) {
6324
+ const el = widenDraftConstraint(c.element);
6325
+ return {
6326
+ __nudoConstraint: true,
6327
+ preds: stripEqLits(c.preds ?? []),
6328
+ ...el ? { element: el } : {}
6329
+ };
6330
+ }
6331
+ if (c.fn) return void 0;
6332
+ const preds = stripEqLits(c.preds ?? []);
6333
+ const hasBounds = preds.length > 0;
6334
+ if (!c.prim && !hasBounds && !isIntFlag(c)) return void 0;
6335
+ return {
6336
+ __nudoConstraint: true,
6337
+ ...c.prim ? { prim: c.prim } : {},
6338
+ preds,
6339
+ // builder 上 .int 是链式方法,truthy 恒真;必须经 isIntFlag
6340
+ ...isIntFlag(c) ? { int: true } : {}
6341
+ };
6342
+ }
6343
+ function litPrimOf(c) {
6344
+ if (!c) return void 0;
6345
+ if (c.prim === "number" || c.prim === "string" || c.prim === "boolean") return c.prim;
6346
+ const eq = c.preds?.find(
6347
+ (p) => p.op === "eq" && p.b?.op === "lit"
6348
+ );
6349
+ const v = eq && eq.b?.op === "lit" ? eq.b.value : void 0;
6350
+ if (typeof v === "number") return "number";
6351
+ if (typeof v === "string") return "string";
6352
+ if (typeof v === "boolean") return "boolean";
6353
+ return c.members?.[0] ? litPrimOf(c.members[0]) : void 0;
6354
+ }
6355
+ function projectDraftParams(fn, paramCases, bodyByParam, formals) {
6356
+ const bodyFor = (name, index) => {
6357
+ if (!bodyByParam) return void 0;
6358
+ const hit = bodyByParam.get(name);
6359
+ if (hit) return hit;
6360
+ const formal = formals?.[index];
6361
+ if (formal && formal.kind === "pattern") {
6362
+ for (const b of formal.bound) {
6363
+ const boundHit = bodyByParam.get(b);
6364
+ if (boundHit) return boundHit;
6365
+ }
6366
+ }
6367
+ return bodyByParam.get(`_p${index}`);
6368
+ };
6369
+ return fn.paramNames.map((name, i) => {
6370
+ const bodyAccesses = bodyFor(name, i) ? [...bodyFor(name, i)].sort() : void 0;
6371
+ const argAbs = [];
6372
+ for (const c of paramCases) {
6373
+ const a = c.argAbs[i];
6374
+ if (a !== void 0) argAbs.push(a);
6375
+ }
6376
+ if (argAbs.length === 0) {
6377
+ if (bodyAccesses && bodyAccesses.length > 0) {
6378
+ return {
6379
+ name,
6380
+ display: `/* body-read { ${bodyAccesses.join(", ")} } \u2014 fill types when accepting */`,
6381
+ projected: false,
6382
+ bodyAccesses
6383
+ };
6384
+ }
6385
+ return { name, display: "/* no evidence \u2014 tighten */", projected: false };
6386
+ }
6387
+ const raw = joinThenProject2(argAbs);
6388
+ if (raw === void 0) {
6389
+ return {
6390
+ name,
6391
+ display: `/* not projectable: ${argAbs.map((a) => formatShape6(a)).join(" | ")} */`,
6392
+ projected: false,
6393
+ ...bodyAccesses ? { bodyAccesses } : {}
6394
+ };
6395
+ }
6396
+ const constraint = widenDraftConstraint(raw);
6397
+ if (constraint === void 0) {
6398
+ const observed2 = formatConstraint3(raw);
6399
+ return {
6400
+ name,
6401
+ display: `/* observed: ${observed2} \u2014 widen/confirm before accepting */`,
6402
+ projected: false,
6403
+ ...bodyAccesses ? { bodyAccesses } : {}
6404
+ };
6405
+ }
6406
+ let display = formatConstraint3(constraint);
6407
+ const observed = formatConstraint3(raw);
6408
+ if (observed !== display) {
6409
+ display += ` /* observed: ${observed} */`;
6410
+ }
6411
+ if (bodyAccesses && bodyAccesses.length > 0) {
6412
+ display += ` /* body also reads: ${bodyAccesses.join(", ")} */`;
6413
+ }
6414
+ return {
6415
+ name,
6416
+ constraint,
6417
+ display,
6418
+ projected: true,
6419
+ ...bodyAccesses ? { bodyAccesses } : {}
6420
+ };
6421
+ });
6422
+ }
6423
+ function projectDraftReturn(fn, returnCases, source, rawEvidence, loadModule, fromFile) {
6424
+ const retAbs = [];
6425
+ for (const c of returnCases) {
6426
+ if (c.throwsAbs.shape.k !== "never") continue;
6427
+ retAbs.push(c.abs);
6428
+ }
6429
+ if (retAbs.length > 0) {
6430
+ const raw = joinThenProject2(retAbs);
6431
+ if (raw !== void 0) {
6432
+ if (rawEvidence === "none" || rawEvidence === "body") {
6433
+ return {
6434
+ display: `/* observed: ${formatConstraint3(raw)} \u2014 confirm before accepting */`,
6435
+ projected: false,
6436
+ evidence: rawEvidence === "none" ? "body" : rawEvidence
6437
+ };
6438
+ }
6439
+ const constraint = widenDraftConstraint(raw);
6440
+ if (constraint === void 0) {
6441
+ return {
6442
+ display: `/* observed: ${formatConstraint3(raw)} \u2014 widen/confirm */`,
6443
+ projected: false,
6444
+ evidence: rawEvidence
6445
+ };
6446
+ }
6447
+ return {
6448
+ constraint,
6449
+ display: formatConstraint3(constraint),
6450
+ projected: true,
6451
+ evidence: rawEvidence
6452
+ };
6453
+ }
6454
+ const shapeText = fn.combinedAbs ? formatShape6(fn.combinedAbs) : formatShape6(retAbs[0]);
6455
+ return {
6456
+ display: `/* not projectable: ${shapeText} */`,
6457
+ projected: false,
6458
+ evidence: rawEvidence
6459
+ };
6460
+ }
6461
+ try {
6462
+ const g = generalizeFromAst4(fn.name, source, {
6463
+ refine: {
6464
+ ...loadModule ? { loadModule } : {},
6465
+ ...fromFile ? { fromFile } : {}
6466
+ }
6467
+ });
6468
+ if (g?.symbolic) {
6469
+ return {
6470
+ display: `/* symbolic: ${formatShape6(g.symbolic)}${g.display ? ` \u2014 ${g.display}` : ""} */`,
6471
+ projected: false,
6472
+ evidence: "symbolic"
6473
+ };
6474
+ }
6475
+ } catch {
6476
+ }
6477
+ return { display: "/* no evidence */", projected: false, evidence: "none" };
6478
+ }
6479
+ function toDraftBuilderDsl(display) {
6480
+ const s = display.replace(/\s*\/\*[\s\S]*?\*\/\s*/g, "").trim();
6481
+ return s.replace(
6482
+ /([A-Za-z_$][\w$]*)\?\s*:\s*([^,}\n]+)/g,
6483
+ (_m, name, val) => {
6484
+ const v = val.trim();
6485
+ if (v.includes(".optional()")) return `${name}: ${v}`;
6486
+ return `${name}: ${v}.optional()`;
6487
+ }
6488
+ );
6489
+ }
6490
+ function draftExportName(fnName) {
6491
+ return fnName.includes(".") ? fnName.replace(/\./g, "_") : fnName;
6492
+ }
6493
+ function draftDsl(entry) {
6494
+ const parts = entry.params.filter((p) => p.projected && p.constraint !== void 0).map((p) => {
6495
+ const pure = toDraftBuilderDsl(formatConstraint3(p.constraint));
6496
+ return `${p.name}: ${pure}`;
6497
+ });
6498
+ const obj = parts.length === 0 ? "{}" : `{ ${parts.join(", ")} }`;
6499
+ const ret = entry.returns?.projected && entry.returns.constraint !== void 0 ? toDraftBuilderDsl(formatConstraint3(entry.returns.constraint)) : void 0;
6500
+ return ret === void 0 || ret === "" ? `fn(${obj})` : `fn(${obj}, ${ret})`;
6501
+ }
6502
+ function suggestedBodyDsl(fnName, params) {
6503
+ const withBody = params.filter((p) => p.bodyAccesses && p.bodyAccesses.length > 0 && !p.projected);
6504
+ if (withBody.length === 0) return void 0;
6505
+ const parts = withBody.map((p) => {
6506
+ const fields = p.bodyAccesses.map((k) => `${k}: /* TODO */`).join(", ");
6507
+ return `${p.name}: shape({ ${fields} })`;
6508
+ });
6509
+ return `// suggested (body-read, not a contract): ${fnName} = fn({ ${parts.join(", ")} })`;
6510
+ }
6511
+ async function draftInterface(filePath, opts = {}) {
6512
+ const source = opts.source ?? readFileSync9(filePath, "utf-8");
6513
+ const loadModule = opts.loadModule ?? defaultLoadModule;
6514
+ const sidecarPath = sidecarPathOf4(filePath);
6515
+ const wantBody = opts.bodyAccesses !== false;
6516
+ const analysis = await analyzeFileAsync(filePath, source, void 0, opts.records, loadModule);
6517
+ const exported = localNamedExports3(source);
6518
+ const selected = opts.fnNames && opts.fnNames.length > 0 ? new Set(opts.fnNames) : exported;
6519
+ const bodyMap = wantBody ? collectParamBodyAccesses(source) : /* @__PURE__ */ new Map();
6520
+ const entries = [];
6521
+ for (const fn of analysis.functions) {
6522
+ if (!selected.has(fn.name) && !opts.fnNames?.includes(fn.name)) continue;
6523
+ if (!exported.has(fn.name)) {
6524
+ if (opts.fnNames?.includes(fn.name)) {
6525
+ entries.push({
6526
+ fn: fn.name,
6527
+ params: fn.paramNames.map((n) => ({
6528
+ name: n,
6529
+ display: "/* not an export */",
6530
+ projected: false
6531
+ })),
6532
+ paramEvidence: "none",
6533
+ returnEvidence: "none",
6534
+ skipped: "not-an-export"
6535
+ });
6536
+ }
6537
+ continue;
6538
+ }
6539
+ const eff = effectiveInterface2(source, fn.name, {
6540
+ loadModule,
6541
+ fromFile: filePath,
6542
+ autoBind: true
6543
+ });
6544
+ if (eff?.source === "handwritten") {
6545
+ entries.push({
6546
+ fn: fn.name,
6547
+ params: eff.params.map((p) => ({
6548
+ name: p.param,
6549
+ constraint: p.constraint,
6550
+ display: formatConstraint3(p.constraint),
6551
+ projected: true
6552
+ })),
6553
+ ...eff.returns ? {
6554
+ returns: {
6555
+ constraint: eff.returns.constraint,
6556
+ display: formatConstraint3(eff.returns.constraint),
6557
+ projected: true
6558
+ }
6559
+ } : {},
6560
+ paramEvidence: "none",
6561
+ returnEvidence: "none",
6562
+ skipped: "handwritten"
6563
+ });
6564
+ continue;
6565
+ }
6566
+ const { paramCases, returnCases, paramEvidence, rawReturnEvidence } = caseEvidence(fn);
6567
+ const bodyByParam = bodyMap.get(fn.name);
6568
+ const params = projectDraftParams(fn, paramCases, bodyByParam, fn.formals);
6569
+ const ret = projectDraftReturn(fn, returnCases, source, rawReturnEvidence, loadModule, filePath);
6570
+ const { evidence: returnEvidence, ...returns } = ret;
6571
+ let evidence = paramEvidence;
6572
+ if (evidence === "none") {
6573
+ const anyBody = params.some((p) => p.bodyAccesses && p.bodyAccesses.length > 0);
6574
+ if (anyBody) evidence = "body";
6575
+ }
6576
+ entries.push({
6577
+ fn: fn.name,
6578
+ params,
6579
+ returns,
6580
+ paramEvidence: evidence,
6581
+ returnEvidence,
6582
+ dsl: draftDsl({ params, returns })
6583
+ });
6584
+ }
6585
+ const draftSource = formatDraftModule(filePath, entries, sidecarPath);
6586
+ return { file: filePath, entries, draftSource, sidecarPath };
6587
+ }
6588
+ function formatDraftModule(filePath, entries, sidecarPath) {
6589
+ const target = sidecarPath ?? sidecarPathOf4(filePath);
6590
+ const draftName = sidecarDraftPath(filePath).split(/[/\\]/).pop() ?? "*.nudo.draft.js";
6591
+ const draftable = entries.filter((e) => e.dsl !== void 0 && e.skipped === void 0);
6592
+ const BUILDERS = ["fn", "number", "string", "boolean", "any", "shape", "array", "lit", "union"];
6593
+ const used = /* @__PURE__ */ new Set(["fn"]);
6594
+ for (const e of draftable) {
6595
+ const blob = `${e.dsl ?? ""}
6596
+ ${suggestedBodyDsl(e.fn, e.params) ?? ""}`;
6597
+ for (const b of BUILDERS) {
6598
+ if (new RegExp(`\\b${b}\\b`).test(blob)) used.add(b);
6599
+ }
6600
+ }
6601
+ const importList = BUILDERS.filter((b) => used.has(b)).join(", ");
6602
+ const lines = [
6603
+ "// @nudo:draft",
6604
+ `// Generated by \`nudo interface --draft\` from ${filePath}`,
6605
+ `// This ${draftName} file is NOT loaded as a sidecar contract.`,
6606
+ `// Review each export, then copy it into ${target} to accept.`,
6607
+ "//",
6608
+ "// Evidence: callsite/directive = observed args; body = fields the",
6609
+ "// implementation reads (suggestion only \u2014 never a check obligation);",
6610
+ "// symbolic = generalize; omitted params = no evidence.",
6611
+ "// Handwritten contracts are never overwritten.",
6612
+ "",
6613
+ `import { ${importList} } from "@nudojs/core";`,
6614
+ ""
6615
+ ];
6616
+ if (draftable.length === 0) {
6617
+ lines.push("// (no draftable exports \u2014 handwritten / non-export / empty)");
6618
+ lines.push("");
6619
+ }
6620
+ for (const e of draftable) {
6621
+ lines.push(`// ${e.fn} \u2014 param: ${e.paramEvidence}, return: ${e.returnEvidence}`);
6622
+ for (const p of e.params) {
6623
+ if (!p.projected) {
6624
+ lines.push(`// ${p.name}: ${p.display}`);
6625
+ } else if (p.display.includes("/* observed") || p.display.includes("/* body also reads")) {
6626
+ lines.push(`// ${p.name}: ${p.display}`);
6627
+ }
6628
+ }
6629
+ const suggested = suggestedBodyDsl(e.fn, e.params);
6630
+ if (suggested) lines.push(suggested);
6631
+ if (e.returns && !e.returns.projected) {
6632
+ lines.push(`// returns: ${e.returns.display}`);
6633
+ } else if (e.returns?.display.includes("/* observed")) {
6634
+ lines.push(`// returns: ${e.returns.display}`);
6635
+ } else if (e.returnEvidence === "symbolic") {
6636
+ lines.push(`// returns: ${e.returns?.display ?? ""} (symbolic)`);
6637
+ }
6638
+ lines.push(`export const ${draftExportName(e.fn)} = ${e.dsl};`);
6639
+ if (e.fn.includes(".")) {
6640
+ lines.push(`// sidecar key may also be written as \`${e.fn}\` / nested { ${e.fn.split(".")[1]}: \u2026 }`);
6641
+ }
6642
+ lines.push("");
6643
+ }
6644
+ const skipped = entries.filter((e) => e.skipped !== void 0);
6645
+ if (skipped.length > 0) {
6646
+ lines.push("// Skipped:");
6647
+ for (const s of skipped) {
6648
+ lines.push(`// ${s.fn} (${s.skipped})`);
6649
+ }
6650
+ lines.push("");
6651
+ }
6652
+ return lines.join("\n");
6653
+ }
6654
+ function sidecarDraftPath(filePath) {
6655
+ return sidecarPathOf4(filePath).replace(/\.nudo\.([cm]?[jt]s)$/, ".nudo.draft.$1");
6656
+ }
6657
+ function safeRealpath(p) {
6658
+ try {
6659
+ return realpathSync2(p);
6660
+ } catch {
6661
+ try {
6662
+ return join7(realpathSync2(dirname13(p)), basename3(p));
6663
+ } catch {
6664
+ return p;
6665
+ }
6666
+ }
6667
+ }
6668
+ function isDraftableEntry(entries) {
6669
+ return entries.some((e) => e.dsl !== void 0 && e.skipped === void 0);
6670
+ }
6671
+ function writeInterfaceDraft(filePath, draftSource, opts = {}) {
6672
+ const draftPath = sidecarDraftPath(filePath);
6673
+ const formalPath = sidecarPathOf4(filePath);
6674
+ if (draftPath === formalPath) {
6675
+ throw new Error(
6676
+ `draft write refused: draft path equals formal sidecar (${formalPath}); never overwrite handwritten contracts`
6677
+ );
6678
+ }
6679
+ if (/[/\\]node_modules[/\\]/.test(draftPath) || /[/\\]node_modules[/\\]/.test(filePath)) {
6680
+ throw new Error(`draft write refused: path is inside node_modules (${draftPath})`);
6681
+ }
6682
+ if (opts.projectDir) {
6683
+ const rootReal = safeRealpath(opts.projectDir);
6684
+ const draftReal = safeRealpath(draftPath);
6685
+ const rel = relative3(rootReal, draftReal);
6686
+ if (rel.startsWith("..") || isAbsolute2(rel)) {
6687
+ throw new Error(`draft write refused: outside project root ${opts.projectDir}`);
6688
+ }
6689
+ }
6690
+ const draftable = opts.draftable !== void 0 ? opts.draftable : opts.entries !== void 0 ? isDraftableEntry(opts.entries) : /export\s+const\s+[\p{ID_Start}$_][\p{ID_Continue}$]*\s*=/u.test(draftSource);
6691
+ const prev = existsSync11(draftPath) ? readFileSync9(draftPath, "utf-8") : void 0;
6692
+ const changed = prev !== draftSource;
6693
+ const written = !opts.dryRun && changed && draftable;
6694
+ if (written) {
6695
+ writeFileSync3(draftPath, draftSource, "utf-8");
6696
+ }
6697
+ return {
6698
+ draftPath,
6699
+ written,
6700
+ changed,
6701
+ draftable,
6702
+ draftSource
6703
+ };
6704
+ }
6705
+ function formatDraftSummary(sourceRel, draftRel, result, write) {
6706
+ const lines = [sourceRel];
6707
+ for (const e of result.entries) {
6708
+ if (e.skipped === "handwritten") {
6709
+ lines.push(` ${e.fn} [handwritten] skipped (draft never overwrites)`);
6710
+ } else if (e.skipped === "not-an-export") {
6711
+ lines.push(` ${e.fn} [not-an-export] skipped`);
6712
+ } else if (e.dsl) {
6713
+ lines.push(` ${e.fn} [draft ${e.paramEvidence}/${e.returnEvidence}] ${e.dsl}`);
6714
+ }
6715
+ }
6716
+ if (write) {
6717
+ if (write.changed && write.draftable) {
6718
+ lines.push(
6719
+ write.written ? `Draft written \u2192 ${draftRel}` : `[dry-run] would write \u2192 ${draftRel}`
6720
+ );
6721
+ } else if (write.changed && !write.draftable) {
6722
+ if (result.entries.length > 0) {
6723
+ const skipped = result.entries.filter((e) => e.skipped !== void 0).length;
6724
+ lines.push(
6725
+ skipped > 0 ? `Draft not written (${skipped} skipped, no writeable exports); nothing written \u2192 ${draftRel}` : `Draft not written (no writeable exports); nothing written \u2192 ${draftRel}`
6726
+ );
6727
+ } else {
6728
+ lines.push(`Draft empty (no draftable exports); nothing written \u2192 ${draftRel}`);
6729
+ }
6730
+ } else {
6731
+ lines.push(`${draftRel}: draft unchanged`);
6732
+ }
6733
+ lines.push(` review, then copy accepted exports into ${sidecarPathOf4(result.file)}`);
6734
+ } else {
6735
+ lines.push("");
6736
+ lines.push(result.draftSource.trimEnd());
6737
+ }
6738
+ return lines;
6739
+ }
6740
+
6741
+ // src/interface-derivation.ts
6742
+ import { existsSync as existsSync12, readFileSync as readFileSync10, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
6743
+ import { basename as basename4, dirname as dirname14, relative as relative4, resolve as resolve9 } from "path";
6744
+ import { parse as parse10 } from "@nudojs/parser";
4979
6745
  import {
4980
6746
  analyzeFn,
4981
6747
  beginDerivationSession,
4982
6748
  constraintToEntryAbs,
4983
6749
  derivationChain,
4984
- effectiveInterface as effectiveInterface2,
6750
+ effectiveInterface as effectiveInterface3,
4985
6751
  endDerivationSession,
4986
6752
  execNudoModule as execNudoModule2,
4987
- formatConstraint as formatConstraint3,
6753
+ formatConstraint as formatConstraint4,
4988
6754
  getDerivation,
4989
6755
  interfaceDiagCount as interfaceDiagCount3,
4990
6756
  isNodeModulesPath as isNodeModulesPath2,
4991
6757
  isNudoConstraint as isNudoConstraint2,
4992
- joinThenProject as joinThenProject2,
6758
+ joinThenProject as joinThenProject3,
4993
6759
  parseSource as parseSource3,
4994
6760
  projectDerivationDsl,
4995
6761
  refineDiagCount as refineDiagCount3,
4996
6762
  setAbsCallCollector as setAbsCallCollector2,
4997
- sidecarPathOf as sidecarPathOf2,
6763
+ sidecarPathOf as sidecarPathOf5,
4998
6764
  tagDerivationRoot,
4999
6765
  takeInterfaceDiagsSince as takeInterfaceDiagsSince3,
5000
6766
  takeRefineDiagsSince as takeRefineDiagsSince3,
@@ -5004,7 +6770,7 @@ function extractFnConstraintSources(sidecarSrc, fnName) {
5004
6770
  const out = { params: {} };
5005
6771
  let ast;
5006
6772
  try {
5007
- ast = parse9(sidecarSrc);
6773
+ ast = parse10(sidecarSrc);
5008
6774
  } catch {
5009
6775
  return out;
5010
6776
  }
@@ -5075,7 +6841,7 @@ function extractFnConstraintSources(sidecarSrc, fnName) {
5075
6841
  }
5076
6842
  function functionParamNames(source, fnName) {
5077
6843
  try {
5078
- const ast = parse9(source);
6844
+ const ast = parse10(source);
5079
6845
  for (const stmt of ast.program.body) {
5080
6846
  const d = stmt.type === "ExportNamedDeclaration" ? stmt.declaration ?? void 0 : stmt;
5081
6847
  if (!d) continue;
@@ -5104,7 +6870,7 @@ function paramNameOf(p) {
5104
6870
  function topLevelFnNames(source) {
5105
6871
  const out = [];
5106
6872
  try {
5107
- const ast = parse9(source);
6873
+ const ast = parse10(source);
5108
6874
  for (const stmt of ast.program.body) {
5109
6875
  const d = stmt.type === "ExportNamedDeclaration" ? stmt.declaration ?? void 0 : stmt;
5110
6876
  if (!d) continue;
@@ -5124,16 +6890,16 @@ function topLevelFnNames(source) {
5124
6890
  function importLocalMap(source, fromFile) {
5125
6891
  const out = /* @__PURE__ */ new Map();
5126
6892
  try {
5127
- const ast = parse9(source);
5128
- const base = dirname10(resolve8(fromFile));
6893
+ const ast = parse10(source);
6894
+ const base = dirname14(resolve9(fromFile));
5129
6895
  for (const stmt of ast.program.body) {
5130
6896
  if (stmt.type !== "ImportDeclaration") continue;
5131
6897
  const spec = stmt.source.value;
5132
6898
  if (!spec.startsWith(".") && !spec.startsWith("/")) continue;
5133
- const raw = resolve8(base, spec);
6899
+ const raw = resolve9(base, spec);
5134
6900
  let modulePath = null;
5135
6901
  for (const cand of [raw, `${raw}.js`, `${raw}.ts`, `${raw}.mjs`]) {
5136
- if (existsSync7(cand)) {
6902
+ if (existsSync12(cand)) {
5137
6903
  modulePath = cand;
5138
6904
  break;
5139
6905
  }
@@ -5151,14 +6917,14 @@ function importLocalMap(source, fromFile) {
5151
6917
  return out;
5152
6918
  }
5153
6919
  function resolveRelImport(fromSpec, fromDir, targetDir) {
5154
- const abs = resolve8(fromDir, fromSpec);
5155
- let rel = relative2(targetDir, abs);
6920
+ const abs = resolve9(fromDir, fromSpec);
6921
+ let rel = relative4(targetDir, abs);
5156
6922
  if (!rel.startsWith(".")) rel = `./${rel}`;
5157
6923
  return rel.split("\\").join("/");
5158
6924
  }
5159
6925
  function projectParamSlot(absList, paramName) {
5160
6926
  if (absList.length === 0) return void 0;
5161
- const constraint = joinThenProject2(absList);
6927
+ const constraint = joinThenProject3(absList);
5162
6928
  if (constraint === void 0) return void 0;
5163
6929
  if (absList.length === 1) {
5164
6930
  const node = getDerivation(absList[0]);
@@ -5182,7 +6948,7 @@ function projectParamSlot(absList, paramName) {
5182
6948
  }
5183
6949
  return {
5184
6950
  constraint,
5185
- dsl: formatConstraint3(constraint),
6951
+ dsl: formatConstraint4(constraint),
5186
6952
  prelude: [],
5187
6953
  imports: [],
5188
6954
  compositional: false
@@ -5190,7 +6956,7 @@ function projectParamSlot(absList, paramName) {
5190
6956
  }
5191
6957
  function projectReturnSlot(retAbs, params, paramNames) {
5192
6958
  if (retAbs.length === 0) return void 0;
5193
- const constraint = joinThenProject2(retAbs);
6959
+ const constraint = joinThenProject3(retAbs);
5194
6960
  if (constraint === void 0) return void 0;
5195
6961
  if (retAbs.length === 1) {
5196
6962
  const node = getDerivation(retAbs[0]);
@@ -5219,7 +6985,7 @@ function projectReturnSlot(retAbs, params, paramNames) {
5219
6985
  }
5220
6986
  return {
5221
6987
  constraint,
5222
- dsl: formatConstraint3(constraint),
6988
+ dsl: formatConstraint4(constraint),
5223
6989
  prelude: [],
5224
6990
  imports: [],
5225
6991
  compositional: false
@@ -5251,18 +7017,18 @@ function projectReturnRelativeToParams(node, params, paramNames) {
5251
7017
  return void 0;
5252
7018
  }
5253
7019
  function deriveFromRoot(filePath, opts = {}) {
5254
- const abs = resolve8(filePath);
5255
- const source = readFileSync8(abs, "utf-8");
7020
+ const abs = resolve9(filePath);
7021
+ const source = readFileSync10(abs, "utf-8");
5256
7022
  const since = interfaceDiagCount3();
5257
- const autoBind = opts.autoBind ?? interfaceConfig(findProjectConfig(dirname10(abs))?.config).autoBind;
7023
+ const autoBind = opts.autoBind ?? interfaceConfig(findProjectConfig(dirname14(abs))?.config).autoBind;
5258
7024
  const loadModule = opts.loadModule ?? defaultLoadModule;
5259
- const sidecarPath = sidecarPathOf2(abs);
5260
- const sidecarSrc = existsSync7(sidecarPath) && !isNodeModulesPath2(sidecarPath) ? readFileSync8(sidecarPath, "utf-8") : "";
7025
+ const sidecarPath = sidecarPathOf5(abs);
7026
+ const sidecarSrc = existsSync12(sidecarPath) && !isNodeModulesPath2(sidecarPath) ? readFileSync10(sidecarPath, "utf-8") : "";
5261
7027
  const fnNames = topLevelFnNames(source);
5262
7028
  const roots = [];
5263
7029
  const plans = [];
5264
7030
  for (const fn of fnNames) {
5265
- const eff = effectiveInterface2(source, fn, { loadModule, fromFile: abs, autoBind });
7031
+ const eff = effectiveInterface3(source, fn, { loadModule, fromFile: abs, autoBind });
5266
7032
  if (!eff || eff.source !== "handwritten") continue;
5267
7033
  roots.push(fn);
5268
7034
  const sources = sidecarSrc ? extractFnConstraintSources(sidecarSrc, fn) : { params: {} };
@@ -5298,8 +7064,8 @@ function deriveFromRoot(filePath, opts = {}) {
5298
7064
  const wanted = opts.fnNames && opts.fnNames.length > 0 ? new Set(opts.fnNames) : void 0;
5299
7065
  const derived = [];
5300
7066
  for (const plan of plans) {
5301
- const relRoot = relative2(process.cwd(), abs);
5302
- const label = `${relRoot === "" || relRoot.startsWith("..") ? basename3(abs) : relRoot}:${plan.fnName}`;
7067
+ const relRoot = relative4(process.cwd(), abs);
7068
+ const label = `${relRoot === "" || relRoot.startsWith("..") ? basename4(abs) : relRoot}:${plan.fnName}`;
5303
7069
  const rows = deriveOneRoot(plan, source, abs, modules, importLocals, label);
5304
7070
  for (const row of rows) {
5305
7071
  if (wanted && !wanted.has(row.fn)) continue;
@@ -5346,7 +7112,7 @@ function deriveOneRoot(plan, source, file, modules, importLocals, label) {
5346
7112
  targetFile = file;
5347
7113
  }
5348
7114
  if (isNodeModulesPath2(targetFile)) continue;
5349
- if (!existsSync7(targetFile)) continue;
7115
+ if (!existsSync12(targetFile)) continue;
5350
7116
  const key = `${targetFile}::${targetExport}`;
5351
7117
  let agg = byCallee.get(key);
5352
7118
  if (!agg) {
@@ -5360,7 +7126,7 @@ function deriveOneRoot(plan, source, file, modules, importLocals, label) {
5360
7126
  for (const agg of byCallee.values()) {
5361
7127
  let targetSource;
5362
7128
  try {
5363
- targetSource = readFileSync8(agg.targetFile, "utf-8");
7129
+ targetSource = readFileSync10(agg.targetFile, "utf-8");
5364
7130
  } catch {
5365
7131
  continue;
5366
7132
  }
@@ -5597,7 +7363,7 @@ function sidecarAssembles(text, fromFile, loadModule) {
5597
7363
  }
5598
7364
  }
5599
7365
  function emitDerivedFromRoot(rootFile, opts) {
5600
- const abs = resolve8(rootFile);
7366
+ const abs = resolve9(rootFile);
5601
7367
  const derive = deriveFromRoot(abs, {
5602
7368
  ...opts.loadModule ? { loadModule: opts.loadModule } : {},
5603
7369
  ...opts.autoBind !== void 0 ? { autoBind: opts.autoBind } : {},
@@ -5606,10 +7372,10 @@ function emitDerivedFromRoot(rootFile, opts) {
5606
7372
  if (!derive.hasRoot) {
5607
7373
  return { sidecars: [], hasRoot: false, roots: [], entryOnly: true };
5608
7374
  }
5609
- const proj = findProjectConfig(dirname10(abs));
7375
+ const proj = findProjectConfig(dirname14(abs));
5610
7376
  const allow = interfaceConfig(proj?.config).emit;
5611
7377
  const projectDir = proj?.projectDir;
5612
- const rootSidecarDir = dirname10(sidecarPathOf2(abs));
7378
+ const rootSidecarDir = dirname14(sidecarPathOf5(abs));
5613
7379
  const wanted = opts.fnNames && opts.fnNames.length > 0 ? new Set(opts.fnNames) : void 0;
5614
7380
  const result = {
5615
7381
  sidecars: [],
@@ -5620,7 +7386,7 @@ function emitDerivedFromRoot(rootFile, opts) {
5620
7386
  for (const row of derive.derived) {
5621
7387
  if (wanted && !wanted.has(row.fn)) continue;
5622
7388
  if (!matchesEmitAllowlist(row.file, projectDir, allow)) continue;
5623
- const sp = sidecarPathOf2(row.file);
7389
+ const sp = sidecarPathOf5(row.file);
5624
7390
  if (isNodeModulesPath2(sp)) continue;
5625
7391
  const list = bySidecar.get(sp) ?? [];
5626
7392
  list.push(row);
@@ -5628,8 +7394,8 @@ function emitDerivedFromRoot(rootFile, opts) {
5628
7394
  }
5629
7395
  for (const [sidecarPath, rows0] of bySidecar) {
5630
7396
  const targetFile = rows0[0].file;
5631
- const targetSidecarDir = dirname10(sidecarPath);
5632
- const prevSrc = existsSync7(sidecarPath) ? readFileSync8(sidecarPath, "utf-8") : "";
7397
+ const targetSidecarDir = dirname14(sidecarPath);
7398
+ const prevSrc = existsSync12(sidecarPath) ? readFileSync10(sidecarPath, "utf-8") : "";
5633
7399
  const rows = opts.refreshExistingOnly ? rows0.filter((r) => findGeneratedSectionText(prevSrc, r.fn) !== void 0) : rows0;
5634
7400
  if (rows.length === 0) continue;
5635
7401
  const handwritten = handwrittenNames(prevSrc);
@@ -5648,7 +7414,7 @@ function emitDerivedFromRoot(rootFile, opts) {
5648
7414
  issues.push({
5649
7415
  code: "nudo:interface-name-clash",
5650
7416
  severity: "error",
5651
- message: `sidecar already has a handwritten binding '${row.fn}' (${relative2(process.cwd(), sidecarPath) || sidecarPath}); handwritten wins \u2014 skipping emit`
7417
+ message: `sidecar already has a handwritten binding '${row.fn}' (${relative4(process.cwd(), sidecarPath) || sidecarPath}); handwritten wins \u2014 skipping emit`
5652
7418
  });
5653
7419
  anySkip = "name-clash";
5654
7420
  continue;
@@ -5676,7 +7442,7 @@ function emitDerivedFromRoot(rootFile, opts) {
5676
7442
  continue;
5677
7443
  }
5678
7444
  for (const n of body.usedNames) taken.add(n);
5679
- const srcRel = relative2(targetSidecarDir, targetFile) || basename3(targetFile);
7445
+ const srcRel = relative4(targetSidecarDir, targetFile) || basename4(targetFile);
5680
7446
  const section = [
5681
7447
  DERIVED_HEADER,
5682
7448
  `// source: ${srcRel}:${row.fn}`,
@@ -5696,8 +7462,8 @@ function emitDerivedFromRoot(rootFile, opts) {
5696
7462
  for (const n of collectTopLevelNames(prevSection)) taken.add(n);
5697
7463
  continue;
5698
7464
  }
5699
- const norm = normalizeSectionText(section);
5700
- if (prevSection !== void 0 && normalizeSectionText(prevSection) === norm) {
7465
+ const norm2 = normalizeSectionText(section);
7466
+ if (prevSection !== void 0 && normalizeSectionText(prevSection) === norm2) {
5701
7467
  anySkip = "no-change";
5702
7468
  accepted.push({ fn: row.fn, text: prevSection, prevText: prevSection });
5703
7469
  for (const n of collectTopLevelNames(prevSection)) taken.add(n);
@@ -5726,7 +7492,7 @@ function emitDerivedFromRoot(rootFile, opts) {
5726
7492
  issues.push({
5727
7493
  code: "nudo:interface-not-projectable",
5728
7494
  severity: "error",
5729
- message: `assembled sidecar failed round-trip (${relative2(process.cwd(), sidecarPath) || sidecarPath}); refusing to write`
7495
+ message: `assembled sidecar failed round-trip (${relative4(process.cwd(), sidecarPath) || sidecarPath}); refusing to write`
5730
7496
  });
5731
7497
  anySkip = "not-projectable";
5732
7498
  result.sidecars.push({
@@ -5741,15 +7507,15 @@ function emitDerivedFromRoot(rootFile, opts) {
5741
7507
  continue;
5742
7508
  }
5743
7509
  const changed = finalContent !== prevSrc;
5744
- const diff = changed ? unifiedDiff(prevSrc, finalContent, relative2(process.cwd(), sidecarPath) || sidecarPath) : void 0;
7510
+ const diff = changed ? unifiedDiff(prevSrc, finalContent, relative4(process.cwd(), sidecarPath) || sidecarPath) : void 0;
5745
7511
  if (changed && !opts.dryRun) {
5746
7512
  const tmp = `${sidecarPath}.tmp-${process.pid}-${Math.random().toString(16).slice(2, 10)}`;
5747
7513
  try {
5748
- writeFileSync2(tmp, finalContent, "utf-8");
7514
+ writeFileSync4(tmp, finalContent, "utf-8");
5749
7515
  renameSync2(tmp, sidecarPath);
5750
7516
  } catch (e) {
5751
7517
  try {
5752
- if (existsSync7(tmp)) unlinkSync2(tmp);
7518
+ if (existsSync12(tmp)) unlinkSync2(tmp);
5753
7519
  } catch {
5754
7520
  }
5755
7521
  throw e;
@@ -5900,10 +7666,18 @@ function derivedRoundTrips(text, fn, fromFile, loadModule) {
5900
7666
  }
5901
7667
  }
5902
7668
  export {
7669
+ ANALYSIS_ABI,
7670
+ DEFAULT_ANALYSIS_MODE,
7671
+ DiskCache,
7672
+ HARVEST_NODE_DEFAULT_MAX_FILES,
7673
+ HARVEST_NODE_DEFAULT_MAX_MS,
5903
7674
  SEMANTIC_TOKEN_MODIFIERS,
5904
7675
  SEMANTIC_TOKEN_TYPES,
5905
7676
  absToTSType,
5906
7677
  absToZodSchema,
7678
+ ambientSourcesOfSidecar,
7679
+ analysisConfig,
7680
+ analysisFileCacheKey,
5907
7681
  analyzeExportsFromSource,
5908
7682
  analyzeFile,
5909
7683
  analyzeFileAsync,
@@ -5913,12 +7687,16 @@ export {
5913
7687
  buildCaseDirective,
5914
7688
  buildModuleGraph,
5915
7689
  buildSemanticTokens,
7690
+ checkCacheKey,
5916
7691
  clearAbsModuleCache,
5917
7692
  clearAnalysisFileCache,
5918
7693
  clearAnalysisSessionCaches,
5919
7694
  clearBPathCache,
7695
+ clearEnvPathDeps,
5920
7696
  clearFnAnalysisCache,
5921
7697
  clearHarvestCache,
7698
+ clearNodeHarvestCache,
7699
+ clearPathEnvCaches,
5922
7700
  collectAbsBindingsFromGraph,
5923
7701
  collectAbsInlays,
5924
7702
  collectBPathDiagnostics,
@@ -5929,14 +7707,20 @@ export {
5929
7707
  collectDtsFromEntry,
5930
7708
  collectEnvGlobals,
5931
7709
  collectEnvModules,
7710
+ collectLoadDepContents,
7711
+ collectParamBodyAccesses,
5932
7712
  collectStaticImports,
5933
7713
  computeDirtySet,
5934
7714
  defaultAbsLoadModule,
5935
7715
  defaultLoadModule,
5936
7716
  deriveFromRoot,
7717
+ diagnosticsLevelForFile,
7718
+ diskCacheRoot,
7719
+ draftInterface,
5937
7720
  emitDerivedFromRoot,
5938
7721
  emitInterface,
5939
7722
  encodeSemanticTokens,
7723
+ envPathDependents,
5940
7724
  evalAbsModuleGraph,
5941
7725
  evalProgramAbsWithModules,
5942
7726
  evictAbsModuleCacheFiles,
@@ -5945,8 +7729,12 @@ export {
5945
7729
  evictBPathCacheForFiles,
5946
7730
  evictFnAnalysisCacheForFiles,
5947
7731
  extractFnConstraintSources,
7732
+ extractNudoImportSpecs,
7733
+ filterDiagnosticsByLevel,
5948
7734
  findProjectConfig,
5949
7735
  formatDerivedSection,
7736
+ formatDraftModule,
7737
+ formatDraftSummary,
5950
7738
  formatEmitSummary,
5951
7739
  formatHarvestSummary,
5952
7740
  formatInterfaceSurfaceLine,
@@ -5957,9 +7745,12 @@ export {
5957
7745
  getAbsAtPosition,
5958
7746
  getAbsAtPositionAsync,
5959
7747
  getAnalysisFileCacheSize,
7748
+ getAnalysisSession,
5960
7749
  getCasesForFile,
5961
7750
  getCompletionsAtPosition,
7751
+ getEnvHarvestConflictCollector,
5962
7752
  getHoverAtPosition,
7753
+ getNodeHarvestCacheSize,
5963
7754
  getTypeAtPosition,
5964
7755
  getTypeAtPositionAsync,
5965
7756
  harvestNodeTypes,
@@ -5967,24 +7758,42 @@ export {
5967
7758
  harvestPackageCached,
5968
7759
  harvestToAbsModules,
5969
7760
  harvestedValueToAbs,
7761
+ ifaceCacheKey,
5970
7762
  insertGeneratedCaseDirectives,
5971
7763
  interfaceConfig,
5972
7764
  interfaceSurface,
7765
+ interfaceTierModifierBit,
5973
7766
  isBPathCapable,
7767
+ isDraftableEntry,
7768
+ isEnvTemplatePath,
7769
+ isHarvestNodeDisabled,
5974
7770
  isNudoTargetPath,
7771
+ isProjectConfigPath,
7772
+ isSidecarPath,
7773
+ isWatchRelevantPath,
5975
7774
  lookupHarvested,
5976
7775
  matchesEmitAllowlist,
7776
+ mergeHarvestUnderEnv,
5977
7777
  mockDirectivesToAbsSeeds,
7778
+ noteEnvPathDeps,
5978
7779
  packageHarvestToAbsModules,
7780
+ relativizePath,
5979
7781
  resetAllAnalysisCaches,
5980
7782
  resolvePackageRoot,
5981
7783
  serializeCaseArg,
5982
7784
  serializeInferJson,
7785
+ setAnalysisSession,
7786
+ setEnvHarvestConflictCollector,
7787
+ sha256Hex,
7788
+ shouldAnalyzeFile,
7789
+ sidecarDraftPath,
7790
+ hasNudoDirectives as sourceHasNudoDirectives,
5983
7791
  stripGeneratedCaseDirectives,
5984
7792
  summarizeNodeEnv,
5985
7793
  topoSortDirty,
5986
7794
  tryBPathCall,
5987
7795
  tryBPathCallFull,
5988
7796
  tryRunBPath,
5989
- unifiedDiff
7797
+ unifiedDiff,
7798
+ writeInterfaceDraft
5990
7799
  };