@nudojs/service 2.0.0 → 4.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.d.ts CHANGED
@@ -479,7 +479,7 @@ declare function lookupHarvested(h: PackageHarvest, moduleName: string, exportNa
479
479
  * 手动 `nudo harvest` 仍保留;这里是分析路径上的按需注入。
480
480
  */
481
481
 
482
- /** 裸说明符 → 包名(含 scope);相对/绝对/node: 内建返回 undefined */
482
+ /** 裸说明符 → 包名(含 scope);相对/绝对/node: 与裸 Node 内建返回 undefined */
483
483
  declare function barePackageName(spec: string): string | undefined;
484
484
  declare function collectBarePackages(source: string): string[];
485
485
  declare function harvestPackageCached(pkg: string, fromDir: string): PackageHarvest | null;
@@ -573,6 +573,34 @@ declare function bareSpecToAbsModules(spec: string, fromFile: string): AbsModule
573
573
  declare function collectEnvGlobals(envNames: string[]): Record<string, Abs>;
574
574
  /** @nudo:env modules(path / node:path / fs…)→ AbsModuleExports */
575
575
  declare function collectEnvModules(envNames: string[]): Record<string, AbsModuleExports>;
576
+ /** Conflict when handwritten env overwrote a harvest module/export (B8). */
577
+ type EnvHarvestConflict = {
578
+ module: string;
579
+ /** export names where env replaced a harvest binding (empty + defaulted = default only) */
580
+ exports: string[];
581
+ defaultOverwritten: boolean;
582
+ };
583
+ /**
584
+ * Install conflict collector; returns the previous one so nested/concurrent
585
+ * analyzeFile callers can save/restore (module-global is not re-entrant).
586
+ */
587
+ declare function setEnvHarvestConflictCollector(collector: ((c: EnvHarvestConflict) => void) | null): ((c: EnvHarvestConflict) => void) | null;
588
+ /** Read-only peek for tests / nested restore. */
589
+ declare function getEnvHarvestConflictCollector(): ((c: EnvHarvestConflict) => void) | null;
590
+ type MergeHarvestOptions = {
591
+ /**
592
+ * Per-call conflict sink. Takes precedence over the module-global collector
593
+ * installed via `setEnvHarvestConflictCollector`.
594
+ */
595
+ onConflict?: (c: EnvHarvestConflict) => void;
596
+ };
597
+ /**
598
+ * Handwritten `@nudojs/env` wins over harvest / graph modules on overlapping
599
+ * module keys and overlapping export names (docs/versioning.md B8 + website
600
+ * harvester API). Harvest-only modules/exports are kept as fill-in.
601
+ * Overwrites notify `opts.onConflict` or the global collector.
602
+ */
603
+ declare function mergeHarvestUnderEnv(harvestModules: Record<string, AbsModuleExports>, envModules: Record<string, AbsModuleExports>, opts?: MergeHarvestOptions): Record<string, AbsModuleExports>;
576
604
  /** 收集 @nudo:replace + @nudo:as → transpile 注入表 */
577
605
  declare function collectBPathReplacements(source: string): {
578
606
  targets: Array<{
@@ -821,18 +849,57 @@ declare function collectBPathDiagnostics(source: string, extraKnown?: Iterable<s
821
849
  /**
822
850
  * 预置 Node API env:从 @types/node 的 .d.ts harvest。
823
851
  * 与 packages/env 的手写 env 并行;此路径自动、可刷新。
852
+ *
853
+ * Productization (P0-B B2/B7):
854
+ * - In-process cache keyed by package root + package.json mtime/size.
855
+ * - Terminal failures (`not-found`/`no-dts`/`failed`) also cache — no retry storm.
856
+ * `disabled` is never cached (env var can flip mid-process).
857
+ * - `clearNodeHarvestCache()` for tests / watch invalidation.
858
+ * - `NUDO_HARVEST_NODE=off` disables harvest (explicit skip, not silent).
859
+ * - Defaults stay IDE-budgeted: maxFiles=12, maxMs=2500.
824
860
  */
825
861
 
862
+ /** IDE-startup budgets for @types/node harvest. Do not raise casually. */
863
+ declare const HARVEST_NODE_DEFAULT_MAX_FILES = 12;
864
+ declare const HARVEST_NODE_DEFAULT_MAX_MS = 2500;
865
+ type HarvestNodeStats = {
866
+ files: number;
867
+ symbols: number;
868
+ skipped: number;
869
+ };
826
870
  type NodeEnvResult = {
827
871
  ok: true;
828
872
  env: HarvestedEnv;
829
873
  root: string;
830
874
  files: number;
875
+ stats: HarvestNodeStats;
876
+ /** true when this result came from the in-process cache */
877
+ cached: boolean;
831
878
  } | {
832
879
  ok: false;
833
880
  error: string;
881
+ reason: "disabled" | "not-found" | "no-dts" | "failed";
882
+ /** true when this failure came from the in-process cache */
883
+ cached?: boolean;
834
884
  };
835
- /** harvest @types/node(限制文件数 + 时间预算,避免拖垮启动) */
885
+ /**
886
+ * Clear the in-process @types/node harvest cache.
887
+ * Drops **success and terminal-failure** entries (including `not-found`), so
888
+ * a mid-session `@types/node` install becomes visible without process restart.
889
+ * Tests, watch-mode dep-hash invalidation, and package-manager hooks should
890
+ * call this when `node_modules/@types/node` changes underneath the process.
891
+ */
892
+ declare function clearNodeHarvestCache(): void;
893
+ /** Number of cached harvest results (diagnostics / tests). */
894
+ declare function getNodeHarvestCacheSize(): number;
895
+ declare function isHarvestNodeDisabled(env?: NodeJS.ProcessEnv): boolean;
896
+ /**
897
+ * harvest @types/node(限制文件数 + 时间预算,避免拖垮启动)。
898
+ * Success **and** terminal failures (`not-found` / `no-dts` / `failed`) are
899
+ * cached in-process so IDE analysis does not re-walk / re-timeout every file.
900
+ * `disabled` is never cached — the env var can flip mid-process.
901
+ * Call `clearNodeHarvestCache()` after `@types/node` changes.
902
+ */
836
903
  declare function harvestNodeTypes(fromDir?: string, maxFiles?: number, maxMs?: number): NodeEnvResult;
837
904
  /** 把 harvest 结果压成「模块名 → 导出名列表」摘要,便于日志/测试 */
838
905
  declare function summarizeNodeEnv(env: HarvestedEnv): {
@@ -1321,4 +1388,4 @@ declare function emitDerivedFromRoot(rootFile: string, opts: {
1321
1388
  refreshExistingOnly?: boolean;
1322
1389
  }): EmitDerivedResult;
1323
1390
 
1324
- export { ANALYSIS_ABI, type AbsGraphOptions, type AbsMockSeeds, type AbsModuleCacheEntry, type AbsModuleGraphResult, type AbsModuleLoadIssue, AnalysisConfig, type AnalysisResult, type AnalysisSession, type BPathBuiltinUnknown, type BPathDiagnostics, type BPathRunResult, type BPathUnreachable, type BindingInfo, type BuildSemanticTokensOpts, CallRecord, type CaseHint, type CaseInfo, type CaseResult, type CompletionItem, type ConstraintSourceExpr, type DepContent, type DerivedExport, type DerivedParam, type Diagnostic, type DiagnosticSeverity, type DiagnosticTag, DiagnosticsLevel, DiskCache, type DiskCacheOptions, type DraftEvidence, type EmitDerivedResult, type EmitInterfaceOpts, type EmitInterfaceResult, type EmitInterfaceSkipReason, type EmitResult, type EmitSkipReason, type FunctionAnalysis, type HoverInfo, type InferJson, type InferJsonCase, type InferJsonFunction, type InterfaceDraftEntry, type InterfaceDraftOpts, type InterfaceDraftResult, type InterfaceSurfaceEntry, type InterfaceSurfaceOpts, type LoadModule, type ModuleExports, type ModuleGraphCache, type NodeEnvResult, type PackageHarvest, type ReferenceInfo, type RootDeriveOpts, type RootDeriveResult, SEMANTIC_TOKEN_MODIFIERS, SEMANTIC_TOKEN_TYPES, type SemanticToken, type SourceLocation, type SymbolInfo, type SymbolTable, type WriteDraftResult, absToTSType, absToZodSchema, ambientSourcesOfSidecar, analysisFileCacheKey, analyzeExportsFromSource, analyzeFile, analyzeFileAsync, autoHarvestModules, barePackageName, bareSpecToAbsModules, buildCaseDirective, buildModuleGraph, buildSemanticTokens, checkCacheKey, clearAbsModuleCache, clearAnalysisFileCache, clearAnalysisSessionCaches, clearBPathCache, clearEnvPathDeps, clearFnAnalysisCache, clearHarvestCache, collectAbsBindingsFromGraph, collectBPathDiagnostics, collectBPathReplacements, collectBarePackages, collectCallRecords, collectDependencySpecs, collectDtsFromEntry, collectEnvGlobals, collectEnvModules, collectLoadDepContents, collectParamBodyAccesses, collectStaticImports, computeDirtySet, defaultAbsLoadModule, defaultLoadModule, deriveFromRoot, diagnosticsLevelForFile, draftInterface, emitDerivedFromRoot, emitInterface, encodeSemanticTokens, envPathDependents, evalAbsModuleGraph, evalProgramAbsWithModules, evictAbsModuleCacheFiles, evictAnalysisCachesForFiles, evictAnalysisFileCacheForFiles, evictBPathCacheForFiles, evictFnAnalysisCacheForFiles, extractFnConstraintSources, extractNudoImportSpecs, filterDiagnosticsByLevel, formatDerivedSection, formatDraftModule, formatDraftSummary, formatEmitSummary, formatHarvestSummary, formatInterfaceSurfaceLine, generateDts, generateFunctionDtsLines, generateGuardFunction, generateGuardFunctionFromAbs, getAbsAtPosition, getAbsAtPositionAsync, getAnalysisFileCacheSize, getAnalysisSession, getCasesForFile, getCompletionsAtPosition, getHoverAtPosition, getTypeAtPosition, getTypeAtPositionAsync, harvestNodeTypes, harvestPackage, harvestPackageCached, harvestToAbsModules, harvestedValueToAbs, ifaceCacheKey, insertGeneratedCaseDirectives, interfaceSurface, interfaceTierModifierBit, isBPathCapable, isDraftableEntry, isEnvTemplatePath, isNudoTargetPath, isProjectConfigPath, isSidecarPath, isWatchRelevantPath, lookupHarvested, mockDirectivesToAbsSeeds, noteEnvPathDeps, packageHarvestToAbsModules, relativizePath, resetAllAnalysisCaches, resolvePackageRoot, serializeCaseArg, serializeInferJson, setAnalysisSession, sha256Hex, shouldAnalyzeFile, sidecarDraftPath, hasNudoDirectives as sourceHasNudoDirectives, stripGeneratedCaseDirectives, summarizeNodeEnv, topoSortDirty, tryBPathCall, tryBPathCallFull, tryRunBPath, unifiedDiff, writeInterfaceDraft };
1391
+ export { ANALYSIS_ABI, type AbsGraphOptions, type AbsMockSeeds, type AbsModuleCacheEntry, type AbsModuleGraphResult, type AbsModuleLoadIssue, AnalysisConfig, type AnalysisResult, type AnalysisSession, type BPathBuiltinUnknown, type BPathDiagnostics, type BPathRunResult, type BPathUnreachable, type BindingInfo, type BuildSemanticTokensOpts, CallRecord, type CaseHint, type CaseInfo, type CaseResult, type CompletionItem, type ConstraintSourceExpr, type DepContent, type DerivedExport, type DerivedParam, type Diagnostic, type DiagnosticSeverity, type DiagnosticTag, DiagnosticsLevel, DiskCache, type DiskCacheOptions, type DraftEvidence, type EmitDerivedResult, type EmitInterfaceOpts, type EmitInterfaceResult, type EmitInterfaceSkipReason, type EmitResult, type EmitSkipReason, type EnvHarvestConflict, type FunctionAnalysis, HARVEST_NODE_DEFAULT_MAX_FILES, HARVEST_NODE_DEFAULT_MAX_MS, type HarvestNodeStats, type HoverInfo, type InferJson, type InferJsonCase, type InferJsonFunction, type InterfaceDraftEntry, type InterfaceDraftOpts, type InterfaceDraftResult, type InterfaceSurfaceEntry, type InterfaceSurfaceOpts, type LoadModule, type MergeHarvestOptions, type ModuleExports, type ModuleGraphCache, type NodeEnvResult, type PackageHarvest, type ReferenceInfo, type RootDeriveOpts, type RootDeriveResult, SEMANTIC_TOKEN_MODIFIERS, SEMANTIC_TOKEN_TYPES, type SemanticToken, type SourceLocation, type SymbolInfo, type SymbolTable, type WriteDraftResult, absToTSType, absToZodSchema, ambientSourcesOfSidecar, analysisFileCacheKey, analyzeExportsFromSource, analyzeFile, analyzeFileAsync, autoHarvestModules, barePackageName, bareSpecToAbsModules, buildCaseDirective, buildModuleGraph, buildSemanticTokens, checkCacheKey, clearAbsModuleCache, clearAnalysisFileCache, clearAnalysisSessionCaches, clearBPathCache, clearEnvPathDeps, clearFnAnalysisCache, clearHarvestCache, clearNodeHarvestCache, collectAbsBindingsFromGraph, collectBPathDiagnostics, collectBPathReplacements, collectBarePackages, collectCallRecords, collectDependencySpecs, collectDtsFromEntry, collectEnvGlobals, collectEnvModules, collectLoadDepContents, collectParamBodyAccesses, collectStaticImports, computeDirtySet, defaultAbsLoadModule, defaultLoadModule, deriveFromRoot, diagnosticsLevelForFile, draftInterface, emitDerivedFromRoot, emitInterface, encodeSemanticTokens, envPathDependents, evalAbsModuleGraph, evalProgramAbsWithModules, evictAbsModuleCacheFiles, evictAnalysisCachesForFiles, evictAnalysisFileCacheForFiles, evictBPathCacheForFiles, evictFnAnalysisCacheForFiles, extractFnConstraintSources, extractNudoImportSpecs, filterDiagnosticsByLevel, formatDerivedSection, formatDraftModule, formatDraftSummary, formatEmitSummary, formatHarvestSummary, formatInterfaceSurfaceLine, generateDts, generateFunctionDtsLines, generateGuardFunction, generateGuardFunctionFromAbs, getAbsAtPosition, getAbsAtPositionAsync, getAnalysisFileCacheSize, getAnalysisSession, getCasesForFile, getCompletionsAtPosition, getEnvHarvestConflictCollector, getHoverAtPosition, getNodeHarvestCacheSize, getTypeAtPosition, getTypeAtPositionAsync, harvestNodeTypes, harvestPackage, harvestPackageCached, harvestToAbsModules, harvestedValueToAbs, ifaceCacheKey, insertGeneratedCaseDirectives, interfaceSurface, interfaceTierModifierBit, isBPathCapable, isDraftableEntry, isEnvTemplatePath, isHarvestNodeDisabled, isNudoTargetPath, isProjectConfigPath, isSidecarPath, isWatchRelevantPath, lookupHarvested, mergeHarvestUnderEnv, mockDirectivesToAbsSeeds, noteEnvPathDeps, packageHarvestToAbsModules, relativizePath, resetAllAnalysisCaches, resolvePackageRoot, serializeCaseArg, serializeInferJson, setAnalysisSession, setEnvHarvestConflictCollector, sha256Hex, shouldAnalyzeFile, sidecarDraftPath, hasNudoDirectives as sourceHasNudoDirectives, stripGeneratedCaseDirectives, summarizeNodeEnv, topoSortDirty, tryBPathCall, tryBPathCallFull, tryRunBPath, unifiedDiff, writeInterfaceDraft };
package/dist/index.js CHANGED
@@ -778,6 +778,49 @@ function lookupHarvested(h, moduleName, exportName) {
778
778
  }
779
779
 
780
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
+ ]);
781
824
  function barePackageName(spec) {
782
825
  if (!spec) return void 0;
783
826
  if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
@@ -785,7 +828,9 @@ function barePackageName(spec) {
785
828
  if (spec.startsWith("@")) {
786
829
  return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : void 0;
787
830
  }
788
- return parts[0];
831
+ const name = parts[0];
832
+ if (NODE_BUILTINS.has(name)) return void 0;
833
+ return name;
789
834
  }
790
835
  function collectBarePackages(source) {
791
836
  try {
@@ -1314,6 +1359,50 @@ function collectEnvModules(envNames) {
1314
1359
  }
1315
1360
  return out;
1316
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
+ }
1317
1406
  function collectBPathReplacements(source) {
1318
1407
  const targets = [];
1319
1408
  const values = {};
@@ -1428,7 +1517,7 @@ function tryRunBPath(source, filePath, opts = {}) {
1428
1517
  try {
1429
1518
  const { modules: graphMods, issues } = evalAbsModuleGraph(source, filePath);
1430
1519
  const envMods = collectEnvModules(opts.envNames ?? []);
1431
- const modules = { ...envMods, ...graphMods };
1520
+ const modules = mergeHarvestUnderEnv(graphMods, envMods);
1432
1521
  const { targets, values, asTargets, asValues } = collectBPathReplacements(source);
1433
1522
  const envGlobals = {
1434
1523
  ...collectEnvGlobals(opts.envNames ?? []),
@@ -1813,7 +1902,8 @@ function validateMockDirectives(directives, diagnostics) {
1813
1902
  for (const d of directives) {
1814
1903
  if (d.kind !== "mock" || !d.expression) continue;
1815
1904
  const expr = d.expression.trim();
1816
- if (expr.includes("(") && expr.includes(")") && !expr.startsWith("T.")) {
1905
+ const isTypeExpr = /^(number|string|boolean|any|array|shape|lit|union|fn|partial|pick|omit|record|required|readonly|nonNullable|and)\s*\(/.test(expr) || expr === "unknown" || expr === "any" || expr === "never" || expr === "true" || expr === "false" || expr === "null" || expr === "undefined" || /^-?\d+(\.\d+)?$/.test(expr) || (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) || expr.startsWith("{") || expr.startsWith("[");
1906
+ if (expr.includes("(") && expr.includes(")") && !isTypeExpr && !expr.includes("=>")) {
1817
1907
  diagnostics.push({
1818
1908
  range: { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
1819
1909
  severity: "warning",
@@ -1821,7 +1911,8 @@ function validateMockDirectives(directives, diagnostics) {
1821
1911
  code: "nudo:mock-invalid",
1822
1912
  suggestions: [
1823
1913
  "Supported formats: stub(), stub().returns(value), spy(), mock()",
1824
- "Arrow functions: (args) => expression or (args) => { statements; return value; }"
1914
+ "Arrow functions: (args) => expression or (args) => { statements; return value; }",
1915
+ "Type expressions: number(), string(), shape({...}), union(...), or concrete literals"
1825
1916
  ]
1826
1917
  });
1827
1918
  }
@@ -2352,6 +2443,24 @@ function analyzeFileUncached(filePath, source, activeCases, externalCallRecords,
2352
2443
  () => analyzeFileUncachedInner(filePath, source, activeCases, externalCallRecords, loadModule)
2353
2444
  );
2354
2445
  }
2446
+ function findModuleImportLoc(source, module) {
2447
+ const bare = module.startsWith("node:") ? module.slice("node:".length) : module;
2448
+ const alts = [.../* @__PURE__ */ new Set([module, bare, `node:${bare}`])];
2449
+ const lines = source.split(/\r?\n/);
2450
+ for (let i = 0; i < lines.length; i++) {
2451
+ const line = lines[i];
2452
+ for (const alt of alts) {
2453
+ const re = new RegExp(
2454
+ `["'\`]${alt.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["'\`]`
2455
+ );
2456
+ const m = re.exec(line);
2457
+ if (m && m.index !== void 0) {
2458
+ return { line: i, column: m.index, length: m[0].length };
2459
+ }
2460
+ }
2461
+ }
2462
+ return null;
2463
+ }
2355
2464
  function analyzeFileUncachedInner(filePath, source, activeCases, externalCallRecords, loadModule) {
2356
2465
  const ast = parse5(source);
2357
2466
  const functions = extractDirectives(ast);
@@ -2360,6 +2469,18 @@ function analyzeFileUncachedInner(filePath, source, activeCases, externalCallRec
2360
2469
  const nodeAbsMap = /* @__PURE__ */ new Map();
2361
2470
  const functionResults = [];
2362
2471
  const caseHints = [];
2472
+ const envHarvestConflicts = [];
2473
+ const prevHarvestConflictCollector = setEnvHarvestConflictCollector((c) => {
2474
+ if (!envHarvestConflicts.some((x) => x.module === c.module)) {
2475
+ envHarvestConflicts.push(c);
2476
+ } else {
2477
+ const prev = envHarvestConflicts.find((x) => x.module === c.module);
2478
+ for (const e of c.exports) {
2479
+ if (!prev.exports.includes(e)) prev.exports.push(e);
2480
+ }
2481
+ prev.defaultOverwritten ||= c.defaultOverwritten;
2482
+ }
2483
+ });
2363
2484
  const fileDirectives = extractFileDirectives(ast);
2364
2485
  const fileEnvNames = fileDirectives.filter((d) => d.kind === "env").flatMap((d) => d.envs);
2365
2486
  const projectConfig = findProjectConfig(dirname7(filePath));
@@ -2449,7 +2570,7 @@ function analyzeFileUncachedInner(filePath, source, activeCases, externalCallRec
2449
2570
  seedFns: seeds.seedFns,
2450
2571
  ...loadModule ? { loadModule } : {}
2451
2572
  });
2452
- absGraphModules = { ...collectEnvModules(envNames), ...g.modules };
2573
+ absGraphModules = mergeHarvestUnderEnv(g.modules, collectEnvModules(envNames));
2453
2574
  pushBModuleIssues(g.issues);
2454
2575
  } catch {
2455
2576
  }
@@ -2772,7 +2893,7 @@ function analyzeFileUncachedInner(filePath, source, activeCases, externalCallRec
2772
2893
  diagnostics.push({
2773
2894
  range: { start: { line: directive.commentLine, column: 0 }, end: { line: directive.commentLine, column: 999 } },
2774
2895
  severity: "error",
2775
- message: `Case "${directive.name}": expected ${formatShape2(directive.expected)}, got ${formatShape2(caseAbs)}. The inferred return type does not match the expected type declared in the @nudo:case directive`,
2896
+ message: `debug "${directive.name}": expected ${formatShape2(directive.expected)}, got ${formatShape2(caseAbs)}. The inferred return type does not match the expected type declared in the @nudo:case witness`,
2776
2897
  code: "nudo:case-expected"
2777
2898
  });
2778
2899
  }
@@ -3055,6 +3176,24 @@ function analyzeFileUncachedInner(filePath, source, activeCases, externalCallRec
3055
3176
  filePath,
3056
3177
  analysisConfig(projectConfig?.config).callSiteBudget
3057
3178
  );
3179
+ setEnvHarvestConflictCollector(prevHarvestConflictCollector);
3180
+ for (const c of envHarvestConflicts) {
3181
+ const parts = [];
3182
+ if (c.exports.length > 0) parts.push(`export(s) ${c.exports.join(", ")}`);
3183
+ if (c.defaultOverwritten) parts.push("default");
3184
+ const detail = parts.length > 0 ? ` \u2014 ${parts.join("; ")}` : "";
3185
+ const loc = findModuleImportLoc(source, c.module);
3186
+ const range = loc ? {
3187
+ start: { line: loc.line, column: loc.column },
3188
+ end: { line: loc.line, column: loc.column + loc.length }
3189
+ } : { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } };
3190
+ diagnostics.push({
3191
+ range,
3192
+ severity: "warning",
3193
+ message: `handwritten @nudo:env wins over harvest on module "${c.module}"${detail}; harvest only fills missing slots (B8). code=nudo:env-harvest-conflict`,
3194
+ code: "nudo:env-harvest-conflict"
3195
+ });
3196
+ }
3058
3197
  return {
3059
3198
  functions: functionResults,
3060
3199
  diagnostics,
@@ -3167,7 +3306,7 @@ function collectAbsCallRecords(source, seeds, filePath, precomputedModules, envN
3167
3306
  }
3168
3307
  }
3169
3308
  if (envNames.length > 0) {
3170
- modules = { ...collectEnvModules(envNames), ...modules ?? {} };
3309
+ modules = mergeHarvestUnderEnv(modules ?? {}, collectEnvModules(envNames));
3171
3310
  }
3172
3311
  try {
3173
3312
  importLocals = buildAbsImportLocalMap(source, filePath);
@@ -4293,19 +4432,97 @@ function collectLoadDepContents(filePath, source, loadModule) {
4293
4432
  }
4294
4433
 
4295
4434
  // src/harvest-node.ts
4296
- import { existsSync as existsSync8 } from "fs";
4435
+ import { existsSync as existsSync8, statSync as statSync6 } from "fs";
4436
+ import { join as join6 } from "path";
4297
4437
  import { harvestDts as harvestDts2 } from "@nudojs/harvester";
4298
- function harvestNodeTypes(fromDir, maxFiles = 12, maxMs = 2500) {
4438
+ var HARVEST_NODE_DEFAULT_MAX_FILES = 12;
4439
+ var HARVEST_NODE_DEFAULT_MAX_MS = 2500;
4440
+ var nodeHarvestCache = /* @__PURE__ */ new Map();
4441
+ function notFoundCacheKey(fromDir) {
4442
+ return `not-found|@types/node|${fromDir ?? ""}`;
4443
+ }
4444
+ function nodeHarvestCacheKey(root, maxFiles, maxMs) {
4445
+ let sig = "nostat";
4446
+ try {
4447
+ const st = statSync6(join6(root, "package.json"));
4448
+ sig = `${st.size}:${Math.floor(st.mtimeMs)}`;
4449
+ } catch {
4450
+ }
4451
+ return `${root}|${maxFiles}|${maxMs}|${sig}`;
4452
+ }
4453
+ function clearNodeHarvestCache() {
4454
+ nodeHarvestCache.clear();
4455
+ }
4456
+ function getNodeHarvestCacheSize() {
4457
+ return nodeHarvestCache.size;
4458
+ }
4459
+ function isHarvestNodeDisabled(env = process.env) {
4460
+ return env.NUDO_HARVEST_NODE === "off";
4461
+ }
4462
+ function harvestNodeTypes(fromDir, maxFiles = HARVEST_NODE_DEFAULT_MAX_FILES, maxMs = HARVEST_NODE_DEFAULT_MAX_MS) {
4463
+ if (isHarvestNodeDisabled()) {
4464
+ return {
4465
+ ok: false,
4466
+ error: "harvest disabled via NUDO_HARVEST_NODE=off",
4467
+ reason: "disabled"
4468
+ };
4469
+ }
4299
4470
  const root = resolvePackageRoot("@types/node", fromDir) ?? resolvePackageRoot("node", fromDir);
4300
4471
  if (!root || !existsSync8(root)) {
4301
- return { ok: false, error: "@types/node not found" };
4472
+ const missKey = notFoundCacheKey(fromDir);
4473
+ const miss = nodeHarvestCache.get(missKey);
4474
+ if (miss && !miss.ok && miss.reason === "not-found") {
4475
+ return { ...miss, cached: true };
4476
+ }
4477
+ const result = {
4478
+ ok: false,
4479
+ error: "@types/node not found",
4480
+ reason: "not-found"
4481
+ };
4482
+ nodeHarvestCache.set(missKey, result);
4483
+ return result;
4484
+ }
4485
+ const key = nodeHarvestCacheKey(root, maxFiles, maxMs);
4486
+ const hit = nodeHarvestCache.get(key);
4487
+ if (hit) {
4488
+ return hit.ok ? { ...hit, cached: true } : { ...hit, cached: true };
4302
4489
  }
4303
4490
  const dts = collectDtsFiles(root, maxFiles);
4304
4491
  if (dts.length === 0) {
4305
- return { ok: false, error: `no .d.ts under ${root}` };
4492
+ const result = {
4493
+ ok: false,
4494
+ error: `no .d.ts under ${root}`,
4495
+ reason: "no-dts"
4496
+ };
4497
+ nodeHarvestCache.set(key, result);
4498
+ return result;
4499
+ }
4500
+ try {
4501
+ const env = harvestDts2(dts, { maxMs });
4502
+ const stats = {
4503
+ files: env.stats.files,
4504
+ symbols: env.stats.symbols,
4505
+ skipped: env.stats.skipped
4506
+ };
4507
+ const result = {
4508
+ ok: true,
4509
+ env,
4510
+ root,
4511
+ files: stats.files,
4512
+ stats,
4513
+ cached: false
4514
+ };
4515
+ nodeHarvestCache.set(key, result);
4516
+ return result;
4517
+ } catch (e) {
4518
+ const result = {
4519
+ ok: false,
4520
+ error: `harvest failed: ${e.message}`,
4521
+ reason: "failed"
4522
+ };
4523
+ nodeHarvestCache.set(key, result);
4524
+ return result;
4306
4525
  }
4307
- const env = harvestDts2(dts, { maxMs });
4308
- return { ok: true, env, root, files: env.stats.files };
4309
4526
  }
4310
4527
  function summarizeNodeEnv(env) {
4311
4528
  return {
@@ -5078,15 +5295,15 @@ function serializeCaseArg(a) {
5078
5295
  }
5079
5296
  switch (s.k) {
5080
5297
  case "prim":
5081
- if (s.type === "number" || s.type === "string" || s.type === "boolean") {
5082
- return `T.${s.type}`;
5083
- }
5298
+ if (s.type === "number") return "number()";
5299
+ if (s.type === "string") return "string()";
5300
+ if (s.type === "boolean") return "boolean()";
5084
5301
  return null;
5085
5302
  case "unknown":
5086
5303
  case "any":
5087
- return "T.unknown";
5304
+ return "any()";
5088
5305
  case "never":
5089
- return "T.never";
5306
+ return "never";
5090
5307
  case "sum": {
5091
5308
  const parts = [];
5092
5309
  for (const member of s.members) {
@@ -5094,11 +5311,11 @@ function serializeCaseArg(a) {
5094
5311
  if (ser === null) return null;
5095
5312
  parts.push(ser);
5096
5313
  }
5097
- return `T.union(${parts.join(", ")})`;
5314
+ return `union(${parts.join(", ")})`;
5098
5315
  }
5099
5316
  case "arr": {
5100
5317
  const el = serializeCaseArg(s.element);
5101
- return el === null ? null : `T.array(${el})`;
5318
+ return el === null ? null : `array(${el})`;
5102
5319
  }
5103
5320
  case "tuple": {
5104
5321
  const parts = [];
@@ -5916,7 +6133,7 @@ function joinSections(base, sectionTexts) {
5916
6133
 
5917
6134
  // src/interface-draft.ts
5918
6135
  import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync3, realpathSync as realpathSync2 } from "fs";
5919
- import { basename as basename3, dirname as dirname13, isAbsolute as isAbsolute2, join as join6, relative as relative3 } from "path";
6136
+ import { basename as basename3, dirname as dirname13, isAbsolute as isAbsolute2, join as join7, relative as relative3 } from "path";
5920
6137
  import {
5921
6138
  effectiveInterface as effectiveInterface2,
5922
6139
  formatConstraint as formatConstraint3,
@@ -6444,7 +6661,7 @@ function safeRealpath(p) {
6444
6661
  return realpathSync2(p);
6445
6662
  } catch {
6446
6663
  try {
6447
- return join6(realpathSync2(dirname13(p)), basename3(p));
6664
+ return join7(realpathSync2(dirname13(p)), basename3(p));
6448
6665
  } catch {
6449
6666
  return p;
6450
6667
  }
@@ -7454,6 +7671,8 @@ export {
7454
7671
  ANALYSIS_ABI,
7455
7672
  DEFAULT_ANALYSIS_MODE,
7456
7673
  DiskCache,
7674
+ HARVEST_NODE_DEFAULT_MAX_FILES,
7675
+ HARVEST_NODE_DEFAULT_MAX_MS,
7457
7676
  SEMANTIC_TOKEN_MODIFIERS,
7458
7677
  SEMANTIC_TOKEN_TYPES,
7459
7678
  absToTSType,
@@ -7478,6 +7697,7 @@ export {
7478
7697
  clearEnvPathDeps,
7479
7698
  clearFnAnalysisCache,
7480
7699
  clearHarvestCache,
7700
+ clearNodeHarvestCache,
7481
7701
  clearPathEnvCaches,
7482
7702
  collectAbsBindingsFromGraph,
7483
7703
  collectAbsInlays,
@@ -7530,7 +7750,9 @@ export {
7530
7750
  getAnalysisSession,
7531
7751
  getCasesForFile,
7532
7752
  getCompletionsAtPosition,
7753
+ getEnvHarvestConflictCollector,
7533
7754
  getHoverAtPosition,
7755
+ getNodeHarvestCacheSize,
7534
7756
  getTypeAtPosition,
7535
7757
  getTypeAtPositionAsync,
7536
7758
  harvestNodeTypes,
@@ -7546,12 +7768,14 @@ export {
7546
7768
  isBPathCapable,
7547
7769
  isDraftableEntry,
7548
7770
  isEnvTemplatePath,
7771
+ isHarvestNodeDisabled,
7549
7772
  isNudoTargetPath,
7550
7773
  isProjectConfigPath,
7551
7774
  isSidecarPath,
7552
7775
  isWatchRelevantPath,
7553
7776
  lookupHarvested,
7554
7777
  matchesEmitAllowlist,
7778
+ mergeHarvestUnderEnv,
7555
7779
  mockDirectivesToAbsSeeds,
7556
7780
  noteEnvPathDeps,
7557
7781
  packageHarvestToAbsModules,
@@ -7561,6 +7785,7 @@ export {
7561
7785
  serializeCaseArg,
7562
7786
  serializeInferJson,
7563
7787
  setAnalysisSession,
7788
+ setEnvHarvestConflictCollector,
7564
7789
  sha256Hex,
7565
7790
  shouldAnalyzeFile,
7566
7791
  sidecarDraftPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nudojs/service",
3
- "version": "2.0.0",
3
+ "version": "4.0.0",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },
@@ -45,10 +45,10 @@
45
45
  "dependencies": {
46
46
  "@babel/traverse": "^7.29.0",
47
47
  "@babel/types": "^7.29.0",
48
- "@nudojs/core": "2.0.0",
49
- "@nudojs/parser": "0.5.0",
50
- "@nudojs/env": "0.3.0",
51
- "@nudojs/harvester": "0.2.5"
48
+ "@nudojs/core": "2.1.0",
49
+ "@nudojs/parser": "1.0.0",
50
+ "@nudojs/env": "0.4.1",
51
+ "@nudojs/harvester": "0.2.7"
52
52
  },
53
53
  "scripts": {
54
54
  "build": "tsup src/index.ts src/evaluator/evaluator-api.ts --format esm --dts"