gitnexus 1.1.9 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +50 -59
  2. package/dist/cli/analyze.js +114 -32
  3. package/dist/cli/eval-server.d.ts +30 -0
  4. package/dist/cli/eval-server.js +372 -0
  5. package/dist/cli/index.js +52 -1
  6. package/dist/cli/mcp.js +9 -0
  7. package/dist/cli/setup.js +44 -7
  8. package/dist/cli/tool.d.ts +37 -0
  9. package/dist/cli/tool.js +91 -0
  10. package/dist/cli/wiki.d.ts +14 -0
  11. package/dist/cli/wiki.js +275 -0
  12. package/dist/core/embeddings/embedder.d.ts +2 -2
  13. package/dist/core/embeddings/embedder.js +10 -10
  14. package/dist/core/embeddings/embedding-pipeline.d.ts +2 -1
  15. package/dist/core/embeddings/embedding-pipeline.js +12 -4
  16. package/dist/core/embeddings/types.d.ts +2 -2
  17. package/dist/core/ingestion/call-processor.d.ts +7 -0
  18. package/dist/core/ingestion/call-processor.js +61 -23
  19. package/dist/core/ingestion/community-processor.js +34 -26
  20. package/dist/core/ingestion/filesystem-walker.js +15 -10
  21. package/dist/core/ingestion/heritage-processor.d.ts +6 -0
  22. package/dist/core/ingestion/heritage-processor.js +68 -5
  23. package/dist/core/ingestion/import-processor.d.ts +22 -0
  24. package/dist/core/ingestion/import-processor.js +214 -19
  25. package/dist/core/ingestion/parsing-processor.d.ts +8 -1
  26. package/dist/core/ingestion/parsing-processor.js +66 -25
  27. package/dist/core/ingestion/pipeline.js +103 -39
  28. package/dist/core/ingestion/workers/parse-worker.d.ts +58 -0
  29. package/dist/core/ingestion/workers/parse-worker.js +451 -0
  30. package/dist/core/ingestion/workers/worker-pool.d.ts +22 -0
  31. package/dist/core/ingestion/workers/worker-pool.js +65 -0
  32. package/dist/core/kuzu/kuzu-adapter.d.ts +15 -1
  33. package/dist/core/kuzu/kuzu-adapter.js +177 -67
  34. package/dist/core/kuzu/schema.d.ts +1 -1
  35. package/dist/core/kuzu/schema.js +3 -0
  36. package/dist/core/wiki/generator.d.ts +97 -0
  37. package/dist/core/wiki/generator.js +683 -0
  38. package/dist/core/wiki/graph-queries.d.ts +80 -0
  39. package/dist/core/wiki/graph-queries.js +238 -0
  40. package/dist/core/wiki/html-viewer.d.ts +10 -0
  41. package/dist/core/wiki/html-viewer.js +297 -0
  42. package/dist/core/wiki/llm-client.d.ts +36 -0
  43. package/dist/core/wiki/llm-client.js +111 -0
  44. package/dist/core/wiki/prompts.d.ts +53 -0
  45. package/dist/core/wiki/prompts.js +174 -0
  46. package/dist/mcp/core/embedder.js +4 -2
  47. package/dist/mcp/core/kuzu-adapter.d.ts +2 -1
  48. package/dist/mcp/core/kuzu-adapter.js +35 -15
  49. package/dist/mcp/local/local-backend.js +9 -2
  50. package/dist/mcp/server.js +1 -1
  51. package/dist/storage/git.d.ts +0 -1
  52. package/dist/storage/git.js +1 -8
  53. package/dist/storage/repo-manager.d.ts +17 -0
  54. package/dist/storage/repo-manager.js +26 -0
  55. package/package.json +1 -1
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { glob } from 'glob';
4
4
  import { shouldIgnorePath } from '../../config/ignore-service.js';
5
+ const READ_CONCURRENCY = 32;
5
6
  export const walkRepository = async (repoPath, onProgress) => {
6
7
  const files = await glob('**/*', {
7
8
  cwd: repoPath,
@@ -10,16 +11,20 @@ export const walkRepository = async (repoPath, onProgress) => {
10
11
  });
11
12
  const filtered = files.filter(file => !shouldIgnorePath(file));
12
13
  const entries = [];
13
- for (let i = 0; i < filtered.length; i++) {
14
- const relativePath = filtered[i];
15
- const fullPath = path.join(repoPath, relativePath);
16
- try {
17
- const content = await fs.readFile(fullPath, 'utf-8');
18
- entries.push({ path: relativePath.replace(/\\/g, '/'), content });
19
- onProgress?.(i + 1, filtered.length, relativePath);
20
- }
21
- catch {
22
- onProgress?.(i + 1, filtered.length, relativePath);
14
+ let processed = 0;
15
+ for (let start = 0; start < filtered.length; start += READ_CONCURRENCY) {
16
+ const batch = filtered.slice(start, start + READ_CONCURRENCY);
17
+ const results = await Promise.allSettled(batch.map(relativePath => fs.readFile(path.join(repoPath, relativePath), 'utf-8')
18
+ .then(content => ({ path: relativePath.replace(/\\/g, '/'), content }))));
19
+ for (const result of results) {
20
+ processed++;
21
+ if (result.status === 'fulfilled') {
22
+ entries.push(result.value);
23
+ onProgress?.(processed, filtered.length, result.value.path);
24
+ }
25
+ else {
26
+ onProgress?.(processed, filtered.length, batch[results.indexOf(result)]);
27
+ }
23
28
  }
24
29
  }
25
30
  return entries;
@@ -8,7 +8,13 @@
8
8
  import { KnowledgeGraph } from '../graph/types.js';
9
9
  import { ASTCache } from './ast-cache.js';
10
10
  import { SymbolTable } from './symbol-table.js';
11
+ import type { ExtractedHeritage } from './workers/parse-worker.js';
11
12
  export declare const processHeritage: (graph: KnowledgeGraph, files: {
12
13
  path: string;
13
14
  content: string;
14
15
  }[], astCache: ASTCache, symbolTable: SymbolTable, onProgress?: (current: number, total: number) => void) => Promise<void>;
16
+ /**
17
+ * Fast path: resolve pre-extracted heritage from workers.
18
+ * No AST parsing — workers already extracted className + parentName + kind.
19
+ */
20
+ export declare const processHeritageFromExtracted: (graph: KnowledgeGraph, extractedHeritage: ExtractedHeritage[], symbolTable: SymbolTable, onProgress?: (current: number, total: number) => void) => Promise<void>;
@@ -39,6 +39,8 @@ export const processHeritage = async (graph, files, astCache, symbolTable, onPro
39
39
  continue;
40
40
  }
41
41
  wasReparsed = true;
42
+ // Cache re-parsed tree for potential future use
43
+ astCache.set(file.path, tree);
42
44
  }
43
45
  let query;
44
46
  let matches;
@@ -49,8 +51,6 @@ export const processHeritage = async (graph, files, astCache, symbolTable, onPro
49
51
  }
50
52
  catch (queryError) {
51
53
  console.warn(`Heritage query error for ${file.path}:`, queryError);
52
- if (wasReparsed)
53
- tree.delete?.();
54
54
  continue;
55
55
  }
56
56
  // 4. Process heritage matches
@@ -126,9 +126,72 @@ export const processHeritage = async (graph, files, astCache, symbolTable, onPro
126
126
  }
127
127
  }
128
128
  });
129
- // Cleanup
130
- if (wasReparsed) {
131
- tree.delete?.();
129
+ // Tree is now owned by the LRU cache — no manual delete needed
130
+ }
131
+ };
132
+ /**
133
+ * Fast path: resolve pre-extracted heritage from workers.
134
+ * No AST parsing — workers already extracted className + parentName + kind.
135
+ */
136
+ export const processHeritageFromExtracted = async (graph, extractedHeritage, symbolTable, onProgress) => {
137
+ const total = extractedHeritage.length;
138
+ for (let i = 0; i < extractedHeritage.length; i++) {
139
+ if (i % 500 === 0) {
140
+ onProgress?.(i, total);
141
+ await yieldToEventLoop();
142
+ }
143
+ const h = extractedHeritage[i];
144
+ if (h.kind === 'extends') {
145
+ const childId = symbolTable.lookupExact(h.filePath, h.className) ||
146
+ symbolTable.lookupFuzzy(h.className)[0]?.nodeId ||
147
+ generateId('Class', `${h.filePath}:${h.className}`);
148
+ const parentId = symbolTable.lookupFuzzy(h.parentName)[0]?.nodeId ||
149
+ generateId('Class', `${h.parentName}`);
150
+ if (childId && parentId && childId !== parentId) {
151
+ graph.addRelationship({
152
+ id: generateId('EXTENDS', `${childId}->${parentId}`),
153
+ sourceId: childId,
154
+ targetId: parentId,
155
+ type: 'EXTENDS',
156
+ confidence: 1.0,
157
+ reason: '',
158
+ });
159
+ }
160
+ }
161
+ else if (h.kind === 'implements') {
162
+ const classId = symbolTable.lookupExact(h.filePath, h.className) ||
163
+ symbolTable.lookupFuzzy(h.className)[0]?.nodeId ||
164
+ generateId('Class', `${h.filePath}:${h.className}`);
165
+ const interfaceId = symbolTable.lookupFuzzy(h.parentName)[0]?.nodeId ||
166
+ generateId('Interface', `${h.parentName}`);
167
+ if (classId && interfaceId) {
168
+ graph.addRelationship({
169
+ id: generateId('IMPLEMENTS', `${classId}->${interfaceId}`),
170
+ sourceId: classId,
171
+ targetId: interfaceId,
172
+ type: 'IMPLEMENTS',
173
+ confidence: 1.0,
174
+ reason: '',
175
+ });
176
+ }
177
+ }
178
+ else if (h.kind === 'trait-impl') {
179
+ const structId = symbolTable.lookupExact(h.filePath, h.className) ||
180
+ symbolTable.lookupFuzzy(h.className)[0]?.nodeId ||
181
+ generateId('Struct', `${h.filePath}:${h.className}`);
182
+ const traitId = symbolTable.lookupFuzzy(h.parentName)[0]?.nodeId ||
183
+ generateId('Trait', `${h.parentName}`);
184
+ if (structId && traitId) {
185
+ graph.addRelationship({
186
+ id: generateId('IMPLEMENTS', `${structId}->${traitId}`),
187
+ sourceId: structId,
188
+ targetId: traitId,
189
+ type: 'IMPLEMENTS',
190
+ confidence: 1.0,
191
+ reason: 'trait-impl',
192
+ });
193
+ }
132
194
  }
133
195
  }
196
+ onProgress?.(total, total);
134
197
  };
@@ -1,8 +1,30 @@
1
1
  import { KnowledgeGraph } from '../graph/types.js';
2
2
  import { ASTCache } from './ast-cache.js';
3
+ import type { ExtractedImport } from './workers/parse-worker.js';
3
4
  export type ImportMap = Map<string, Set<string>>;
4
5
  export declare const createImportMap: () => ImportMap;
6
+ /**
7
+ * Build a suffix index for O(1) endsWith lookups.
8
+ * Maps every possible path suffix to its original file path.
9
+ * e.g. for "src/com/example/Foo.java":
10
+ * "Foo.java" -> "src/com/example/Foo.java"
11
+ * "example/Foo.java" -> "src/com/example/Foo.java"
12
+ * "com/example/Foo.java" -> "src/com/example/Foo.java"
13
+ * etc.
14
+ */
15
+ export interface SuffixIndex {
16
+ /** Exact suffix lookup (case-sensitive) */
17
+ get(suffix: string): string | undefined;
18
+ /** Case-insensitive suffix lookup */
19
+ getInsensitive(suffix: string): string | undefined;
20
+ /** Get all files in a directory suffix */
21
+ getFilesInDir(dirSuffix: string, extension: string): string[];
22
+ }
5
23
  export declare const processImports: (graph: KnowledgeGraph, files: {
6
24
  path: string;
7
25
  content: string;
8
26
  }[], astCache: ASTCache, importMap: ImportMap, onProgress?: (current: number, total: number) => void, repoRoot?: string) => Promise<void>;
27
+ export declare const processImportsFromExtracted: (graph: KnowledgeGraph, files: {
28
+ path: string;
29
+ content: string;
30
+ }[], extractedImports: ExtractedImport[], importMap: ImportMap, onProgress?: (current: number, total: number) => void, repoRoot?: string) => Promise<void>;
@@ -101,11 +101,73 @@ function tryResolveWithExtensions(basePath, allFiles) {
101
101
  }
102
102
  return null;
103
103
  }
104
+ function buildSuffixIndex(normalizedFileList, allFileList) {
105
+ // Map: normalized suffix -> original file path
106
+ const exactMap = new Map();
107
+ // Map: lowercase suffix -> original file path
108
+ const lowerMap = new Map();
109
+ // Map: directory suffix -> list of file paths in that directory
110
+ const dirMap = new Map();
111
+ for (let i = 0; i < normalizedFileList.length; i++) {
112
+ const normalized = normalizedFileList[i];
113
+ const original = allFileList[i];
114
+ const parts = normalized.split('/');
115
+ // Index all suffixes: "a/b/c.java" -> ["c.java", "b/c.java", "a/b/c.java"]
116
+ for (let j = parts.length - 1; j >= 0; j--) {
117
+ const suffix = parts.slice(j).join('/');
118
+ // Only store first match (longest path wins for ambiguous suffixes)
119
+ if (!exactMap.has(suffix)) {
120
+ exactMap.set(suffix, original);
121
+ }
122
+ const lower = suffix.toLowerCase();
123
+ if (!lowerMap.has(lower)) {
124
+ lowerMap.set(lower, original);
125
+ }
126
+ }
127
+ // Index directory membership
128
+ const lastSlash = normalized.lastIndexOf('/');
129
+ if (lastSlash >= 0) {
130
+ // Build all directory suffixes
131
+ const dirParts = parts.slice(0, -1);
132
+ const fileName = parts[parts.length - 1];
133
+ const ext = fileName.substring(fileName.lastIndexOf('.'));
134
+ for (let j = dirParts.length - 1; j >= 0; j--) {
135
+ const dirSuffix = dirParts.slice(j).join('/');
136
+ const key = `${dirSuffix}:${ext}`;
137
+ let list = dirMap.get(key);
138
+ if (!list) {
139
+ list = [];
140
+ dirMap.set(key, list);
141
+ }
142
+ list.push(original);
143
+ }
144
+ }
145
+ }
146
+ return {
147
+ get: (suffix) => exactMap.get(suffix),
148
+ getInsensitive: (suffix) => lowerMap.get(suffix.toLowerCase()),
149
+ getFilesInDir: (dirSuffix, extension) => {
150
+ return dirMap.get(`${dirSuffix}:${extension}`) || [];
151
+ },
152
+ };
153
+ }
104
154
  /**
105
- * Suffix-based resolution: try progressively shorter suffixes against all files.
106
- * Used for package-style imports (Java, Python, etc.).
155
+ * Suffix-based resolution using index. O(1) per lookup instead of O(files).
107
156
  */
108
- function suffixResolve(pathParts, normalizedFileList, allFileList) {
157
+ function suffixResolve(pathParts, normalizedFileList, allFileList, index) {
158
+ if (index) {
159
+ for (let i = 0; i < pathParts.length; i++) {
160
+ const suffix = pathParts.slice(i).join('/');
161
+ for (const ext of EXTENSIONS) {
162
+ const suffixWithExt = suffix + ext;
163
+ const result = index.get(suffixWithExt) || index.getInsensitive(suffixWithExt);
164
+ if (result)
165
+ return result;
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+ // Fallback: linear scan (for backward compatibility)
109
171
  for (let i = 0; i < pathParts.length; i++) {
110
172
  const suffix = pathParts.slice(i).join('/');
111
173
  for (const ext of EXTENSIONS) {
@@ -129,7 +191,7 @@ function suffixResolve(pathParts, normalizedFileList, allFileList) {
129
191
  * Java wildcards and Go package imports are handled separately in processImports
130
192
  * because they resolve to multiple files.
131
193
  */
132
- const resolveImportPath = (currentFile, importPath, allFiles, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths) => {
194
+ const resolveImportPath = (currentFile, importPath, allFiles, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index) => {
133
195
  const cacheKey = `${currentFile}::${importPath}`;
134
196
  if (resolveCache.has(cacheKey))
135
197
  return resolveCache.get(cacheKey) ?? null;
@@ -154,7 +216,7 @@ const resolveImportPath = (currentFile, importPath, allFiles, allFileList, norma
154
216
  return cache(resolved);
155
217
  // Try suffix matching as fallback
156
218
  const parts = rewritten.split('/').filter(Boolean);
157
- const suffixResult = suffixResolve(parts, normalizedFileList, allFileList);
219
+ const suffixResult = suffixResolve(parts, normalizedFileList, allFileList, index);
158
220
  if (suffixResult)
159
221
  return cache(suffixResult);
160
222
  }
@@ -194,7 +256,7 @@ const resolveImportPath = (currentFile, importPath, allFiles, allFileList, norma
194
256
  ? importPath
195
257
  : importPath.replace(/\./g, '/');
196
258
  const pathParts = pathLike.split('/').filter(Boolean);
197
- const resolved = suffixResolve(pathParts, normalizedFileList, allFileList);
259
+ const resolved = suffixResolve(pathParts, normalizedFileList, allFileList, index);
198
260
  return cache(resolved);
199
261
  };
200
262
  // ============================================================================
@@ -276,15 +338,29 @@ function tryRustModulePath(modulePath, allFiles) {
276
338
  * Resolve a Java wildcard import (com.example.*) to all matching .java files.
277
339
  * Returns an array of file paths.
278
340
  */
279
- function resolveJavaWildcard(importPath, normalizedFileList, allFileList) {
341
+ function resolveJavaWildcard(importPath, normalizedFileList, allFileList, index) {
280
342
  // "com.example.util.*" -> "com/example/util"
281
343
  const packagePath = importPath.slice(0, -2).replace(/\./g, '/');
344
+ if (index) {
345
+ // Use directory index: get all .java files in this package directory
346
+ const candidates = index.getFilesInDir(packagePath, '.java');
347
+ // Filter to only direct children (no subdirectories)
348
+ const packageSuffix = '/' + packagePath + '/';
349
+ return candidates.filter(f => {
350
+ const normalized = f.replace(/\\/g, '/');
351
+ const idx = normalized.indexOf(packageSuffix);
352
+ if (idx < 0)
353
+ return false;
354
+ const afterPkg = normalized.substring(idx + packageSuffix.length);
355
+ return !afterPkg.includes('/');
356
+ });
357
+ }
358
+ // Fallback: linear scan
282
359
  const packageSuffix = '/' + packagePath + '/';
283
360
  const matches = [];
284
361
  for (let i = 0; i < normalizedFileList.length; i++) {
285
362
  const normalized = normalizedFileList[i];
286
363
  if (normalized.includes(packageSuffix) && normalized.endsWith('.java')) {
287
- // Ensure the file is directly in the package (not a subdirectory)
288
364
  const afterPackage = normalized.substring(normalized.indexOf(packageSuffix) + packageSuffix.length);
289
365
  if (!afterPackage.includes('/')) {
290
366
  matches.push(allFileList[i]);
@@ -297,7 +373,7 @@ function resolveJavaWildcard(importPath, normalizedFileList, allFileList) {
297
373
  * Try to resolve a Java static import by stripping the member name.
298
374
  * "com.example.Constants.VALUE" -> resolve "com.example.Constants"
299
375
  */
300
- function resolveJavaStaticImport(importPath, normalizedFileList, allFileList) {
376
+ function resolveJavaStaticImport(importPath, normalizedFileList, allFileList, index) {
301
377
  // Static imports look like: com.example.Constants.VALUE or com.example.Constants.*
302
378
  // The last segment is a member name (field/method) if it starts with lowercase or is ALL_CAPS
303
379
  const segments = importPath.split('.');
@@ -307,10 +383,15 @@ function resolveJavaStaticImport(importPath, normalizedFileList, allFileList) {
307
383
  // If last segment is a wildcard or ALL_CAPS constant or starts with lowercase, strip it
308
384
  if (lastSeg === '*' || /^[a-z]/.test(lastSeg) || /^[A-Z_]+$/.test(lastSeg)) {
309
385
  const classPath = segments.slice(0, -1).join('/');
310
- const classSuffix = '/' + classPath + '.java';
386
+ const classSuffix = classPath + '.java';
387
+ if (index) {
388
+ return index.get(classSuffix) || index.getInsensitive(classSuffix) || null;
389
+ }
390
+ // Fallback: linear scan
391
+ const fullSuffix = '/' + classSuffix;
311
392
  for (let i = 0; i < normalizedFileList.length; i++) {
312
- if (normalizedFileList[i].endsWith(classSuffix) ||
313
- normalizedFileList[i].toLowerCase().endsWith(classSuffix.toLowerCase())) {
393
+ if (normalizedFileList[i].endsWith(fullSuffix) ||
394
+ normalizedFileList[i].toLowerCase().endsWith(fullSuffix.toLowerCase())) {
314
395
  return allFileList[i];
315
396
  }
316
397
  }
@@ -356,6 +437,8 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
356
437
  const allFileList = files.map(f => f.path);
357
438
  // Pre-compute normalized file list once (forward slashes)
358
439
  const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
440
+ // Build suffix index for O(1) lookups
441
+ const index = buildSuffixIndex(normalizedFileList, allFileList);
359
442
  // Track import statistics
360
443
  let totalImportsFound = 0;
361
444
  let totalImportsResolved = 0;
@@ -407,6 +490,8 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
407
490
  continue;
408
491
  }
409
492
  wasReparsed = true;
493
+ // Cache re-parsed tree so call/heritage phases get hits
494
+ astCache.set(file.path, tree);
410
495
  }
411
496
  let query;
412
497
  let matches;
@@ -447,14 +532,14 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
447
532
  // ---- Java: handle wildcards and static imports specially ----
448
533
  if (language === SupportedLanguages.Java) {
449
534
  if (rawImportPath.endsWith('.*')) {
450
- const matchedFiles = resolveJavaWildcard(rawImportPath, normalizedFileList, allFileList);
535
+ const matchedFiles = resolveJavaWildcard(rawImportPath, normalizedFileList, allFileList, index);
451
536
  for (const matchedFile of matchedFiles) {
452
537
  addImportEdge(file.path, matchedFile);
453
538
  }
454
539
  return; // skip single-file resolution
455
540
  }
456
541
  // Try static import resolution (strip member name)
457
- const staticResolved = resolveJavaStaticImport(rawImportPath, normalizedFileList, allFileList);
542
+ const staticResolved = resolveJavaStaticImport(rawImportPath, normalizedFileList, allFileList, index);
458
543
  if (staticResolved) {
459
544
  addImportEdge(file.path, staticResolved);
460
545
  return;
@@ -473,18 +558,128 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
473
558
  // Fall through if no files found (package might be external)
474
559
  }
475
560
  // ---- Standard single-file resolution ----
476
- const resolvedPath = resolveImportPath(file.path, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths);
561
+ const resolvedPath = resolveImportPath(file.path, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
477
562
  if (resolvedPath) {
478
563
  addImportEdge(file.path, resolvedPath);
479
564
  }
480
565
  }
481
566
  });
482
- // If re-parsed just for this, delete the tree to save memory
483
- if (wasReparsed) {
484
- tree.delete?.();
485
- }
567
+ // Tree is now owned by the LRU cache no manual delete needed
486
568
  }
487
569
  if (isDev) {
488
570
  console.log(`📊 Import processing complete: ${totalImportsResolved}/${totalImportsFound} imports resolved to graph edges`);
489
571
  }
490
572
  };
573
+ // ============================================================================
574
+ // FAST PATH: Resolve pre-extracted imports (no parsing needed)
575
+ // ============================================================================
576
+ export const processImportsFromExtracted = async (graph, files, extractedImports, importMap, onProgress, repoRoot) => {
577
+ const allFilePaths = new Set(files.map(f => f.path));
578
+ const resolveCache = new Map();
579
+ const allFileList = files.map(f => f.path);
580
+ const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
581
+ // Build suffix index for O(1) lookups
582
+ const index = buildSuffixIndex(normalizedFileList, allFileList);
583
+ let totalImportsFound = 0;
584
+ let totalImportsResolved = 0;
585
+ const effectiveRoot = repoRoot || '';
586
+ const tsconfigPaths = await loadTsconfigPaths(effectiveRoot);
587
+ const goModule = await loadGoModulePath(effectiveRoot);
588
+ const addImportEdge = (filePath, resolvedPath) => {
589
+ const sourceId = generateId('File', filePath);
590
+ const targetId = generateId('File', resolvedPath);
591
+ const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
592
+ totalImportsResolved++;
593
+ graph.addRelationship({
594
+ id: relId,
595
+ sourceId,
596
+ targetId,
597
+ type: 'IMPORTS',
598
+ confidence: 1.0,
599
+ reason: '',
600
+ });
601
+ if (!importMap.has(filePath)) {
602
+ importMap.set(filePath, new Set());
603
+ }
604
+ importMap.get(filePath).add(resolvedPath);
605
+ };
606
+ // Group by file for progress reporting (users see file count, not import count)
607
+ const importsByFile = new Map();
608
+ for (const imp of extractedImports) {
609
+ let list = importsByFile.get(imp.filePath);
610
+ if (!list) {
611
+ list = [];
612
+ importsByFile.set(imp.filePath, list);
613
+ }
614
+ list.push(imp);
615
+ }
616
+ const totalFiles = importsByFile.size;
617
+ let filesProcessed = 0;
618
+ // Pre-build a suffix index for O(1) suffix lookups instead of O(n) linear scans
619
+ const suffixIndex = new Map();
620
+ for (let i = 0; i < normalizedFileList.length; i++) {
621
+ const normalized = normalizedFileList[i];
622
+ // Index by last path segment (filename) for fast suffix matching
623
+ const lastSlash = normalized.lastIndexOf('/');
624
+ const filename = lastSlash >= 0 ? normalized.substring(lastSlash + 1) : normalized;
625
+ let list = suffixIndex.get(filename);
626
+ if (!list) {
627
+ list = [];
628
+ suffixIndex.set(filename, list);
629
+ }
630
+ list.push(allFileList[i]);
631
+ }
632
+ for (const [filePath, fileImports] of importsByFile) {
633
+ filesProcessed++;
634
+ if (filesProcessed % 100 === 0) {
635
+ onProgress?.(filesProcessed, totalFiles);
636
+ await yieldToEventLoop();
637
+ }
638
+ for (const { rawImportPath, language } of fileImports) {
639
+ totalImportsFound++;
640
+ // Check resolve cache first
641
+ const cacheKey = `${filePath}::${rawImportPath}`;
642
+ if (resolveCache.has(cacheKey)) {
643
+ const cached = resolveCache.get(cacheKey);
644
+ if (cached)
645
+ addImportEdge(filePath, cached);
646
+ continue;
647
+ }
648
+ // Java: handle wildcards and static imports
649
+ if (language === SupportedLanguages.Java) {
650
+ if (rawImportPath.endsWith('.*')) {
651
+ const matchedFiles = resolveJavaWildcard(rawImportPath, normalizedFileList, allFileList, index);
652
+ for (const matchedFile of matchedFiles) {
653
+ addImportEdge(filePath, matchedFile);
654
+ }
655
+ continue;
656
+ }
657
+ const staticResolved = resolveJavaStaticImport(rawImportPath, normalizedFileList, allFileList, index);
658
+ if (staticResolved) {
659
+ resolveCache.set(cacheKey, staticResolved);
660
+ addImportEdge(filePath, staticResolved);
661
+ continue;
662
+ }
663
+ }
664
+ // Go: handle package-level imports
665
+ if (language === SupportedLanguages.Go && goModule && rawImportPath.startsWith(goModule.modulePath)) {
666
+ const pkgFiles = resolveGoPackage(rawImportPath, goModule, normalizedFileList, allFileList);
667
+ if (pkgFiles.length > 0) {
668
+ for (const pkgFile of pkgFiles) {
669
+ addImportEdge(filePath, pkgFile);
670
+ }
671
+ continue;
672
+ }
673
+ }
674
+ // Standard resolution (has its own internal cache)
675
+ const resolvedPath = resolveImportPath(filePath, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
676
+ if (resolvedPath) {
677
+ addImportEdge(filePath, resolvedPath);
678
+ }
679
+ }
680
+ }
681
+ onProgress?.(totalFiles, totalFiles);
682
+ if (isDev) {
683
+ console.log(`📊 Import processing (fast path): ${totalImportsResolved}/${totalImportsFound} imports resolved to graph edges`);
684
+ }
685
+ };
@@ -1,8 +1,15 @@
1
1
  import { KnowledgeGraph } from '../graph/types.js';
2
2
  import { SymbolTable } from './symbol-table.js';
3
3
  import { ASTCache } from './ast-cache.js';
4
+ import { WorkerPool } from './workers/worker-pool.js';
5
+ import type { ExtractedImport, ExtractedCall, ExtractedHeritage } from './workers/parse-worker.js';
4
6
  export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
7
+ export interface WorkerExtractedData {
8
+ imports: ExtractedImport[];
9
+ calls: ExtractedCall[];
10
+ heritage: ExtractedHeritage[];
11
+ }
5
12
  export declare const processParsing: (graph: KnowledgeGraph, files: {
6
13
  path: string;
7
14
  content: string;
8
- }[], symbolTable: SymbolTable, astCache: ASTCache, onFileProgress?: FileProgressCallback) => Promise<void>;
15
+ }[], symbolTable: SymbolTable, astCache: ASTCache, onFileProgress?: FileProgressCallback, workerPool?: WorkerPool) => Promise<WorkerExtractedData | null>;