gitnexus 1.4.1 → 1.4.6

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 (169) hide show
  1. package/README.md +215 -194
  2. package/dist/cli/ai-context.d.ts +2 -1
  3. package/dist/cli/ai-context.js +117 -90
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +57 -30
  6. package/dist/cli/augment.js +1 -1
  7. package/dist/cli/eval-server.d.ts +1 -1
  8. package/dist/cli/eval-server.js +14 -6
  9. package/dist/cli/index.js +18 -25
  10. package/dist/cli/lazy-action.d.ts +6 -0
  11. package/dist/cli/lazy-action.js +18 -0
  12. package/dist/cli/mcp.js +1 -1
  13. package/dist/cli/setup.js +42 -32
  14. package/dist/cli/skill-gen.d.ts +26 -0
  15. package/dist/cli/skill-gen.js +549 -0
  16. package/dist/cli/status.js +13 -4
  17. package/dist/cli/tool.d.ts +3 -2
  18. package/dist/cli/tool.js +48 -13
  19. package/dist/cli/wiki.js +2 -2
  20. package/dist/config/ignore-service.d.ts +25 -0
  21. package/dist/config/ignore-service.js +76 -0
  22. package/dist/config/supported-languages.d.ts +1 -0
  23. package/dist/config/supported-languages.js +1 -1
  24. package/dist/core/augmentation/engine.js +99 -72
  25. package/dist/core/embeddings/embedder.d.ts +1 -1
  26. package/dist/core/embeddings/embedder.js +1 -1
  27. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  28. package/dist/core/embeddings/embedding-pipeline.js +74 -47
  29. package/dist/core/embeddings/types.d.ts +1 -1
  30. package/dist/core/graph/types.d.ts +5 -2
  31. package/dist/core/ingestion/ast-cache.js +3 -2
  32. package/dist/core/ingestion/call-processor.d.ts +5 -7
  33. package/dist/core/ingestion/call-processor.js +430 -283
  34. package/dist/core/ingestion/call-routing.d.ts +53 -0
  35. package/dist/core/ingestion/call-routing.js +108 -0
  36. package/dist/core/ingestion/cluster-enricher.js +16 -16
  37. package/dist/core/ingestion/constants.d.ts +16 -0
  38. package/dist/core/ingestion/constants.js +16 -0
  39. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  40. package/dist/core/ingestion/entry-point-scoring.js +94 -24
  41. package/dist/core/ingestion/export-detection.d.ts +18 -0
  42. package/dist/core/ingestion/export-detection.js +231 -0
  43. package/dist/core/ingestion/filesystem-walker.js +4 -3
  44. package/dist/core/ingestion/framework-detection.d.ts +5 -1
  45. package/dist/core/ingestion/framework-detection.js +48 -8
  46. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  47. package/dist/core/ingestion/heritage-processor.js +109 -55
  48. package/dist/core/ingestion/import-processor.d.ts +16 -20
  49. package/dist/core/ingestion/import-processor.js +202 -696
  50. package/dist/core/ingestion/language-config.d.ts +46 -0
  51. package/dist/core/ingestion/language-config.js +167 -0
  52. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  53. package/dist/core/ingestion/mro-processor.js +369 -0
  54. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  55. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  56. package/dist/core/ingestion/parsing-processor.d.ts +3 -11
  57. package/dist/core/ingestion/parsing-processor.js +85 -181
  58. package/dist/core/ingestion/pipeline.d.ts +5 -1
  59. package/dist/core/ingestion/pipeline.js +192 -116
  60. package/dist/core/ingestion/process-processor.js +2 -1
  61. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  62. package/dist/core/ingestion/resolution-context.js +132 -0
  63. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  64. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  65. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  66. package/dist/core/ingestion/resolvers/go.js +42 -0
  67. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  68. package/dist/core/ingestion/resolvers/index.js +13 -0
  69. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  70. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  71. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  72. package/dist/core/ingestion/resolvers/php.js +35 -0
  73. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  74. package/dist/core/ingestion/resolvers/python.js +52 -0
  75. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  76. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  77. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  78. package/dist/core/ingestion/resolvers/rust.js +73 -0
  79. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  80. package/dist/core/ingestion/resolvers/standard.js +123 -0
  81. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  82. package/dist/core/ingestion/resolvers/utils.js +122 -0
  83. package/dist/core/ingestion/symbol-table.d.ts +21 -1
  84. package/dist/core/ingestion/symbol-table.js +40 -12
  85. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -11
  86. package/dist/core/ingestion/tree-sitter-queries.js +642 -485
  87. package/dist/core/ingestion/type-env.d.ts +49 -0
  88. package/dist/core/ingestion/type-env.js +611 -0
  89. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  90. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  91. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  92. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  93. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  94. package/dist/core/ingestion/type-extractors/go.js +467 -0
  95. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  96. package/dist/core/ingestion/type-extractors/index.js +31 -0
  97. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  98. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  99. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  100. package/dist/core/ingestion/type-extractors/php.js +549 -0
  101. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  102. package/dist/core/ingestion/type-extractors/python.js +406 -0
  103. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  104. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  105. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  106. package/dist/core/ingestion/type-extractors/rust.js +449 -0
  107. package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
  108. package/dist/core/ingestion/type-extractors/shared.js +703 -0
  109. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  110. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  111. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  112. package/dist/core/ingestion/type-extractors/types.js +1 -0
  113. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  114. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  115. package/dist/core/ingestion/utils.d.ts +98 -0
  116. package/dist/core/ingestion/utils.js +1064 -9
  117. package/dist/core/ingestion/workers/parse-worker.d.ts +38 -4
  118. package/dist/core/ingestion/workers/parse-worker.js +251 -359
  119. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  120. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
  121. package/dist/core/{kuzu → lbug}/csv-generator.js +20 -4
  122. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
  123. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +82 -82
  124. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  125. package/dist/core/{kuzu → lbug}/schema.js +304 -289
  126. package/dist/core/search/bm25-index.d.ts +4 -4
  127. package/dist/core/search/bm25-index.js +17 -16
  128. package/dist/core/search/hybrid-search.d.ts +2 -2
  129. package/dist/core/search/hybrid-search.js +9 -9
  130. package/dist/core/tree-sitter/parser-loader.js +9 -2
  131. package/dist/core/wiki/generator.d.ts +4 -52
  132. package/dist/core/wiki/generator.js +53 -552
  133. package/dist/core/wiki/graph-queries.d.ts +4 -46
  134. package/dist/core/wiki/graph-queries.js +103 -282
  135. package/dist/core/wiki/html-viewer.js +192 -192
  136. package/dist/core/wiki/llm-client.js +11 -73
  137. package/dist/core/wiki/prompts.d.ts +8 -52
  138. package/dist/core/wiki/prompts.js +86 -200
  139. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  140. package/dist/mcp/compatible-stdio-transport.js +200 -0
  141. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +7 -9
  142. package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +77 -79
  143. package/dist/mcp/local/local-backend.d.ts +7 -6
  144. package/dist/mcp/local/local-backend.js +176 -147
  145. package/dist/mcp/resources.js +42 -42
  146. package/dist/mcp/server.js +18 -19
  147. package/dist/mcp/tools.js +103 -104
  148. package/dist/server/api.js +12 -12
  149. package/dist/server/mcp-http.d.ts +1 -1
  150. package/dist/server/mcp-http.js +1 -1
  151. package/dist/storage/repo-manager.d.ts +20 -2
  152. package/dist/storage/repo-manager.js +55 -1
  153. package/dist/types/pipeline.d.ts +1 -1
  154. package/hooks/claude/gitnexus-hook.cjs +238 -155
  155. package/hooks/claude/pre-tool-use.sh +79 -79
  156. package/hooks/claude/session-start.sh +42 -42
  157. package/package.json +99 -96
  158. package/scripts/patch-tree-sitter-swift.cjs +74 -74
  159. package/skills/gitnexus-cli.md +82 -82
  160. package/skills/gitnexus-debugging.md +89 -89
  161. package/skills/gitnexus-exploring.md +78 -78
  162. package/skills/gitnexus-guide.md +64 -64
  163. package/skills/gitnexus-impact-analysis.md +97 -97
  164. package/skills/gitnexus-pr-review.md +163 -163
  165. package/skills/gitnexus-refactoring.md +121 -121
  166. package/vendor/leiden/index.cjs +355 -355
  167. package/vendor/leiden/utils.cjs +392 -392
  168. package/dist/core/wiki/diagrams.d.ts +0 -27
  169. package/dist/core/wiki/diagrams.js +0 -163
@@ -1,16 +1,27 @@
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, resolveRubyImport, resolvePythonImport, } from './resolvers/index.js';
11
+ import { callRouters } from './call-routing.js';
9
12
  const isDev = process.env.NODE_ENV === 'development';
10
- 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
+ /**
14
+ * Check if a file path is directly inside a package directory identified by its suffix.
15
+ * Used by the symbol resolver for Go and C# directory-level import matching.
16
+ */
17
+ export function isFileInPackageDir(filePath, dirSuffix) {
18
+ // Prepend '/' so paths like "internal/auth/service.go" match suffix "/internal/auth/"
19
+ const normalized = '/' + filePath.replace(/\\/g, '/');
20
+ if (!normalized.includes(dirSuffix))
21
+ return false;
22
+ const afterDir = normalized.substring(normalized.indexOf(dirSuffix) + dirSuffix.length);
23
+ return !afterDir.includes('/');
24
+ }
14
25
  export function buildImportResolutionContext(allPaths) {
15
26
  const allFileList = allPaths;
16
27
  const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
@@ -19,550 +30,159 @@ export function buildImportResolutionContext(allPaths) {
19
30
  return { allFilePaths, allFileList, normalizedFileList, suffixIndex, resolveCache: new Map() };
20
31
  }
21
32
  /**
22
- * Parse tsconfig.json to extract path aliases.
23
- * Tries tsconfig.json, tsconfig.app.json, tsconfig.base.json in order.
33
+ * Shared language dispatch for import resolution.
34
+ * Used by both processImports and processImportsFromExtracted.
24
35
  */
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);
36
+ function resolveLanguageImport(filePath, rawImportPath, language, configs, ctx) {
37
+ const { allFilePaths, allFileList, normalizedFileList, index, resolveCache } = ctx;
38
+ const { tsconfigPaths, goModule, composerConfig, swiftPackageConfig, csharpConfigs } = configs;
39
+ // JVM languages (Java + Kotlin): handle wildcards and member imports
40
+ if (language === SupportedLanguages.Java || language === SupportedLanguages.Kotlin) {
41
+ const exts = language === SupportedLanguages.Java ? ['.java'] : KOTLIN_EXTENSIONS;
42
+ if (rawImportPath.endsWith('.*')) {
43
+ const matchedFiles = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, exts, index);
44
+ if (matchedFiles.length === 0 && language === SupportedLanguages.Kotlin) {
45
+ const javaMatches = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
46
+ if (javaMatches.length > 0)
47
+ return { kind: 'files', files: javaMatches };
47
48
  }
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
49
+ if (matchedFiles.length > 0)
50
+ return { kind: 'files', files: matchedFiles };
51
+ // Fall through to standard resolution
57
52
  }
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
- }
53
+ else {
54
+ let memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index);
55
+ if (!memberResolved && language === SupportedLanguages.Kotlin) {
56
+ memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
118
57
  }
58
+ if (memberResolved)
59
+ return { kind: 'files', files: [memberResolved] };
60
+ // Fall through to standard resolution
119
61
  }
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
62
  }
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);
63
+ // Go: handle package-level imports
64
+ if (language === SupportedLanguages.Go && goModule && rawImportPath.startsWith(goModule.modulePath)) {
65
+ const pkgSuffix = resolveGoPackageDir(rawImportPath, goModule);
66
+ if (pkgSuffix) {
67
+ const pkgFiles = resolveGoPackage(rawImportPath, goModule, normalizedFileList, allFileList);
68
+ if (pkgFiles.length > 0) {
69
+ return { kind: 'package', files: pkgFiles, dirSuffix: pkgSuffix };
192
70
  }
193
71
  }
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;
72
+ // Fall through if no files found (package might be external)
73
+ }
74
+ // C#: handle namespace-based imports (using directives)
75
+ if (language === SupportedLanguages.CSharp && csharpConfigs.length > 0) {
76
+ const resolvedFiles = resolveCSharpImport(rawImportPath, csharpConfigs, normalizedFileList, allFileList, index);
77
+ if (resolvedFiles.length > 1) {
78
+ const dirSuffix = resolveCSharpNamespaceDir(rawImportPath, csharpConfigs);
79
+ if (dirSuffix) {
80
+ return { kind: 'package', files: resolvedFiles, dirSuffix };
233
81
  }
234
82
  }
83
+ if (resolvedFiles.length > 0)
84
+ return { kind: 'files', files: resolvedFiles };
235
85
  return null;
236
86
  }
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);
87
+ // PHP: handle namespace-based imports (use statements)
88
+ if (language === SupportedLanguages.PHP) {
89
+ const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index);
90
+ return resolved ? { kind: 'files', files: [resolved] } : null;
91
+ }
92
+ // Swift: handle module imports
93
+ if (language === SupportedLanguages.Swift && swiftPackageConfig) {
94
+ const targetDir = swiftPackageConfig.targets.get(rawImportPath);
95
+ if (targetDir) {
96
+ const dirPrefix = targetDir + '/';
97
+ const files = [];
98
+ for (let i = 0; i < normalizedFileList.length; i++) {
99
+ if (normalizedFileList[i].startsWith(dirPrefix) && normalizedFileList[i].endsWith('.swift')) {
100
+ files.push(allFileList[i]);
101
+ }
299
102
  }
103
+ if (files.length > 0)
104
+ return { kind: 'files', files };
300
105
  }
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);
106
+ return null; // External framework (Foundation, UIKit, etc.)
107
+ }
108
+ // Python: relative imports (PEP 328) + proximity-based bare imports
109
+ // Falls through to standard suffix resolution when proximity finds no match.
110
+ if (language === SupportedLanguages.Python) {
111
+ const resolved = resolvePythonImport(filePath, rawImportPath, allFilePaths);
112
+ if (resolved)
113
+ return { kind: 'files', files: [resolved] };
114
+ if (rawImportPath.startsWith('.'))
115
+ return null; // relative but unresolved — don't suffix-match
116
+ }
117
+ // Ruby: require / require_relative
118
+ if (language === SupportedLanguages.Ruby) {
119
+ const resolved = resolveRubyImport(rawImportPath, normalizedFileList, allFileList, index);
120
+ return resolved ? { kind: 'files', files: [resolved] } : null;
121
+ }
122
+ // Rust: expand top-level grouped imports: use {crate::a, crate::b}
123
+ if (language === SupportedLanguages.Rust && rawImportPath.startsWith('{') && rawImportPath.endsWith('}')) {
124
+ const inner = rawImportPath.slice(1, -1);
125
+ const parts = inner.split(',').map(p => p.trim()).filter(Boolean);
126
+ const resolved = [];
127
+ for (const part of parts) {
128
+ const r = resolveRustImport(filePath, part, allFilePaths);
129
+ if (r)
130
+ resolved.push(r);
320
131
  }
132
+ return resolved.length > 0 ? { kind: 'files', files: resolved } : null;
321
133
  }
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;
134
+ // Standard single-file resolution
135
+ const resolvedPath = resolveImportPath(filePath, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
136
+ return resolvedPath ? { kind: 'files', files: [resolvedPath] } : null;
410
137
  }
411
138
  /**
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.
139
+ * Apply an ImportResult: emit graph edges and update ImportMap/PackageMap.
140
+ * If namedBindings are provided and the import resolves to a single file,
141
+ * also populate the NamedImportMap for precise Tier 2a resolution.
414
142
  */
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}.*`;
143
+ function applyImportResult(result, filePath, importMap, packageMap, addImportEdge, addImportGraphEdge, namedBindings, namedImportMap) {
144
+ if (!result)
145
+ return;
146
+ if (result.kind === 'package' && packageMap) {
147
+ // Store directory suffix in PackageMap (skip ImportMap expansion)
148
+ for (const resolvedFile of result.files) {
149
+ addImportGraphEdge(filePath, resolvedFile);
419
150
  }
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
- }
459
- }
460
- }
461
- return matches;
462
- }
463
- /**
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"
467
- */
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
- }
493
- }
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
- }
151
+ if (!packageMap.has(filePath))
152
+ packageMap.set(filePath, new Set());
153
+ packageMap.get(filePath).add(result.dirSuffix);
154
+ }
155
+ else {
156
+ // 'files' kind, or 'package' without PackageMap — use ImportMap directly
157
+ const files = result.files;
158
+ for (const resolvedFile of files) {
159
+ addImportEdge(filePath, resolvedFile);
521
160
  }
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
- }
161
+ // Record named bindings for precise Tier 2a resolution
162
+ if (namedBindings && namedImportMap && files.length === 1) {
163
+ const resolvedFile = files[0];
164
+ if (!namedImportMap.has(filePath))
165
+ namedImportMap.set(filePath, new Map());
166
+ const fileBindings = namedImportMap.get(filePath);
167
+ for (const binding of namedBindings) {
168
+ fileBindings.set(binding.local, { sourcePath: resolvedFile, exportedName: binding.exported });
551
169
  }
552
170
  }
553
171
  }
554
- // Fallback: suffix matching (works without composer.json)
555
- const pathParts = normalized.split('/').filter(Boolean);
556
- return suffixResolve(pathParts, normalizedFileList, allFileList, index);
557
172
  }
558
173
  // ============================================================================
559
174
  // MAIN IMPORT PROCESSOR
560
175
  // ============================================================================
561
- export const processImports = async (graph, files, astCache, importMap, onProgress, repoRoot, allPaths) => {
176
+ export const processImports = async (graph, files, astCache, ctx, onProgress, repoRoot, allPaths) => {
177
+ const importMap = ctx.importMap;
178
+ const packageMap = ctx.packageMap;
179
+ const namedImportMap = ctx.namedImportMap;
562
180
  // Use allPaths (full repo) when available for cross-chunk resolution, else fall back to chunk files
563
181
  const allFileList = allPaths ?? files.map(f => f.path);
564
182
  const allFilePaths = new Set(allFileList);
565
183
  const parser = await loadParser();
184
+ const logSkipped = isVerboseIngestionEnabled();
185
+ const skippedByLang = logSkipped ? new Map() : null;
566
186
  const resolveCache = new Map();
567
187
  // Pre-compute normalized file list once (forward slashes)
568
188
  const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
@@ -573,12 +193,16 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
573
193
  let totalImportsResolved = 0;
574
194
  // Load language-specific configs once before the file loop
575
195
  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) => {
196
+ const configs = {
197
+ tsconfigPaths: await loadTsconfigPaths(effectiveRoot),
198
+ goModule: await loadGoModulePath(effectiveRoot),
199
+ composerConfig: await loadComposerConfig(effectiveRoot),
200
+ swiftPackageConfig: await loadSwiftPackageConfig(effectiveRoot),
201
+ csharpConfigs: await loadCSharpProjectConfig(effectiveRoot),
202
+ };
203
+ const resolveCtx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
204
+ // Helper: add an IMPORTS edge to the graph only (no ImportMap update)
205
+ const addImportGraphEdge = (filePath, resolvedPath) => {
582
206
  const sourceId = generateId('File', filePath);
583
207
  const targetId = generateId('File', resolvedPath);
584
208
  const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
@@ -591,6 +215,10 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
591
215
  confidence: 1.0,
592
216
  reason: '',
593
217
  });
218
+ };
219
+ // Helper: add an IMPORTS edge + update import map
220
+ const addImportEdge = (filePath, resolvedPath) => {
221
+ addImportGraphEdge(filePath, resolvedPath);
594
222
  if (!importMap.has(filePath)) {
595
223
  importMap.set(filePath, new Set());
596
224
  }
@@ -605,6 +233,12 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
605
233
  const language = getLanguageFromFilename(file.path);
606
234
  if (!language)
607
235
  continue;
236
+ if (!isLanguageAvailable(language)) {
237
+ if (skippedByLang) {
238
+ skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
239
+ }
240
+ continue;
241
+ }
608
242
  const queryStr = LANGUAGE_QUERIES[language];
609
243
  if (!queryStr)
610
244
  continue;
@@ -615,7 +249,7 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
615
249
  let wasReparsed = false;
616
250
  if (!tree) {
617
251
  try {
618
- tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 });
252
+ tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
619
253
  }
620
254
  catch (parseError) {
621
255
  continue;
@@ -662,83 +296,31 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
662
296
  ? appendKotlinWildcard(sourceNode.text.replace(/['"<>]/g, ''), captureMap['import'])
663
297
  : sourceNode.text.replace(/['"<>]/g, '');
664
298
  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;
299
+ const result = resolveLanguageImport(file.path, rawImportPath, language, configs, resolveCtx);
300
+ const bindings = namedImportMap ? extractNamedBindings(captureMap['import'], language) : undefined;
301
+ applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge, bindings, namedImportMap);
302
+ }
303
+ // ---- Language-specific call-as-import routing (Ruby require, etc.) ----
304
+ if (captureMap['call']) {
305
+ const callNameNode = captureMap['call.name'];
306
+ if (callNameNode) {
307
+ const callRouter = callRouters[language];
308
+ const routed = callRouter(callNameNode.text, captureMap['call']);
309
+ if (routed && routed.kind === 'import') {
310
+ totalImportsFound++;
311
+ const result = resolveLanguageImport(file.path, routed.importPath, language, configs, resolveCtx);
312
+ applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge);
729
313
  }
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
314
  }
738
315
  }
739
316
  });
740
317
  // Tree is now owned by the LRU cache — no manual delete needed
741
318
  }
319
+ if (skippedByLang && skippedByLang.size > 0) {
320
+ for (const [lang, count] of skippedByLang.entries()) {
321
+ console.warn(`[ingestion] Skipped ${count} ${lang} file(s) in import processing — ${lang} parser not available.`);
322
+ }
323
+ }
742
324
  if (isDev) {
743
325
  console.log(`📊 Import processing complete: ${totalImportsResolved}/${totalImportsFound} imports resolved to graph edges`);
744
326
  }
@@ -746,17 +328,25 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
746
328
  // ============================================================================
747
329
  // FAST PATH: Resolve pre-extracted imports (no parsing needed)
748
330
  // ============================================================================
749
- export const processImportsFromExtracted = async (graph, files, extractedImports, importMap, onProgress, repoRoot, prebuiltCtx) => {
750
- const ctx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
751
- const { allFilePaths, allFileList, normalizedFileList, suffixIndex: index, resolveCache } = ctx;
331
+ export const processImportsFromExtracted = async (graph, files, extractedImports, ctx, onProgress, repoRoot, prebuiltCtx) => {
332
+ const importMap = ctx.importMap;
333
+ const packageMap = ctx.packageMap;
334
+ const namedImportMap = ctx.namedImportMap;
335
+ const importCtx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
336
+ const { allFilePaths, allFileList, normalizedFileList, suffixIndex: index, resolveCache } = importCtx;
752
337
  let totalImportsFound = 0;
753
338
  let totalImportsResolved = 0;
754
339
  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) => {
340
+ const configs = {
341
+ tsconfigPaths: await loadTsconfigPaths(effectiveRoot),
342
+ goModule: await loadGoModulePath(effectiveRoot),
343
+ composerConfig: await loadComposerConfig(effectiveRoot),
344
+ swiftPackageConfig: await loadSwiftPackageConfig(effectiveRoot),
345
+ csharpConfigs: await loadCSharpProjectConfig(effectiveRoot),
346
+ };
347
+ const resolveCtx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
348
+ // Helper: add an IMPORTS edge to the graph only (no ImportMap update)
349
+ const addImportGraphEdge = (filePath, resolvedPath) => {
760
350
  const sourceId = generateId('File', filePath);
761
351
  const targetId = generateId('File', resolvedPath);
762
352
  const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
@@ -769,6 +359,9 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
769
359
  confidence: 1.0,
770
360
  reason: '',
771
361
  });
362
+ };
363
+ const addImportEdge = (filePath, resolvedPath) => {
364
+ addImportGraphEdge(filePath, resolvedPath);
772
365
  if (!importMap.has(filePath)) {
773
366
  importMap.set(filePath, new Set());
774
367
  }
@@ -786,103 +379,16 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
786
379
  }
787
380
  const totalFiles = importsByFile.size;
788
381
  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
382
  for (const [filePath, fileImports] of importsByFile) {
804
383
  filesProcessed++;
805
384
  if (filesProcessed % 100 === 0) {
806
385
  onProgress?.(filesProcessed, totalFiles);
807
386
  await yieldToEventLoop();
808
387
  }
809
- for (const { rawImportPath, language } of fileImports) {
388
+ for (const imp of fileImports) {
810
389
  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
- }
390
+ const result = resolveLanguageImport(filePath, imp.rawImportPath, imp.language, configs, resolveCtx);
391
+ applyImportResult(result, filePath, importMap, packageMap, addImportEdge, addImportGraphEdge, imp.namedBindings, namedImportMap);
886
392
  }
887
393
  }
888
394
  onProgress?.(totalFiles, totalFiles);