gitnexus 1.4.0 → 1.4.5
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 +19 -18
- package/dist/cli/analyze.js +37 -28
- package/dist/cli/augment.js +1 -1
- package/dist/cli/eval-server.d.ts +1 -1
- package/dist/cli/eval-server.js +1 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/mcp.js +1 -1
- package/dist/cli/setup.js +25 -13
- package/dist/cli/status.js +13 -4
- package/dist/cli/tool.d.ts +1 -1
- package/dist/cli/tool.js +2 -2
- package/dist/cli/wiki.js +2 -2
- package/dist/config/ignore-service.d.ts +25 -0
- package/dist/config/ignore-service.js +76 -0
- package/dist/config/supported-languages.d.ts +1 -0
- package/dist/config/supported-languages.js +1 -1
- package/dist/core/augmentation/engine.js +94 -67
- package/dist/core/embeddings/embedder.d.ts +1 -1
- package/dist/core/embeddings/embedder.js +1 -1
- package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
- package/dist/core/embeddings/embedding-pipeline.js +52 -25
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/ingestion/call-processor.d.ts +6 -7
- package/dist/core/ingestion/call-processor.js +490 -127
- package/dist/core/ingestion/call-routing.d.ts +53 -0
- package/dist/core/ingestion/call-routing.js +108 -0
- package/dist/core/ingestion/entry-point-scoring.js +13 -2
- package/dist/core/ingestion/export-detection.js +1 -0
- package/dist/core/ingestion/filesystem-walker.js +4 -3
- package/dist/core/ingestion/framework-detection.js +9 -0
- package/dist/core/ingestion/heritage-processor.d.ts +3 -4
- package/dist/core/ingestion/heritage-processor.js +40 -50
- package/dist/core/ingestion/import-processor.d.ts +3 -5
- package/dist/core/ingestion/import-processor.js +41 -10
- package/dist/core/ingestion/parsing-processor.d.ts +2 -1
- package/dist/core/ingestion/parsing-processor.js +41 -4
- package/dist/core/ingestion/pipeline.d.ts +5 -1
- package/dist/core/ingestion/pipeline.js +174 -121
- package/dist/core/ingestion/resolution-context.d.ts +53 -0
- package/dist/core/ingestion/resolution-context.js +132 -0
- package/dist/core/ingestion/resolvers/index.d.ts +2 -0
- package/dist/core/ingestion/resolvers/index.js +2 -0
- package/dist/core/ingestion/resolvers/python.d.ts +19 -0
- package/dist/core/ingestion/resolvers/python.js +52 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
- package/dist/core/ingestion/resolvers/ruby.js +15 -0
- package/dist/core/ingestion/resolvers/standard.js +0 -22
- package/dist/core/ingestion/resolvers/utils.js +2 -0
- package/dist/core/ingestion/symbol-table.d.ts +3 -0
- package/dist/core/ingestion/symbol-table.js +1 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +3 -2
- package/dist/core/ingestion/tree-sitter-queries.js +53 -1
- package/dist/core/ingestion/type-env.d.ts +32 -10
- package/dist/core/ingestion/type-env.js +520 -47
- package/dist/core/ingestion/type-extractors/c-cpp.js +326 -1
- package/dist/core/ingestion/type-extractors/csharp.js +282 -2
- package/dist/core/ingestion/type-extractors/go.js +333 -2
- package/dist/core/ingestion/type-extractors/index.d.ts +3 -2
- package/dist/core/ingestion/type-extractors/index.js +3 -1
- package/dist/core/ingestion/type-extractors/jvm.js +537 -4
- package/dist/core/ingestion/type-extractors/php.js +387 -7
- package/dist/core/ingestion/type-extractors/python.js +356 -5
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +389 -0
- package/dist/core/ingestion/type-extractors/rust.js +399 -2
- package/dist/core/ingestion/type-extractors/shared.d.ts +116 -1
- package/dist/core/ingestion/type-extractors/shared.js +488 -14
- package/dist/core/ingestion/type-extractors/swift.js +95 -1
- package/dist/core/ingestion/type-extractors/types.d.ts +81 -0
- package/dist/core/ingestion/type-extractors/typescript.js +436 -2
- package/dist/core/ingestion/utils.d.ts +33 -2
- package/dist/core/ingestion/utils.js +399 -27
- package/dist/core/ingestion/workers/parse-worker.d.ts +18 -1
- package/dist/core/ingestion/workers/parse-worker.js +169 -19
- package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
- package/dist/core/{kuzu → lbug}/csv-generator.js +1 -1
- package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
- package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +70 -65
- package/dist/core/{kuzu → lbug}/schema.d.ts +1 -1
- package/dist/core/{kuzu → lbug}/schema.js +1 -1
- package/dist/core/search/bm25-index.d.ts +4 -4
- package/dist/core/search/bm25-index.js +10 -10
- package/dist/core/search/hybrid-search.d.ts +2 -2
- package/dist/core/search/hybrid-search.js +6 -6
- package/dist/core/tree-sitter/parser-loader.js +9 -2
- package/dist/core/wiki/generator.d.ts +2 -2
- package/dist/core/wiki/generator.js +4 -4
- package/dist/core/wiki/graph-queries.d.ts +4 -4
- package/dist/core/wiki/graph-queries.js +7 -7
- package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +7 -7
- package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +72 -43
- package/dist/mcp/local/local-backend.d.ts +6 -6
- package/dist/mcp/local/local-backend.js +25 -18
- package/dist/server/api.js +12 -12
- package/dist/server/mcp-http.d.ts +1 -1
- package/dist/server/mcp-http.js +1 -1
- package/dist/storage/repo-manager.d.ts +20 -2
- package/dist/storage/repo-manager.js +55 -1
- package/dist/types/pipeline.d.ts +1 -1
- package/package.json +5 -3
- package/dist/core/ingestion/symbol-resolver.d.ts +0 -32
- package/dist/core/ingestion/symbol-resolver.js +0 -83
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import Parser from 'tree-sitter';
|
|
2
|
-
import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
2
|
+
import { loadParser, loadLanguage, isLanguageAvailable } from '../tree-sitter/parser-loader.js';
|
|
3
3
|
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
|
4
4
|
import { generateId } from '../../lib/utils.js';
|
|
5
5
|
import { getLanguageFromFilename, yieldToEventLoop, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature } from './utils.js';
|
|
6
6
|
import { isNodeExported } from './export-detection.js';
|
|
7
7
|
import { detectFrameworkFromAST } from './framework-detection.js';
|
|
8
|
+
import { typeConfigs } from './type-extractors/index.js';
|
|
8
9
|
import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';
|
|
9
10
|
// isNodeExported imported from ./export-detection.js (shared module)
|
|
10
11
|
// Re-export for backward compatibility with any external consumers
|
|
@@ -21,7 +22,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
|
|
|
21
22
|
parseableFiles.push({ path: file.path, content: file.content });
|
|
22
23
|
}
|
|
23
24
|
if (parseableFiles.length === 0)
|
|
24
|
-
return { imports: [], calls: [], heritage: [], routes: [] };
|
|
25
|
+
return { imports: [], calls: [], heritage: [], routes: [], constructorBindings: [] };
|
|
25
26
|
const total = files.length;
|
|
26
27
|
// Dispatch to worker pool — pool handles splitting into chunks and sub-batching
|
|
27
28
|
const chunkResults = await workerPool.dispatch(parseableFiles, (filesProcessed) => {
|
|
@@ -32,6 +33,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
|
|
|
32
33
|
const allCalls = [];
|
|
33
34
|
const allHeritage = [];
|
|
34
35
|
const allRoutes = [];
|
|
36
|
+
const allConstructorBindings = [];
|
|
35
37
|
for (const result of chunkResults) {
|
|
36
38
|
for (const node of result.nodes) {
|
|
37
39
|
graph.addNode({
|
|
@@ -46,6 +48,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
|
|
|
46
48
|
for (const sym of result.symbols) {
|
|
47
49
|
symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type, {
|
|
48
50
|
parameterCount: sym.parameterCount,
|
|
51
|
+
returnType: sym.returnType,
|
|
49
52
|
ownerId: sym.ownerId,
|
|
50
53
|
});
|
|
51
54
|
}
|
|
@@ -53,10 +56,24 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
|
|
|
53
56
|
allCalls.push(...result.calls);
|
|
54
57
|
allHeritage.push(...result.heritage);
|
|
55
58
|
allRoutes.push(...result.routes);
|
|
59
|
+
allConstructorBindings.push(...result.constructorBindings);
|
|
60
|
+
}
|
|
61
|
+
// Merge and log skipped languages from workers
|
|
62
|
+
const skippedLanguages = new Map();
|
|
63
|
+
for (const result of chunkResults) {
|
|
64
|
+
for (const [lang, count] of Object.entries(result.skippedLanguages)) {
|
|
65
|
+
skippedLanguages.set(lang, (skippedLanguages.get(lang) || 0) + count);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (skippedLanguages.size > 0) {
|
|
69
|
+
const summary = Array.from(skippedLanguages.entries())
|
|
70
|
+
.map(([lang, count]) => `${lang}: ${count}`)
|
|
71
|
+
.join(', ');
|
|
72
|
+
console.warn(` Skipped unsupported languages: ${summary}`);
|
|
56
73
|
}
|
|
57
74
|
// Final progress
|
|
58
75
|
onFileProgress?.(total, total, 'done');
|
|
59
|
-
return { imports: allImports, calls: allCalls, heritage: allHeritage, routes: allRoutes };
|
|
76
|
+
return { imports: allImports, calls: allCalls, heritage: allHeritage, routes: allRoutes, constructorBindings: allConstructorBindings };
|
|
60
77
|
};
|
|
61
78
|
// ============================================================================
|
|
62
79
|
// Sequential fallback (original implementation)
|
|
@@ -64,6 +81,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
|
|
|
64
81
|
const processParsingSequential = async (graph, files, symbolTable, astCache, onFileProgress) => {
|
|
65
82
|
const parser = await loadParser();
|
|
66
83
|
const total = files.length;
|
|
84
|
+
const skippedLanguages = new Map();
|
|
67
85
|
for (let i = 0; i < files.length; i++) {
|
|
68
86
|
const file = files[i];
|
|
69
87
|
onFileProgress?.(i + 1, total, file.path);
|
|
@@ -72,6 +90,11 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
|
|
|
72
90
|
const language = getLanguageFromFilename(file.path);
|
|
73
91
|
if (!language)
|
|
74
92
|
continue;
|
|
93
|
+
// Skip unsupported languages (e.g. Swift when tree-sitter-swift not installed)
|
|
94
|
+
if (!isLanguageAvailable(language)) {
|
|
95
|
+
skippedLanguages.set(language, (skippedLanguages.get(language) || 0) + 1);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
75
98
|
// Skip files larger than the max tree-sitter buffer (32 MB)
|
|
76
99
|
if (file.content.length > TREE_SITTER_MAX_BUFFER)
|
|
77
100
|
continue;
|
|
@@ -79,7 +102,7 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
|
|
|
79
102
|
await loadLanguage(language, file.path);
|
|
80
103
|
}
|
|
81
104
|
catch {
|
|
82
|
-
continue; // parser unavailable —
|
|
105
|
+
continue; // parser unavailable — safety net
|
|
83
106
|
}
|
|
84
107
|
let tree;
|
|
85
108
|
try {
|
|
@@ -177,6 +200,13 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
|
|
|
177
200
|
const methodSig = (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor')
|
|
178
201
|
? extractMethodSignature(definitionNode)
|
|
179
202
|
: undefined;
|
|
203
|
+
// Language-specific return type fallback (e.g. Ruby YARD @return [Type])
|
|
204
|
+
if (methodSig && !methodSig.returnType && definitionNode) {
|
|
205
|
+
const tc = typeConfigs[language];
|
|
206
|
+
if (tc?.extractReturnType) {
|
|
207
|
+
methodSig.returnType = tc.extractReturnType(definitionNode);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
180
210
|
const node = {
|
|
181
211
|
id: nodeId,
|
|
182
212
|
label: nodeLabel,
|
|
@@ -204,6 +234,7 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
|
|
|
204
234
|
const enclosingClassId = needsOwner ? findEnclosingClassId(nameNode || definitionNodeForRange, file.path) : null;
|
|
205
235
|
symbolTable.add(file.path, nodeName, nodeId, nodeLabel, {
|
|
206
236
|
parameterCount: methodSig?.parameterCount,
|
|
237
|
+
returnType: methodSig?.returnType,
|
|
207
238
|
ownerId: enclosingClassId ?? undefined,
|
|
208
239
|
});
|
|
209
240
|
const fileId = generateId('File', file.path);
|
|
@@ -230,6 +261,12 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
|
|
|
230
261
|
}
|
|
231
262
|
});
|
|
232
263
|
}
|
|
264
|
+
if (skippedLanguages.size > 0) {
|
|
265
|
+
const summary = Array.from(skippedLanguages.entries())
|
|
266
|
+
.map(([lang, count]) => `${lang}: ${count}`)
|
|
267
|
+
.join(', ');
|
|
268
|
+
console.warn(` Skipped unsupported languages: ${summary}`);
|
|
269
|
+
}
|
|
233
270
|
};
|
|
234
271
|
// ============================================================================
|
|
235
272
|
// Public API
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import { PipelineProgress, PipelineResult } from '../../types/pipeline.js';
|
|
2
|
-
export
|
|
2
|
+
export interface PipelineOptions {
|
|
3
|
+
/** Skip MRO, community detection, and process extraction for faster test runs. */
|
|
4
|
+
skipGraphPhases?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare const runPipelineFromRepo: (repoPath: string, onProgress: (progress: PipelineProgress) => void, options?: PipelineOptions) => Promise<PipelineResult>;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createKnowledgeGraph } from '../graph/graph.js';
|
|
2
2
|
import { processStructure } from './structure-processor.js';
|
|
3
3
|
import { processParsing } from './parsing-processor.js';
|
|
4
|
-
import { processImports, processImportsFromExtracted,
|
|
4
|
+
import { processImports, processImportsFromExtracted, buildImportResolutionContext } from './import-processor.js';
|
|
5
5
|
import { processCalls, processCallsFromExtracted, processRoutesFromExtracted } from './call-processor.js';
|
|
6
6
|
import { processHeritage, processHeritageFromExtracted } from './heritage-processor.js';
|
|
7
7
|
import { computeMRO } from './mro-processor.js';
|
|
8
8
|
import { processCommunities } from './community-processor.js';
|
|
9
9
|
import { processProcesses } from './process-processor.js';
|
|
10
|
-
import {
|
|
10
|
+
import { createResolutionContext } from './resolution-context.js';
|
|
11
11
|
import { createASTCache } from './ast-cache.js';
|
|
12
12
|
import { walkRepositoryPaths, readFileContents } from './filesystem-walker.js';
|
|
13
13
|
import { getLanguageFromFilename } from './utils.js';
|
|
@@ -24,16 +24,14 @@ const isDev = process.env.NODE_ENV === 'development';
|
|
|
24
24
|
const CHUNK_BYTE_BUDGET = 20 * 1024 * 1024; // 20MB
|
|
25
25
|
/** Max AST trees to keep in LRU cache */
|
|
26
26
|
const AST_CACHE_CAP = 50;
|
|
27
|
-
export const runPipelineFromRepo = async (repoPath, onProgress) => {
|
|
27
|
+
export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
28
28
|
const graph = createKnowledgeGraph();
|
|
29
|
-
const
|
|
29
|
+
const ctx = createResolutionContext();
|
|
30
|
+
const symbolTable = ctx.symbols;
|
|
30
31
|
let astCache = createASTCache(AST_CACHE_CAP);
|
|
31
|
-
const importMap = createImportMap();
|
|
32
|
-
const packageMap = createPackageMap();
|
|
33
|
-
const namedImportMap = createNamedImportMap();
|
|
34
32
|
const cleanup = () => {
|
|
35
33
|
astCache.clear();
|
|
36
|
-
|
|
34
|
+
ctx.clear();
|
|
37
35
|
};
|
|
38
36
|
try {
|
|
39
37
|
// ── Phase 1: Scan paths only (no content read) ─────────────────────
|
|
@@ -127,24 +125,30 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
|
|
|
127
125
|
message: `Parsing ${totalParseable} files in ${numChunks} chunk${numChunks !== 1 ? 's' : ''}...`,
|
|
128
126
|
stats: { filesProcessed: 0, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
|
129
127
|
});
|
|
128
|
+
// Don't spawn workers for tiny repos — overhead exceeds benefit
|
|
129
|
+
const MIN_FILES_FOR_WORKERS = 15;
|
|
130
|
+
const MIN_BYTES_FOR_WORKERS = 512 * 1024;
|
|
131
|
+
const totalBytes = parseableScanned.reduce((s, f) => s + f.size, 0);
|
|
130
132
|
// Create worker pool once, reuse across chunks
|
|
131
133
|
let workerPool;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
if (totalParseable >= MIN_FILES_FOR_WORKERS || totalBytes >= MIN_BYTES_FOR_WORKERS) {
|
|
135
|
+
try {
|
|
136
|
+
let workerUrl = new URL('./workers/parse-worker.js', import.meta.url);
|
|
137
|
+
// When running under vitest, import.meta.url points to src/ where no .js exists.
|
|
138
|
+
// Fall back to the compiled dist/ worker so the pool can spawn real worker threads.
|
|
139
|
+
const thisDir = fileURLToPath(new URL('.', import.meta.url));
|
|
140
|
+
if (!fs.existsSync(fileURLToPath(workerUrl))) {
|
|
141
|
+
const distWorker = path.resolve(thisDir, '..', '..', '..', 'dist', 'core', 'ingestion', 'workers', 'parse-worker.js');
|
|
142
|
+
if (fs.existsSync(distWorker)) {
|
|
143
|
+
workerUrl = pathToFileURL(distWorker);
|
|
144
|
+
}
|
|
141
145
|
}
|
|
146
|
+
workerPool = createWorkerPool(workerUrl);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (isDev)
|
|
150
|
+
console.warn('Worker pool creation failed, using sequential fallback:', err.message);
|
|
142
151
|
}
|
|
143
|
-
workerPool = createWorkerPool(workerUrl);
|
|
144
|
-
}
|
|
145
|
-
catch (err) {
|
|
146
|
-
if (isDev)
|
|
147
|
-
console.warn('Worker pool creation failed, using sequential fallback:', err.message);
|
|
148
152
|
}
|
|
149
153
|
let filesParsedSoFar = 0;
|
|
150
154
|
// AST cache sized for one chunk (sequential fallback uses it for import/call/heritage)
|
|
@@ -179,20 +183,53 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
|
|
|
179
183
|
stats: { filesProcessed: globalCurrent, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
|
180
184
|
});
|
|
181
185
|
}, workerPool);
|
|
186
|
+
const chunkBasePercent = 20 + ((filesParsedSoFar / totalParseable) * 62);
|
|
182
187
|
if (chunkWorkerData) {
|
|
183
188
|
// Imports
|
|
184
|
-
await processImportsFromExtracted(graph, allPathObjects, chunkWorkerData.imports,
|
|
189
|
+
await processImportsFromExtracted(graph, allPathObjects, chunkWorkerData.imports, ctx, (current, total) => {
|
|
190
|
+
onProgress({
|
|
191
|
+
phase: 'parsing',
|
|
192
|
+
percent: Math.round(chunkBasePercent),
|
|
193
|
+
message: `Resolving imports (chunk ${chunkIdx + 1}/${numChunks})...`,
|
|
194
|
+
detail: `${current}/${total} files`,
|
|
195
|
+
stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
|
196
|
+
});
|
|
197
|
+
}, repoPath, importCtx);
|
|
185
198
|
// Calls + Heritage + Routes — resolve in parallel (no shared mutable state between them)
|
|
186
199
|
// This is safe because each writes disjoint relationship types into idempotent id-keyed Maps,
|
|
187
200
|
// and the single-threaded event loop prevents races between synchronous addRelationship calls.
|
|
188
201
|
await Promise.all([
|
|
189
|
-
processCallsFromExtracted(graph, chunkWorkerData.calls,
|
|
190
|
-
|
|
191
|
-
|
|
202
|
+
processCallsFromExtracted(graph, chunkWorkerData.calls, ctx, (current, total) => {
|
|
203
|
+
onProgress({
|
|
204
|
+
phase: 'parsing',
|
|
205
|
+
percent: Math.round(chunkBasePercent),
|
|
206
|
+
message: `Resolving calls (chunk ${chunkIdx + 1}/${numChunks})...`,
|
|
207
|
+
detail: `${current}/${total} files`,
|
|
208
|
+
stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
|
209
|
+
});
|
|
210
|
+
}, chunkWorkerData.constructorBindings),
|
|
211
|
+
processHeritageFromExtracted(graph, chunkWorkerData.heritage, ctx, (current, total) => {
|
|
212
|
+
onProgress({
|
|
213
|
+
phase: 'parsing',
|
|
214
|
+
percent: Math.round(chunkBasePercent),
|
|
215
|
+
message: `Resolving heritage (chunk ${chunkIdx + 1}/${numChunks})...`,
|
|
216
|
+
detail: `${current}/${total} records`,
|
|
217
|
+
stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
|
218
|
+
});
|
|
219
|
+
}),
|
|
220
|
+
processRoutesFromExtracted(graph, chunkWorkerData.routes ?? [], ctx, (current, total) => {
|
|
221
|
+
onProgress({
|
|
222
|
+
phase: 'parsing',
|
|
223
|
+
percent: Math.round(chunkBasePercent),
|
|
224
|
+
message: `Resolving routes (chunk ${chunkIdx + 1}/${numChunks})...`,
|
|
225
|
+
detail: `${current}/${total} routes`,
|
|
226
|
+
stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
|
227
|
+
});
|
|
228
|
+
}),
|
|
192
229
|
]);
|
|
193
230
|
}
|
|
194
231
|
else {
|
|
195
|
-
await processImports(graph, chunkFiles, astCache,
|
|
232
|
+
await processImports(graph, chunkFiles, astCache, ctx, undefined, repoPath, allPaths);
|
|
196
233
|
sequentialChunkPaths.push(chunkPaths);
|
|
197
234
|
}
|
|
198
235
|
filesParsedSoFar += chunkFiles.length;
|
|
@@ -211,123 +248,139 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
|
|
|
211
248
|
.filter(p => chunkContents.has(p))
|
|
212
249
|
.map(p => ({ path: p, content: chunkContents.get(p) }));
|
|
213
250
|
astCache = createASTCache(chunkFiles.length);
|
|
214
|
-
await processCalls(graph, chunkFiles, astCache,
|
|
215
|
-
await processHeritage(graph, chunkFiles, astCache,
|
|
251
|
+
const rubyHeritage = await processCalls(graph, chunkFiles, astCache, ctx);
|
|
252
|
+
await processHeritage(graph, chunkFiles, astCache, ctx);
|
|
253
|
+
if (rubyHeritage.length > 0) {
|
|
254
|
+
await processHeritageFromExtracted(graph, rubyHeritage, ctx);
|
|
255
|
+
}
|
|
216
256
|
astCache.clear();
|
|
217
257
|
}
|
|
258
|
+
// Log resolution cache stats
|
|
259
|
+
if (isDev) {
|
|
260
|
+
const rcStats = ctx.getStats();
|
|
261
|
+
const total = rcStats.cacheHits + rcStats.cacheMisses;
|
|
262
|
+
const hitRate = total > 0 ? ((rcStats.cacheHits / total) * 100).toFixed(1) : '0';
|
|
263
|
+
console.log(`🔍 Resolution cache: ${rcStats.cacheHits} hits, ${rcStats.cacheMisses} misses (${hitRate}% hit rate)`);
|
|
264
|
+
}
|
|
218
265
|
// Free import resolution context — suffix index + resolve cache no longer needed
|
|
219
266
|
// (allPathObjects and importCtx hold ~94MB+ for large repos)
|
|
220
267
|
allPathObjects.length = 0;
|
|
221
268
|
importCtx.resolveCache.clear();
|
|
222
269
|
importCtx.suffixIndex = null;
|
|
223
270
|
importCtx.normalizedFileList = null;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
message: 'Detecting code communities...',
|
|
240
|
-
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
|
241
|
-
});
|
|
242
|
-
const communityResult = await processCommunities(graph, (message, progress) => {
|
|
243
|
-
const communityProgress = 82 + (progress * 0.10);
|
|
271
|
+
let communityResult;
|
|
272
|
+
let processResult;
|
|
273
|
+
if (!options?.skipGraphPhases) {
|
|
274
|
+
// ── Phase 4.5: Method Resolution Order ──────────────────────────────
|
|
275
|
+
onProgress({
|
|
276
|
+
phase: 'parsing',
|
|
277
|
+
percent: 81,
|
|
278
|
+
message: 'Computing method resolution order...',
|
|
279
|
+
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
|
280
|
+
});
|
|
281
|
+
const mroResult = computeMRO(graph);
|
|
282
|
+
if (isDev && mroResult.entries.length > 0) {
|
|
283
|
+
console.log(`🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities found, ${mroResult.overrideEdges} OVERRIDES edges`);
|
|
284
|
+
}
|
|
285
|
+
// ── Phase 5: Communities ───────────────────────────────────────────
|
|
244
286
|
onProgress({
|
|
245
287
|
phase: 'communities',
|
|
246
|
-
percent:
|
|
247
|
-
message,
|
|
288
|
+
percent: 82,
|
|
289
|
+
message: 'Detecting code communities...',
|
|
248
290
|
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
|
249
291
|
});
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
properties: {
|
|
259
|
-
name: comm.label,
|
|
260
|
-
filePath: '',
|
|
261
|
-
heuristicLabel: comm.heuristicLabel,
|
|
262
|
-
cohesion: comm.cohesion,
|
|
263
|
-
symbolCount: comm.symbolCount,
|
|
264
|
-
}
|
|
292
|
+
communityResult = await processCommunities(graph, (message, progress) => {
|
|
293
|
+
const communityProgress = 82 + (progress * 0.10);
|
|
294
|
+
onProgress({
|
|
295
|
+
phase: 'communities',
|
|
296
|
+
percent: Math.round(communityProgress),
|
|
297
|
+
message,
|
|
298
|
+
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
|
299
|
+
});
|
|
265
300
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
301
|
+
if (isDev) {
|
|
302
|
+
console.log(`🏘️ Community detection: ${communityResult.stats.totalCommunities} communities found (modularity: ${communityResult.stats.modularity.toFixed(3)})`);
|
|
303
|
+
}
|
|
304
|
+
communityResult.communities.forEach(comm => {
|
|
305
|
+
graph.addNode({
|
|
306
|
+
id: comm.id,
|
|
307
|
+
label: 'Community',
|
|
308
|
+
properties: {
|
|
309
|
+
name: comm.label,
|
|
310
|
+
filePath: '',
|
|
311
|
+
heuristicLabel: comm.heuristicLabel,
|
|
312
|
+
cohesion: comm.cohesion,
|
|
313
|
+
symbolCount: comm.symbolCount,
|
|
314
|
+
}
|
|
315
|
+
});
|
|
275
316
|
});
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const dynamicMaxProcesses = Math.max(20, Math.min(300, Math.round(symbolCount / 10)));
|
|
288
|
-
const processResult = await processProcesses(graph, communityResult.memberships, (message, progress) => {
|
|
289
|
-
const processProgress = 94 + (progress * 0.05);
|
|
317
|
+
communityResult.memberships.forEach(membership => {
|
|
318
|
+
graph.addRelationship({
|
|
319
|
+
id: `${membership.nodeId}_member_of_${membership.communityId}`,
|
|
320
|
+
type: 'MEMBER_OF',
|
|
321
|
+
sourceId: membership.nodeId,
|
|
322
|
+
targetId: membership.communityId,
|
|
323
|
+
confidence: 1.0,
|
|
324
|
+
reason: 'leiden-algorithm',
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
// ── Phase 6: Processes ─────────────────────────────────────────────
|
|
290
328
|
onProgress({
|
|
291
329
|
phase: 'processes',
|
|
292
|
-
percent:
|
|
293
|
-
message,
|
|
330
|
+
percent: 94,
|
|
331
|
+
message: 'Detecting execution flows...',
|
|
294
332
|
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
|
295
333
|
});
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
334
|
+
let symbolCount = 0;
|
|
335
|
+
graph.forEachNode(n => { if (n.label !== 'File')
|
|
336
|
+
symbolCount++; });
|
|
337
|
+
const dynamicMaxProcesses = Math.max(20, Math.min(300, Math.round(symbolCount / 10)));
|
|
338
|
+
processResult = await processProcesses(graph, communityResult.memberships, (message, progress) => {
|
|
339
|
+
const processProgress = 94 + (progress * 0.05);
|
|
340
|
+
onProgress({
|
|
341
|
+
phase: 'processes',
|
|
342
|
+
percent: Math.round(processProgress),
|
|
343
|
+
message,
|
|
344
|
+
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
|
345
|
+
});
|
|
346
|
+
}, { maxProcesses: dynamicMaxProcesses, minSteps: 3 });
|
|
347
|
+
if (isDev) {
|
|
348
|
+
console.log(`🔄 Process detection: ${processResult.stats.totalProcesses} processes found (${processResult.stats.crossCommunityCount} cross-community)`);
|
|
349
|
+
}
|
|
350
|
+
processResult.processes.forEach(proc => {
|
|
351
|
+
graph.addNode({
|
|
352
|
+
id: proc.id,
|
|
353
|
+
label: 'Process',
|
|
354
|
+
properties: {
|
|
355
|
+
name: proc.label,
|
|
356
|
+
filePath: '',
|
|
357
|
+
heuristicLabel: proc.heuristicLabel,
|
|
358
|
+
processType: proc.processType,
|
|
359
|
+
stepCount: proc.stepCount,
|
|
360
|
+
communities: proc.communities,
|
|
361
|
+
entryPointId: proc.entryPointId,
|
|
362
|
+
terminalId: proc.terminalId,
|
|
363
|
+
}
|
|
364
|
+
});
|
|
314
365
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
366
|
+
processResult.steps.forEach(step => {
|
|
367
|
+
graph.addRelationship({
|
|
368
|
+
id: `${step.nodeId}_step_${step.step}_${step.processId}`,
|
|
369
|
+
type: 'STEP_IN_PROCESS',
|
|
370
|
+
sourceId: step.nodeId,
|
|
371
|
+
targetId: step.processId,
|
|
372
|
+
confidence: 1.0,
|
|
373
|
+
reason: 'trace-detection',
|
|
374
|
+
step: step.step,
|
|
375
|
+
});
|
|
325
376
|
});
|
|
326
|
-
}
|
|
377
|
+
}
|
|
327
378
|
onProgress({
|
|
328
379
|
phase: 'complete',
|
|
329
380
|
percent: 100,
|
|
330
|
-
message:
|
|
381
|
+
message: communityResult && processResult
|
|
382
|
+
? `Graph complete! ${communityResult.stats.totalCommunities} communities, ${processResult.stats.totalProcesses} processes detected.`
|
|
383
|
+
: 'Graph complete! (graph phases skipped)',
|
|
331
384
|
stats: {
|
|
332
385
|
filesProcessed: totalFiles,
|
|
333
386
|
totalFiles,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution Context
|
|
3
|
+
*
|
|
4
|
+
* Single implementation of tiered name resolution. Replaces the duplicated
|
|
5
|
+
* tier-selection logic previously split between symbol-resolver.ts and
|
|
6
|
+
* call-processor.ts.
|
|
7
|
+
*
|
|
8
|
+
* Resolution tiers (highest confidence first):
|
|
9
|
+
* 1. Same file (lookupExactFull — authoritative)
|
|
10
|
+
* 2a-named. Named binding chain (walkBindingChain via NamedImportMap)
|
|
11
|
+
* 2a. Import-scoped (lookupFuzzy filtered by ImportMap)
|
|
12
|
+
* 2b. Package-scoped (lookupFuzzy filtered by PackageMap)
|
|
13
|
+
* 3. Global (all candidates — consumers must check candidate count)
|
|
14
|
+
*/
|
|
15
|
+
import type { SymbolTable, SymbolDefinition } from './symbol-table.js';
|
|
16
|
+
import type { NamedImportBinding } from './import-processor.js';
|
|
17
|
+
/** Resolution tier for tracking, logging, and test assertions. */
|
|
18
|
+
export type ResolutionTier = 'same-file' | 'import-scoped' | 'global';
|
|
19
|
+
/** Tier-selected candidates with metadata. */
|
|
20
|
+
export interface TieredCandidates {
|
|
21
|
+
readonly candidates: readonly SymbolDefinition[];
|
|
22
|
+
readonly tier: ResolutionTier;
|
|
23
|
+
}
|
|
24
|
+
/** Confidence scores per resolution tier. */
|
|
25
|
+
export declare const TIER_CONFIDENCE: Record<ResolutionTier, number>;
|
|
26
|
+
export type ImportMap = Map<string, Set<string>>;
|
|
27
|
+
export type PackageMap = Map<string, Set<string>>;
|
|
28
|
+
export type NamedImportMap = Map<string, Map<string, NamedImportBinding>>;
|
|
29
|
+
export interface ResolutionContext {
|
|
30
|
+
/**
|
|
31
|
+
* The only resolution API. Returns all candidates at the winning tier.
|
|
32
|
+
*
|
|
33
|
+
* Tier 3 ('global') returns ALL candidates regardless of count —
|
|
34
|
+
* consumers must check candidates.length and refuse ambiguous matches.
|
|
35
|
+
*/
|
|
36
|
+
resolve(name: string, fromFile: string): TieredCandidates | null;
|
|
37
|
+
/** Symbol table — used by parsing-processor to populate symbols. */
|
|
38
|
+
readonly symbols: SymbolTable;
|
|
39
|
+
/** Raw maps — used by import-processor to populate import data. */
|
|
40
|
+
readonly importMap: ImportMap;
|
|
41
|
+
readonly packageMap: PackageMap;
|
|
42
|
+
readonly namedImportMap: NamedImportMap;
|
|
43
|
+
enableCache(filePath: string): void;
|
|
44
|
+
clearCache(): void;
|
|
45
|
+
getStats(): {
|
|
46
|
+
fileCount: number;
|
|
47
|
+
globalSymbolCount: number;
|
|
48
|
+
cacheHits: number;
|
|
49
|
+
cacheMisses: number;
|
|
50
|
+
};
|
|
51
|
+
clear(): void;
|
|
52
|
+
}
|
|
53
|
+
export declare const createResolutionContext: () => ResolutionContext;
|