@zuvia-software-solutions/code-mapper 2.3.8 → 2.3.9
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/dist/cli/analyze.js +34 -3
- package/dist/core/ingestion/call-processor.d.ts +1 -1
- package/dist/core/ingestion/call-processor.js +36 -35
- package/dist/core/ingestion/pipeline.js +3 -3
- package/dist/core/semantic/tsgo-service.js +3 -3
- package/dist/types/pipeline.d.ts +1 -0
- package/package.json +1 -1
package/dist/cli/analyze.js
CHANGED
|
@@ -136,10 +136,12 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
136
136
|
const t0Global = Date.now();
|
|
137
137
|
const cpuStart = process.cpuUsage();
|
|
138
138
|
let peakRssMB = 0;
|
|
139
|
-
// Phase timing tracker — records wall time and
|
|
139
|
+
// Phase timing tracker — records wall time, RSS, file count, and worker count per phase
|
|
140
140
|
const phaseTimes = [];
|
|
141
141
|
let currentPhaseName = 'init';
|
|
142
142
|
let currentPhaseStart = Date.now();
|
|
143
|
+
let currentPhaseFiles = 0;
|
|
144
|
+
let currentPhaseWorkers = 0;
|
|
143
145
|
const recordPhase = (nextPhase) => {
|
|
144
146
|
const now = Date.now();
|
|
145
147
|
const elapsed = now - currentPhaseStart;
|
|
@@ -148,10 +150,14 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
148
150
|
name: currentPhaseName,
|
|
149
151
|
ms: elapsed,
|
|
150
152
|
rssMB: Math.round(process.memoryUsage.rss() / (1024 * 1024)),
|
|
153
|
+
...(currentPhaseFiles > 0 ? { fileCount: currentPhaseFiles } : {}),
|
|
154
|
+
...(currentPhaseWorkers > 0 ? { workerCount: currentPhaseWorkers } : {}),
|
|
151
155
|
});
|
|
152
156
|
}
|
|
153
157
|
currentPhaseName = nextPhase;
|
|
154
158
|
currentPhaseStart = now;
|
|
159
|
+
currentPhaseFiles = 0;
|
|
160
|
+
currentPhaseWorkers = 0;
|
|
155
161
|
};
|
|
156
162
|
// Live resource stats for the progress bar
|
|
157
163
|
const cpuCount = os.cpus().length;
|
|
@@ -222,7 +228,23 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
222
228
|
const baseLabel = PHASE_LABELS[progress.phase] || progress.phase;
|
|
223
229
|
let phaseLabel = baseLabel;
|
|
224
230
|
if (progress.stats && progress.stats.totalFiles > 0) {
|
|
225
|
-
|
|
231
|
+
const current = progress.stats.filesProcessed;
|
|
232
|
+
const total = progress.stats.totalFiles;
|
|
233
|
+
// Track peak file count and worker count for the summary
|
|
234
|
+
currentPhaseFiles = Math.max(currentPhaseFiles, total);
|
|
235
|
+
if (progress.stats.workerCount)
|
|
236
|
+
currentPhaseWorkers = Math.max(currentPhaseWorkers, progress.stats.workerCount);
|
|
237
|
+
phaseLabel += ` (${current.toLocaleString()}/${total.toLocaleString()})`;
|
|
238
|
+
// Show rate (files/s) after 1s
|
|
239
|
+
const elapsedSec = (Date.now() - phaseStart) / 1000;
|
|
240
|
+
if (elapsedSec >= 1 && current > 0) {
|
|
241
|
+
const rate = Math.round(current / elapsedSec);
|
|
242
|
+
phaseLabel += ` ${rate}/s`;
|
|
243
|
+
}
|
|
244
|
+
// Show worker/process count if available
|
|
245
|
+
if (progress.stats.workerCount && progress.stats.workerCount > 1) {
|
|
246
|
+
phaseLabel += ` [${progress.stats.workerCount}p]`;
|
|
247
|
+
}
|
|
226
248
|
}
|
|
227
249
|
const scaled = Math.round(progress.percent * 0.6);
|
|
228
250
|
updateBar(scaled, phaseLabel, baseLabel);
|
|
@@ -433,7 +455,16 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
433
455
|
const pct = Math.round((phase.ms / totalMs) * 100);
|
|
434
456
|
const name = PHASE_DISPLAY_NAMES[phase.name] || phase.name;
|
|
435
457
|
const bar = pct >= 2 ? ' ' + '█'.repeat(Math.max(1, Math.round(pct / 3))) : '';
|
|
436
|
-
|
|
458
|
+
// Build extra stats: rate + workers
|
|
459
|
+
let extra = '';
|
|
460
|
+
if (phase.fileCount && phase.ms > 0) {
|
|
461
|
+
const rate = Math.round(phase.fileCount / (phase.ms / 1000));
|
|
462
|
+
extra += ` ${phase.fileCount.toLocaleString()} files (${rate}/s)`;
|
|
463
|
+
}
|
|
464
|
+
if (phase.workerCount && phase.workerCount > 1) {
|
|
465
|
+
extra += ` [${phase.workerCount}p]`;
|
|
466
|
+
}
|
|
467
|
+
console.log(` ${name.padEnd(22)} ${sec.padStart(6)}s ${String(pct).padStart(3)}% ${phase.rssMB}MB${bar}${extra}`);
|
|
437
468
|
}
|
|
438
469
|
console.log(` ${'─'.repeat(50)}`);
|
|
439
470
|
console.log(` ${'Total'.padEnd(22)} ${totalTime.padStart(6)}s 100% ${peakRssMB}MB peak`);
|
|
@@ -10,7 +10,7 @@ export declare const processCalls: (graph: KnowledgeGraph, files: {
|
|
|
10
10
|
}[], astCache: ASTCache, ctx: ResolutionContext, onProgress?: (current: number, total: number) => void) => Promise<ExtractedHeritage[]>;
|
|
11
11
|
export declare const extractReturnTypeName: (raw: string, depth?: number) => string | undefined;
|
|
12
12
|
/** Resolve pre-extracted call sites from workers (no AST parsing needed) */
|
|
13
|
-
export declare const processCallsFromExtracted: (graph: KnowledgeGraph, extractedCalls: ExtractedCall[], ctx: ResolutionContext, onProgress?: (current: number, total: number) => void, constructorBindings?: FileConstructorBindings[], tsgoService?: TsgoService | null, repoPath?: string) => Promise<void>;
|
|
13
|
+
export declare const processCallsFromExtracted: (graph: KnowledgeGraph, extractedCalls: ExtractedCall[], ctx: ResolutionContext, onProgress?: (current: number, total: number, workerCount?: number) => void, constructorBindings?: FileConstructorBindings[], tsgoService?: TsgoService | null, repoPath?: string) => Promise<void>;
|
|
14
14
|
/** Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods */
|
|
15
15
|
export declare const processRoutesFromExtracted: (graph: KnowledgeGraph, extractedRoutes: ExtractedRoute[], ctx: ResolutionContext, onProgress?: (current: number, total: number) => void) => Promise<void>;
|
|
16
16
|
/**
|
|
@@ -778,11 +778,11 @@ async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPat
|
|
|
778
778
|
]);
|
|
779
779
|
// Pre-filter calls where tsgo won't add value:
|
|
780
780
|
// A. Free-form calls with unambiguous name — heuristic resolves perfectly
|
|
781
|
-
// B. Member calls
|
|
782
|
-
//
|
|
781
|
+
// B. Member calls on built-in receivers — tsgo always fails on these
|
|
782
|
+
// Note: member calls with known receiver types are NOT skipped — tsgo provides
|
|
783
|
+
// compiler-verified 0.99 confidence that the heuristic can't match.
|
|
783
784
|
const tsgoEligible = [];
|
|
784
785
|
let skippedUnambiguous = 0;
|
|
785
|
-
const skippedKnownType = 0;
|
|
786
786
|
let skippedBuiltin = 0;
|
|
787
787
|
for (const call of eligible) {
|
|
788
788
|
// A. Free-form, unique name match
|
|
@@ -811,7 +811,7 @@ async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPat
|
|
|
811
811
|
list.push(call);
|
|
812
812
|
}
|
|
813
813
|
const t0 = Date.now();
|
|
814
|
-
const skippedTotal = skippedUnambiguous +
|
|
814
|
+
const skippedTotal = skippedUnambiguous + skippedBuiltin;
|
|
815
815
|
// Adaptive parallelism based on three constraints:
|
|
816
816
|
// 1. CPU: 75% of cores — parsing workers are done, leave 25% for Node.js event loop + OS
|
|
817
817
|
// 2. Memory: each tsgo loads the full project (~500MB estimate) — cap by free system memory
|
|
@@ -823,25 +823,34 @@ async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPat
|
|
|
823
823
|
const maxByMemory = Math.max(1, Math.floor(freeMemGB / 0.5));
|
|
824
824
|
const maxByWorkload = Math.max(1, Math.floor(tsgoByFile.size / 50));
|
|
825
825
|
const actualWorkers = Math.min(maxByCpu, maxByMemory, maxByWorkload);
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const fileEntries = [...tsgoByFile.entries()];
|
|
829
|
-
const workerSlices = Array.from({ length: actualWorkers }, () => []);
|
|
830
|
-
for (let i = 0; i < fileEntries.length; i++) {
|
|
831
|
-
workerSlices[i % actualWorkers].push(fileEntries[i]);
|
|
826
|
+
if (process.env['CODE_MAPPER_VERBOSE']) {
|
|
827
|
+
console.error(`Code Mapper: tsgo resolving ${tsgoEligible.length} calls across ${tsgoByFile.size} files with ${actualWorkers} process${actualWorkers > 1 ? 'es' : ''} (skipped ${skippedTotal}: ${skippedUnambiguous} unambiguous, ${skippedBuiltin} builtin)...`);
|
|
832
828
|
}
|
|
833
|
-
//
|
|
829
|
+
// Dynamic dispatch: shared queue, each process grabs the next file when done.
|
|
830
|
+
// Naturally self-balancing — fast processes get more work, zero idle time.
|
|
831
|
+
// Sort heaviest files first so they're assigned early (avoids tail latency).
|
|
832
|
+
const fileEntries = [...tsgoByFile.entries()];
|
|
833
|
+
fileEntries.sort((a, b) => b[1].length - a[1].length);
|
|
834
|
+
// Shared progress counter and file queue (single-threaded, no mutex needed)
|
|
834
835
|
let totalFilesProcessed = 0;
|
|
836
|
+
let nextFileIdx = 0;
|
|
835
837
|
const tsgoTotalFiles = tsgoByFile.size;
|
|
836
|
-
|
|
837
|
-
|
|
838
|
+
const getNextFile = () => {
|
|
839
|
+
if (nextFileIdx >= fileEntries.length)
|
|
840
|
+
return null;
|
|
841
|
+
return fileEntries[nextFileIdx++];
|
|
842
|
+
};
|
|
843
|
+
/** Resolve files from the shared queue using a single tsgo service */
|
|
844
|
+
const resolveWorker = async (service) => {
|
|
838
845
|
const sliceResults = new Map();
|
|
839
846
|
let sliceResolved = 0;
|
|
840
847
|
let sliceFailed = 0;
|
|
841
|
-
|
|
848
|
+
let entry;
|
|
849
|
+
while ((entry = getNextFile()) !== null) {
|
|
850
|
+
const [filePath, calls] = entry;
|
|
842
851
|
totalFilesProcessed++;
|
|
843
852
|
if (totalFilesProcessed % 25 === 0) {
|
|
844
|
-
onProgress?.(totalFilesProcessed, tsgoTotalFiles);
|
|
853
|
+
onProgress?.(totalFilesProcessed, tsgoTotalFiles, actualWorkers);
|
|
845
854
|
}
|
|
846
855
|
const absFilePath = path.resolve(repoPath, filePath);
|
|
847
856
|
for (const call of calls) {
|
|
@@ -899,6 +908,9 @@ async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPat
|
|
|
899
908
|
sliceFailed++;
|
|
900
909
|
}
|
|
901
910
|
}
|
|
911
|
+
// Close file after all its calls are resolved — frees tsgo memory,
|
|
912
|
+
// prevents progressive slowdown as the type graph grows
|
|
913
|
+
service.notifyFileDeleted(absFilePath);
|
|
902
914
|
}
|
|
903
915
|
return { resolved: sliceResolved, failed: sliceFailed, results: sliceResults };
|
|
904
916
|
};
|
|
@@ -906,14 +918,14 @@ async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPat
|
|
|
906
918
|
let failed = 0;
|
|
907
919
|
if (actualWorkers === 1) {
|
|
908
920
|
// Single process — use the existing service (already started)
|
|
909
|
-
const outcome = await
|
|
921
|
+
const outcome = await resolveWorker(tsgoService);
|
|
910
922
|
resolved = outcome.resolved;
|
|
911
923
|
failed = outcome.failed;
|
|
912
924
|
for (const [k, v] of outcome.results)
|
|
913
925
|
results.set(k, v);
|
|
914
926
|
}
|
|
915
927
|
else {
|
|
916
|
-
// Parallel — spawn extra services,
|
|
928
|
+
// Parallel — spawn extra services, all pull from shared queue
|
|
917
929
|
const extraServices = [];
|
|
918
930
|
try {
|
|
919
931
|
// Start extra tsgo processes in parallel
|
|
@@ -933,22 +945,10 @@ async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPat
|
|
|
933
945
|
}
|
|
934
946
|
// Build final service list: original + extras that started successfully
|
|
935
947
|
const services = [tsgoService, ...extraServices];
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
const orphanSlice = workerSlices[i];
|
|
941
|
-
if (orphanSlice) {
|
|
942
|
-
// Distribute orphan files round-robin across active services
|
|
943
|
-
for (let j = 0; j < orphanSlice.length; j++) {
|
|
944
|
-
activeSlices[j % services.length].push(orphanSlice[j]);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
console.error(`Code Mapper: ${services.length} tsgo processes ready, resolving in parallel...`);
|
|
950
|
-
// Resolve all slices in parallel
|
|
951
|
-
const outcomes = await Promise.all(activeSlices.map((slice, i) => resolveSlice(services[i], slice)));
|
|
948
|
+
if (process.env['CODE_MAPPER_VERBOSE'])
|
|
949
|
+
console.error(`Code Mapper: ${services.length} tsgo processes ready, resolving with dynamic dispatch...`);
|
|
950
|
+
// All workers pull from the shared queue — naturally self-balancing
|
|
951
|
+
const outcomes = await Promise.all(services.map(svc => resolveWorker(svc)));
|
|
952
952
|
for (const outcome of outcomes) {
|
|
953
953
|
resolved += outcome.resolved;
|
|
954
954
|
failed += outcome.failed;
|
|
@@ -963,7 +963,8 @@ async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPat
|
|
|
963
963
|
}
|
|
964
964
|
}
|
|
965
965
|
const elapsed = Date.now() - t0;
|
|
966
|
-
|
|
966
|
+
if (process.env['CODE_MAPPER_VERBOSE'])
|
|
967
|
+
console.error(`Code Mapper: tsgo resolved ${resolved}/${eligible.length} calls in ${elapsed}ms (${failed} unresolvable, ${actualWorkers} process${actualWorkers > 1 ? 'es' : ''})`);
|
|
967
968
|
return results;
|
|
968
969
|
}
|
|
969
970
|
/** Generic method names that produce false edges when receiver type is unknown (worker-extracted path) */
|
|
@@ -1009,7 +1010,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
1009
1010
|
for (const [filePath, calls] of byFile) {
|
|
1010
1011
|
filesProcessed++;
|
|
1011
1012
|
if (filesProcessed % 25 === 0) {
|
|
1012
|
-
onProgress?.(filesProcessed, totalFiles);
|
|
1013
|
+
onProgress?.(filesProcessed, totalFiles, 1);
|
|
1013
1014
|
await yieldToEventLoop();
|
|
1014
1015
|
}
|
|
1015
1016
|
ctx.enableCache(filePath);
|
|
@@ -192,7 +192,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, opts) => {
|
|
|
192
192
|
percent: Math.round(parsingProgress),
|
|
193
193
|
message: `Parsing chunk ${chunkIdx + 1}/${numChunks}...`,
|
|
194
194
|
detail: filePath,
|
|
195
|
-
stats: { filesProcessed: globalCurrent, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
|
195
|
+
stats: { filesProcessed: globalCurrent, totalFiles: totalParseable, nodesCreated: graph.nodeCount, ...(workerPool ? { workerCount: workerPool.size } : {}) },
|
|
196
196
|
});
|
|
197
197
|
}, workerPool);
|
|
198
198
|
const parseMs = Date.now() - parseStart;
|
|
@@ -297,13 +297,13 @@ export const runPipelineFromRepo = async (repoPath, onProgress, opts) => {
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
try {
|
|
300
|
-
await processCallsFromExtracted(graph, allExtractedCalls, ctx, (current, total) => {
|
|
300
|
+
await processCallsFromExtracted(graph, allExtractedCalls, ctx, (current, total, workerCount) => {
|
|
301
301
|
const callPercent = 70 + Math.round((current / Math.max(total, 1)) * 12);
|
|
302
302
|
onProgress({
|
|
303
303
|
phase: 'calls',
|
|
304
304
|
percent: callPercent,
|
|
305
305
|
message: `Resolving calls: ${current}/${total} files...`,
|
|
306
|
-
stats: { filesProcessed: current, totalFiles: total, nodesCreated: graph.nodeCount },
|
|
306
|
+
stats: { filesProcessed: current, totalFiles: total, nodesCreated: graph.nodeCount, ...(workerCount ? { workerCount } : {}) },
|
|
307
307
|
});
|
|
308
308
|
}, allConstructorBindings.length > 0 ? allConstructorBindings : undefined, tsgoService, repoPath);
|
|
309
309
|
}
|
|
@@ -258,10 +258,10 @@ export class TsgoService {
|
|
|
258
258
|
this.process.stderr.on('data', (chunk) => {
|
|
259
259
|
const msg = chunk.toString().trim();
|
|
260
260
|
if (msg)
|
|
261
|
-
|
|
261
|
+
verbose('stderr:', msg);
|
|
262
262
|
});
|
|
263
263
|
this.process.on('exit', (code, signal) => {
|
|
264
|
-
|
|
264
|
+
verbose(`process exited (code=${code}, signal=${signal})`);
|
|
265
265
|
this.ready = false;
|
|
266
266
|
this.process = null;
|
|
267
267
|
});
|
|
@@ -284,7 +284,7 @@ export class TsgoService {
|
|
|
284
284
|
// Send initialized notification
|
|
285
285
|
this.send({ jsonrpc: '2.0', method: 'initialized', params: {} });
|
|
286
286
|
this.ready = true;
|
|
287
|
-
|
|
287
|
+
verbose('LSP ready');
|
|
288
288
|
return true;
|
|
289
289
|
}
|
|
290
290
|
catch (err) {
|
package/dist/types/pipeline.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zuvia-software-solutions/code-mapper",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.9",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|