ucn 3.2.0 → 3.3.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/README.md CHANGED
@@ -201,7 +201,7 @@ FIND CODE
201
201
  ═══════════════════════════════════════════════════════════════════════════════
202
202
  find <name> Find symbol definitions (top 5 by usage count)
203
203
  usages <name> All usages grouped: definitions, calls, imports, references
204
- toc Table of contents (functions, classes, state)
204
+ toc Table of contents (compact; --detailed lists all symbols)
205
205
  search <term> Text search (for simple patterns, consider grep instead)
206
206
  tests <name> Find test files for a function
207
207
 
@@ -250,13 +250,17 @@ Common Flags:
250
250
  --top=N / --all Limit or show all results
251
251
  --include-tests Include test files
252
252
  --include-methods Include method calls (obj.fn) in caller/callee analysis
253
+ --include-uncertain Include ambiguous/uncertain call matches
254
+ --include-exported Include exported symbols in deadcode
255
+ --detailed List all symbols in toc (compact counts by default)
253
256
  --no-cache Disable caching
254
257
  --clear-cache Clear cache before running
255
258
  --no-follow-symlinks Don't follow symbolic links
256
259
  -i, --interactive Keep index in memory for multiple queries
257
260
 
258
261
  Quick Start:
259
- ucn toc # See project structure
262
+ ucn toc # See project structure (compact)
263
+ ucn toc --detailed # List all functions/classes
260
264
  ucn about handleRequest # Understand a function
261
265
  ucn impact handleRequest # Before modifying
262
266
  ucn fn handleRequest --file api # Extract specific function
package/cli/index.js CHANGED
@@ -46,6 +46,10 @@ const flags = {
46
46
  includeTests: args.includes('--include-tests'),
47
47
  // Deadcode options
48
48
  includeExported: args.includes('--include-exported'),
49
+ // Uncertain matches (off by default)
50
+ includeUncertain: args.includes('--include-uncertain'),
51
+ // Detailed listing (e.g. toc with all symbols)
52
+ detailed: args.includes('--detailed'),
49
53
  // Output depth
50
54
  depth: args.find(a => a.startsWith('--depth='))?.split('=')[1] || null,
51
55
  // Inline expansion for callees
@@ -78,7 +82,7 @@ const knownFlags = new Set([
78
82
  '--json', '--verbose', '--no-quiet', '--quiet',
79
83
  '--code-only', '--with-types', '--top-level', '--exact',
80
84
  '--no-cache', '--clear-cache', '--include-tests',
81
- '--include-exported', '--expand', '--interactive', '-i', '--all', '--include-methods',
85
+ '--include-exported', '--expand', '--interactive', '-i', '--all', '--include-methods', '--include-uncertain', '--detailed',
82
86
  '--file', '--context', '--exclude', '--not', '--in',
83
87
  '--depth', '--add-param', '--remove-param', '--rename-to',
84
88
  '--default', '--top', '--no-follow-symlinks'
@@ -652,7 +656,7 @@ function runProjectCommand(rootDir, command, arg) {
652
656
 
653
657
  switch (command) {
654
658
  case 'toc': {
655
- const toc = index.getToc();
659
+ const toc = index.getToc({ detailed: flags.detailed, topLevel: flags.topLevel });
656
660
  printOutput(toc, output.formatTocJson, printProjectToc);
657
661
  break;
658
662
  }
@@ -699,7 +703,15 @@ function runProjectCommand(rootDir, command, arg) {
699
703
 
700
704
  case 'context': {
701
705
  requireArg(arg, 'Usage: ucn . context <name>');
702
- const ctx = index.context(arg, { includeMethods: flags.includeMethods, file: flags.file });
706
+ const ctx = index.context(arg, {
707
+ includeMethods: flags.includeMethods,
708
+ includeUncertain: flags.includeUncertain,
709
+ file: flags.file
710
+ });
711
+ if (!ctx) {
712
+ console.log(`Symbol "${arg}" not found.`);
713
+ break;
714
+ }
703
715
  printOutput(ctx,
704
716
  output.formatContextJson,
705
717
  r => { printContext(r, { expand: flags.expand, root: index.root }); }
@@ -730,7 +742,11 @@ function runProjectCommand(rootDir, command, arg) {
730
742
 
731
743
  case 'smart': {
732
744
  requireArg(arg, 'Usage: ucn . smart <name>');
733
- const smart = index.smart(arg, { withTypes: flags.withTypes, includeMethods: flags.includeMethods });
745
+ const smart = index.smart(arg, {
746
+ withTypes: flags.withTypes,
747
+ includeMethods: flags.includeMethods,
748
+ includeUncertain: flags.includeUncertain
749
+ });
734
750
  if (smart) {
735
751
  printOutput(smart, output.formatSmartJson, printSmart);
736
752
  } else {
@@ -906,6 +922,9 @@ function runProjectCommand(rootDir, command, arg) {
906
922
  const exported = item.isExported ? ' [exported]' : '';
907
923
  console.log(` ${output.lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}`);
908
924
  }
925
+ if (!flags.includeExported) {
926
+ console.log(`\nExported symbols excluded by default. Add --include-exported to include them.`);
927
+ }
909
928
  }
910
929
  );
911
930
  }
@@ -977,26 +996,27 @@ function extractFunctionFromProject(index, name) {
977
996
  return;
978
997
  }
979
998
 
999
+ let match;
980
1000
  if (matches.length > 1 && !flags.file) {
981
- // Disambiguation needed
982
- console.log(output.formatDisambiguation(matches, name, 'fn'));
983
- return;
1001
+ // Auto-select best match using same scoring as resolveSymbol
1002
+ match = pickBestDefinition(matches);
1003
+ console.error(`Note: Found ${matches.length} definitions for "${name}". Using ${match.relativePath}:${match.startLine}. Use --file to disambiguate.`);
1004
+ } else {
1005
+ match = matches[0];
984
1006
  }
985
1007
 
986
- const match = matches[0];
1008
+ // Extract code directly using symbol index location (works for class methods and overloads)
987
1009
  const code = fs.readFileSync(match.file, 'utf-8');
988
- const language = detectLanguage(match.file);
989
- const { fn, code: fnCode } = extractFunction(code, language, match.name);
1010
+ const lines = code.split('\n');
1011
+ const fnCode = lines.slice(match.startLine - 1, match.endLine).join('\n');
990
1012
 
991
- if (fn) {
992
- if (flags.json) {
993
- console.log(output.formatFunctionJson(fn, fnCode));
994
- } else {
995
- console.log(`${match.relativePath}:${fn.startLine}`);
996
- console.log(`${output.lineRange(fn.startLine, fn.endLine)} ${output.formatFunctionSignature(fn)}`);
997
- console.log('─'.repeat(60));
998
- console.log(fnCode);
999
- }
1013
+ if (flags.json) {
1014
+ console.log(output.formatFunctionJson(match, fnCode));
1015
+ } else {
1016
+ console.log(`${match.relativePath}:${match.startLine}`);
1017
+ console.log(`${output.lineRange(match.startLine, match.endLine)} ${output.formatFunctionSignature(match)}`);
1018
+ console.log('─'.repeat(60));
1019
+ console.log(fnCode);
1000
1020
  }
1001
1021
  }
1002
1022
 
@@ -1010,13 +1030,15 @@ function extractClassFromProject(index, name) {
1010
1030
  return;
1011
1031
  }
1012
1032
 
1033
+ let match;
1013
1034
  if (matches.length > 1 && !flags.file) {
1014
- // Disambiguation needed
1015
- console.log(output.formatDisambiguation(matches, name, 'class'));
1016
- return;
1035
+ // Auto-select best match using same scoring as resolveSymbol
1036
+ match = pickBestDefinition(matches);
1037
+ console.error(`Note: Found ${matches.length} definitions for "${name}". Using ${match.relativePath}:${match.startLine}. Use --file to disambiguate.`);
1038
+ } else {
1039
+ match = matches[0];
1017
1040
  }
1018
1041
 
1019
- const match = matches[0];
1020
1042
  const code = fs.readFileSync(match.file, 'utf-8');
1021
1043
  const language = detectLanguage(match.file);
1022
1044
  const { cls, code: clsCode } = extractClass(code, language, match.name);
@@ -1034,29 +1056,61 @@ function extractClassFromProject(index, name) {
1034
1056
  }
1035
1057
 
1036
1058
  function printProjectToc(toc) {
1037
- console.log(`PROJECT: ${toc.totalFiles} files, ${toc.totalLines} lines`);
1038
- console.log(` ${toc.totalFunctions} functions, ${toc.totalClasses} classes, ${toc.totalState} state objects`);
1039
- console.log('═'.repeat(60));
1059
+ const t = toc.totals;
1060
+ console.log(`PROJECT: ${t.files} files, ${t.lines} lines`);
1061
+ console.log(` ${t.functions} functions, ${t.classes} classes, ${t.state} state objects`);
1040
1062
 
1041
- for (const file of toc.byFile) {
1042
- // Filter for top-level only if flag is set
1043
- let functions = file.functions;
1044
- if (flags.topLevel) {
1045
- functions = functions.filter(fn => !fn.isNested && (!fn.indent || fn.indent === 0));
1046
- }
1047
-
1048
- if (functions.length === 0 && file.classes.length === 0) continue;
1049
-
1050
- console.log(`\n${file.file} (${file.lines} lines)`);
1063
+ // Show meta warnings only when there's something noteworthy
1064
+ const meta = toc.meta || {};
1065
+ const warnings = [];
1066
+ if (meta.dynamicImports) warnings.push(`${meta.dynamicImports} dynamic import(s)`);
1067
+ if (meta.uncertain) warnings.push(`${meta.uncertain} uncertain reference(s)`);
1068
+ if (warnings.length) {
1069
+ const hint = meta.uncertain ? ' — use --include-uncertain to include all' : '';
1070
+ console.log(` Note: ${warnings.join(', ')}${hint}`);
1071
+ }
1051
1072
 
1052
- for (const fn of functions) {
1053
- console.log(` ${output.lineRange(fn.startLine, fn.endLine)} ${output.formatFunctionSignature(fn)}`);
1073
+ // Hints
1074
+ if (toc.summary) {
1075
+ if (toc.summary.topFunctionFiles?.length) {
1076
+ const hint = toc.summary.topFunctionFiles.map(f => `${f.file} (${f.functions})`).join(', ');
1077
+ console.log(` Most functions: ${hint}`);
1078
+ }
1079
+ if (toc.summary.topLineFiles?.length) {
1080
+ const hint = toc.summary.topLineFiles.map(f => `${f.file} (${f.lines})`).join(', ');
1081
+ console.log(` Largest files: ${hint}`);
1054
1082
  }
1083
+ if (toc.summary.entryFiles?.length) {
1084
+ console.log(` Entry points: ${toc.summary.entryFiles.join(', ')}`);
1085
+ }
1086
+ }
1055
1087
 
1056
- for (const cls of file.classes) {
1057
- console.log(` ${output.lineRange(cls.startLine, cls.endLine)} ${output.formatClassSignature(cls)}`);
1088
+ console.log('═'.repeat(60));
1089
+ const hasDetail = toc.files.some(f => f.symbols);
1090
+ for (const file of toc.files) {
1091
+ const parts = [`${file.lines} lines`];
1092
+ if (file.functions) parts.push(`${file.functions} fn`);
1093
+ if (file.classes) parts.push(`${file.classes} cls`);
1094
+ if (file.state) parts.push(`${file.state} state`);
1095
+
1096
+ if (hasDetail) {
1097
+ console.log(`\n${file.file} (${parts.join(', ')})`);
1098
+ if (file.symbols) {
1099
+ for (const fn of file.symbols.functions) {
1100
+ console.log(` ${output.lineRange(fn.startLine, fn.endLine)} ${output.formatFunctionSignature(fn)}`);
1101
+ }
1102
+ for (const cls of file.symbols.classes) {
1103
+ console.log(` ${output.lineRange(cls.startLine, cls.endLine)} ${output.formatClassSignature(cls)}`);
1104
+ }
1105
+ }
1106
+ } else {
1107
+ console.log(` ${file.file} — ${parts.join(', ')}`);
1058
1108
  }
1059
1109
  }
1110
+
1111
+ if (!hasDetail) {
1112
+ console.log(`\nAdd --detailed to list all functions, or "ucn . about <name>" for full details on a symbol`);
1113
+ }
1060
1114
  }
1061
1115
 
1062
1116
  function printSymbols(symbols, query, options = {}) {
@@ -1506,7 +1560,7 @@ function printBestExample(index, name) {
1506
1560
 
1507
1561
  function printContext(ctx, options = {}) {
1508
1562
  // Handle struct/interface types differently
1509
- if (ctx.type && ['struct', 'interface', 'type'].includes(ctx.type)) {
1563
+ if (ctx.type && ['class', 'struct', 'interface', 'type'].includes(ctx.type)) {
1510
1564
  console.log(`Context for ${ctx.type} ${ctx.name}:`);
1511
1565
  console.log('═'.repeat(60));
1512
1566
 
@@ -1568,6 +1622,17 @@ function printContext(ctx, options = {}) {
1568
1622
  console.log(`Context for ${ctx.function}:`);
1569
1623
  console.log('═'.repeat(60));
1570
1624
 
1625
+ // Show meta note when results may be incomplete
1626
+ if (ctx.meta) {
1627
+ const notes = [];
1628
+ if (ctx.meta.dynamicImports) notes.push(`${ctx.meta.dynamicImports} dynamic import(s)`);
1629
+ if (ctx.meta.uncertain) notes.push(`${ctx.meta.uncertain} uncertain call(s) skipped`);
1630
+ if (notes.length) {
1631
+ const hint = ctx.meta.uncertain ? ' — use --include-uncertain to include all' : '';
1632
+ console.log(` Note: ${notes.join(', ')}${hint}`);
1633
+ }
1634
+ }
1635
+
1571
1636
  // Display warnings if any
1572
1637
  if (ctx.warnings && ctx.warnings.length > 0) {
1573
1638
  console.log('\n⚠️ WARNINGS:');
@@ -1717,6 +1782,18 @@ function printExpandedItem(item, root) {
1717
1782
  function printSmart(smart) {
1718
1783
  console.log(`${smart.target.name} (${smart.target.file}:${smart.target.startLine})`);
1719
1784
  console.log('═'.repeat(60));
1785
+
1786
+ // Show meta note when results may be incomplete
1787
+ if (smart.meta) {
1788
+ const notes = [];
1789
+ if (smart.meta.dynamicImports) notes.push(`${smart.meta.dynamicImports} dynamic import(s)`);
1790
+ if (smart.meta.uncertain) notes.push(`${smart.meta.uncertain} uncertain call(s) skipped`);
1791
+ if (notes.length) {
1792
+ const hint = smart.meta.uncertain ? ' — use --include-uncertain to include all' : '';
1793
+ console.log(` Note: ${notes.join(', ')}${hint}`);
1794
+ }
1795
+ }
1796
+
1720
1797
  console.log(smart.target.code);
1721
1798
 
1722
1799
  if (smart.dependencies.length > 0) {
@@ -2076,6 +2153,30 @@ function printLines(lines, range) {
2076
2153
  }
2077
2154
  }
2078
2155
 
2156
+ /**
2157
+ * Pick the best definition from multiple matches using same scoring as resolveSymbol.
2158
+ * Prefers lib/src/core over test/examples/vendor directories.
2159
+ */
2160
+ function pickBestDefinition(matches) {
2161
+ const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
2162
+ const scored = matches.map(m => {
2163
+ let score = 0;
2164
+ const rp = m.relativePath || '';
2165
+ // Prefer class/struct/interface types (+1000) - same as resolveSymbol
2166
+ if (typeOrder.has(m.type)) score += 1000;
2167
+ if (isTestFile(rp, detectLanguage(m.file))) score -= 500;
2168
+ if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) score -= 300;
2169
+ if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) score += 200;
2170
+ // Tiebreaker: prefer larger function bodies (more important/complex)
2171
+ if (m.startLine && m.endLine) {
2172
+ score += Math.min(m.endLine - m.startLine, 100);
2173
+ }
2174
+ return { match: m, score };
2175
+ });
2176
+ scored.sort((a, b) => b.score - a.score);
2177
+ return scored[0].match;
2178
+ }
2179
+
2079
2180
  function suggestSimilar(query, names) {
2080
2181
  const lower = query.toLowerCase();
2081
2182
  const similar = names.filter(n => n.toLowerCase().includes(lower));
@@ -2109,6 +2210,7 @@ Usage:
2109
2210
  ucn <file> [command] [args] Single file mode
2110
2211
  ucn <dir> [command] [args] Project mode (specific directory)
2111
2212
  ucn "pattern" [command] [args] Glob pattern mode
2213
+ (Default output is text; add --json for machine-readable JSON)
2112
2214
 
2113
2215
  ═══════════════════════════════════════════════════════════════════════════════
2114
2216
  UNDERSTAND CODE (UCN's strength - semantic analysis)
@@ -2321,7 +2423,7 @@ function executeInteractiveCommand(index, command, arg) {
2321
2423
  console.log('Usage: context <name>');
2322
2424
  return;
2323
2425
  }
2324
- const ctx = index.context(arg);
2426
+ const ctx = index.context(arg, { includeUncertain: flags.includeUncertain });
2325
2427
  printContext(ctx, { expand: flags.expand, root: index.root });
2326
2428
  break;
2327
2429
  }
@@ -2331,7 +2433,7 @@ function executeInteractiveCommand(index, command, arg) {
2331
2433
  console.log('Usage: smart <name>');
2332
2434
  return;
2333
2435
  }
2334
- const smart = index.smart(arg, {});
2436
+ const smart = index.smart(arg, { includeUncertain: flags.includeUncertain });
2335
2437
  if (smart) {
2336
2438
  printSmart(smart);
2337
2439
  } else {
package/core/imports.js CHANGED
@@ -26,14 +26,15 @@ function extractImports(content, language) {
26
26
  const parser = getParser(normalizedLang);
27
27
  if (parser) {
28
28
  const imports = langModule.findImportsInCode(content, parser);
29
- return { imports };
29
+ const dynamicCount = imports.filter(i => i.dynamic).length;
30
+ return { imports, dynamicCount };
30
31
  }
31
32
  } catch (e) {
32
33
  // AST parsing failed
33
34
  }
34
35
  }
35
36
 
36
- return { imports: [] };
37
+ return { imports: [], dynamicCount: 0 };
37
38
  }
38
39
 
39
40
  /**
@@ -115,11 +116,37 @@ function resolveImport(importPath, fromFile, config = {}) {
115
116
  if (resolved) return resolved;
116
117
  }
117
118
 
119
+ // Rust: crate::, super::, self:: paths and mod declarations
120
+ if (config.language === 'rust') {
121
+ const resolved = resolveRustImport(importPath, fromFile, config.root);
122
+ if (resolved) return resolved;
123
+ }
124
+
118
125
  return null; // External package
119
126
  }
120
127
 
128
+ // Python relative imports: translate dot-prefix notation to file paths
129
+ // e.g., ".models" -> "./models", "..utils" -> "../utils", "." -> "."
130
+ let normalizedPath = importPath;
131
+ if (config.language === 'python') {
132
+ // Count leading dots and convert to filesystem relative path
133
+ const dotMatch = importPath.match(/^(\.+)(.*)/);
134
+ if (dotMatch) {
135
+ const dots = dotMatch[1];
136
+ const rest = dotMatch[2];
137
+ if (dots.length === 1) {
138
+ // ".models" -> "./models", "." -> "."
139
+ normalizedPath = rest ? './' + rest.replace(/\./g, '/') : '.';
140
+ } else {
141
+ // "..models" -> "../models", "...models" -> "../../models"
142
+ const upDirs = '../'.repeat(dots.length - 1);
143
+ normalizedPath = rest ? upDirs + rest.replace(/\./g, '/') : upDirs.slice(0, -1);
144
+ }
145
+ }
146
+ }
147
+
121
148
  // Relative imports
122
- const resolved = path.resolve(fromDir, importPath);
149
+ const resolved = path.resolve(fromDir, normalizedPath);
123
150
  return resolveFilePath(resolved, config.extensions || getExtensions(config.language));
124
151
  }
125
152
 
@@ -200,6 +227,125 @@ function resolveGoImport(importPath, fromFile, projectRoot) {
200
227
  return null;
201
228
  }
202
229
 
230
+ // Cache for Rust crate roots (Cargo.toml locations)
231
+ const cargoCache = new Map();
232
+
233
+ /**
234
+ * Find the nearest Cargo.toml and return the crate's source root
235
+ * @param {string} startDir - Directory to start searching from
236
+ * @returns {{root: string, srcDir: string}|null}
237
+ */
238
+ function findCargoRoot(startDir) {
239
+ if (cargoCache.has(startDir)) {
240
+ return cargoCache.get(startDir);
241
+ }
242
+
243
+ let dir = startDir;
244
+ while (dir !== path.dirname(dir)) {
245
+ const cargoPath = path.join(dir, 'Cargo.toml');
246
+ if (fs.existsSync(cargoPath)) {
247
+ const srcDir = path.join(dir, 'src');
248
+ const result = fs.existsSync(srcDir) ? { root: dir, srcDir } : null;
249
+ cargoCache.set(startDir, result);
250
+ return result;
251
+ }
252
+ dir = path.dirname(dir);
253
+ }
254
+
255
+ cargoCache.set(startDir, null);
256
+ return null;
257
+ }
258
+
259
+ /**
260
+ * Try to resolve a Rust module path to a file
261
+ * Checks both <path>.rs and <path>/mod.rs
262
+ * @param {string} dir - Base directory
263
+ * @param {string[]} segments - Path segments to resolve
264
+ * @returns {string|null}
265
+ */
266
+ function resolveRustModulePath(dir, segments) {
267
+ // Try progressively shorter paths (items at the end may be types, not modules)
268
+ for (let len = segments.length; len >= 1; len--) {
269
+ const modPath = path.join(dir, ...segments.slice(0, len));
270
+ // Try <path>.rs
271
+ const rsFile = modPath + '.rs';
272
+ if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile()) {
273
+ return rsFile;
274
+ }
275
+ // Try <path>/mod.rs
276
+ const modFile = path.join(modPath, 'mod.rs');
277
+ if (fs.existsSync(modFile) && fs.statSync(modFile).isFile()) {
278
+ return modFile;
279
+ }
280
+ }
281
+ return null;
282
+ }
283
+
284
+ /**
285
+ * Resolve Rust import paths to local files
286
+ * Handles: crate::, super::, self::, and mod declarations
287
+ * @param {string} importPath - Rust import path (e.g., "crate::display::Display" or "display")
288
+ * @param {string} fromFile - File containing the import
289
+ * @param {string} projectRoot - Project root directory
290
+ * @returns {string|null}
291
+ */
292
+ function resolveRustImport(importPath, fromFile, projectRoot) {
293
+ const fromDir = path.dirname(fromFile);
294
+
295
+ // crate:: paths - resolve from the crate's src/ directory
296
+ if (importPath.startsWith('crate::')) {
297
+ const cargo = findCargoRoot(fromDir);
298
+ if (!cargo) return null;
299
+
300
+ const rest = importPath.slice('crate::'.length);
301
+ const segments = rest.split('::');
302
+ return resolveRustModulePath(cargo.srcDir, segments);
303
+ }
304
+
305
+ // super:: paths - resolve relative to parent directory
306
+ if (importPath.startsWith('super::')) {
307
+ let dir = fromDir;
308
+ let rest = importPath;
309
+ while (rest.startsWith('super::')) {
310
+ // If current file is mod.rs, go up one more directory
311
+ const basename = path.basename(fromFile);
312
+ if (basename === 'mod.rs' && dir === fromDir) {
313
+ dir = path.dirname(dir);
314
+ }
315
+ dir = path.dirname(dir);
316
+ rest = rest.slice('super::'.length);
317
+ }
318
+ const segments = rest.split('::');
319
+ return resolveRustModulePath(dir, segments);
320
+ }
321
+
322
+ // self:: paths - resolve within current module directory
323
+ if (importPath.startsWith('self::')) {
324
+ const rest = importPath.slice('self::'.length);
325
+ const segments = rest.split('::');
326
+ // If current file is mod.rs, resolve relative to its directory
327
+ const basename = path.basename(fromFile);
328
+ const dir = basename === 'mod.rs' ? fromDir : path.dirname(fromDir);
329
+ return resolveRustModulePath(dir, segments);
330
+ }
331
+
332
+ // Plain module name without :: (potential mod declaration)
333
+ // e.g., "display" from `mod display;` - resolve relative to declaring file
334
+ if (!importPath.includes('::')) {
335
+ // For mod declarations: <dir>/<name>.rs or <dir>/<name>/mod.rs
336
+ const rsFile = path.join(fromDir, importPath + '.rs');
337
+ if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile()) {
338
+ return rsFile;
339
+ }
340
+ const modFile = path.join(fromDir, importPath, 'mod.rs');
341
+ if (fs.existsSync(modFile) && fs.statSync(modFile).isFile()) {
342
+ return modFile;
343
+ }
344
+ }
345
+
346
+ return null;
347
+ }
348
+
203
349
  /**
204
350
  * Try to resolve a path with various extensions
205
351
  */
@@ -215,11 +361,14 @@ function resolveFilePath(basePath, extensions) {
215
361
  if (fs.existsSync(withExt)) return withExt;
216
362
  }
217
363
 
218
- // Try index files
364
+ // Try index files (index.js for JS/TS, __init__.py for Python)
219
365
  for (const ext of extensions) {
220
366
  const indexPath = path.join(basePath, 'index' + ext);
221
367
  if (fs.existsSync(indexPath)) return indexPath;
222
368
  }
369
+ // Python __init__.py
370
+ const initPath = path.join(basePath, '__init__.py');
371
+ if (fs.existsSync(initPath)) return initPath;
223
372
 
224
373
  return null;
225
374
  }