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.
- package/README.md +50 -59
- package/dist/cli/analyze.js +114 -32
- package/dist/cli/eval-server.d.ts +30 -0
- package/dist/cli/eval-server.js +372 -0
- package/dist/cli/index.js +52 -1
- package/dist/cli/mcp.js +9 -0
- package/dist/cli/setup.js +44 -7
- package/dist/cli/tool.d.ts +37 -0
- package/dist/cli/tool.js +91 -0
- package/dist/cli/wiki.d.ts +14 -0
- package/dist/cli/wiki.js +275 -0
- package/dist/core/embeddings/embedder.d.ts +2 -2
- package/dist/core/embeddings/embedder.js +10 -10
- package/dist/core/embeddings/embedding-pipeline.d.ts +2 -1
- package/dist/core/embeddings/embedding-pipeline.js +12 -4
- 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 +214 -19
- 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 +103 -39
- 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 -67
- package/dist/core/kuzu/schema.d.ts +1 -1
- package/dist/core/kuzu/schema.js +3 -0
- package/dist/core/wiki/generator.d.ts +97 -0
- package/dist/core/wiki/generator.js +683 -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.js +9 -2
- package/dist/mcp/server.js +1 -1
- 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/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
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
//
|
|
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>;
|
|
@@ -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>;
|