gitnexus 1.1.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +50 -59
  2. package/dist/cli/ai-context.js +9 -9
  3. package/dist/cli/analyze.js +139 -47
  4. package/dist/cli/augment.d.ts +13 -0
  5. package/dist/cli/augment.js +33 -0
  6. package/dist/cli/claude-hooks.d.ts +22 -0
  7. package/dist/cli/claude-hooks.js +97 -0
  8. package/dist/cli/eval-server.d.ts +30 -0
  9. package/dist/cli/eval-server.js +372 -0
  10. package/dist/cli/index.js +56 -1
  11. package/dist/cli/mcp.js +9 -0
  12. package/dist/cli/setup.js +184 -5
  13. package/dist/cli/tool.d.ts +37 -0
  14. package/dist/cli/tool.js +91 -0
  15. package/dist/cli/wiki.d.ts +13 -0
  16. package/dist/cli/wiki.js +199 -0
  17. package/dist/core/augmentation/engine.d.ts +26 -0
  18. package/dist/core/augmentation/engine.js +213 -0
  19. package/dist/core/embeddings/embedder.d.ts +2 -2
  20. package/dist/core/embeddings/embedder.js +11 -11
  21. package/dist/core/embeddings/embedding-pipeline.d.ts +2 -1
  22. package/dist/core/embeddings/embedding-pipeline.js +13 -5
  23. package/dist/core/embeddings/types.d.ts +2 -2
  24. package/dist/core/ingestion/call-processor.d.ts +7 -0
  25. package/dist/core/ingestion/call-processor.js +61 -23
  26. package/dist/core/ingestion/community-processor.js +34 -26
  27. package/dist/core/ingestion/filesystem-walker.js +15 -10
  28. package/dist/core/ingestion/heritage-processor.d.ts +6 -0
  29. package/dist/core/ingestion/heritage-processor.js +68 -5
  30. package/dist/core/ingestion/import-processor.d.ts +22 -0
  31. package/dist/core/ingestion/import-processor.js +215 -20
  32. package/dist/core/ingestion/parsing-processor.d.ts +8 -1
  33. package/dist/core/ingestion/parsing-processor.js +66 -25
  34. package/dist/core/ingestion/pipeline.js +104 -40
  35. package/dist/core/ingestion/process-processor.js +1 -1
  36. package/dist/core/ingestion/workers/parse-worker.d.ts +58 -0
  37. package/dist/core/ingestion/workers/parse-worker.js +451 -0
  38. package/dist/core/ingestion/workers/worker-pool.d.ts +22 -0
  39. package/dist/core/ingestion/workers/worker-pool.js +65 -0
  40. package/dist/core/kuzu/kuzu-adapter.d.ts +15 -1
  41. package/dist/core/kuzu/kuzu-adapter.js +177 -63
  42. package/dist/core/kuzu/schema.d.ts +1 -1
  43. package/dist/core/kuzu/schema.js +3 -0
  44. package/dist/core/search/bm25-index.js +13 -15
  45. package/dist/core/wiki/generator.d.ts +96 -0
  46. package/dist/core/wiki/generator.js +674 -0
  47. package/dist/core/wiki/graph-queries.d.ts +80 -0
  48. package/dist/core/wiki/graph-queries.js +238 -0
  49. package/dist/core/wiki/html-viewer.d.ts +10 -0
  50. package/dist/core/wiki/html-viewer.js +297 -0
  51. package/dist/core/wiki/llm-client.d.ts +36 -0
  52. package/dist/core/wiki/llm-client.js +111 -0
  53. package/dist/core/wiki/prompts.d.ts +53 -0
  54. package/dist/core/wiki/prompts.js +174 -0
  55. package/dist/mcp/core/embedder.js +4 -2
  56. package/dist/mcp/core/kuzu-adapter.d.ts +2 -1
  57. package/dist/mcp/core/kuzu-adapter.js +35 -15
  58. package/dist/mcp/local/local-backend.d.ts +54 -1
  59. package/dist/mcp/local/local-backend.js +716 -171
  60. package/dist/mcp/resources.d.ts +1 -1
  61. package/dist/mcp/resources.js +111 -73
  62. package/dist/mcp/server.d.ts +1 -1
  63. package/dist/mcp/server.js +91 -22
  64. package/dist/mcp/tools.js +80 -61
  65. package/dist/storage/git.d.ts +0 -1
  66. package/dist/storage/git.js +1 -8
  67. package/dist/storage/repo-manager.d.ts +17 -0
  68. package/dist/storage/repo-manager.js +26 -0
  69. package/hooks/claude/gitnexus-hook.cjs +135 -0
  70. package/hooks/claude/pre-tool-use.sh +78 -0
  71. package/hooks/claude/session-start.sh +42 -0
  72. package/package.json +4 -2
  73. package/skills/debugging.md +24 -22
  74. package/skills/exploring.md +26 -24
  75. package/skills/impact-analysis.md +19 -13
  76. package/skills/refactoring.md +37 -26
@@ -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>;
@@ -6,7 +6,7 @@ import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
6
6
  import { generateId } from '../../lib/utils.js';
7
7
  import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
8
8
  import { SupportedLanguages } from '../../config/supported-languages.js';
9
- const isDev = process.env.NODE_ENV !== 'production';
9
+ const isDev = process.env.NODE_ENV === 'development';
10
10
  export const createImportMap = () => new Map();
11
11
  /**
12
12
  * Parse tsconfig.json to extract path aliases.
@@ -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>;
@@ -98,40 +98,80 @@ const isNodeExported = (node, name, language) => {
98
98
  return false;
99
99
  }
100
100
  };
101
- export const processParsing = async (graph, files, symbolTable, astCache, onFileProgress) => {
101
+ // ============================================================================
102
+ // Worker-based parallel parsing
103
+ // ============================================================================
104
+ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, workerPool, onFileProgress) => {
105
+ // Filter to parseable files only
106
+ const parseableFiles = [];
107
+ for (const file of files) {
108
+ const lang = getLanguageFromFilename(file.path);
109
+ if (lang) {
110
+ parseableFiles.push({ path: file.path, content: file.content });
111
+ }
112
+ }
113
+ if (parseableFiles.length === 0)
114
+ return { imports: [], calls: [], heritage: [] };
115
+ const total = files.length;
116
+ // Dispatch to worker pool — pool handles splitting into chunks
117
+ // Workers send progress messages during parsing so the bar updates smoothly
118
+ const chunkResults = await workerPool.dispatch(parseableFiles, (filesProcessed) => {
119
+ onFileProgress?.(Math.min(filesProcessed, total), total, 'Parsing...');
120
+ });
121
+ // Merge results from all workers into graph and symbol table
122
+ const allImports = [];
123
+ const allCalls = [];
124
+ const allHeritage = [];
125
+ for (const result of chunkResults) {
126
+ for (const node of result.nodes) {
127
+ graph.addNode({
128
+ id: node.id,
129
+ label: node.label,
130
+ properties: node.properties,
131
+ });
132
+ }
133
+ for (const rel of result.relationships) {
134
+ graph.addRelationship(rel);
135
+ }
136
+ for (const sym of result.symbols) {
137
+ symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type);
138
+ }
139
+ allImports.push(...result.imports);
140
+ allCalls.push(...result.calls);
141
+ allHeritage.push(...result.heritage);
142
+ }
143
+ // Final progress
144
+ onFileProgress?.(total, total, 'done');
145
+ return { imports: allImports, calls: allCalls, heritage: allHeritage };
146
+ };
147
+ // ============================================================================
148
+ // Sequential fallback (original implementation)
149
+ // ============================================================================
150
+ const processParsingSequential = async (graph, files, symbolTable, astCache, onFileProgress) => {
102
151
  const parser = await loadParser();
103
152
  const total = files.length;
104
153
  for (let i = 0; i < files.length; i++) {
105
154
  const file = files[i];
106
- // Report progress for each file
107
155
  onFileProgress?.(i + 1, total, file.path);
108
- // Yield to event loop periodically so spinner can update
109
156
  if (i % 20 === 0)
110
157
  await yieldToEventLoop();
111
158
  const language = getLanguageFromFilename(file.path);
112
159
  if (!language)
113
160
  continue;
114
161
  await loadLanguage(language, file.path);
115
- // 3. Parse the text content into an AST
116
- // Use larger bufferSize for files > 32KB (default limit)
117
162
  let tree;
118
163
  try {
119
164
  tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 });
120
165
  }
121
166
  catch (parseError) {
122
- // Skip files that can't be parsed (binary, encoding issues, etc.)
123
167
  console.warn(`Skipping unparseable file: ${file.path}`);
124
168
  continue;
125
169
  }
126
- // Store in cache immediately (this might evict an old one)
127
170
  astCache.set(file.path, tree);
128
- // 4. Get the specific query string for this language
129
171
  const queryString = LANGUAGE_QUERIES[language];
130
172
  if (!queryString) {
131
173
  continue;
132
174
  }
133
- // 5. Run the query against the AST root node
134
- // This looks for patterns like (function_declaration)
135
175
  let query;
136
176
  let matches;
137
177
  try {
@@ -143,18 +183,14 @@ export const processParsing = async (graph, files, symbolTable, astCache, onFile
143
183
  console.warn(`Query error for ${file.path}:`, queryError);
144
184
  continue;
145
185
  }
146
- // 6. Process every match found
147
186
  matches.forEach(match => {
148
187
  const captureMap = {};
149
188
  match.captures.forEach(c => {
150
189
  captureMap[c.name] = c.node;
151
190
  });
152
- // Skip imports here - they are handled by import-processor.ts
153
- // which creates proper File -> IMPORTS -> File relationships
154
191
  if (captureMap['import']) {
155
192
  return;
156
193
  }
157
- // Skip call expressions - they are handled by call-processor.ts
158
194
  if (captureMap['call']) {
159
195
  return;
160
196
  }
@@ -163,7 +199,6 @@ export const processParsing = async (graph, files, symbolTable, astCache, onFile
163
199
  return;
164
200
  const nodeName = nameNode.text;
165
201
  let nodeLabel = 'CodeElement';
166
- // Core types
167
202
  if (captureMap['definition.function'])
168
203
  nodeLabel = 'Function';
169
204
  else if (captureMap['definition.class'])
@@ -172,18 +207,14 @@ export const processParsing = async (graph, files, symbolTable, astCache, onFile
172
207
  nodeLabel = 'Interface';
173
208
  else if (captureMap['definition.method'])
174
209
  nodeLabel = 'Method';
175
- // Struct types (C, C++, Go, Rust, C#)
176
210
  else if (captureMap['definition.struct'])
177
211
  nodeLabel = 'Struct';
178
- // Enum types
179
212
  else if (captureMap['definition.enum'])
180
213
  nodeLabel = 'Enum';
181
- // Namespace/Module (C++, C#, Rust)
182
214
  else if (captureMap['definition.namespace'])
183
215
  nodeLabel = 'Namespace';
184
216
  else if (captureMap['definition.module'])
185
217
  nodeLabel = 'Module';
186
- // Rust-specific
187
218
  else if (captureMap['definition.trait'])
188
219
  nodeLabel = 'Trait';
189
220
  else if (captureMap['definition.impl'])
@@ -194,26 +225,22 @@ export const processParsing = async (graph, files, symbolTable, astCache, onFile
194
225
  nodeLabel = 'Const';
195
226
  else if (captureMap['definition.static'])
196
227
  nodeLabel = 'Static';
197
- // C-specific
198
228
  else if (captureMap['definition.typedef'])
199
229
  nodeLabel = 'Typedef';
200
230
  else if (captureMap['definition.macro'])
201
231
  nodeLabel = 'Macro';
202
232
  else if (captureMap['definition.union'])
203
233
  nodeLabel = 'Union';
204
- // C#-specific
205
234
  else if (captureMap['definition.property'])
206
235
  nodeLabel = 'Property';
207
236
  else if (captureMap['definition.record'])
208
237
  nodeLabel = 'Record';
209
238
  else if (captureMap['definition.delegate'])
210
239
  nodeLabel = 'Delegate';
211
- // Java-specific
212
240
  else if (captureMap['definition.annotation'])
213
241
  nodeLabel = 'Annotation';
214
242
  else if (captureMap['definition.constructor'])
215
243
  nodeLabel = 'Constructor';
216
- // C++ template
217
244
  else if (captureMap['definition.template'])
218
245
  nodeLabel = 'Template';
219
246
  const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`);
@@ -230,7 +257,6 @@ export const processParsing = async (graph, files, symbolTable, astCache, onFile
230
257
  }
231
258
  };
232
259
  graph.addNode(node);
233
- // Register in Symbol Table (only definitions, not imports)
234
260
  symbolTable.add(file.path, nodeName, nodeId, nodeLabel);
235
261
  const fileId = generateId('File', file.path);
236
262
  const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
@@ -244,6 +270,21 @@ export const processParsing = async (graph, files, symbolTable, astCache, onFile
244
270
  };
245
271
  graph.addRelationship(relationship);
246
272
  });
247
- // Don't delete tree here - LRU cache handles cleanup when evicted
248
273
  }
249
274
  };
275
+ // ============================================================================
276
+ // Public API
277
+ // ============================================================================
278
+ export const processParsing = async (graph, files, symbolTable, astCache, onFileProgress, workerPool) => {
279
+ if (workerPool) {
280
+ try {
281
+ return await processParsingWithWorkers(graph, files, symbolTable, astCache, workerPool, onFileProgress);
282
+ }
283
+ catch (err) {
284
+ console.warn('Worker pool parsing failed, falling back to sequential:', err);
285
+ }
286
+ }
287
+ // Fallback: sequential parsing (no pre-extracted data)
288
+ await processParsingSequential(graph, files, symbolTable, astCache, onFileProgress);
289
+ return null;
290
+ };