ucn 3.8.26 → 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.
@@ -9,24 +9,56 @@
9
9
 
10
10
  // Resolution types ordered from most to least confident
11
11
  const RESOLUTION = {
12
- EXACT_BINDING: 'exact-binding',
13
- SAME_CLASS: 'same-class',
14
- RECEIVER_HINT: 'receiver-hint',
15
- SCOPE_MATCH: 'scope-match',
16
- NAME_ONLY: 'name-only',
17
- UNCERTAIN: 'uncertain',
12
+ EXACT_BINDING: 'exact-binding',
13
+ SAME_CLASS: 'same-class',
14
+ RECEIVER_HINT: 'receiver-hint',
15
+ SCOPE_MATCH: 'scope-match',
16
+ POSSIBLE_DISPATCH: 'possible-dispatch',
17
+ NAME_ONLY: 'name-only',
18
+ METHOD_AMBIGUOUS: 'method-ambiguous',
19
+ UNCERTAIN: 'uncertain',
18
20
  };
19
21
 
20
22
  // Seed scores per resolution type (tunable)
21
23
  const SCORES = {
22
- [RESOLUTION.EXACT_BINDING]: 0.98,
23
- [RESOLUTION.SAME_CLASS]: 0.92,
24
- [RESOLUTION.RECEIVER_HINT]: 0.80,
25
- [RESOLUTION.SCOPE_MATCH]: 0.65,
26
- [RESOLUTION.NAME_ONLY]: 0.40,
27
- [RESOLUTION.UNCERTAIN]: 0.25,
24
+ [RESOLUTION.EXACT_BINDING]: 0.98,
25
+ [RESOLUTION.SAME_CLASS]: 0.92,
26
+ [RESOLUTION.RECEIVER_HINT]: 0.80,
27
+ [RESOLUTION.SCOPE_MATCH]: 0.65,
28
+ [RESOLUTION.POSSIBLE_DISPATCH]: 0.50,
29
+ [RESOLUTION.NAME_ONLY]: 0.40,
30
+ [RESOLUTION.METHOD_AMBIGUOUS]: 0.35,
31
+ [RESOLUTION.UNCERTAIN]: 0.25,
28
32
  };
29
33
 
34
+ // Trust tiers for the tiered caller contract. CONFIRMED = the resolution
35
+ // rests on binding/receiver/import evidence; UNVERIFIED = name match without
36
+ // evidence. The mapping is resolution-based, never language-based — evidence
37
+ // flags already come from langTraits dispatch in callers.js, so every
38
+ // language gets correct tiers automatically.
39
+ const TIER = { CONFIRMED: 'confirmed', UNVERIFIED: 'unverified' };
40
+ const RESOLUTION_TIER = {
41
+ [RESOLUTION.EXACT_BINDING]: TIER.CONFIRMED,
42
+ [RESOLUTION.SAME_CLASS]: TIER.CONFIRMED,
43
+ [RESOLUTION.RECEIVER_HINT]: TIER.CONFIRMED,
44
+ // scope-match is only assigned with import/receiver/callback evidence
45
+ // (see scoreEdge below) — that satisfies the contract's evidence clause.
46
+ [RESOLUTION.SCOPE_MATCH]: TIER.CONFIRMED,
47
+ // Nominal dispatch tiering: a call that CAN reach the target through
48
+ // virtual dispatch (interface/supertype-typed receiver) or whose untyped
49
+ // receiver faces multiple same-name owners is evidence a call happens —
50
+ // not evidence it reaches THIS definition. Unverified by construction.
51
+ [RESOLUTION.POSSIBLE_DISPATCH]: TIER.UNVERIFIED,
52
+ [RESOLUTION.NAME_ONLY]: TIER.UNVERIFIED,
53
+ [RESOLUTION.METHOD_AMBIGUOUS]: TIER.UNVERIFIED,
54
+ [RESOLUTION.UNCERTAIN]: TIER.UNVERIFIED,
55
+ };
56
+
57
+ /** Map a RESOLUTION value to its trust tier (unknown values are unverified). */
58
+ function tierForResolution(resolution) {
59
+ return RESOLUTION_TIER[resolution] || TIER.UNVERIFIED;
60
+ }
61
+
30
62
  /**
31
63
  * Score a caller/callee edge based on resolution evidence.
32
64
  *
@@ -44,6 +76,25 @@ const SCORES = {
44
76
  function scoreEdge(evidence) {
45
77
  const reasons = [];
46
78
 
79
+ // Known receiver/path type mismatch — checked FIRST: positive evidence the
80
+ // call targets a different symbol overrides any receiver-type signal
81
+ // (without this, a known mismatch would score receiver-hint 0.80).
82
+ if (evidence.typeMismatch) {
83
+ reasons.push('receiver type mismatch');
84
+ return { confidence: SCORES[RESOLUTION.UNCERTAIN], resolution: RESOLUTION.UNCERTAIN, evidence: reasons };
85
+ }
86
+
87
+ // Nominal dispatch tiering (contract surface only — callers.js sets these
88
+ // flags exclusively under collectAccount, so legacy paths never see them).
89
+ if (evidence.possibleDispatch) {
90
+ reasons.push('interface/supertype dispatch');
91
+ return { confidence: SCORES[RESOLUTION.POSSIBLE_DISPATCH], resolution: RESOLUTION.POSSIBLE_DISPATCH, evidence: reasons };
92
+ }
93
+ if (evidence.methodAmbiguous) {
94
+ reasons.push('untyped receiver, multiple same-name definitions');
95
+ return { confidence: SCORES[RESOLUTION.METHOD_AMBIGUOUS], resolution: RESOLUTION.METHOD_AMBIGUOUS, evidence: reasons };
96
+ }
97
+
47
98
  // Exact binding match (highest confidence)
48
99
  if (evidence.hasBindingId) {
49
100
  reasons.push('binding-id match');
@@ -64,16 +115,25 @@ function scoreEdge(evidence) {
64
115
  return { confidence: SCORES[RESOLUTION.RECEIVER_HINT], resolution: RESOLUTION.RECEIVER_HINT, evidence: reasons };
65
116
  }
66
117
 
118
+ // Function reference (callback / passed-as-argument). Argument position is
119
+ // only confirming when the name demonstrably reaches the target (same file,
120
+ // same package, or an import edge) — otherwise it's a bare name match: a
121
+ // local variable or an unrelated same-name symbol shadows it invisibly.
122
+ if (evidence.isFunctionReference) {
123
+ reasons.push('function reference');
124
+ if (evidence.hasImportEvidence || evidence.hasSamePackageEvidence) {
125
+ reasons.push(evidence.hasImportEvidence ? 'import-supported' : 'same package/module');
126
+ return { confidence: SCORES[RESOLUTION.SCOPE_MATCH], resolution: RESOLUTION.SCOPE_MATCH, evidence: reasons };
127
+ }
128
+ reasons.push('no import evidence');
129
+ return { confidence: SCORES[RESOLUTION.NAME_ONLY], resolution: RESOLUTION.NAME_ONLY, evidence: reasons };
130
+ }
131
+
67
132
  // Scope/import-supported match
68
- if (evidence.hasImportEvidence || evidence.hasReceiverEvidence) {
133
+ if (evidence.hasImportEvidence || evidence.hasReceiverEvidence || evidence.hasSamePackageEvidence) {
69
134
  if (evidence.hasImportEvidence) reasons.push('import-supported');
70
135
  if (evidence.hasReceiverEvidence) reasons.push('receiver binding in scope');
71
- return { confidence: SCORES[RESOLUTION.SCOPE_MATCH], resolution: RESOLUTION.SCOPE_MATCH, evidence: reasons };
72
- }
73
-
74
- // Function reference (callback)
75
- if (evidence.isFunctionReference) {
76
- reasons.push('function reference');
136
+ if (evidence.hasSamePackageEvidence) reasons.push('same package/module');
77
137
  return { confidence: SCORES[RESOLUTION.SCOPE_MATCH], resolution: RESOLUTION.SCOPE_MATCH, evidence: reasons };
78
138
  }
79
139
 
@@ -111,6 +171,9 @@ function filterByConfidence(edges, minConfidence) {
111
171
  module.exports = {
112
172
  RESOLUTION,
113
173
  SCORES,
174
+ TIER,
175
+ RESOLUTION_TIER,
176
+ tierForResolution,
114
177
  scoreEdge,
115
178
  filterByConfidence,
116
179
  };
package/core/deadcode.js CHANGED
@@ -177,6 +177,45 @@ function buildUsageIndex(index, filterNames) {
177
177
  return usageIndex;
178
178
  }
179
179
 
180
+ /**
181
+ * Is a symbol part of the public/exported API surface?
182
+ *
183
+ * Beyond direct evidence (export list, export/public modifiers, Go
184
+ * capitalization), methods of an exported class count as exported in
185
+ * languages where class members are public by default (implicitlyPublicMembers
186
+ * trait — JS/TS/Python): they are reachable through the class from outside
187
+ * the project, so claiming them dead invites deleting public API (fix #211 —
188
+ * zod's `strictImplement` is called by zero project files but is documented
189
+ * public API). Private-by-shape members (#name, _name, `private` modifier)
190
+ * stay claimable.
191
+ */
192
+ function symbolIsExported(index, symbol, fileEntry) {
193
+ if (!fileEntry) return false;
194
+ const name = symbol.name;
195
+ const mods = symbol.modifiers || [];
196
+ if (fileEntry.exports.includes(name) || mods.includes('export') || mods.includes('public')) {
197
+ return true;
198
+ }
199
+ const traits = langTraits(fileEntry.language);
200
+ if (traits?.exportVisibility === 'capitalization') {
201
+ return /^[A-Z]/.test(name);
202
+ }
203
+ if (traits?.implicitlyPublicMembers && symbol.className &&
204
+ !mods.includes('private') && !name.startsWith('#') && !name.startsWith('_')) {
205
+ const classSyms = index.symbols.get(symbol.className) || [];
206
+ const cls = classSyms.find(c => c.file === symbol.file &&
207
+ (c.type === 'class' || c.type === 'interface'));
208
+ if (cls) {
209
+ const cmods = cls.modifiers || [];
210
+ if (fileEntry.exports.includes(symbol.className) ||
211
+ cmods.includes('export') || cmods.includes('public')) {
212
+ return true;
213
+ }
214
+ }
215
+ }
216
+ return false;
217
+ }
218
+
180
219
  /**
181
220
  * Find dead code (unused functions/classes)
182
221
  * @param {object} index - ProjectIndex instance
@@ -220,15 +259,7 @@ function deadcode(index, options = {}) {
220
259
  for (const name of potentiallyDeadNames) {
221
260
  const syms = index.symbols.get(name) || [];
222
261
  // Keep the name only if at least one definition is NOT exported
223
- const allExported = syms.every(s => {
224
- const fe = index.files.get(s.file);
225
- const lang = fe?.language;
226
- if (!fe) return false;
227
- return fe.exports.includes(name) ||
228
- (s.modifiers || []).includes('export') ||
229
- (s.modifiers || []).includes('public') ||
230
- (langTraits(lang)?.exportVisibility === 'capitalization' && /^[A-Z]/.test(name));
231
- });
262
+ const allExported = syms.every(s => symbolIsExported(index, s, index.files.get(s.file)));
232
263
  if (!allExported) narrowed.add(name);
233
264
  }
234
265
  potentiallyDeadNames = narrowed;
@@ -260,10 +291,18 @@ function deadcode(index, options = {}) {
260
291
  const content = index._readFile(filePath);
261
292
  // Fast pre-filter: extract identifiers from file, intersect with target names.
262
293
  // One regex pass over content (O(content)) vs O(names × content) substring searches.
294
+ // Names the identifier regex can never produce — quoted member names
295
+ // (zod's `"~validate"`), $-containing JS names — fall back to a substring
296
+ // check, or they would scan as zero-usage and be falsely claimed dead
297
+ // (fix #211: `this["~validate"](data)` is a real usage; the quotes in
298
+ // the symbol name make the substring search self-delimiting).
263
299
  const fileIdentifiers = new Set(content.match(/\b[a-zA-Z_]\w*\b/g));
264
300
  const namesInFile = [];
265
301
  for (const name of potentiallyDeadNames) {
266
- if (fileIdentifiers.has(name)) namesInFile.push(name);
302
+ const present = /^[a-zA-Z_]\w*$/.test(name)
303
+ ? fileIdentifiers.has(name)
304
+ : content.includes(name);
305
+ if (present) namesInFile.push(name);
267
306
  }
268
307
  if (namesInFile.length === 0) continue;
269
308
  const lines = content.split('\n');
@@ -291,9 +330,37 @@ function deadcode(index, options = {}) {
291
330
  (hashIdx === 0 || /\s/.test(line[hashIdx - 1]))) continue;
292
331
  // Skip if inside a string literal
293
332
  if (isInsideString(line, pos)) continue;
294
- // Skip property/field access: preceded by '.' unless followed by '(' (method call)
333
+ // Property/field access (preceded by '.'), not a
334
+ // call: resolve the RECEIVER (fix #216, express-
335
+ // measured false-dead — `app.all(route, user.load)`
336
+ // is a callback reference to user.js's load, and
337
+ // deleting it breaks the route).
338
+ // - import-bound module receiver → usage scoped
339
+ // to the module's resolved file
340
+ // - this/self/cls receiver → usage scoped to the
341
+ // same file (same-class member reference)
342
+ // - any other receiver (local object literal,
343
+ // instance) → NOT a usage of a standalone
344
+ // symbol (fix #123: `Primitives.Separator` has
345
+ // its own key; must not keep the export alive)
346
+ let dottedScope;
295
347
  if (pos > 0 && line[pos - 1] === '.' &&
296
- (pos + nameLen >= line.length || line[pos + nameLen] !== '(')) continue;
348
+ (pos + nameLen >= line.length || line[pos + nameLen] !== '(')) {
349
+ let r = pos - 2;
350
+ while (r >= 0 && /[\w$]/.test(line[r])) r--;
351
+ const receiver = line.slice(r + 1, pos - 1);
352
+ if (!receiver) continue;
353
+ if (['this', 'self', 'cls'].includes(receiver)) {
354
+ dottedScope = 'same-file';
355
+ } else {
356
+ const binding = (fileEntry.importBindings || [])
357
+ .find(b => b.name === receiver);
358
+ const resolved = binding && fileEntry.moduleResolved &&
359
+ fileEntry.moduleResolved[binding.module];
360
+ if (!resolved) continue;
361
+ dottedScope = resolved;
362
+ }
363
+ }
297
364
  // Skip object literal key: name followed by ':' (not '::' for Rust paths)
298
365
  const afterChar = pos + nameLen < line.length ? line[pos + nameLen] : '';
299
366
  const afterChar2 = pos + nameLen + 1 < line.length ? line[pos + nameLen + 1] : '';
@@ -303,7 +370,8 @@ function deadcode(index, options = {}) {
303
370
  usageIndex.get(name).push({
304
371
  file: filePath,
305
372
  line: i + 1,
306
- relativePath: fileEntry.relativePath
373
+ relativePath: fileEntry.relativePath,
374
+ ...(dottedScope && { dottedScope })
307
375
  });
308
376
  break; // one match per line is enough for deadcode
309
377
  }
@@ -365,12 +433,7 @@ function deadcode(index, options = {}) {
365
433
  continue;
366
434
  }
367
435
 
368
- const isExported = fileEntry && (
369
- fileEntry.exports.includes(name) ||
370
- mods.includes('export') ||
371
- mods.includes('public') ||
372
- (langTraits(lang)?.exportVisibility === 'capitalization' && /^[A-Z]/.test(name))
373
- );
436
+ const isExported = symbolIsExported(index, symbol, fileEntry);
374
437
 
375
438
  // Skip exported unless requested
376
439
  if (isExported && !options.includeExported) {
@@ -389,8 +452,15 @@ function deadcode(index, options = {}) {
389
452
  // Filter out usages that are at the definition location
390
453
  // nameLine: when decorators/annotations are present, startLine is the decorator line
391
454
  // but the name identifier is on a different line (nameLine). Check both.
455
+ // Dotted usages (fix #216) are scoped to the file their receiver
456
+ // resolves to — `user.load` keeps user.js's load alive, never an
457
+ // unrelated module's same-name symbol.
392
458
  let nonDefUsages = allUsages.filter(u =>
393
- !(u.file === symbol.file && (u.line === symbol.startLine || u.line === symbol.nameLine))
459
+ !(u.file === symbol.file && (u.line === symbol.startLine || u.line === symbol.nameLine)) &&
460
+ (!u.dottedScope ||
461
+ (u.dottedScope === 'same-file'
462
+ ? u.file === symbol.file
463
+ : u.dottedScope === symbol.relativePath))
394
464
  );
395
465
 
396
466
  // For exported symbols in --include-exported mode, also filter out export-site
@@ -428,6 +498,28 @@ function deadcode(index, options = {}) {
428
498
  ? mods.filter(m => !javaKw.has(m))
429
499
  : [];
430
500
 
501
+ // Interface/trait member declarations are contract surface, not
502
+ // executable code: "unreferenced" is true, but deleting one
503
+ // changes the API contract rather than removing dead logic (Go
504
+ // marker interfaces exist SOLELY as uncallable declarations —
505
+ // grpc-go-measured: its entire default deadcode output was this
506
+ // family). Label so the claim self-explains (fix #211). Only
507
+ // body-less declarations qualify — Java `default` and Rust
508
+ // default-bodied trait methods are executable code, detected
509
+ // generically by a brace in the member's source range.
510
+ const declaredOn = (() => {
511
+ if (!symbol.className) return null;
512
+ const enclosing = (index.symbols.get(symbol.className) || []).find(c =>
513
+ c.file === symbol.file && (c.type === 'interface' || c.type === 'trait'));
514
+ if (!enclosing) return null;
515
+ try {
516
+ const content = index._readFile(symbol.file);
517
+ const range = content.split('\n').slice(symbol.startLine - 1, symbol.endLine).join('\n');
518
+ if (range.includes('{')) return null;
519
+ } catch { return null; }
520
+ return { kind: enclosing.type, name: symbol.className };
521
+ })();
522
+
431
523
  results.push({
432
524
  name: symbol.name,
433
525
  type: symbol.type,
@@ -437,7 +529,8 @@ function deadcode(index, options = {}) {
437
529
  isExported,
438
530
  usageCount: 0,
439
531
  ...(decorators.length > 0 && { decorators }),
440
- ...(annotations.length > 0 && { annotations })
532
+ ...(annotations.length > 0 && { annotations }),
533
+ ...(declaredOn && { declaredOn })
441
534
  });
442
535
  }
443
536
  }
package/core/execute.js CHANGED
@@ -292,6 +292,7 @@ const HANDLERS = {
292
292
  const result = index.context(p.name, {
293
293
  ...buildCallerOptions(p),
294
294
  unreachableOnly: !!p.unreachableOnly,
295
+ all: !!p.all,
295
296
  });
296
297
  if (!result) return { ok: false, error: `Symbol "${p.name}" not found.` };
297
298
  const tNote = truncationNote(index);
@@ -337,6 +338,7 @@ const HANDLERS = {
337
338
  ...buildCallerOptions(p),
338
339
  depth: depthVal ?? 3,
339
340
  all: p.all || depthVal !== undefined,
341
+ expandUnverified: !!p.expandUnverified,
340
342
  });
341
343
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
342
344
  const note = treeNote(result);
@@ -358,6 +360,7 @@ const HANDLERS = {
358
360
  ...buildCallerOptions(p),
359
361
  depth: depthVal ?? 5,
360
362
  all: p.all || depthVal !== undefined,
363
+ expandUnverified: !!p.expandUnverified,
361
364
  });
362
365
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
363
366
  const note = treeNote(result);
@@ -396,6 +399,7 @@ const HANDLERS = {
396
399
  ...buildCallerOptions(p),
397
400
  depth: depthVal ?? 3,
398
401
  all: p.all || depthVal !== undefined,
402
+ expandUnverified: !!p.expandUnverified,
399
403
  });
400
404
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
401
405
  const note = treeNote(result);
@@ -105,6 +105,13 @@ function buildImportGraph(index) {
105
105
  for (const [filePath, fileEntry] of index.files) {
106
106
  const importedFiles = new Set();
107
107
  const seenModules = new Set();
108
+ // Per-module resolution map (fix #209): module string → resolved
109
+ // project file (ROOT-RELATIVE — fileEntry persists in the cache, so
110
+ // paths must stay portable). Lets query-time code answer "which FILE
111
+ // does the module behind this import binding live in" — file-level
112
+ // importGraph edges can't (a file importing the target for OTHER
113
+ // names is not evidence about THIS name's module).
114
+ const moduleResolved = {};
108
115
 
109
116
  for (const importModule of fileEntry.imports) {
110
117
  // Skip null modules (e.g., dynamic include! macros in Rust)
@@ -128,6 +135,7 @@ function buildImportGraph(index) {
128
135
  }
129
136
 
130
137
  if (resolved && index.files.has(resolved)) {
138
+ moduleResolved[importModule] = path.relative(index.root, resolved);
131
139
  // For Go, a package import means all files in that directory are dependencies
132
140
  // (Go packages span multiple files in the same directory)
133
141
  const filesToLink = [resolved];
@@ -154,6 +162,7 @@ function buildImportGraph(index) {
154
162
  }
155
163
 
156
164
  index.importGraph.set(filePath, importedFiles);
165
+ fileEntry.moduleResolved = moduleResolved;
157
166
  }
158
167
  }
159
168
 
@@ -181,8 +190,15 @@ function buildInheritanceGraph(index) {
181
190
  }
182
191
 
183
192
  if (symbol.extends) {
184
- // Parse comma-separated parents (Python MRO: "Flyable, Swimmable")
185
- const parents = symbol.extends.split(',').map(s => s.trim()).filter(Boolean);
193
+ // Parse comma-separated parents (Python MRO: "Flyable, Swimmable").
194
+ // Commas inside type arguments do NOT separate parents:
195
+ // `extends Base<string, object>` is ONE parent `Base`, and
196
+ // `class C(Mapping[str, int], Base)` is `Mapping` + `Base`.
197
+ // The naive split made every generically-extended class
198
+ // parentless (fix #214 — zod's whole ZodType hierarchy had no
199
+ // ancestor edges, measured: 12 true base-class dispatch edges
200
+ // demoted because `Base<string` never equals `Base`).
201
+ const parents = splitParentList(symbol.extends);
186
202
 
187
203
  // Resolve aliased parent names via import aliases
188
204
  // e.g., const { BaseHandler: Handler } = require('./base')
@@ -221,4 +237,30 @@ function buildInheritanceGraph(index) {
221
237
  }
222
238
  }
223
239
 
240
+ /**
241
+ * Split an extends/bases clause on TOP-LEVEL commas only and strip each
242
+ * parent's trailing type-argument suffix: `Base<string, object>` → `Base`,
243
+ * `Mapping[str, int], Flyable` → `Mapping`, `Flyable`. Depth-tracks <>, [],
244
+ * and () so argument commas never split (fix #214).
245
+ */
246
+ function splitParentList(clause) {
247
+ const parts = [];
248
+ let depth = 0;
249
+ let current = '';
250
+ for (const ch of String(clause)) {
251
+ if (ch === '<' || ch === '[' || ch === '(') depth++;
252
+ else if (ch === '>' || ch === ']' || ch === ')') depth = Math.max(0, depth - 1);
253
+ if (ch === ',' && depth === 0) {
254
+ parts.push(current);
255
+ current = '';
256
+ } else {
257
+ current += ch;
258
+ }
259
+ }
260
+ parts.push(current);
261
+ return parts
262
+ .map(s => s.trim().replace(/[<[(].*$/s, '').trim())
263
+ .filter(Boolean);
264
+ }
265
+
224
266
  module.exports = { buildDirIndex, buildImportGraph, buildInheritanceGraph, _resolveJavaPackageImport };
package/core/imports.js CHANGED
@@ -120,6 +120,10 @@ function resolveImport(importPath, fromFile, config = {}) {
120
120
  if (result) return result;
121
121
  }
122
122
  }
123
+
124
+ // Package self-reference (import own package by name)
125
+ const selfResolved = resolveSelfReference(importPath, fromDir, config);
126
+ if (selfResolved) return selfResolved;
123
127
  }
124
128
 
125
129
  // Check Go module imports
@@ -412,12 +416,124 @@ function resolveRustImport(importPath, fromFile, projectRoot) {
412
416
  /**
413
417
  * Try to resolve a path with various extensions
414
418
  */
419
+ // package.json lookup cache for self-reference resolution (dir -> info|null).
420
+ // Process-lifetime cache: package.json name/exports churn is rare enough that
421
+ // long-lived servers (MCP) tolerate it.
422
+ const _pkgCache = new Map();
423
+
424
+ function _findPackageJson(fromDir, stopDir) {
425
+ let current = fromDir;
426
+ for (let i = 0; i < 8; i++) {
427
+ let info;
428
+ if (_pkgCache.has(current)) {
429
+ info = _pkgCache.get(current);
430
+ } else {
431
+ info = null;
432
+ const candidate = path.join(current, 'package.json');
433
+ try {
434
+ if (fs.existsSync(candidate)) {
435
+ const pkg = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
436
+ info = { dir: current, name: pkg.name, exports: pkg.exports, main: pkg.main };
437
+ }
438
+ } catch { /* unreadable or invalid JSON */ }
439
+ _pkgCache.set(current, info);
440
+ }
441
+ if (info) return info;
442
+ if (stopDir && current === stopDir) break;
443
+ const parent = path.dirname(current);
444
+ if (parent === current) break;
445
+ current = parent;
446
+ }
447
+ return null;
448
+ }
449
+
450
+ /** Flatten an exports-map entry to candidate targets (condition objects in
451
+ * insertion order, arrays in order). 'types' conditions are skipped — they
452
+ * name declaration files, not runtime sources. */
453
+ function _collectExportTargets(entry, out = []) {
454
+ if (typeof entry === 'string') {
455
+ out.push(entry);
456
+ } else if (Array.isArray(entry)) {
457
+ for (const e of entry) _collectExportTargets(e, out);
458
+ } else if (entry && typeof entry === 'object') {
459
+ for (const [cond, v] of Object.entries(entry)) {
460
+ if (cond === 'types') continue;
461
+ _collectExportTargets(v, out);
462
+ }
463
+ }
464
+ return out;
465
+ }
466
+
467
+ /**
468
+ * Package self-reference: a file importing its own package by name
469
+ * (`import * as z from "zod/v3"` inside the zod repo) — standard in monorepo
470
+ * tests and benchmarks. Resolves through package.json "exports" (conditional
471
+ * objects, arrays, '*' wildcards), accepting the first condition target that
472
+ * lands on a real file.
473
+ */
474
+ function resolveSelfReference(importPath, fromDir, config) {
475
+ const pkg = _findPackageJson(fromDir, config.root ? path.dirname(config.root) : null);
476
+ if (!pkg || !pkg.name) return null;
477
+ if (importPath !== pkg.name && !importPath.startsWith(pkg.name + '/')) return null;
478
+ const subpath = importPath === pkg.name ? '.' : './' + importPath.slice(pkg.name.length + 1);
479
+ const extensions = config.extensions || getExtensions(config.language);
480
+ const tryTargets = (entry, wildcard) => {
481
+ for (const target of _collectExportTargets(entry)) {
482
+ const concrete = wildcard != null ? target.replace(/\*/g, wildcard) : target;
483
+ const resolved = resolveFilePath(path.resolve(pkg.dir, concrete), extensions);
484
+ if (resolved) return resolved;
485
+ }
486
+ return null;
487
+ };
488
+ const exp = pkg.exports;
489
+ if (typeof exp === 'string') {
490
+ return subpath === '.' ? tryTargets(exp, null) : null;
491
+ }
492
+ if (exp && typeof exp === 'object') {
493
+ if (exp[subpath] !== undefined) {
494
+ const hit = tryTargets(exp[subpath], null);
495
+ if (hit) return hit;
496
+ }
497
+ for (const [key, val] of Object.entries(exp)) {
498
+ const star = key.indexOf('*');
499
+ if (star === -1) continue;
500
+ const pre = key.slice(0, star);
501
+ const post = key.slice(star + 1);
502
+ if (subpath.length > pre.length + post.length &&
503
+ subpath.startsWith(pre) && subpath.endsWith(post)) {
504
+ const wild = subpath.slice(pre.length, subpath.length - post.length);
505
+ const hit = tryTargets(val, wild);
506
+ if (hit) return hit;
507
+ }
508
+ }
509
+ return null;
510
+ }
511
+ // No exports map: bare name -> main/index; subpath -> direct file
512
+ if (subpath === '.') {
513
+ return (pkg.main && resolveFilePath(path.resolve(pkg.dir, pkg.main), extensions)) ||
514
+ resolveFilePath(path.resolve(pkg.dir, 'index'), extensions);
515
+ }
516
+ return resolveFilePath(path.resolve(pkg.dir, subpath), extensions);
517
+ }
518
+
415
519
  function resolveFilePath(basePath, extensions) {
416
520
  // Check exact path
417
521
  if (fs.existsSync(basePath) && fs.statSync(basePath).isFile()) {
418
522
  return basePath;
419
523
  }
420
524
 
525
+ // TS-ESM: explicit '.js'/'.mjs' specifiers refer to '.ts'/'.mts' sources
526
+ // (import specifiers name the compiled output). Remap before probing.
527
+ const esmRemap = { '.js': ['.ts', '.tsx'], '.jsx': ['.tsx'], '.mjs': ['.mts'], '.cjs': ['.cts'] };
528
+ const explicitExt = path.extname(basePath);
529
+ if (esmRemap[explicitExt]) {
530
+ const stem = basePath.slice(0, -explicitExt.length);
531
+ for (const tsExt of esmRemap[explicitExt]) {
532
+ const candidate = stem + tsExt;
533
+ try { if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate; } catch { /* skip */ }
534
+ }
535
+ }
536
+
421
537
  // Try adding extensions
422
538
  for (const ext of extensions) {
423
539
  const withExt = basePath + ext;
@@ -633,5 +749,6 @@ module.exports = {
633
749
  extractImports,
634
750
  extractExports,
635
751
  resolveImport,
636
- resolveFilePath
752
+ resolveFilePath,
753
+ findGoModule
637
754
  };