gitnexus 1.4.0 → 1.4.1

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 (131) hide show
  1. package/README.md +194 -214
  2. package/dist/cli/ai-context.d.ts +1 -2
  3. package/dist/cli/ai-context.js +90 -117
  4. package/dist/cli/analyze.d.ts +0 -2
  5. package/dist/cli/analyze.js +2 -20
  6. package/dist/cli/index.js +25 -17
  7. package/dist/cli/setup.js +19 -17
  8. package/dist/core/augmentation/engine.js +20 -20
  9. package/dist/core/embeddings/embedding-pipeline.js +26 -26
  10. package/dist/core/graph/types.d.ts +2 -5
  11. package/dist/core/ingestion/ast-cache.js +2 -3
  12. package/dist/core/ingestion/call-processor.d.ts +5 -5
  13. package/dist/core/ingestion/call-processor.js +258 -173
  14. package/dist/core/ingestion/cluster-enricher.js +16 -16
  15. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -2
  16. package/dist/core/ingestion/entry-point-scoring.js +22 -81
  17. package/dist/core/ingestion/framework-detection.d.ts +1 -5
  18. package/dist/core/ingestion/framework-detection.js +8 -39
  19. package/dist/core/ingestion/heritage-processor.d.ts +4 -13
  20. package/dist/core/ingestion/heritage-processor.js +28 -92
  21. package/dist/core/ingestion/import-processor.d.ts +19 -17
  22. package/dist/core/ingestion/import-processor.js +695 -170
  23. package/dist/core/ingestion/parsing-processor.d.ts +10 -1
  24. package/dist/core/ingestion/parsing-processor.js +177 -41
  25. package/dist/core/ingestion/pipeline.js +26 -49
  26. package/dist/core/ingestion/process-processor.js +1 -2
  27. package/dist/core/ingestion/symbol-table.d.ts +1 -12
  28. package/dist/core/ingestion/symbol-table.js +12 -19
  29. package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -11
  30. package/dist/core/ingestion/tree-sitter-queries.js +485 -590
  31. package/dist/core/ingestion/utils.d.ts +0 -67
  32. package/dist/core/ingestion/utils.js +9 -692
  33. package/dist/core/ingestion/workers/parse-worker.d.ts +3 -20
  34. package/dist/core/ingestion/workers/parse-worker.js +345 -84
  35. package/dist/core/ingestion/workers/worker-pool.js +0 -8
  36. package/dist/core/kuzu/csv-generator.js +3 -19
  37. package/dist/core/kuzu/kuzu-adapter.js +19 -14
  38. package/dist/core/kuzu/schema.d.ts +3 -3
  39. package/dist/core/kuzu/schema.js +288 -303
  40. package/dist/core/search/bm25-index.js +6 -7
  41. package/dist/core/search/hybrid-search.js +3 -3
  42. package/dist/core/wiki/diagrams.d.ts +27 -0
  43. package/dist/core/wiki/diagrams.js +163 -0
  44. package/dist/core/wiki/generator.d.ts +50 -2
  45. package/dist/core/wiki/generator.js +548 -49
  46. package/dist/core/wiki/graph-queries.d.ts +42 -0
  47. package/dist/core/wiki/graph-queries.js +276 -97
  48. package/dist/core/wiki/html-viewer.js +192 -192
  49. package/dist/core/wiki/llm-client.js +73 -11
  50. package/dist/core/wiki/prompts.d.ts +52 -8
  51. package/dist/core/wiki/prompts.js +200 -86
  52. package/dist/mcp/core/kuzu-adapter.d.ts +3 -1
  53. package/dist/mcp/core/kuzu-adapter.js +44 -13
  54. package/dist/mcp/local/local-backend.js +128 -128
  55. package/dist/mcp/resources.js +42 -42
  56. package/dist/mcp/server.js +19 -18
  57. package/dist/mcp/tools.js +104 -103
  58. package/hooks/claude/gitnexus-hook.cjs +155 -238
  59. package/hooks/claude/pre-tool-use.sh +79 -79
  60. package/hooks/claude/session-start.sh +42 -42
  61. package/package.json +96 -96
  62. package/scripts/patch-tree-sitter-swift.cjs +74 -74
  63. package/skills/gitnexus-cli.md +82 -82
  64. package/skills/gitnexus-debugging.md +89 -89
  65. package/skills/gitnexus-exploring.md +78 -78
  66. package/skills/gitnexus-guide.md +64 -64
  67. package/skills/gitnexus-impact-analysis.md +97 -97
  68. package/skills/gitnexus-pr-review.md +163 -163
  69. package/skills/gitnexus-refactoring.md +121 -121
  70. package/vendor/leiden/index.cjs +355 -355
  71. package/vendor/leiden/utils.cjs +392 -392
  72. package/dist/cli/lazy-action.d.ts +0 -6
  73. package/dist/cli/lazy-action.js +0 -18
  74. package/dist/cli/skill-gen.d.ts +0 -26
  75. package/dist/cli/skill-gen.js +0 -549
  76. package/dist/core/ingestion/constants.d.ts +0 -16
  77. package/dist/core/ingestion/constants.js +0 -16
  78. package/dist/core/ingestion/export-detection.d.ts +0 -18
  79. package/dist/core/ingestion/export-detection.js +0 -230
  80. package/dist/core/ingestion/language-config.d.ts +0 -46
  81. package/dist/core/ingestion/language-config.js +0 -167
  82. package/dist/core/ingestion/mro-processor.d.ts +0 -45
  83. package/dist/core/ingestion/mro-processor.js +0 -369
  84. package/dist/core/ingestion/named-binding-extraction.d.ts +0 -61
  85. package/dist/core/ingestion/named-binding-extraction.js +0 -363
  86. package/dist/core/ingestion/resolvers/csharp.d.ts +0 -22
  87. package/dist/core/ingestion/resolvers/csharp.js +0 -109
  88. package/dist/core/ingestion/resolvers/go.d.ts +0 -19
  89. package/dist/core/ingestion/resolvers/go.js +0 -42
  90. package/dist/core/ingestion/resolvers/index.d.ts +0 -16
  91. package/dist/core/ingestion/resolvers/index.js +0 -11
  92. package/dist/core/ingestion/resolvers/jvm.d.ts +0 -23
  93. package/dist/core/ingestion/resolvers/jvm.js +0 -87
  94. package/dist/core/ingestion/resolvers/php.d.ts +0 -15
  95. package/dist/core/ingestion/resolvers/php.js +0 -35
  96. package/dist/core/ingestion/resolvers/rust.d.ts +0 -15
  97. package/dist/core/ingestion/resolvers/rust.js +0 -73
  98. package/dist/core/ingestion/resolvers/standard.d.ts +0 -28
  99. package/dist/core/ingestion/resolvers/standard.js +0 -145
  100. package/dist/core/ingestion/resolvers/utils.d.ts +0 -33
  101. package/dist/core/ingestion/resolvers/utils.js +0 -120
  102. package/dist/core/ingestion/symbol-resolver.d.ts +0 -32
  103. package/dist/core/ingestion/symbol-resolver.js +0 -83
  104. package/dist/core/ingestion/type-env.d.ts +0 -27
  105. package/dist/core/ingestion/type-env.js +0 -86
  106. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +0 -2
  107. package/dist/core/ingestion/type-extractors/c-cpp.js +0 -60
  108. package/dist/core/ingestion/type-extractors/csharp.d.ts +0 -2
  109. package/dist/core/ingestion/type-extractors/csharp.js +0 -89
  110. package/dist/core/ingestion/type-extractors/go.d.ts +0 -2
  111. package/dist/core/ingestion/type-extractors/go.js +0 -105
  112. package/dist/core/ingestion/type-extractors/index.d.ts +0 -21
  113. package/dist/core/ingestion/type-extractors/index.js +0 -29
  114. package/dist/core/ingestion/type-extractors/jvm.d.ts +0 -3
  115. package/dist/core/ingestion/type-extractors/jvm.js +0 -121
  116. package/dist/core/ingestion/type-extractors/php.d.ts +0 -2
  117. package/dist/core/ingestion/type-extractors/php.js +0 -31
  118. package/dist/core/ingestion/type-extractors/python.d.ts +0 -2
  119. package/dist/core/ingestion/type-extractors/python.js +0 -41
  120. package/dist/core/ingestion/type-extractors/rust.d.ts +0 -2
  121. package/dist/core/ingestion/type-extractors/rust.js +0 -39
  122. package/dist/core/ingestion/type-extractors/shared.d.ts +0 -17
  123. package/dist/core/ingestion/type-extractors/shared.js +0 -97
  124. package/dist/core/ingestion/type-extractors/swift.d.ts +0 -2
  125. package/dist/core/ingestion/type-extractors/swift.js +0 -43
  126. package/dist/core/ingestion/type-extractors/types.d.ts +0 -14
  127. package/dist/core/ingestion/type-extractors/types.js +0 -1
  128. package/dist/core/ingestion/type-extractors/typescript.d.ts +0 -2
  129. package/dist/core/ingestion/type-extractors/typescript.js +0 -46
  130. package/dist/mcp/compatible-stdio-transport.d.ts +0 -25
  131. package/dist/mcp/compatible-stdio-transport.js +0 -200
@@ -1,29 +1,16 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
1
3
  import Parser from 'tree-sitter';
2
- import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
4
+ import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
3
5
  import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
4
6
  import { generateId } from '../../lib/utils.js';
5
- import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop } from './utils.js';
7
+ import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
6
8
  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';
11
9
  const isDev = process.env.NODE_ENV === 'development';
12
10
  export const createImportMap = () => new Map();
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
- }
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;
27
14
  export function buildImportResolutionContext(allPaths) {
28
15
  const allFileList = allPaths;
29
16
  const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
@@ -32,142 +19,550 @@ export function buildImportResolutionContext(allPaths) {
32
19
  return { allFilePaths, allFileList, normalizedFileList, suffixIndex, resolveCache: new Map() };
33
20
  }
34
21
  /**
35
- * Shared language dispatch for import resolution.
36
- * Used by both processImports and processImportsFromExtracted.
22
+ * Parse tsconfig.json to extract path aliases.
23
+ * Tries tsconfig.json, tsconfig.app.json, tsconfig.base.json in order.
37
24
  */
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 };
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);
47
+ }
48
+ if (aliases.size > 0) {
49
+ if (isDev) {
50
+ console.log(`📦 Loaded ${aliases.size} path aliases from ${filename}`);
51
+ }
52
+ return { aliases, baseUrl };
50
53
  }
51
- if (matchedFiles.length > 0)
52
- return { kind: 'files', files: matchedFiles };
53
- // Fall through to standard resolution
54
54
  }
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);
55
+ catch {
56
+ // File doesn't exist or isn't valid JSON - try next
57
+ }
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]}`);
59
72
  }
60
- if (memberResolved)
61
- return { kind: 'files', files: [memberResolved] };
62
- // Fall through to standard resolution
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`);
63
97
  }
98
+ return { psr4 };
99
+ }
100
+ catch {
101
+ return null;
64
102
  }
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 };
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
+ }
118
+ }
119
+ }
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
+ }
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);
72
192
  }
73
193
  }
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 };
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;
83
233
  }
84
234
  }
85
- if (resolvedFiles.length > 0)
86
- return { kind: 'files', files: resolvedFiles };
87
235
  return null;
88
236
  }
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
- }
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);
104
299
  }
105
- if (files.length > 0)
106
- return { kind: 'files', files };
107
300
  }
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);
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
+ }
119
459
  }
120
- return resolved.length > 0 ? { kind: 'files', files: resolved } : null;
121
460
  }
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;
461
+ return matches;
125
462
  }
126
463
  /**
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.
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"
130
467
  */
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);
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
+ }
138
493
  }
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);
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
+ }
148
521
  }
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 });
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
+ }
157
551
  }
158
552
  }
159
553
  }
554
+ // Fallback: suffix matching (works without composer.json)
555
+ const pathParts = normalized.split('/').filter(Boolean);
556
+ return suffixResolve(pathParts, normalizedFileList, allFileList, index);
160
557
  }
161
558
  // ============================================================================
162
559
  // MAIN IMPORT PROCESSOR
163
560
  // ============================================================================
164
- export const processImports = async (graph, files, astCache, importMap, onProgress, repoRoot, allPaths, packageMap, namedImportMap) => {
561
+ export const processImports = async (graph, files, astCache, importMap, onProgress, repoRoot, allPaths) => {
165
562
  // Use allPaths (full repo) when available for cross-chunk resolution, else fall back to chunk files
166
563
  const allFileList = allPaths ?? files.map(f => f.path);
167
564
  const allFilePaths = new Set(allFileList);
168
565
  const parser = await loadParser();
169
- const logSkipped = isVerboseIngestionEnabled();
170
- const skippedByLang = logSkipped ? new Map() : null;
171
566
  const resolveCache = new Map();
172
567
  // Pre-compute normalized file list once (forward slashes)
173
568
  const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
@@ -178,16 +573,12 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
178
573
  let totalImportsResolved = 0;
179
574
  // Load language-specific configs once before the file loop
180
575
  const effectiveRoot = repoRoot || '';
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) => {
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) => {
191
582
  const sourceId = generateId('File', filePath);
192
583
  const targetId = generateId('File', resolvedPath);
193
584
  const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
@@ -200,10 +591,6 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
200
591
  confidence: 1.0,
201
592
  reason: '',
202
593
  });
203
- };
204
- // Helper: add an IMPORTS edge + update import map
205
- const addImportEdge = (filePath, resolvedPath) => {
206
- addImportGraphEdge(filePath, resolvedPath);
207
594
  if (!importMap.has(filePath)) {
208
595
  importMap.set(filePath, new Set());
209
596
  }
@@ -218,12 +605,6 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
218
605
  const language = getLanguageFromFilename(file.path);
219
606
  if (!language)
220
607
  continue;
221
- if (!isLanguageAvailable(language)) {
222
- if (skippedByLang) {
223
- skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
224
- }
225
- continue;
226
- }
227
608
  const queryStr = LANGUAGE_QUERIES[language];
228
609
  if (!queryStr)
229
610
  continue;
@@ -234,7 +615,7 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
234
615
  let wasReparsed = false;
235
616
  if (!tree) {
236
617
  try {
237
- tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
618
+ tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 });
238
619
  }
239
620
  catch (parseError) {
240
621
  continue;
@@ -281,18 +662,83 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
281
662
  ? appendKotlinWildcard(sourceNode.text.replace(/['"<>]/g, ''), captureMap['import'])
282
663
  : sourceNode.text.replace(/['"<>]/g, '');
283
664
  totalImportsFound++;
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);
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
+ }
287
738
  }
288
739
  });
289
740
  // Tree is now owned by the LRU cache — no manual delete needed
290
741
  }
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
- }
296
742
  if (isDev) {
297
743
  console.log(`📊 Import processing complete: ${totalImportsResolved}/${totalImportsFound} imports resolved to graph edges`);
298
744
  }
@@ -300,22 +746,17 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
300
746
  // ============================================================================
301
747
  // FAST PATH: Resolve pre-extracted imports (no parsing needed)
302
748
  // ============================================================================
303
- export const processImportsFromExtracted = async (graph, files, extractedImports, importMap, onProgress, repoRoot, prebuiltCtx, packageMap, namedImportMap) => {
749
+ export const processImportsFromExtracted = async (graph, files, extractedImports, importMap, onProgress, repoRoot, prebuiltCtx) => {
304
750
  const ctx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
305
751
  const { allFilePaths, allFileList, normalizedFileList, suffixIndex: index, resolveCache } = ctx;
306
752
  let totalImportsFound = 0;
307
753
  let totalImportsResolved = 0;
308
754
  const effectiveRoot = repoRoot || '';
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) => {
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) => {
319
760
  const sourceId = generateId('File', filePath);
320
761
  const targetId = generateId('File', resolvedPath);
321
762
  const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
@@ -328,9 +769,6 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
328
769
  confidence: 1.0,
329
770
  reason: '',
330
771
  });
331
- };
332
- const addImportEdge = (filePath, resolvedPath) => {
333
- addImportGraphEdge(filePath, resolvedPath);
334
772
  if (!importMap.has(filePath)) {
335
773
  importMap.set(filePath, new Set());
336
774
  }
@@ -348,16 +786,103 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
348
786
  }
349
787
  const totalFiles = importsByFile.size;
350
788
  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
+ }
351
803
  for (const [filePath, fileImports] of importsByFile) {
352
804
  filesProcessed++;
353
805
  if (filesProcessed % 100 === 0) {
354
806
  onProgress?.(filesProcessed, totalFiles);
355
807
  await yieldToEventLoop();
356
808
  }
357
- for (const imp of fileImports) {
809
+ for (const { rawImportPath, language } of fileImports) {
358
810
  totalImportsFound++;
359
- const result = resolveLanguageImport(filePath, imp.rawImportPath, imp.language, configs, resolveCtx);
360
- applyImportResult(result, filePath, importMap, packageMap, addImportEdge, addImportGraphEdge, imp.namedBindings, namedImportMap);
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
+ }
361
886
  }
362
887
  }
363
888
  onProgress?.(totalFiles, totalFiles);