gitnexus 1.3.10 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +22 -2
  2. package/dist/cli/ai-context.d.ts +2 -1
  3. package/dist/cli/ai-context.js +33 -6
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +20 -2
  6. package/dist/cli/index.js +2 -0
  7. package/dist/cli/setup.js +17 -19
  8. package/dist/cli/skill-gen.d.ts +26 -0
  9. package/dist/cli/skill-gen.js +549 -0
  10. package/dist/core/graph/types.d.ts +5 -2
  11. package/dist/core/ingestion/call-processor.d.ts +5 -5
  12. package/dist/core/ingestion/call-processor.js +173 -260
  13. package/dist/core/ingestion/constants.d.ts +16 -0
  14. package/dist/core/ingestion/constants.js +16 -0
  15. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  16. package/dist/core/ingestion/entry-point-scoring.js +81 -22
  17. package/dist/core/ingestion/export-detection.d.ts +18 -0
  18. package/dist/core/ingestion/export-detection.js +230 -0
  19. package/dist/core/ingestion/framework-detection.d.ts +5 -1
  20. package/dist/core/ingestion/framework-detection.js +39 -8
  21. package/dist/core/ingestion/heritage-processor.d.ts +13 -4
  22. package/dist/core/ingestion/heritage-processor.js +92 -28
  23. package/dist/core/ingestion/import-processor.d.ts +17 -19
  24. package/dist/core/ingestion/import-processor.js +170 -695
  25. package/dist/core/ingestion/language-config.d.ts +46 -0
  26. package/dist/core/ingestion/language-config.js +167 -0
  27. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  28. package/dist/core/ingestion/mro-processor.js +369 -0
  29. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  30. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  31. package/dist/core/ingestion/parsing-processor.d.ts +1 -10
  32. package/dist/core/ingestion/parsing-processor.js +41 -177
  33. package/dist/core/ingestion/pipeline.js +41 -26
  34. package/dist/core/ingestion/process-processor.js +2 -1
  35. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  36. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  37. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  38. package/dist/core/ingestion/resolvers/go.js +42 -0
  39. package/dist/core/ingestion/resolvers/index.d.ts +16 -0
  40. package/dist/core/ingestion/resolvers/index.js +11 -0
  41. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  42. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  43. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  44. package/dist/core/ingestion/resolvers/php.js +35 -0
  45. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  46. package/dist/core/ingestion/resolvers/rust.js +73 -0
  47. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  48. package/dist/core/ingestion/resolvers/standard.js +145 -0
  49. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  50. package/dist/core/ingestion/resolvers/utils.js +120 -0
  51. package/dist/core/ingestion/symbol-resolver.d.ts +32 -0
  52. package/dist/core/ingestion/symbol-resolver.js +83 -0
  53. package/dist/core/ingestion/symbol-table.d.ts +12 -1
  54. package/dist/core/ingestion/symbol-table.js +19 -12
  55. package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -11
  56. package/dist/core/ingestion/tree-sitter-queries.js +114 -9
  57. package/dist/core/ingestion/type-env.d.ts +27 -0
  58. package/dist/core/ingestion/type-env.js +86 -0
  59. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  60. package/dist/core/ingestion/type-extractors/c-cpp.js +60 -0
  61. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  62. package/dist/core/ingestion/type-extractors/csharp.js +89 -0
  63. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  64. package/dist/core/ingestion/type-extractors/go.js +105 -0
  65. package/dist/core/ingestion/type-extractors/index.d.ts +21 -0
  66. package/dist/core/ingestion/type-extractors/index.js +29 -0
  67. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  68. package/dist/core/ingestion/type-extractors/jvm.js +121 -0
  69. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  70. package/dist/core/ingestion/type-extractors/php.js +31 -0
  71. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  72. package/dist/core/ingestion/type-extractors/python.js +41 -0
  73. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  74. package/dist/core/ingestion/type-extractors/rust.js +39 -0
  75. package/dist/core/ingestion/type-extractors/shared.d.ts +17 -0
  76. package/dist/core/ingestion/type-extractors/shared.js +97 -0
  77. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  78. package/dist/core/ingestion/type-extractors/swift.js +43 -0
  79. package/dist/core/ingestion/type-extractors/types.d.ts +14 -0
  80. package/dist/core/ingestion/type-extractors/types.js +1 -0
  81. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  82. package/dist/core/ingestion/type-extractors/typescript.js +46 -0
  83. package/dist/core/ingestion/utils.d.ts +67 -0
  84. package/dist/core/ingestion/utils.js +691 -4
  85. package/dist/core/ingestion/workers/parse-worker.d.ts +20 -3
  86. package/dist/core/ingestion/workers/parse-worker.js +84 -345
  87. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  88. package/dist/core/kuzu/csv-generator.js +19 -3
  89. package/dist/core/kuzu/kuzu-adapter.js +5 -2
  90. package/dist/core/kuzu/schema.d.ts +3 -3
  91. package/dist/core/kuzu/schema.js +16 -1
  92. package/dist/core/search/bm25-index.js +2 -1
  93. package/dist/mcp/core/kuzu-adapter.js +6 -18
  94. package/dist/mcp/tools.js +12 -3
  95. package/hooks/claude/gitnexus-hook.cjs +149 -66
  96. package/package.json +1 -1
  97. package/skills/gitnexus-cli.md +1 -1
@@ -1,16 +1,29 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
1
  import Parser from 'tree-sitter';
4
- import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
2
+ import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
5
3
  import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
6
4
  import { generateId } from '../../lib/utils.js';
7
- import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
5
+ import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop } from './utils.js';
8
6
  import { SupportedLanguages } from '../../config/supported-languages.js';
7
+ import { extractNamedBindings } from './named-binding-extraction.js';
8
+ import { getTreeSitterBufferSize } from './constants.js';
9
+ import { loadTsconfigPaths, loadGoModulePath, loadComposerConfig, loadCSharpProjectConfig, loadSwiftPackageConfig, } from './language-config.js';
10
+ import { buildSuffixIndex, resolveImportPath, appendKotlinWildcard, KOTLIN_EXTENSIONS, resolveJvmWildcard, resolveJvmMemberImport, resolveGoPackageDir, resolveGoPackage, resolveCSharpImport, resolveCSharpNamespaceDir, resolvePhpImport, resolveRustImport, } from './resolvers/index.js';
9
11
  const isDev = process.env.NODE_ENV === 'development';
10
12
  export const createImportMap = () => new Map();
11
- /** Max entries in the resolve cache. Beyond this, the cache is cleared to bound memory.
12
- * 100K entries 15MB covers the most common import patterns. */
13
- const RESOLVE_CACHE_CAP = 100_000;
13
+ export const createPackageMap = () => new Map();
14
+ export const createNamedImportMap = () => new Map();
15
+ /**
16
+ * Check if a file path is directly inside a package directory identified by its suffix.
17
+ * Used by the symbol resolver for Go and C# directory-level import matching.
18
+ */
19
+ export function isFileInPackageDir(filePath, dirSuffix) {
20
+ // Prepend '/' so paths like "internal/auth/service.go" match suffix "/internal/auth/"
21
+ const normalized = '/' + filePath.replace(/\\/g, '/');
22
+ if (!normalized.includes(dirSuffix))
23
+ return false;
24
+ const afterDir = normalized.substring(normalized.indexOf(dirSuffix) + dirSuffix.length);
25
+ return !afterDir.includes('/');
26
+ }
14
27
  export function buildImportResolutionContext(allPaths) {
15
28
  const allFileList = allPaths;
16
29
  const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
@@ -19,550 +32,142 @@ export function buildImportResolutionContext(allPaths) {
19
32
  return { allFilePaths, allFileList, normalizedFileList, suffixIndex, resolveCache: new Map() };
20
33
  }
21
34
  /**
22
- * Parse tsconfig.json to extract path aliases.
23
- * Tries tsconfig.json, tsconfig.app.json, tsconfig.base.json in order.
35
+ * Shared language dispatch for import resolution.
36
+ * Used by both processImports and processImportsFromExtracted.
24
37
  */
25
- async function loadTsconfigPaths(repoRoot) {
26
- const candidates = ['tsconfig.json', 'tsconfig.app.json', 'tsconfig.base.json'];
27
- for (const filename of candidates) {
28
- try {
29
- const tsconfigPath = path.join(repoRoot, filename);
30
- const raw = await fs.readFile(tsconfigPath, 'utf-8');
31
- // Strip JSON comments (// and /* */ style) for robustness
32
- const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
33
- const tsconfig = JSON.parse(stripped);
34
- const compilerOptions = tsconfig.compilerOptions;
35
- if (!compilerOptions?.paths)
36
- continue;
37
- const baseUrl = compilerOptions.baseUrl || '.';
38
- const aliases = new Map();
39
- for (const [pattern, targets] of Object.entries(compilerOptions.paths)) {
40
- if (!Array.isArray(targets) || targets.length === 0)
41
- continue;
42
- const target = targets[0];
43
- // Convert glob patterns: "@/*" -> "@/", "src/*" -> "src/"
44
- const aliasPrefix = pattern.endsWith('/*') ? pattern.slice(0, -1) : pattern;
45
- const targetPrefix = target.endsWith('/*') ? target.slice(0, -1) : target;
46
- aliases.set(aliasPrefix, targetPrefix);
38
+ function resolveLanguageImport(filePath, rawImportPath, language, configs, ctx) {
39
+ const { allFilePaths, allFileList, normalizedFileList, index, resolveCache } = ctx;
40
+ const { tsconfigPaths, goModule, composerConfig, swiftPackageConfig, csharpConfigs } = configs;
41
+ // JVM languages (Java + Kotlin): handle wildcards and member imports
42
+ if (language === SupportedLanguages.Java || language === SupportedLanguages.Kotlin) {
43
+ const exts = language === SupportedLanguages.Java ? ['.java'] : KOTLIN_EXTENSIONS;
44
+ if (rawImportPath.endsWith('.*')) {
45
+ const matchedFiles = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, exts, index);
46
+ if (matchedFiles.length === 0 && language === SupportedLanguages.Kotlin) {
47
+ const javaMatches = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
48
+ if (javaMatches.length > 0)
49
+ return { kind: 'files', files: javaMatches };
47
50
  }
48
- if (aliases.size > 0) {
49
- if (isDev) {
50
- console.log(`📦 Loaded ${aliases.size} path aliases from ${filename}`);
51
- }
52
- return { aliases, baseUrl };
53
- }
54
- }
55
- catch {
56
- // File doesn't exist or isn't valid JSON - try next
51
+ if (matchedFiles.length > 0)
52
+ return { kind: 'files', files: matchedFiles };
53
+ // Fall through to standard resolution
57
54
  }
58
- }
59
- return null;
60
- }
61
- /**
62
- * Parse go.mod to extract module path.
63
- */
64
- async function loadGoModulePath(repoRoot) {
65
- try {
66
- const goModPath = path.join(repoRoot, 'go.mod');
67
- const content = await fs.readFile(goModPath, 'utf-8');
68
- const match = content.match(/^module\s+(\S+)/m);
69
- if (match) {
70
- if (isDev) {
71
- console.log(`📦 Loaded Go module path: ${match[1]}`);
72
- }
73
- return { modulePath: match[1] };
74
- }
75
- }
76
- catch {
77
- // No go.mod
78
- }
79
- return null;
80
- }
81
- async function loadComposerConfig(repoRoot) {
82
- try {
83
- const composerPath = path.join(repoRoot, 'composer.json');
84
- const raw = await fs.readFile(composerPath, 'utf-8');
85
- const composer = JSON.parse(raw);
86
- const psr4Raw = composer.autoload?.['psr-4'] ?? {};
87
- const psr4Dev = composer['autoload-dev']?.['psr-4'] ?? {};
88
- const merged = { ...psr4Raw, ...psr4Dev };
89
- const psr4 = new Map();
90
- for (const [ns, dir] of Object.entries(merged)) {
91
- const nsNorm = ns.replace(/\\+$/, '');
92
- const dirNorm = dir.replace(/\\/g, '/').replace(/\/+$/, '');
93
- psr4.set(nsNorm, dirNorm);
94
- }
95
- if (isDev) {
96
- console.log(`📦 Loaded ${psr4.size} PSR-4 mappings from composer.json`);
97
- }
98
- return { psr4 };
99
- }
100
- catch {
101
- return null;
102
- }
103
- }
104
- async function loadSwiftPackageConfig(repoRoot) {
105
- // Swift imports are module-name based (e.g., `import SiuperModel`)
106
- // SPM convention: Sources/<TargetName>/ or Package/Sources/<TargetName>/
107
- // We scan for these directories to build a target map
108
- const targets = new Map();
109
- const sourceDirs = ['Sources', 'Package/Sources', 'src'];
110
- for (const sourceDir of sourceDirs) {
111
- try {
112
- const fullPath = path.join(repoRoot, sourceDir);
113
- const entries = await fs.readdir(fullPath, { withFileTypes: true });
114
- for (const entry of entries) {
115
- if (entry.isDirectory()) {
116
- targets.set(entry.name, sourceDir + '/' + entry.name);
117
- }
55
+ else {
56
+ let memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index);
57
+ if (!memberResolved && language === SupportedLanguages.Kotlin) {
58
+ memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
118
59
  }
60
+ if (memberResolved)
61
+ return { kind: 'files', files: [memberResolved] };
62
+ // Fall through to standard resolution
119
63
  }
120
- catch {
121
- // Directory doesn't exist
122
- }
123
- }
124
- if (targets.size > 0) {
125
- if (isDev) {
126
- console.log(`📦 Loaded ${targets.size} Swift package targets`);
127
- }
128
- return { targets };
129
- }
130
- return null;
131
- }
132
- // ============================================================================
133
- // IMPORT PATH RESOLUTION
134
- // ============================================================================
135
- /** All file extensions to try during resolution */
136
- const EXTENSIONS = [
137
- '',
138
- // TypeScript/JavaScript
139
- '.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js',
140
- // Python
141
- '.py', '/__init__.py',
142
- // Java
143
- '.java',
144
- // Kotlin
145
- '.kt', '.kts',
146
- // C/C++
147
- '.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hxx', '.hh',
148
- // C#
149
- '.cs',
150
- // Go
151
- '.go',
152
- // Rust
153
- '.rs', '/mod.rs',
154
- // PHP
155
- '.php', '.phtml',
156
- // Swift
157
- '.swift',
158
- ];
159
- /**
160
- * Try to match a path (with extensions) against the known file set.
161
- * Returns the matched file path or null.
162
- */
163
- function tryResolveWithExtensions(basePath, allFiles) {
164
- for (const ext of EXTENSIONS) {
165
- const candidate = basePath + ext;
166
- if (allFiles.has(candidate))
167
- return candidate;
168
64
  }
169
- return null;
170
- }
171
- function buildSuffixIndex(normalizedFileList, allFileList) {
172
- // Map: normalized suffix -> original file path
173
- const exactMap = new Map();
174
- // Map: lowercase suffix -> original file path
175
- const lowerMap = new Map();
176
- // Map: directory suffix -> list of file paths in that directory
177
- const dirMap = new Map();
178
- for (let i = 0; i < normalizedFileList.length; i++) {
179
- const normalized = normalizedFileList[i];
180
- const original = allFileList[i];
181
- const parts = normalized.split('/');
182
- // Index all suffixes: "a/b/c.java" -> ["c.java", "b/c.java", "a/b/c.java"]
183
- for (let j = parts.length - 1; j >= 0; j--) {
184
- const suffix = parts.slice(j).join('/');
185
- // Only store first match (longest path wins for ambiguous suffixes)
186
- if (!exactMap.has(suffix)) {
187
- exactMap.set(suffix, original);
188
- }
189
- const lower = suffix.toLowerCase();
190
- if (!lowerMap.has(lower)) {
191
- lowerMap.set(lower, original);
65
+ // Go: handle package-level imports
66
+ if (language === SupportedLanguages.Go && goModule && rawImportPath.startsWith(goModule.modulePath)) {
67
+ const pkgSuffix = resolveGoPackageDir(rawImportPath, goModule);
68
+ if (pkgSuffix) {
69
+ const pkgFiles = resolveGoPackage(rawImportPath, goModule, normalizedFileList, allFileList);
70
+ if (pkgFiles.length > 0) {
71
+ return { kind: 'package', files: pkgFiles, dirSuffix: pkgSuffix };
192
72
  }
193
73
  }
194
- // Index directory membership
195
- const lastSlash = normalized.lastIndexOf('/');
196
- if (lastSlash >= 0) {
197
- // Build all directory suffixes
198
- const dirParts = parts.slice(0, -1);
199
- const fileName = parts[parts.length - 1];
200
- const ext = fileName.substring(fileName.lastIndexOf('.'));
201
- for (let j = dirParts.length - 1; j >= 0; j--) {
202
- const dirSuffix = dirParts.slice(j).join('/');
203
- const key = `${dirSuffix}:${ext}`;
204
- let list = dirMap.get(key);
205
- if (!list) {
206
- list = [];
207
- dirMap.set(key, list);
208
- }
209
- list.push(original);
210
- }
211
- }
212
- }
213
- return {
214
- get: (suffix) => exactMap.get(suffix),
215
- getInsensitive: (suffix) => lowerMap.get(suffix.toLowerCase()),
216
- getFilesInDir: (dirSuffix, extension) => {
217
- return dirMap.get(`${dirSuffix}:${extension}`) || [];
218
- },
219
- };
220
- }
221
- /**
222
- * Suffix-based resolution using index. O(1) per lookup instead of O(files).
223
- */
224
- function suffixResolve(pathParts, normalizedFileList, allFileList, index) {
225
- if (index) {
226
- for (let i = 0; i < pathParts.length; i++) {
227
- const suffix = pathParts.slice(i).join('/');
228
- for (const ext of EXTENSIONS) {
229
- const suffixWithExt = suffix + ext;
230
- const result = index.get(suffixWithExt) || index.getInsensitive(suffixWithExt);
231
- if (result)
232
- return result;
74
+ // Fall through if no files found (package might be external)
75
+ }
76
+ // C#: handle namespace-based imports (using directives)
77
+ if (language === SupportedLanguages.CSharp && csharpConfigs.length > 0) {
78
+ const resolvedFiles = resolveCSharpImport(rawImportPath, csharpConfigs, normalizedFileList, allFileList, index);
79
+ if (resolvedFiles.length > 1) {
80
+ const dirSuffix = resolveCSharpNamespaceDir(rawImportPath, csharpConfigs);
81
+ if (dirSuffix) {
82
+ return { kind: 'package', files: resolvedFiles, dirSuffix };
233
83
  }
234
84
  }
85
+ if (resolvedFiles.length > 0)
86
+ return { kind: 'files', files: resolvedFiles };
235
87
  return null;
236
88
  }
237
- // Fallback: linear scan (for backward compatibility)
238
- for (let i = 0; i < pathParts.length; i++) {
239
- const suffix = pathParts.slice(i).join('/');
240
- for (const ext of EXTENSIONS) {
241
- const suffixWithExt = suffix + ext;
242
- const suffixPattern = '/' + suffixWithExt;
243
- const matchIdx = normalizedFileList.findIndex(filePath => filePath.endsWith(suffixPattern) || filePath.toLowerCase().endsWith(suffixPattern.toLowerCase()));
244
- if (matchIdx !== -1) {
245
- return allFileList[matchIdx];
246
- }
247
- }
248
- }
249
- return null;
250
- }
251
- /**
252
- * Resolve an import path to a file path in the repository.
253
- *
254
- * Language-specific preprocessing is applied before the generic resolution:
255
- * - TypeScript/JavaScript: rewrites tsconfig path aliases
256
- * - Rust: converts crate::/super::/self:: to relative paths
257
- *
258
- * Java wildcards and Go package imports are handled separately in processImports
259
- * because they resolve to multiple files.
260
- */
261
- const resolveImportPath = (currentFile, importPath, allFiles, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index) => {
262
- const cacheKey = `${currentFile}::${importPath}`;
263
- if (resolveCache.has(cacheKey))
264
- return resolveCache.get(cacheKey) ?? null;
265
- const cache = (result) => {
266
- // Evict oldest 20% when cap is reached instead of clearing all
267
- if (resolveCache.size >= RESOLVE_CACHE_CAP) {
268
- const evictCount = Math.floor(RESOLVE_CACHE_CAP * 0.2);
269
- const iter = resolveCache.keys();
270
- for (let i = 0; i < evictCount; i++) {
271
- const key = iter.next().value;
272
- if (key !== undefined)
273
- resolveCache.delete(key);
274
- }
275
- }
276
- resolveCache.set(cacheKey, result);
277
- return result;
278
- };
279
- // ---- TypeScript/JavaScript: rewrite path aliases ----
280
- if ((language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript) &&
281
- tsconfigPaths &&
282
- !importPath.startsWith('.')) {
283
- for (const [aliasPrefix, targetPrefix] of tsconfigPaths.aliases) {
284
- if (importPath.startsWith(aliasPrefix)) {
285
- const remainder = importPath.slice(aliasPrefix.length);
286
- // Build the rewritten path relative to baseUrl
287
- const rewritten = tsconfigPaths.baseUrl === '.'
288
- ? targetPrefix + remainder
289
- : tsconfigPaths.baseUrl + '/' + targetPrefix + remainder;
290
- // Try direct resolution from repo root
291
- const resolved = tryResolveWithExtensions(rewritten, allFiles);
292
- if (resolved)
293
- return cache(resolved);
294
- // Try suffix matching as fallback
295
- const parts = rewritten.split('/').filter(Boolean);
296
- const suffixResult = suffixResolve(parts, normalizedFileList, allFileList, index);
297
- if (suffixResult)
298
- return cache(suffixResult);
89
+ // PHP: handle namespace-based imports (use statements)
90
+ if (language === SupportedLanguages.PHP) {
91
+ const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index);
92
+ return resolved ? { kind: 'files', files: [resolved] } : null;
93
+ }
94
+ // Swift: handle module imports
95
+ if (language === SupportedLanguages.Swift && swiftPackageConfig) {
96
+ const targetDir = swiftPackageConfig.targets.get(rawImportPath);
97
+ if (targetDir) {
98
+ const dirPrefix = targetDir + '/';
99
+ const files = [];
100
+ for (let i = 0; i < normalizedFileList.length; i++) {
101
+ if (normalizedFileList[i].startsWith(dirPrefix) && normalizedFileList[i].endsWith('.swift')) {
102
+ files.push(allFileList[i]);
103
+ }
299
104
  }
105
+ if (files.length > 0)
106
+ return { kind: 'files', files };
300
107
  }
301
- }
302
- // ---- Rust: convert module path syntax to file paths ----
303
- if (language === SupportedLanguages.Rust) {
304
- const rustResult = resolveRustImport(currentFile, importPath, allFiles);
305
- if (rustResult)
306
- return cache(rustResult);
307
- // Fall through to generic resolution if Rust-specific didn't match
308
- }
309
- // ---- Generic relative import resolution (./ and ../) ----
310
- const currentDir = currentFile.split('/').slice(0, -1);
311
- const parts = importPath.split('/');
312
- for (const part of parts) {
313
- if (part === '.')
314
- continue;
315
- if (part === '..') {
316
- currentDir.pop();
317
- }
318
- else {
319
- currentDir.push(part);
320
- }
321
- }
322
- const basePath = currentDir.join('/');
323
- if (importPath.startsWith('.')) {
324
- const resolved = tryResolveWithExtensions(basePath, allFiles);
325
- return cache(resolved);
326
- }
327
- // ---- Generic package/absolute import resolution (suffix matching) ----
328
- // Java wildcards are handled in processImports, not here
329
- if (importPath.endsWith('.*')) {
330
- return cache(null);
331
- }
332
- const pathLike = importPath.includes('/')
333
- ? importPath
334
- : importPath.replace(/\./g, '/');
335
- const pathParts = pathLike.split('/').filter(Boolean);
336
- const resolved = suffixResolve(pathParts, normalizedFileList, allFileList, index);
337
- return cache(resolved);
338
- };
339
- // ============================================================================
340
- // RUST MODULE RESOLUTION
341
- // ============================================================================
342
- /**
343
- * Resolve Rust use-path to a file.
344
- * Handles crate::, super::, self:: prefixes and :: path separators.
345
- */
346
- function resolveRustImport(currentFile, importPath, allFiles) {
347
- let rustPath;
348
- if (importPath.startsWith('crate::')) {
349
- // crate:: resolves from src/ directory (standard Rust layout)
350
- rustPath = importPath.slice(7).replace(/::/g, '/');
351
- // Try from src/ (standard layout)
352
- const fromSrc = tryRustModulePath('src/' + rustPath, allFiles);
353
- if (fromSrc)
354
- return fromSrc;
355
- // Try from repo root (non-standard)
356
- const fromRoot = tryRustModulePath(rustPath, allFiles);
357
- if (fromRoot)
358
- return fromRoot;
359
- return null;
360
- }
361
- if (importPath.startsWith('super::')) {
362
- // super:: = parent directory of current file's module
363
- const currentDir = currentFile.split('/').slice(0, -1);
364
- currentDir.pop(); // Go up one level for super::
365
- rustPath = importPath.slice(7).replace(/::/g, '/');
366
- const fullPath = [...currentDir, rustPath].join('/');
367
- return tryRustModulePath(fullPath, allFiles);
368
- }
369
- if (importPath.startsWith('self::')) {
370
- // self:: = current module's directory
371
- const currentDir = currentFile.split('/').slice(0, -1);
372
- rustPath = importPath.slice(6).replace(/::/g, '/');
373
- const fullPath = [...currentDir, rustPath].join('/');
374
- return tryRustModulePath(fullPath, allFiles);
375
- }
376
- // Bare path without prefix (e.g., from a use in a nested module)
377
- // Convert :: to / and try suffix matching
378
- if (importPath.includes('::')) {
379
- rustPath = importPath.replace(/::/g, '/');
380
- return tryRustModulePath(rustPath, allFiles);
381
- }
382
- return null;
383
- }
384
- /**
385
- * Try to resolve a Rust module path to a file.
386
- * Tries: path.rs, path/mod.rs, and with the last segment stripped
387
- * (last segment might be a symbol name, not a module).
388
- */
389
- function tryRustModulePath(modulePath, allFiles) {
390
- // Try direct: path.rs
391
- if (allFiles.has(modulePath + '.rs'))
392
- return modulePath + '.rs';
393
- // Try directory: path/mod.rs
394
- if (allFiles.has(modulePath + '/mod.rs'))
395
- return modulePath + '/mod.rs';
396
- // Try path/lib.rs (for crate root)
397
- if (allFiles.has(modulePath + '/lib.rs'))
398
- return modulePath + '/lib.rs';
399
- // The last segment might be a symbol (function, struct, etc.), not a module.
400
- // Strip it and try again.
401
- const lastSlash = modulePath.lastIndexOf('/');
402
- if (lastSlash > 0) {
403
- const parentPath = modulePath.substring(0, lastSlash);
404
- if (allFiles.has(parentPath + '.rs'))
405
- return parentPath + '.rs';
406
- if (allFiles.has(parentPath + '/mod.rs'))
407
- return parentPath + '/mod.rs';
408
- }
409
- return null;
410
- }
411
- /**
412
- * Append .* to a Kotlin import path if the AST has a wildcard_import sibling node.
413
- * Pure function — returns a new string without mutating the input.
414
- */
415
- const appendKotlinWildcard = (importPath, importNode) => {
416
- for (let i = 0; i < importNode.childCount; i++) {
417
- if (importNode.child(i)?.type === 'wildcard_import') {
418
- return importPath.endsWith('.*') ? importPath : `${importPath}.*`;
419
- }
420
- }
421
- return importPath;
422
- };
423
- // ============================================================================
424
- // JVM MULTI-FILE RESOLUTION (Java + Kotlin)
425
- // ============================================================================
426
- /** Kotlin file extensions for JVM resolver reuse */
427
- const KOTLIN_EXTENSIONS = ['.kt', '.kts'];
428
- /**
429
- * Resolve a JVM wildcard import (com.example.*) to all matching files.
430
- * Works for both Java (.java) and Kotlin (.kt, .kts).
431
- */
432
- function resolveJvmWildcard(importPath, normalizedFileList, allFileList, extensions, index) {
433
- // "com.example.util.*" -> "com/example/util"
434
- const packagePath = importPath.slice(0, -2).replace(/\./g, '/');
435
- if (index) {
436
- const candidates = extensions.flatMap(ext => index.getFilesInDir(packagePath, ext));
437
- // Filter to only direct children (no subdirectories)
438
- const packageSuffix = '/' + packagePath + '/';
439
- return candidates.filter(f => {
440
- const normalized = f.replace(/\\/g, '/');
441
- const idx = normalized.indexOf(packageSuffix);
442
- if (idx < 0)
443
- return false;
444
- const afterPkg = normalized.substring(idx + packageSuffix.length);
445
- return !afterPkg.includes('/');
446
- });
447
- }
448
- // Fallback: linear scan
449
- const packageSuffix = '/' + packagePath + '/';
450
- const matches = [];
451
- for (let i = 0; i < normalizedFileList.length; i++) {
452
- const normalized = normalizedFileList[i];
453
- if (normalized.includes(packageSuffix) &&
454
- extensions.some(ext => normalized.endsWith(ext))) {
455
- const afterPackage = normalized.substring(normalized.indexOf(packageSuffix) + packageSuffix.length);
456
- if (!afterPackage.includes('/')) {
457
- matches.push(allFileList[i]);
458
- }
108
+ return null; // External framework (Foundation, UIKit, etc.)
109
+ }
110
+ // Rust: expand top-level grouped imports: use {crate::a, crate::b}
111
+ if (language === SupportedLanguages.Rust && rawImportPath.startsWith('{') && rawImportPath.endsWith('}')) {
112
+ const inner = rawImportPath.slice(1, -1);
113
+ const parts = inner.split(',').map(p => p.trim()).filter(Boolean);
114
+ const resolved = [];
115
+ for (const part of parts) {
116
+ const r = resolveRustImport(filePath, part, allFilePaths);
117
+ if (r)
118
+ resolved.push(r);
459
119
  }
120
+ return resolved.length > 0 ? { kind: 'files', files: resolved } : null;
460
121
  }
461
- return matches;
122
+ // Standard single-file resolution
123
+ const resolvedPath = resolveImportPath(filePath, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
124
+ return resolvedPath ? { kind: 'files', files: [resolvedPath] } : null;
462
125
  }
463
126
  /**
464
- * Try to resolve a JVM member/static import by stripping the member name.
465
- * Java: "com.example.Constants.VALUE" -> resolve "com.example.Constants"
466
- * Kotlin: "com.example.Constants.VALUE" -> resolve "com.example.Constants"
127
+ * Apply an ImportResult: emit graph edges and update ImportMap/PackageMap.
128
+ * If namedBindings are provided and the import resolves to a single file,
129
+ * also populate the NamedImportMap for precise Tier 2a resolution.
467
130
  */
468
- function resolveJvmMemberImport(importPath, normalizedFileList, allFileList, extensions, index) {
469
- // Member imports: com.example.Constants.VALUE or com.example.Constants.*
470
- // The last segment is a member name if it starts with lowercase, is ALL_CAPS, or is a wildcard
471
- const segments = importPath.split('.');
472
- if (segments.length < 3)
473
- return null;
474
- const lastSeg = segments[segments.length - 1];
475
- if (lastSeg === '*' || /^[a-z]/.test(lastSeg) || /^[A-Z_]+$/.test(lastSeg)) {
476
- const classPath = segments.slice(0, -1).join('/');
477
- for (const ext of extensions) {
478
- const classSuffix = classPath + ext;
479
- if (index) {
480
- const result = index.get(classSuffix) || index.getInsensitive(classSuffix);
481
- if (result)
482
- return result;
483
- }
484
- else {
485
- const fullSuffix = '/' + classSuffix;
486
- for (let i = 0; i < normalizedFileList.length; i++) {
487
- if (normalizedFileList[i].endsWith(fullSuffix) ||
488
- normalizedFileList[i].toLowerCase().endsWith(fullSuffix.toLowerCase())) {
489
- return allFileList[i];
490
- }
491
- }
492
- }
131
+ function applyImportResult(result, filePath, importMap, packageMap, addImportEdge, addImportGraphEdge, namedBindings, namedImportMap) {
132
+ if (!result)
133
+ return;
134
+ if (result.kind === 'package' && packageMap) {
135
+ // Store directory suffix in PackageMap (skip ImportMap expansion)
136
+ for (const resolvedFile of result.files) {
137
+ addImportGraphEdge(filePath, resolvedFile);
493
138
  }
494
- }
495
- return null;
496
- }
497
- // ============================================================================
498
- // GO PACKAGE RESOLUTION
499
- // ============================================================================
500
- /**
501
- * Resolve a Go internal package import to all .go files in the package directory.
502
- * Returns an array of file paths.
503
- */
504
- function resolveGoPackage(importPath, goModule, normalizedFileList, allFileList) {
505
- if (!importPath.startsWith(goModule.modulePath))
506
- return [];
507
- // Strip module path to get relative package path
508
- const relativePkg = importPath.slice(goModule.modulePath.length + 1); // e.g., "internal/auth"
509
- if (!relativePkg)
510
- return [];
511
- const pkgSuffix = '/' + relativePkg + '/';
512
- const matches = [];
513
- for (let i = 0; i < normalizedFileList.length; i++) {
514
- const normalized = normalizedFileList[i];
515
- // File must be directly in the package directory (not a subdirectory)
516
- if (normalized.includes(pkgSuffix) && normalized.endsWith('.go') && !normalized.endsWith('_test.go')) {
517
- const afterPkg = normalized.substring(normalized.indexOf(pkgSuffix) + pkgSuffix.length);
518
- if (!afterPkg.includes('/')) {
519
- matches.push(allFileList[i]);
520
- }
139
+ if (!packageMap.has(filePath))
140
+ packageMap.set(filePath, new Set());
141
+ packageMap.get(filePath).add(result.dirSuffix);
142
+ }
143
+ else {
144
+ // 'files' kind, or 'package' without PackageMap — use ImportMap directly
145
+ const files = result.files;
146
+ for (const resolvedFile of files) {
147
+ addImportEdge(filePath, resolvedFile);
521
148
  }
522
- }
523
- return matches;
524
- }
525
- // ============================================================================
526
- // PHP PSR-4 IMPORT RESOLUTION
527
- // ============================================================================
528
- /**
529
- * Resolve a PHP use-statement import path using PSR-4 mappings.
530
- * e.g. "App\Http\Controllers\UserController" -> "app/Http/Controllers/UserController.php"
531
- */
532
- function resolvePhpImport(importPath, composerConfig, allFiles, normalizedFileList, allFileList, index) {
533
- // Normalize: replace backslashes with forward slashes
534
- const normalized = importPath.replace(/\\/g, '/');
535
- // Try PSR-4 resolution if composer.json was found
536
- if (composerConfig) {
537
- // Sort namespaces by length descending (longest match wins)
538
- const sorted = [...composerConfig.psr4.entries()].sort((a, b) => b[0].length - a[0].length);
539
- for (const [nsPrefix, dirPrefix] of sorted) {
540
- const nsPrefixSlash = nsPrefix.replace(/\\/g, '/');
541
- if (normalized.startsWith(nsPrefixSlash + '/') || normalized === nsPrefixSlash) {
542
- const remainder = normalized.slice(nsPrefixSlash.length).replace(/^\//, '');
543
- const filePath = dirPrefix + (remainder ? '/' + remainder : '') + '.php';
544
- if (allFiles.has(filePath))
545
- return filePath;
546
- if (index) {
547
- const result = index.getInsensitive(filePath);
548
- if (result)
549
- return result;
550
- }
149
+ // Record named bindings for precise Tier 2a resolution
150
+ if (namedBindings && namedImportMap && files.length === 1) {
151
+ const resolvedFile = files[0];
152
+ if (!namedImportMap.has(filePath))
153
+ namedImportMap.set(filePath, new Map());
154
+ const fileBindings = namedImportMap.get(filePath);
155
+ for (const binding of namedBindings) {
156
+ fileBindings.set(binding.local, { sourcePath: resolvedFile, exportedName: binding.exported });
551
157
  }
552
158
  }
553
159
  }
554
- // Fallback: suffix matching (works without composer.json)
555
- const pathParts = normalized.split('/').filter(Boolean);
556
- return suffixResolve(pathParts, normalizedFileList, allFileList, index);
557
160
  }
558
161
  // ============================================================================
559
162
  // MAIN IMPORT PROCESSOR
560
163
  // ============================================================================
561
- export const processImports = async (graph, files, astCache, importMap, onProgress, repoRoot, allPaths) => {
164
+ export const processImports = async (graph, files, astCache, importMap, onProgress, repoRoot, allPaths, packageMap, namedImportMap) => {
562
165
  // Use allPaths (full repo) when available for cross-chunk resolution, else fall back to chunk files
563
166
  const allFileList = allPaths ?? files.map(f => f.path);
564
167
  const allFilePaths = new Set(allFileList);
565
168
  const parser = await loadParser();
169
+ const logSkipped = isVerboseIngestionEnabled();
170
+ const skippedByLang = logSkipped ? new Map() : null;
566
171
  const resolveCache = new Map();
567
172
  // Pre-compute normalized file list once (forward slashes)
568
173
  const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
@@ -573,12 +178,16 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
573
178
  let totalImportsResolved = 0;
574
179
  // Load language-specific configs once before the file loop
575
180
  const effectiveRoot = repoRoot || '';
576
- const tsconfigPaths = await loadTsconfigPaths(effectiveRoot);
577
- const goModule = await loadGoModulePath(effectiveRoot);
578
- const composerConfig = await loadComposerConfig(effectiveRoot);
579
- const swiftPackageConfig = await loadSwiftPackageConfig(effectiveRoot);
580
- // Helper: add an IMPORTS edge + update import map
581
- const addImportEdge = (filePath, resolvedPath) => {
181
+ const configs = {
182
+ tsconfigPaths: await loadTsconfigPaths(effectiveRoot),
183
+ goModule: await loadGoModulePath(effectiveRoot),
184
+ composerConfig: await loadComposerConfig(effectiveRoot),
185
+ swiftPackageConfig: await loadSwiftPackageConfig(effectiveRoot),
186
+ csharpConfigs: await loadCSharpProjectConfig(effectiveRoot),
187
+ };
188
+ const ctx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
189
+ // Helper: add an IMPORTS edge to the graph only (no ImportMap update)
190
+ const addImportGraphEdge = (filePath, resolvedPath) => {
582
191
  const sourceId = generateId('File', filePath);
583
192
  const targetId = generateId('File', resolvedPath);
584
193
  const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
@@ -591,6 +200,10 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
591
200
  confidence: 1.0,
592
201
  reason: '',
593
202
  });
203
+ };
204
+ // Helper: add an IMPORTS edge + update import map
205
+ const addImportEdge = (filePath, resolvedPath) => {
206
+ addImportGraphEdge(filePath, resolvedPath);
594
207
  if (!importMap.has(filePath)) {
595
208
  importMap.set(filePath, new Set());
596
209
  }
@@ -605,6 +218,12 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
605
218
  const language = getLanguageFromFilename(file.path);
606
219
  if (!language)
607
220
  continue;
221
+ if (!isLanguageAvailable(language)) {
222
+ if (skippedByLang) {
223
+ skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
224
+ }
225
+ continue;
226
+ }
608
227
  const queryStr = LANGUAGE_QUERIES[language];
609
228
  if (!queryStr)
610
229
  continue;
@@ -615,7 +234,7 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
615
234
  let wasReparsed = false;
616
235
  if (!tree) {
617
236
  try {
618
- tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 });
237
+ tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
619
238
  }
620
239
  catch (parseError) {
621
240
  continue;
@@ -662,83 +281,18 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
662
281
  ? appendKotlinWildcard(sourceNode.text.replace(/['"<>]/g, ''), captureMap['import'])
663
282
  : sourceNode.text.replace(/['"<>]/g, '');
664
283
  totalImportsFound++;
665
- // ---- JVM languages (Java + Kotlin): handle wildcards and member imports ----
666
- if (language === SupportedLanguages.Java || language === SupportedLanguages.Kotlin) {
667
- const exts = language === SupportedLanguages.Java ? ['.java'] : KOTLIN_EXTENSIONS;
668
- if (rawImportPath.endsWith('.*')) {
669
- const matchedFiles = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, exts, index);
670
- // Kotlin can import Java files in mixed codebases — try .java as fallback
671
- if (matchedFiles.length === 0 && language === SupportedLanguages.Kotlin) {
672
- const javaMatches = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
673
- for (const matchedFile of javaMatches) {
674
- addImportEdge(file.path, matchedFile);
675
- }
676
- if (javaMatches.length > 0)
677
- return;
678
- }
679
- for (const matchedFile of matchedFiles) {
680
- addImportEdge(file.path, matchedFile);
681
- }
682
- return; // skip single-file resolution
683
- }
684
- // Try member/static import resolution (strip member name)
685
- let memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index);
686
- // Kotlin can import Java files in mixed codebases — try .java as fallback
687
- if (!memberResolved && language === SupportedLanguages.Kotlin) {
688
- memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
689
- }
690
- if (memberResolved) {
691
- addImportEdge(file.path, memberResolved);
692
- return;
693
- }
694
- // Fall through to normal resolution for regular imports
695
- }
696
- // ---- Go: handle package-level imports ----
697
- if (language === SupportedLanguages.Go && goModule && rawImportPath.startsWith(goModule.modulePath)) {
698
- const pkgFiles = resolveGoPackage(rawImportPath, goModule, normalizedFileList, allFileList);
699
- if (pkgFiles.length > 0) {
700
- for (const pkgFile of pkgFiles) {
701
- addImportEdge(file.path, pkgFile);
702
- }
703
- return; // skip single-file resolution
704
- }
705
- // Fall through if no files found (package might be external)
706
- }
707
- // ---- PHP: handle namespace-based imports (use statements) ----
708
- if (language === SupportedLanguages.PHP) {
709
- const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index);
710
- if (resolved) {
711
- addImportEdge(file.path, resolved);
712
- }
713
- return;
714
- }
715
- // ---- Swift: handle module imports ----
716
- if (language === SupportedLanguages.Swift && swiftPackageConfig) {
717
- // Swift imports are module names: `import SiuperModel`
718
- // Resolve to the module's source directory → all .swift files in it
719
- const targetDir = swiftPackageConfig.targets.get(rawImportPath);
720
- if (targetDir) {
721
- // Find all .swift files in this target directory
722
- const dirPrefix = targetDir + '/';
723
- for (const filePath2 of allFileList) {
724
- if (filePath2.startsWith(dirPrefix) && filePath2.endsWith('.swift')) {
725
- addImportEdge(file.path, filePath2);
726
- }
727
- }
728
- return;
729
- }
730
- // External framework (Foundation, UIKit, etc.) — skip
731
- return;
732
- }
733
- // ---- Standard single-file resolution ----
734
- const resolvedPath = resolveImportPath(file.path, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
735
- if (resolvedPath) {
736
- addImportEdge(file.path, resolvedPath);
737
- }
284
+ const result = resolveLanguageImport(file.path, rawImportPath, language, configs, ctx);
285
+ const bindings = namedImportMap ? extractNamedBindings(captureMap['import'], language) : undefined;
286
+ applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge, bindings, namedImportMap);
738
287
  }
739
288
  });
740
289
  // Tree is now owned by the LRU cache — no manual delete needed
741
290
  }
291
+ if (skippedByLang && skippedByLang.size > 0) {
292
+ for (const [lang, count] of skippedByLang.entries()) {
293
+ console.warn(`[ingestion] Skipped ${count} ${lang} file(s) in import processing — ${lang} parser not available.`);
294
+ }
295
+ }
742
296
  if (isDev) {
743
297
  console.log(`📊 Import processing complete: ${totalImportsResolved}/${totalImportsFound} imports resolved to graph edges`);
744
298
  }
@@ -746,17 +300,22 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
746
300
  // ============================================================================
747
301
  // FAST PATH: Resolve pre-extracted imports (no parsing needed)
748
302
  // ============================================================================
749
- export const processImportsFromExtracted = async (graph, files, extractedImports, importMap, onProgress, repoRoot, prebuiltCtx) => {
303
+ export const processImportsFromExtracted = async (graph, files, extractedImports, importMap, onProgress, repoRoot, prebuiltCtx, packageMap, namedImportMap) => {
750
304
  const ctx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
751
305
  const { allFilePaths, allFileList, normalizedFileList, suffixIndex: index, resolveCache } = ctx;
752
306
  let totalImportsFound = 0;
753
307
  let totalImportsResolved = 0;
754
308
  const effectiveRoot = repoRoot || '';
755
- const tsconfigPaths = await loadTsconfigPaths(effectiveRoot);
756
- const goModule = await loadGoModulePath(effectiveRoot);
757
- const composerConfig = await loadComposerConfig(effectiveRoot);
758
- const swiftPackageConfig = await loadSwiftPackageConfig(effectiveRoot);
759
- const addImportEdge = (filePath, resolvedPath) => {
309
+ const configs = {
310
+ tsconfigPaths: await loadTsconfigPaths(effectiveRoot),
311
+ goModule: await loadGoModulePath(effectiveRoot),
312
+ composerConfig: await loadComposerConfig(effectiveRoot),
313
+ swiftPackageConfig: await loadSwiftPackageConfig(effectiveRoot),
314
+ csharpConfigs: await loadCSharpProjectConfig(effectiveRoot),
315
+ };
316
+ const resolveCtx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
317
+ // Helper: add an IMPORTS edge to the graph only (no ImportMap update)
318
+ const addImportGraphEdge = (filePath, resolvedPath) => {
760
319
  const sourceId = generateId('File', filePath);
761
320
  const targetId = generateId('File', resolvedPath);
762
321
  const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
@@ -769,6 +328,9 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
769
328
  confidence: 1.0,
770
329
  reason: '',
771
330
  });
331
+ };
332
+ const addImportEdge = (filePath, resolvedPath) => {
333
+ addImportGraphEdge(filePath, resolvedPath);
772
334
  if (!importMap.has(filePath)) {
773
335
  importMap.set(filePath, new Set());
774
336
  }
@@ -786,103 +348,16 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
786
348
  }
787
349
  const totalFiles = importsByFile.size;
788
350
  let filesProcessed = 0;
789
- // Pre-build a suffix index for O(1) suffix lookups instead of O(n) linear scans
790
- const suffixIndex = new Map();
791
- for (let i = 0; i < normalizedFileList.length; i++) {
792
- const normalized = normalizedFileList[i];
793
- // Index by last path segment (filename) for fast suffix matching
794
- const lastSlash = normalized.lastIndexOf('/');
795
- const filename = lastSlash >= 0 ? normalized.substring(lastSlash + 1) : normalized;
796
- let list = suffixIndex.get(filename);
797
- if (!list) {
798
- list = [];
799
- suffixIndex.set(filename, list);
800
- }
801
- list.push(allFileList[i]);
802
- }
803
351
  for (const [filePath, fileImports] of importsByFile) {
804
352
  filesProcessed++;
805
353
  if (filesProcessed % 100 === 0) {
806
354
  onProgress?.(filesProcessed, totalFiles);
807
355
  await yieldToEventLoop();
808
356
  }
809
- for (const { rawImportPath, language } of fileImports) {
357
+ for (const imp of fileImports) {
810
358
  totalImportsFound++;
811
- // Check resolve cache first
812
- const cacheKey = `${filePath}::${rawImportPath}`;
813
- if (resolveCache.has(cacheKey)) {
814
- const cached = resolveCache.get(cacheKey);
815
- if (cached)
816
- addImportEdge(filePath, cached);
817
- continue;
818
- }
819
- // JVM languages (Java + Kotlin): handle wildcards and member imports
820
- if (language === SupportedLanguages.Java || language === SupportedLanguages.Kotlin) {
821
- const exts = language === SupportedLanguages.Java ? ['.java'] : KOTLIN_EXTENSIONS;
822
- if (rawImportPath.endsWith('.*')) {
823
- const matchedFiles = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, exts, index);
824
- // Kotlin can import Java files in mixed codebases — try .java as fallback
825
- if (matchedFiles.length === 0 && language === SupportedLanguages.Kotlin) {
826
- const javaMatches = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
827
- for (const matchedFile of javaMatches) {
828
- addImportEdge(filePath, matchedFile);
829
- }
830
- if (javaMatches.length > 0)
831
- continue;
832
- }
833
- for (const matchedFile of matchedFiles) {
834
- addImportEdge(filePath, matchedFile);
835
- }
836
- continue;
837
- }
838
- let memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index);
839
- // Kotlin can import Java files in mixed codebases — try .java as fallback
840
- if (!memberResolved && language === SupportedLanguages.Kotlin) {
841
- memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
842
- }
843
- if (memberResolved) {
844
- resolveCache.set(cacheKey, memberResolved);
845
- addImportEdge(filePath, memberResolved);
846
- continue;
847
- }
848
- }
849
- // Go: handle package-level imports
850
- if (language === SupportedLanguages.Go && goModule && rawImportPath.startsWith(goModule.modulePath)) {
851
- const pkgFiles = resolveGoPackage(rawImportPath, goModule, normalizedFileList, allFileList);
852
- if (pkgFiles.length > 0) {
853
- for (const pkgFile of pkgFiles) {
854
- addImportEdge(filePath, pkgFile);
855
- }
856
- continue;
857
- }
858
- }
859
- // PHP: handle namespace-based imports (use statements)
860
- if (language === SupportedLanguages.PHP) {
861
- const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index);
862
- if (resolved) {
863
- resolveCache.set(cacheKey, resolved);
864
- addImportEdge(filePath, resolved);
865
- }
866
- continue;
867
- }
868
- // Swift: handle module imports
869
- if (language === SupportedLanguages.Swift && swiftPackageConfig) {
870
- const targetDir = swiftPackageConfig.targets.get(rawImportPath);
871
- if (targetDir) {
872
- const dirPrefix = targetDir + '/';
873
- for (const fp of allFileList) {
874
- if (fp.startsWith(dirPrefix) && fp.endsWith('.swift')) {
875
- addImportEdge(filePath, fp);
876
- }
877
- }
878
- }
879
- continue;
880
- }
881
- // Standard resolution (has its own internal cache)
882
- const resolvedPath = resolveImportPath(filePath, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
883
- if (resolvedPath) {
884
- addImportEdge(filePath, resolvedPath);
885
- }
359
+ const result = resolveLanguageImport(filePath, imp.rawImportPath, imp.language, configs, resolveCtx);
360
+ applyImportResult(result, filePath, importMap, packageMap, addImportEdge, addImportGraphEdge, imp.namedBindings, namedImportMap);
886
361
  }
887
362
  }
888
363
  onProgress?.(totalFiles, totalFiles);