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.
- package/README.md +50 -59
- package/dist/cli/ai-context.js +9 -9
- package/dist/cli/analyze.js +139 -47
- package/dist/cli/augment.d.ts +13 -0
- package/dist/cli/augment.js +33 -0
- package/dist/cli/claude-hooks.d.ts +22 -0
- package/dist/cli/claude-hooks.js +97 -0
- package/dist/cli/eval-server.d.ts +30 -0
- package/dist/cli/eval-server.js +372 -0
- package/dist/cli/index.js +56 -1
- package/dist/cli/mcp.js +9 -0
- package/dist/cli/setup.js +184 -5
- package/dist/cli/tool.d.ts +37 -0
- package/dist/cli/tool.js +91 -0
- package/dist/cli/wiki.d.ts +13 -0
- package/dist/cli/wiki.js +199 -0
- package/dist/core/augmentation/engine.d.ts +26 -0
- package/dist/core/augmentation/engine.js +213 -0
- package/dist/core/embeddings/embedder.d.ts +2 -2
- package/dist/core/embeddings/embedder.js +11 -11
- package/dist/core/embeddings/embedding-pipeline.d.ts +2 -1
- package/dist/core/embeddings/embedding-pipeline.js +13 -5
- package/dist/core/embeddings/types.d.ts +2 -2
- package/dist/core/ingestion/call-processor.d.ts +7 -0
- package/dist/core/ingestion/call-processor.js +61 -23
- package/dist/core/ingestion/community-processor.js +34 -26
- package/dist/core/ingestion/filesystem-walker.js +15 -10
- package/dist/core/ingestion/heritage-processor.d.ts +6 -0
- package/dist/core/ingestion/heritage-processor.js +68 -5
- package/dist/core/ingestion/import-processor.d.ts +22 -0
- package/dist/core/ingestion/import-processor.js +215 -20
- package/dist/core/ingestion/parsing-processor.d.ts +8 -1
- package/dist/core/ingestion/parsing-processor.js +66 -25
- package/dist/core/ingestion/pipeline.js +104 -40
- package/dist/core/ingestion/process-processor.js +1 -1
- package/dist/core/ingestion/workers/parse-worker.d.ts +58 -0
- package/dist/core/ingestion/workers/parse-worker.js +451 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +22 -0
- package/dist/core/ingestion/workers/worker-pool.js +65 -0
- package/dist/core/kuzu/kuzu-adapter.d.ts +15 -1
- package/dist/core/kuzu/kuzu-adapter.js +177 -63
- package/dist/core/kuzu/schema.d.ts +1 -1
- package/dist/core/kuzu/schema.js +3 -0
- package/dist/core/search/bm25-index.js +13 -15
- package/dist/core/wiki/generator.d.ts +96 -0
- package/dist/core/wiki/generator.js +674 -0
- package/dist/core/wiki/graph-queries.d.ts +80 -0
- package/dist/core/wiki/graph-queries.js +238 -0
- package/dist/core/wiki/html-viewer.d.ts +10 -0
- package/dist/core/wiki/html-viewer.js +297 -0
- package/dist/core/wiki/llm-client.d.ts +36 -0
- package/dist/core/wiki/llm-client.js +111 -0
- package/dist/core/wiki/prompts.d.ts +53 -0
- package/dist/core/wiki/prompts.js +174 -0
- package/dist/mcp/core/embedder.js +4 -2
- package/dist/mcp/core/kuzu-adapter.d.ts +2 -1
- package/dist/mcp/core/kuzu-adapter.js +35 -15
- package/dist/mcp/local/local-backend.d.ts +54 -1
- package/dist/mcp/local/local-backend.js +716 -171
- package/dist/mcp/resources.d.ts +1 -1
- package/dist/mcp/resources.js +111 -73
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +91 -22
- package/dist/mcp/tools.js +80 -61
- package/dist/storage/git.d.ts +0 -1
- package/dist/storage/git.js +1 -8
- package/dist/storage/repo-manager.d.ts +17 -0
- package/dist/storage/repo-manager.js +26 -0
- package/hooks/claude/gitnexus-hook.cjs +135 -0
- package/hooks/claude/pre-tool-use.sh +78 -0
- package/hooks/claude/session-start.sh +42 -0
- package/package.json +4 -2
- package/skills/debugging.md +24 -22
- package/skills/exploring.md +26 -24
- package/skills/impact-analysis.md +19 -13
- 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
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
313
|
-
normalizedFileList[i].toLowerCase().endsWith(
|
|
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
|
-
//
|
|
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<
|
|
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
|
-
|
|
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
|
+
};
|