@veewo/gitnexus 1.3.11 → 1.4.6-rc

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 (181) hide show
  1. package/README.md +37 -80
  2. package/dist/benchmark/agent-context/tool-runner.js +2 -2
  3. package/dist/benchmark/neonspark-candidates.js +3 -3
  4. package/dist/benchmark/tool-runner.js +2 -2
  5. package/dist/cli/ai-context.d.ts +2 -1
  6. package/dist/cli/ai-context.js +16 -12
  7. package/dist/cli/analyze.d.ts +2 -0
  8. package/dist/cli/analyze.js +68 -48
  9. package/dist/cli/augment.js +1 -1
  10. package/dist/cli/eval-server.d.ts +8 -1
  11. package/dist/cli/eval-server.js +30 -13
  12. package/dist/cli/index.js +28 -82
  13. package/dist/cli/lazy-action.d.ts +6 -0
  14. package/dist/cli/lazy-action.js +18 -0
  15. package/dist/cli/mcp.js +3 -1
  16. package/dist/cli/setup.js +87 -48
  17. package/dist/cli/setup.test.js +18 -13
  18. package/dist/cli/skill-gen.d.ts +26 -0
  19. package/dist/cli/skill-gen.js +549 -0
  20. package/dist/cli/status.js +13 -4
  21. package/dist/cli/tool.d.ts +3 -2
  22. package/dist/cli/tool.js +50 -16
  23. package/dist/cli/wiki.js +8 -4
  24. package/dist/config/ignore-service.d.ts +25 -0
  25. package/dist/config/ignore-service.js +76 -0
  26. package/dist/config/supported-languages.d.ts +4 -1
  27. package/dist/config/supported-languages.js +3 -2
  28. package/dist/core/augmentation/engine.js +94 -67
  29. package/dist/core/embeddings/embedder.d.ts +1 -1
  30. package/dist/core/embeddings/embedder.js +1 -1
  31. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  32. package/dist/core/embeddings/embedding-pipeline.js +52 -25
  33. package/dist/core/embeddings/types.d.ts +1 -1
  34. package/dist/core/graph/types.d.ts +7 -2
  35. package/dist/core/ingestion/ast-cache.js +3 -2
  36. package/dist/core/ingestion/call-processor.d.ts +8 -6
  37. package/dist/core/ingestion/call-processor.js +468 -206
  38. package/dist/core/ingestion/call-routing.d.ts +53 -0
  39. package/dist/core/ingestion/call-routing.js +108 -0
  40. package/dist/core/ingestion/constants.d.ts +16 -0
  41. package/dist/core/ingestion/constants.js +16 -0
  42. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  43. package/dist/core/ingestion/entry-point-scoring.js +116 -23
  44. package/dist/core/ingestion/export-detection.d.ts +18 -0
  45. package/dist/core/ingestion/export-detection.js +231 -0
  46. package/dist/core/ingestion/filesystem-walker.js +4 -3
  47. package/dist/core/ingestion/framework-detection.d.ts +19 -4
  48. package/dist/core/ingestion/framework-detection.js +182 -6
  49. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  50. package/dist/core/ingestion/heritage-processor.js +109 -55
  51. package/dist/core/ingestion/import-processor.d.ts +16 -20
  52. package/dist/core/ingestion/import-processor.js +199 -579
  53. package/dist/core/ingestion/language-config.d.ts +46 -0
  54. package/dist/core/ingestion/language-config.js +167 -0
  55. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  56. package/dist/core/ingestion/mro-processor.js +369 -0
  57. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  58. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  59. package/dist/core/ingestion/parsing-processor.d.ts +4 -1
  60. package/dist/core/ingestion/parsing-processor.js +107 -109
  61. package/dist/core/ingestion/pipeline.d.ts +6 -3
  62. package/dist/core/ingestion/pipeline.js +208 -114
  63. package/dist/core/ingestion/process-processor.js +8 -2
  64. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  65. package/dist/core/ingestion/resolution-context.js +132 -0
  66. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  67. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  68. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  69. package/dist/core/ingestion/resolvers/go.js +42 -0
  70. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  71. package/dist/core/ingestion/resolvers/index.js +13 -0
  72. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  73. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  74. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  75. package/dist/core/ingestion/resolvers/php.js +35 -0
  76. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  77. package/dist/core/ingestion/resolvers/python.js +52 -0
  78. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  79. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  80. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  81. package/dist/core/ingestion/resolvers/rust.js +73 -0
  82. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  83. package/dist/core/ingestion/resolvers/standard.js +123 -0
  84. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  85. package/dist/core/ingestion/resolvers/utils.js +122 -0
  86. package/dist/core/ingestion/symbol-table.d.ts +21 -1
  87. package/dist/core/ingestion/symbol-table.js +40 -12
  88. package/dist/core/ingestion/tree-sitter-queries.d.ts +13 -10
  89. package/dist/core/ingestion/tree-sitter-queries.js +297 -7
  90. package/dist/core/ingestion/type-env.d.ts +49 -0
  91. package/dist/core/ingestion/type-env.js +611 -0
  92. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  93. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  94. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  95. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  96. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  97. package/dist/core/ingestion/type-extractors/go.js +467 -0
  98. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  99. package/dist/core/ingestion/type-extractors/index.js +31 -0
  100. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  101. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  102. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  103. package/dist/core/ingestion/type-extractors/php.js +549 -0
  104. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  105. package/dist/core/ingestion/type-extractors/python.js +406 -0
  106. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  107. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  108. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  109. package/dist/core/ingestion/type-extractors/rust.js +449 -0
  110. package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
  111. package/dist/core/ingestion/type-extractors/shared.js +703 -0
  112. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  113. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  114. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  115. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  116. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  117. package/dist/core/ingestion/utils.d.ts +103 -0
  118. package/dist/core/ingestion/utils.js +1085 -4
  119. package/dist/core/ingestion/workers/parse-worker.d.ts +51 -4
  120. package/dist/core/ingestion/workers/parse-worker.js +634 -222
  121. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  122. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +12 -10
  123. package/dist/core/{kuzu → lbug}/csv-generator.js +82 -101
  124. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +20 -25
  125. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +150 -122
  126. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  127. package/dist/core/{kuzu → lbug}/schema.js +23 -22
  128. package/dist/core/lbug/schema.test.d.ts +1 -0
  129. package/dist/core/search/bm25-index.d.ts +4 -4
  130. package/dist/core/search/bm25-index.js +12 -11
  131. package/dist/core/search/hybrid-search.d.ts +2 -2
  132. package/dist/core/search/hybrid-search.js +6 -6
  133. package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
  134. package/dist/core/tree-sitter/parser-loader.js +19 -0
  135. package/dist/core/wiki/generator.d.ts +2 -2
  136. package/dist/core/wiki/generator.js +6 -6
  137. package/dist/core/wiki/graph-queries.d.ts +4 -4
  138. package/dist/core/wiki/graph-queries.js +7 -7
  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} +11 -10
  142. package/dist/mcp/core/lbug-adapter.js +327 -0
  143. package/dist/mcp/local/local-backend.d.ts +21 -16
  144. package/dist/mcp/local/local-backend.js +306 -706
  145. package/dist/mcp/local/unity-parity-seed-loader.d.ts +6 -1
  146. package/dist/mcp/local/unity-parity-seed-loader.js +119 -9
  147. package/dist/mcp/local/unity-parity-seed-loader.test.js +95 -7
  148. package/dist/mcp/resources.js +2 -2
  149. package/dist/mcp/server.js +28 -13
  150. package/dist/mcp/staleness.js +2 -2
  151. package/dist/mcp/tools.js +12 -3
  152. package/dist/server/api.js +12 -12
  153. package/dist/server/mcp-http.d.ts +1 -1
  154. package/dist/server/mcp-http.js +1 -1
  155. package/dist/storage/git.js +4 -1
  156. package/dist/storage/repo-manager.d.ts +20 -2
  157. package/dist/storage/repo-manager.js +74 -4
  158. package/dist/types/pipeline.d.ts +1 -1
  159. package/hooks/claude/gitnexus-hook.cjs +149 -46
  160. package/hooks/claude/pre-tool-use.sh +2 -1
  161. package/hooks/claude/session-start.sh +0 -0
  162. package/package.json +20 -4
  163. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  164. package/skills/gitnexus-cli.md +8 -8
  165. package/skills/gitnexus-debugging.md +1 -1
  166. package/skills/gitnexus-exploring.md +1 -1
  167. package/skills/gitnexus-guide.md +1 -1
  168. package/skills/gitnexus-impact-analysis.md +1 -1
  169. package/skills/gitnexus-pr-review.md +163 -0
  170. package/skills/gitnexus-refactoring.md +1 -1
  171. package/dist/cli/claude-hooks.d.ts +0 -22
  172. package/dist/cli/claude-hooks.js +0 -97
  173. package/dist/mcp/core/kuzu-adapter.js +0 -231
  174. /package/dist/core/{kuzu/csv-generator.test.d.ts → ingestion/type-extractors/types.js} +0 -0
  175. /package/dist/core/{kuzu/relationship-pair-buckets.test.d.ts → lbug/csv-generator.test.d.ts} +0 -0
  176. /package/dist/core/{kuzu → lbug}/csv-generator.test.js +0 -0
  177. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.d.ts +0 -0
  178. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.js +0 -0
  179. /package/dist/core/{kuzu/schema.test.d.ts → lbug/relationship-pair-buckets.test.d.ts} +0 -0
  180. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.test.js +0 -0
  181. /package/dist/core/{kuzu → lbug}/schema.test.js +0 -0
@@ -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,499 +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);
47
- }
48
- if (aliases.size > 0) {
49
- if (isDev) {
50
- console.log(`📦 Loaded ${aliases.size} path aliases from ${filename}`);
51
- }
52
- return { aliases, baseUrl };
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 };
53
48
  }
49
+ if (matchedFiles.length > 0)
50
+ return { kind: 'files', files: matchedFiles };
51
+ // Fall through to standard resolution
54
52
  }
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]}`);
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);
72
57
  }
73
- return { modulePath: match[1] };
58
+ if (memberResolved)
59
+ return { kind: 'files', files: [memberResolved] };
60
+ // Fall through to standard resolution
74
61
  }
75
62
  }
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
- // ============================================================================
105
- // IMPORT PATH RESOLUTION
106
- // ============================================================================
107
- /** All file extensions to try during resolution */
108
- const EXTENSIONS = [
109
- '',
110
- // TypeScript/JavaScript
111
- '.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js',
112
- // Python
113
- '.py', '/__init__.py',
114
- // Java
115
- '.java',
116
- // C/C++
117
- '.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hxx', '.hh',
118
- // C#
119
- '.cs',
120
- // Go
121
- '.go',
122
- // Rust
123
- '.rs', '/mod.rs',
124
- // PHP
125
- '.php', '.phtml',
126
- ];
127
- /**
128
- * Try to match a path (with extensions) against the known file set.
129
- * Returns the matched file path or null.
130
- */
131
- function tryResolveWithExtensions(basePath, allFiles) {
132
- for (const ext of EXTENSIONS) {
133
- const candidate = basePath + ext;
134
- if (allFiles.has(candidate))
135
- return candidate;
136
- }
137
- return null;
138
- }
139
- function buildSuffixIndex(normalizedFileList, allFileList) {
140
- // Map: normalized suffix -> original file path
141
- const exactMap = new Map();
142
- // Map: lowercase suffix -> original file path
143
- const lowerMap = new Map();
144
- // Map: directory suffix -> list of file paths in that directory
145
- const dirMap = new Map();
146
- for (let i = 0; i < normalizedFileList.length; i++) {
147
- const normalized = normalizedFileList[i];
148
- const original = allFileList[i];
149
- const parts = normalized.split('/');
150
- // Index all suffixes: "a/b/c.java" -> ["c.java", "b/c.java", "a/b/c.java"]
151
- for (let j = parts.length - 1; j >= 0; j--) {
152
- const suffix = parts.slice(j).join('/');
153
- // Only store first match (longest path wins for ambiguous suffixes)
154
- if (!exactMap.has(suffix)) {
155
- exactMap.set(suffix, original);
156
- }
157
- const lower = suffix.toLowerCase();
158
- if (!lowerMap.has(lower)) {
159
- lowerMap.set(lower, original);
160
- }
161
- }
162
- // Index directory membership
163
- const lastSlash = normalized.lastIndexOf('/');
164
- if (lastSlash >= 0) {
165
- // Build all directory suffixes
166
- const dirParts = parts.slice(0, -1);
167
- const fileName = parts[parts.length - 1];
168
- const ext = fileName.substring(fileName.lastIndexOf('.'));
169
- for (let j = dirParts.length - 1; j >= 0; j--) {
170
- const dirSuffix = dirParts.slice(j).join('/');
171
- const key = `${dirSuffix}:${ext}`;
172
- let list = dirMap.get(key);
173
- if (!list) {
174
- list = [];
175
- dirMap.set(key, list);
176
- }
177
- list.push(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 };
178
70
  }
179
71
  }
72
+ // Fall through if no files found (package might be external)
180
73
  }
181
- return {
182
- get: (suffix) => exactMap.get(suffix),
183
- getInsensitive: (suffix) => lowerMap.get(suffix.toLowerCase()),
184
- getFilesInDir: (dirSuffix, extension) => {
185
- return dirMap.get(`${dirSuffix}:${extension}`) || [];
186
- },
187
- };
188
- }
189
- /**
190
- * Suffix-based resolution using index. O(1) per lookup instead of O(files).
191
- */
192
- function suffixResolve(pathParts, normalizedFileList, allFileList, index) {
193
- if (index) {
194
- for (let i = 0; i < pathParts.length; i++) {
195
- const suffix = pathParts.slice(i).join('/');
196
- for (const ext of EXTENSIONS) {
197
- const suffixWithExt = suffix + ext;
198
- const result = index.get(suffixWithExt) || index.getInsensitive(suffixWithExt);
199
- if (result)
200
- return result;
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 };
201
81
  }
202
82
  }
83
+ if (resolvedFiles.length > 0)
84
+ return { kind: 'files', files: resolvedFiles };
203
85
  return null;
204
86
  }
205
- // Fallback: linear scan (for backward compatibility)
206
- for (let i = 0; i < pathParts.length; i++) {
207
- const suffix = pathParts.slice(i).join('/');
208
- for (const ext of EXTENSIONS) {
209
- const suffixWithExt = suffix + ext;
210
- const suffixPattern = '/' + suffixWithExt;
211
- const matchIdx = normalizedFileList.findIndex(filePath => filePath.endsWith(suffixPattern) || filePath.toLowerCase().endsWith(suffixPattern.toLowerCase()));
212
- if (matchIdx !== -1) {
213
- return allFileList[matchIdx];
214
- }
215
- }
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;
216
91
  }
217
- return null;
218
- }
219
- /**
220
- * Resolve an import path to a file path in the repository.
221
- *
222
- * Language-specific preprocessing is applied before the generic resolution:
223
- * - TypeScript/JavaScript: rewrites tsconfig path aliases
224
- * - Rust: converts crate::/super::/self:: to relative paths
225
- *
226
- * Java wildcards and Go package imports are handled separately in processImports
227
- * because they resolve to multiple files.
228
- */
229
- const resolveImportPath = (currentFile, importPath, allFiles, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index) => {
230
- const cacheKey = `${currentFile}::${importPath}`;
231
- if (resolveCache.has(cacheKey))
232
- return resolveCache.get(cacheKey) ?? null;
233
- const cache = (result) => {
234
- // Evict oldest 20% when cap is reached instead of clearing all
235
- if (resolveCache.size >= RESOLVE_CACHE_CAP) {
236
- const evictCount = Math.floor(RESOLVE_CACHE_CAP * 0.2);
237
- const iter = resolveCache.keys();
238
- for (let i = 0; i < evictCount; i++) {
239
- const key = iter.next().value;
240
- if (key !== undefined)
241
- resolveCache.delete(key);
242
- }
243
- }
244
- resolveCache.set(cacheKey, result);
245
- return result;
246
- };
247
- // ---- TypeScript/JavaScript: rewrite path aliases ----
248
- if ((language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript) &&
249
- tsconfigPaths &&
250
- !importPath.startsWith('.')) {
251
- for (const [aliasPrefix, targetPrefix] of tsconfigPaths.aliases) {
252
- if (importPath.startsWith(aliasPrefix)) {
253
- const remainder = importPath.slice(aliasPrefix.length);
254
- // Build the rewritten path relative to baseUrl
255
- const rewritten = tsconfigPaths.baseUrl === '.'
256
- ? targetPrefix + remainder
257
- : tsconfigPaths.baseUrl + '/' + targetPrefix + remainder;
258
- // Try direct resolution from repo root
259
- const resolved = tryResolveWithExtensions(rewritten, allFiles);
260
- if (resolved)
261
- return cache(resolved);
262
- // Try suffix matching as fallback
263
- const parts = rewritten.split('/').filter(Boolean);
264
- const suffixResult = suffixResolve(parts, normalizedFileList, allFileList, index);
265
- if (suffixResult)
266
- return cache(suffixResult);
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
+ }
267
102
  }
103
+ if (files.length > 0)
104
+ return { kind: 'files', files };
268
105
  }
106
+ return null; // External framework (Foundation, UIKit, etc.)
269
107
  }
270
- // ---- Rust: convert module path syntax to file paths ----
271
- if (language === SupportedLanguages.Rust) {
272
- const rustResult = resolveRustImport(currentFile, importPath, allFiles);
273
- if (rustResult)
274
- return cache(rustResult);
275
- // Fall through to generic resolution if Rust-specific didn't match
276
- }
277
- // ---- Generic relative import resolution (./ and ../) ----
278
- const currentDir = currentFile.split('/').slice(0, -1);
279
- const parts = importPath.split('/');
280
- for (const part of parts) {
281
- if (part === '.')
282
- continue;
283
- if (part === '..') {
284
- currentDir.pop();
285
- }
286
- else {
287
- currentDir.push(part);
288
- }
289
- }
290
- const basePath = currentDir.join('/');
291
- if (importPath.startsWith('.')) {
292
- const resolved = tryResolveWithExtensions(basePath, allFiles);
293
- return cache(resolved);
294
- }
295
- // ---- Generic package/absolute import resolution (suffix matching) ----
296
- // Java wildcards are handled in processImports, not here
297
- if (importPath.endsWith('.*')) {
298
- return cache(null);
299
- }
300
- const pathLike = importPath.includes('/')
301
- ? importPath
302
- : importPath.replace(/\./g, '/');
303
- const pathParts = pathLike.split('/').filter(Boolean);
304
- const resolved = suffixResolve(pathParts, normalizedFileList, allFileList, index);
305
- return cache(resolved);
306
- };
307
- // ============================================================================
308
- // RUST MODULE RESOLUTION
309
- // ============================================================================
310
- /**
311
- * Resolve Rust use-path to a file.
312
- * Handles crate::, super::, self:: prefixes and :: path separators.
313
- */
314
- function resolveRustImport(currentFile, importPath, allFiles) {
315
- let rustPath;
316
- if (importPath.startsWith('crate::')) {
317
- // crate:: resolves from src/ directory (standard Rust layout)
318
- rustPath = importPath.slice(7).replace(/::/g, '/');
319
- // Try from src/ (standard layout)
320
- const fromSrc = tryRustModulePath('src/' + rustPath, allFiles);
321
- if (fromSrc)
322
- return fromSrc;
323
- // Try from repo root (non-standard)
324
- const fromRoot = tryRustModulePath(rustPath, allFiles);
325
- if (fromRoot)
326
- return fromRoot;
327
- return null;
328
- }
329
- if (importPath.startsWith('super::')) {
330
- // super:: = parent directory of current file's module
331
- const currentDir = currentFile.split('/').slice(0, -1);
332
- currentDir.pop(); // Go up one level for super::
333
- rustPath = importPath.slice(7).replace(/::/g, '/');
334
- const fullPath = [...currentDir, rustPath].join('/');
335
- return tryRustModulePath(fullPath, allFiles);
336
- }
337
- if (importPath.startsWith('self::')) {
338
- // self:: = current module's directory
339
- const currentDir = currentFile.split('/').slice(0, -1);
340
- rustPath = importPath.slice(6).replace(/::/g, '/');
341
- const fullPath = [...currentDir, rustPath].join('/');
342
- return tryRustModulePath(fullPath, allFiles);
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
343
116
  }
344
- // Bare path without prefix (e.g., from a use in a nested module)
345
- // Convert :: to / and try suffix matching
346
- if (importPath.includes('::')) {
347
- rustPath = importPath.replace(/::/g, '/');
348
- return tryRustModulePath(rustPath, allFiles);
349
- }
350
- return null;
351
- }
352
- /**
353
- * Try to resolve a Rust module path to a file.
354
- * Tries: path.rs, path/mod.rs, and with the last segment stripped
355
- * (last segment might be a symbol name, not a module).
356
- */
357
- function tryRustModulePath(modulePath, allFiles) {
358
- // Try direct: path.rs
359
- if (allFiles.has(modulePath + '.rs'))
360
- return modulePath + '.rs';
361
- // Try directory: path/mod.rs
362
- if (allFiles.has(modulePath + '/mod.rs'))
363
- return modulePath + '/mod.rs';
364
- // Try path/lib.rs (for crate root)
365
- if (allFiles.has(modulePath + '/lib.rs'))
366
- return modulePath + '/lib.rs';
367
- // The last segment might be a symbol (function, struct, etc.), not a module.
368
- // Strip it and try again.
369
- const lastSlash = modulePath.lastIndexOf('/');
370
- if (lastSlash > 0) {
371
- const parentPath = modulePath.substring(0, lastSlash);
372
- if (allFiles.has(parentPath + '.rs'))
373
- return parentPath + '.rs';
374
- if (allFiles.has(parentPath + '/mod.rs'))
375
- return parentPath + '/mod.rs';
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;
376
121
  }
377
- return null;
378
- }
379
- // ============================================================================
380
- // JAVA MULTI-FILE RESOLUTION
381
- // ============================================================================
382
- /**
383
- * Resolve a Java wildcard import (com.example.*) to all matching .java files.
384
- * Returns an array of file paths.
385
- */
386
- function resolveJavaWildcard(importPath, normalizedFileList, allFileList, index) {
387
- // "com.example.util.*" -> "com/example/util"
388
- const packagePath = importPath.slice(0, -2).replace(/\./g, '/');
389
- if (index) {
390
- // Use directory index: get all .java files in this package directory
391
- const candidates = index.getFilesInDir(packagePath, '.java');
392
- // Filter to only direct children (no subdirectories)
393
- const packageSuffix = '/' + packagePath + '/';
394
- return candidates.filter(f => {
395
- const normalized = f.replace(/\\/g, '/');
396
- const idx = normalized.indexOf(packageSuffix);
397
- if (idx < 0)
398
- return false;
399
- const afterPkg = normalized.substring(idx + packageSuffix.length);
400
- return !afterPkg.includes('/');
401
- });
402
- }
403
- // Fallback: linear scan
404
- const packageSuffix = '/' + packagePath + '/';
405
- const matches = [];
406
- for (let i = 0; i < normalizedFileList.length; i++) {
407
- const normalized = normalizedFileList[i];
408
- if (normalized.includes(packageSuffix) && normalized.endsWith('.java')) {
409
- const afterPackage = normalized.substring(normalized.indexOf(packageSuffix) + packageSuffix.length);
410
- if (!afterPackage.includes('/')) {
411
- matches.push(allFileList[i]);
412
- }
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);
413
131
  }
132
+ return resolved.length > 0 ? { kind: 'files', files: resolved } : null;
414
133
  }
415
- return matches;
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;
416
137
  }
417
138
  /**
418
- * Try to resolve a Java static import by stripping the member name.
419
- * "com.example.Constants.VALUE" -> resolve "com.example.Constants"
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.
420
142
  */
421
- function resolveJavaStaticImport(importPath, normalizedFileList, allFileList, index) {
422
- // Static imports look like: com.example.Constants.VALUE or com.example.Constants.*
423
- // The last segment is a member name (field/method) if it starts with lowercase or is ALL_CAPS
424
- const segments = importPath.split('.');
425
- if (segments.length < 3)
426
- return null;
427
- const lastSeg = segments[segments.length - 1];
428
- // If last segment is a wildcard or ALL_CAPS constant or starts with lowercase, strip it
429
- if (lastSeg === '*' || /^[a-z]/.test(lastSeg) || /^[A-Z_]+$/.test(lastSeg)) {
430
- const classPath = segments.slice(0, -1).join('/');
431
- const classSuffix = classPath + '.java';
432
- if (index) {
433
- return index.get(classSuffix) || index.getInsensitive(classSuffix) || null;
434
- }
435
- // Fallback: linear scan
436
- const fullSuffix = '/' + classSuffix;
437
- for (let i = 0; i < normalizedFileList.length; i++) {
438
- if (normalizedFileList[i].endsWith(fullSuffix) ||
439
- normalizedFileList[i].toLowerCase().endsWith(fullSuffix.toLowerCase())) {
440
- return allFileList[i];
441
- }
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);
442
150
  }
151
+ if (!packageMap.has(filePath))
152
+ packageMap.set(filePath, new Set());
153
+ packageMap.get(filePath).add(result.dirSuffix);
443
154
  }
444
- return null;
445
- }
446
- // ============================================================================
447
- // GO PACKAGE RESOLUTION
448
- // ============================================================================
449
- /**
450
- * Resolve a Go internal package import to all .go files in the package directory.
451
- * Returns an array of file paths.
452
- */
453
- function resolveGoPackage(importPath, goModule, normalizedFileList, allFileList) {
454
- if (!importPath.startsWith(goModule.modulePath))
455
- return [];
456
- // Strip module path to get relative package path
457
- const relativePkg = importPath.slice(goModule.modulePath.length + 1); // e.g., "internal/auth"
458
- if (!relativePkg)
459
- return [];
460
- const pkgSuffix = '/' + relativePkg + '/';
461
- const matches = [];
462
- for (let i = 0; i < normalizedFileList.length; i++) {
463
- const normalized = normalizedFileList[i];
464
- // File must be directly in the package directory (not a subdirectory)
465
- if (normalized.includes(pkgSuffix) && normalized.endsWith('.go') && !normalized.endsWith('_test.go')) {
466
- const afterPkg = normalized.substring(normalized.indexOf(pkgSuffix) + pkgSuffix.length);
467
- if (!afterPkg.includes('/')) {
468
- matches.push(allFileList[i]);
469
- }
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);
470
160
  }
471
- }
472
- return matches;
473
- }
474
- // ============================================================================
475
- // PHP PSR-4 IMPORT RESOLUTION
476
- // ============================================================================
477
- /**
478
- * Resolve a PHP use-statement import path using PSR-4 mappings.
479
- * e.g. "App\Http\Controllers\UserController" -> "app/Http/Controllers/UserController.php"
480
- */
481
- function resolvePhpImport(importPath, composerConfig, allFiles, normalizedFileList, allFileList, index) {
482
- // Normalize: replace backslashes with forward slashes
483
- const normalized = importPath.replace(/\\/g, '/');
484
- // Try PSR-4 resolution if composer.json was found
485
- if (composerConfig) {
486
- // Sort namespaces by length descending (longest match wins)
487
- const sorted = [...composerConfig.psr4.entries()].sort((a, b) => b[0].length - a[0].length);
488
- for (const [nsPrefix, dirPrefix] of sorted) {
489
- const nsPrefixSlash = nsPrefix.replace(/\\/g, '/');
490
- if (normalized.startsWith(nsPrefixSlash + '/') || normalized === nsPrefixSlash) {
491
- const remainder = normalized.slice(nsPrefixSlash.length).replace(/^\//, '');
492
- const filePath = dirPrefix + (remainder ? '/' + remainder : '') + '.php';
493
- if (allFiles.has(filePath))
494
- return filePath;
495
- if (index) {
496
- const result = index.getInsensitive(filePath);
497
- if (result)
498
- return result;
499
- }
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 });
500
169
  }
501
170
  }
502
171
  }
503
- // Fallback: suffix matching (works without composer.json)
504
- const pathParts = normalized.split('/').filter(Boolean);
505
- return suffixResolve(pathParts, normalizedFileList, allFileList, index);
506
172
  }
507
173
  // ============================================================================
508
174
  // MAIN IMPORT PROCESSOR
509
175
  // ============================================================================
510
- 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;
511
180
  // Use allPaths (full repo) when available for cross-chunk resolution, else fall back to chunk files
512
181
  const allFileList = allPaths ?? files.map(f => f.path);
513
182
  const allFilePaths = new Set(allFileList);
514
183
  const parser = await loadParser();
184
+ const logSkipped = isVerboseIngestionEnabled();
185
+ const skippedByLang = logSkipped ? new Map() : null;
515
186
  const resolveCache = new Map();
516
187
  // Pre-compute normalized file list once (forward slashes)
517
188
  const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
@@ -522,11 +193,16 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
522
193
  let totalImportsResolved = 0;
523
194
  // Load language-specific configs once before the file loop
524
195
  const effectiveRoot = repoRoot || '';
525
- const tsconfigPaths = await loadTsconfigPaths(effectiveRoot);
526
- const goModule = await loadGoModulePath(effectiveRoot);
527
- const composerConfig = await loadComposerConfig(effectiveRoot);
528
- // Helper: add an IMPORTS edge + update import map
529
- 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) => {
530
206
  const sourceId = generateId('File', filePath);
531
207
  const targetId = generateId('File', resolvedPath);
532
208
  const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
@@ -539,6 +215,10 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
539
215
  confidence: 1.0,
540
216
  reason: '',
541
217
  });
218
+ };
219
+ // Helper: add an IMPORTS edge + update import map
220
+ const addImportEdge = (filePath, resolvedPath) => {
221
+ addImportGraphEdge(filePath, resolvedPath);
542
222
  if (!importMap.has(filePath)) {
543
223
  importMap.set(filePath, new Set());
544
224
  }
@@ -553,6 +233,12 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
553
233
  const language = getLanguageFromFilename(file.path);
554
234
  if (!language)
555
235
  continue;
236
+ if (!isLanguageAvailable(language)) {
237
+ if (skippedByLang) {
238
+ skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
239
+ }
240
+ continue;
241
+ }
556
242
  const queryStr = LANGUAGE_QUERIES[language];
557
243
  if (!queryStr)
558
244
  continue;
@@ -563,7 +249,7 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
563
249
  let wasReparsed = false;
564
250
  if (!tree) {
565
251
  try {
566
- tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 });
252
+ tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
567
253
  }
568
254
  catch (parseError) {
569
255
  continue;
@@ -606,53 +292,35 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
606
292
  return;
607
293
  }
608
294
  // Clean path (remove quotes and angle brackets for C/C++ includes)
609
- const rawImportPath = sourceNode.text.replace(/['"<>]/g, '');
295
+ const rawImportPath = language === SupportedLanguages.Kotlin
296
+ ? appendKotlinWildcard(sourceNode.text.replace(/['"<>]/g, ''), captureMap['import'])
297
+ : sourceNode.text.replace(/['"<>]/g, '');
610
298
  totalImportsFound++;
611
- // ---- Java: handle wildcards and static imports specially ----
612
- if (language === SupportedLanguages.Java) {
613
- if (rawImportPath.endsWith('.*')) {
614
- const matchedFiles = resolveJavaWildcard(rawImportPath, normalizedFileList, allFileList, index);
615
- for (const matchedFile of matchedFiles) {
616
- addImportEdge(file.path, matchedFile);
617
- }
618
- return; // skip single-file resolution
619
- }
620
- // Try static import resolution (strip member name)
621
- const staticResolved = resolveJavaStaticImport(rawImportPath, normalizedFileList, allFileList, index);
622
- if (staticResolved) {
623
- addImportEdge(file.path, staticResolved);
624
- return;
625
- }
626
- // Fall through to normal resolution for regular Java imports
627
- }
628
- // ---- Go: handle package-level imports ----
629
- if (language === SupportedLanguages.Go && goModule && rawImportPath.startsWith(goModule.modulePath)) {
630
- const pkgFiles = resolveGoPackage(rawImportPath, goModule, normalizedFileList, allFileList);
631
- if (pkgFiles.length > 0) {
632
- for (const pkgFile of pkgFiles) {
633
- addImportEdge(file.path, pkgFile);
634
- }
635
- return; // skip single-file resolution
636
- }
637
- // Fall through if no files found (package might be external)
638
- }
639
- // ---- PHP: handle namespace-based imports (use statements) ----
640
- if (language === SupportedLanguages.PHP) {
641
- const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index);
642
- if (resolved) {
643
- addImportEdge(file.path, resolved);
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);
644
313
  }
645
- return;
646
- }
647
- // ---- Standard single-file resolution ----
648
- const resolvedPath = resolveImportPath(file.path, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
649
- if (resolvedPath) {
650
- addImportEdge(file.path, resolvedPath);
651
314
  }
652
315
  }
653
316
  });
654
317
  // Tree is now owned by the LRU cache — no manual delete needed
655
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
+ }
656
324
  if (isDev) {
657
325
  console.log(`📊 Import processing complete: ${totalImportsResolved}/${totalImportsFound} imports resolved to graph edges`);
658
326
  }
@@ -660,16 +328,25 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
660
328
  // ============================================================================
661
329
  // FAST PATH: Resolve pre-extracted imports (no parsing needed)
662
330
  // ============================================================================
663
- export const processImportsFromExtracted = async (graph, files, extractedImports, importMap, onProgress, repoRoot, prebuiltCtx) => {
664
- const ctx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
665
- 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;
666
337
  let totalImportsFound = 0;
667
338
  let totalImportsResolved = 0;
668
339
  const effectiveRoot = repoRoot || '';
669
- const tsconfigPaths = await loadTsconfigPaths(effectiveRoot);
670
- const goModule = await loadGoModulePath(effectiveRoot);
671
- const composerConfig = await loadComposerConfig(effectiveRoot);
672
- 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) => {
673
350
  const sourceId = generateId('File', filePath);
674
351
  const targetId = generateId('File', resolvedPath);
675
352
  const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
@@ -682,6 +359,9 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
682
359
  confidence: 1.0,
683
360
  reason: '',
684
361
  });
362
+ };
363
+ const addImportEdge = (filePath, resolvedPath) => {
364
+ addImportGraphEdge(filePath, resolvedPath);
685
365
  if (!importMap.has(filePath)) {
686
366
  importMap.set(filePath, new Set());
687
367
  }
@@ -699,76 +379,16 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
699
379
  }
700
380
  const totalFiles = importsByFile.size;
701
381
  let filesProcessed = 0;
702
- // Pre-build a suffix index for O(1) suffix lookups instead of O(n) linear scans
703
- const suffixIndex = new Map();
704
- for (let i = 0; i < normalizedFileList.length; i++) {
705
- const normalized = normalizedFileList[i];
706
- // Index by last path segment (filename) for fast suffix matching
707
- const lastSlash = normalized.lastIndexOf('/');
708
- const filename = lastSlash >= 0 ? normalized.substring(lastSlash + 1) : normalized;
709
- let list = suffixIndex.get(filename);
710
- if (!list) {
711
- list = [];
712
- suffixIndex.set(filename, list);
713
- }
714
- list.push(allFileList[i]);
715
- }
716
382
  for (const [filePath, fileImports] of importsByFile) {
717
383
  filesProcessed++;
718
384
  if (filesProcessed % 100 === 0) {
719
385
  onProgress?.(filesProcessed, totalFiles);
720
386
  await yieldToEventLoop();
721
387
  }
722
- for (const { rawImportPath, language } of fileImports) {
388
+ for (const imp of fileImports) {
723
389
  totalImportsFound++;
724
- // Check resolve cache first
725
- const cacheKey = `${filePath}::${rawImportPath}`;
726
- if (resolveCache.has(cacheKey)) {
727
- const cached = resolveCache.get(cacheKey);
728
- if (cached)
729
- addImportEdge(filePath, cached);
730
- continue;
731
- }
732
- // Java: handle wildcards and static imports
733
- if (language === SupportedLanguages.Java) {
734
- if (rawImportPath.endsWith('.*')) {
735
- const matchedFiles = resolveJavaWildcard(rawImportPath, normalizedFileList, allFileList, index);
736
- for (const matchedFile of matchedFiles) {
737
- addImportEdge(filePath, matchedFile);
738
- }
739
- continue;
740
- }
741
- const staticResolved = resolveJavaStaticImport(rawImportPath, normalizedFileList, allFileList, index);
742
- if (staticResolved) {
743
- resolveCache.set(cacheKey, staticResolved);
744
- addImportEdge(filePath, staticResolved);
745
- continue;
746
- }
747
- }
748
- // Go: handle package-level imports
749
- if (language === SupportedLanguages.Go && goModule && rawImportPath.startsWith(goModule.modulePath)) {
750
- const pkgFiles = resolveGoPackage(rawImportPath, goModule, normalizedFileList, allFileList);
751
- if (pkgFiles.length > 0) {
752
- for (const pkgFile of pkgFiles) {
753
- addImportEdge(filePath, pkgFile);
754
- }
755
- continue;
756
- }
757
- }
758
- // PHP: handle namespace-based imports (use statements)
759
- if (language === SupportedLanguages.PHP) {
760
- const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index);
761
- if (resolved) {
762
- resolveCache.set(cacheKey, resolved);
763
- addImportEdge(filePath, resolved);
764
- }
765
- continue;
766
- }
767
- // Standard resolution (has its own internal cache)
768
- const resolvedPath = resolveImportPath(filePath, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
769
- if (resolvedPath) {
770
- addImportEdge(filePath, resolvedPath);
771
- }
390
+ const result = resolveLanguageImport(filePath, imp.rawImportPath, imp.language, configs, resolveCtx);
391
+ applyImportResult(result, filePath, importMap, packageMap, addImportEdge, addImportGraphEdge, imp.namedBindings, namedImportMap);
772
392
  }
773
393
  }
774
394
  onProgress?.(totalFiles, totalFiles);