@zuvia-software-solutions/code-mapper 2.3.7 → 2.3.8
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
CHANGED
|
@@ -164,26 +164,27 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
164
164
|
const cpuPct = Math.round(((cpuDelta.user + cpuDelta.system) / 1e3) / wallMs * 100);
|
|
165
165
|
return `${rssMB}MB | CPU ${cpuPct}%`;
|
|
166
166
|
};
|
|
167
|
-
// Track elapsed time per phase
|
|
168
|
-
//
|
|
169
|
-
let
|
|
167
|
+
// Track elapsed time per BASE phase (without counts) so the timer
|
|
168
|
+
// doesn't reset every time the count updates
|
|
169
|
+
let lastBasePhase = 'Initializing...';
|
|
170
|
+
let lastFullLabel = 'Initializing...';
|
|
170
171
|
let phaseStart = Date.now();
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
172
|
+
const updateBar = (value, phaseLabel, basePhase) => {
|
|
173
|
+
const base = basePhase ?? phaseLabel;
|
|
174
|
+
if (base !== lastBasePhase) {
|
|
175
|
+
lastBasePhase = base;
|
|
175
176
|
phaseStart = Date.now();
|
|
176
177
|
}
|
|
178
|
+
lastFullLabel = phaseLabel;
|
|
177
179
|
const elapsed = Math.round((Date.now() - phaseStart) / 1000);
|
|
178
|
-
const display = elapsed >=
|
|
180
|
+
const display = elapsed >= 1 ? `${phaseLabel} (${elapsed}s)` : phaseLabel;
|
|
179
181
|
bar.update(value, { phase: display, resources: getResourceStats() });
|
|
180
182
|
};
|
|
181
183
|
// Tick elapsed seconds for phases with infrequent progress callbacks
|
|
182
|
-
// (e.g. CSV streaming, FTS indexing) — uses the same display format as updateBar
|
|
183
184
|
const elapsedTimer = setInterval(() => {
|
|
184
185
|
const elapsed = Math.round((Date.now() - phaseStart) / 1000);
|
|
185
|
-
if (elapsed >=
|
|
186
|
-
bar.update({ phase: `${
|
|
186
|
+
if (elapsed >= 1) {
|
|
187
|
+
bar.update({ phase: `${lastFullLabel} (${elapsed}s)`, resources: getResourceStats() });
|
|
187
188
|
}
|
|
188
189
|
}, 1000);
|
|
189
190
|
// Cache embeddings from existing index before rebuild
|
|
@@ -218,13 +219,13 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
218
219
|
recordPhase(progress.phase);
|
|
219
220
|
lastPipelinePhase = progress.phase;
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
const baseLabel = PHASE_LABELS[progress.phase] || progress.phase;
|
|
223
|
+
let phaseLabel = baseLabel;
|
|
224
|
+
if (progress.stats && progress.stats.totalFiles > 0) {
|
|
224
225
|
phaseLabel += ` (${progress.stats.filesProcessed.toLocaleString()}/${progress.stats.totalFiles.toLocaleString()})`;
|
|
225
226
|
}
|
|
226
227
|
const scaled = Math.round(progress.percent * 0.6);
|
|
227
|
-
updateBar(scaled, phaseLabel);
|
|
228
|
+
updateBar(scaled, phaseLabel, baseLabel);
|
|
228
229
|
}, options?.tsgo === false ? { tsgo: false } : {});
|
|
229
230
|
// Phase 2: SQLite (60-85%)
|
|
230
231
|
recordPhase('sqlite');
|
|
@@ -3,7 +3,7 @@ import type { KnowledgeGraph } from '../graph/types.js';
|
|
|
3
3
|
import type { ASTCache } from './ast-cache.js';
|
|
4
4
|
import type { ResolutionContext } from './resolution-context.js';
|
|
5
5
|
import type { ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
|
|
6
|
-
import
|
|
6
|
+
import { TsgoService } from '../semantic/tsgo-service.js';
|
|
7
7
|
export declare const processCalls: (graph: KnowledgeGraph, files: {
|
|
8
8
|
path: string;
|
|
9
9
|
content: string;
|
|
@@ -10,6 +10,7 @@ import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, F
|
|
|
10
10
|
import { buildTypeEnv } from './type-env.js';
|
|
11
11
|
import { getTreeSitterBufferSize } from './constants.js';
|
|
12
12
|
import { callRouters } from './call-routing.js';
|
|
13
|
+
import { TsgoService } from '../semantic/tsgo-service.js';
|
|
13
14
|
import path from 'node:path';
|
|
14
15
|
/** Walk up the AST to find the enclosing function/method, or null for top-level code */
|
|
15
16
|
const findEnclosingFunction = (node, filePath, ctx) => {
|
|
@@ -809,80 +810,160 @@ async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPat
|
|
|
809
810
|
}
|
|
810
811
|
list.push(call);
|
|
811
812
|
}
|
|
812
|
-
let resolved = 0;
|
|
813
|
-
let failed = 0;
|
|
814
813
|
const t0 = Date.now();
|
|
815
814
|
const skippedTotal = skippedUnambiguous + skippedKnownType + skippedBuiltin;
|
|
816
|
-
|
|
817
|
-
|
|
815
|
+
// Adaptive parallelism based on three constraints:
|
|
816
|
+
// 1. CPU: 75% of cores — parsing workers are done, leave 25% for Node.js event loop + OS
|
|
817
|
+
// 2. Memory: each tsgo loads the full project (~500MB estimate) — cap by free system memory
|
|
818
|
+
// 3. Workload: at least 50 files per process to amortize ~0.5s startup cost
|
|
819
|
+
const osModule = await import('os');
|
|
820
|
+
const cpuCount = osModule.cpus().length;
|
|
821
|
+
const freeMemGB = osModule.freemem() / (1024 * 1024 * 1024);
|
|
822
|
+
const maxByCpu = Math.max(1, Math.floor(cpuCount * 0.75));
|
|
823
|
+
const maxByMemory = Math.max(1, Math.floor(freeMemGB / 0.5));
|
|
824
|
+
const maxByWorkload = Math.max(1, Math.floor(tsgoByFile.size / 50));
|
|
825
|
+
const actualWorkers = Math.min(maxByCpu, maxByMemory, maxByWorkload);
|
|
826
|
+
console.error(`Code Mapper: tsgo resolving ${tsgoEligible.length} calls across ${tsgoByFile.size} files with ${actualWorkers} process${actualWorkers > 1 ? 'es' : ''} (skipped ${skippedTotal}: ${skippedUnambiguous} unambiguous, ${skippedKnownType} known-type, ${skippedBuiltin} builtin)...`);
|
|
827
|
+
// Split files round-robin across workers for balanced distribution
|
|
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]);
|
|
832
|
+
}
|
|
833
|
+
// Shared progress counter
|
|
834
|
+
let totalFilesProcessed = 0;
|
|
818
835
|
const tsgoTotalFiles = tsgoByFile.size;
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
failed++;
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
// Match by exact startLine, then by range containment
|
|
840
|
-
let bestMatch;
|
|
841
|
-
for (const sym of targetSymbols) {
|
|
842
|
-
const node = graph.getNode(toNodeId(sym.nodeId));
|
|
843
|
-
if (node && node.properties.startLine === def.line) {
|
|
844
|
-
bestMatch = sym;
|
|
845
|
-
break;
|
|
836
|
+
/** Resolve a slice of files using a single tsgo service */
|
|
837
|
+
const resolveSlice = async (service, slice) => {
|
|
838
|
+
const sliceResults = new Map();
|
|
839
|
+
let sliceResolved = 0;
|
|
840
|
+
let sliceFailed = 0;
|
|
841
|
+
for (const [filePath, calls] of slice) {
|
|
842
|
+
totalFilesProcessed++;
|
|
843
|
+
if (totalFilesProcessed % 25 === 0) {
|
|
844
|
+
onProgress?.(totalFilesProcessed, tsgoTotalFiles);
|
|
845
|
+
}
|
|
846
|
+
const absFilePath = path.resolve(repoPath, filePath);
|
|
847
|
+
for (const call of calls) {
|
|
848
|
+
try {
|
|
849
|
+
const def = await service.resolveDefinition(absFilePath, call.callLine - 1, call.callColumn);
|
|
850
|
+
if (!def) {
|
|
851
|
+
sliceFailed++;
|
|
852
|
+
continue;
|
|
846
853
|
}
|
|
847
|
-
|
|
848
|
-
|
|
854
|
+
const targetSymbols = ctx.symbols.lookupAllInFile(def.filePath);
|
|
855
|
+
if (targetSymbols.length === 0) {
|
|
856
|
+
sliceFailed++;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
// Match by exact startLine, then by range containment
|
|
860
|
+
let bestMatch;
|
|
849
861
|
for (const sym of targetSymbols) {
|
|
850
862
|
const node = graph.getNode(toNodeId(sym.nodeId));
|
|
851
|
-
if (node) {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
863
|
+
if (node && node.properties.startLine === def.line) {
|
|
864
|
+
bestMatch = sym;
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (!bestMatch) {
|
|
869
|
+
for (const sym of targetSymbols) {
|
|
870
|
+
const node = graph.getNode(toNodeId(sym.nodeId));
|
|
871
|
+
if (node) {
|
|
872
|
+
const sl = node.properties.startLine;
|
|
873
|
+
const el = node.properties.endLine;
|
|
874
|
+
if (sl !== undefined && el !== undefined && def.line >= sl && def.line <= el) {
|
|
875
|
+
bestMatch = sym;
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
857
878
|
}
|
|
858
879
|
}
|
|
859
880
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
881
|
+
if (bestMatch) {
|
|
882
|
+
if (bestMatch.nodeId === call.sourceId) {
|
|
883
|
+
sliceFailed++;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const callKey = `${call.sourceId}\0${call.calledName}\0${call.callLine}`;
|
|
887
|
+
sliceResults.set(callKey, {
|
|
888
|
+
nodeId: bestMatch.nodeId,
|
|
889
|
+
confidence: TIER_CONFIDENCE['tsgo-resolved'],
|
|
890
|
+
reason: 'tsgo-lsp',
|
|
891
|
+
});
|
|
892
|
+
sliceResolved++;
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
sliceFailed++;
|
|
866
896
|
}
|
|
867
|
-
const callKey = `${call.sourceId}\0${call.calledName}\0${call.callLine}`;
|
|
868
|
-
results.set(callKey, {
|
|
869
|
-
nodeId: bestMatch.nodeId,
|
|
870
|
-
confidence: TIER_CONFIDENCE['tsgo-resolved'],
|
|
871
|
-
reason: 'tsgo-lsp',
|
|
872
|
-
});
|
|
873
|
-
resolved++;
|
|
874
897
|
}
|
|
875
|
-
|
|
876
|
-
|
|
898
|
+
catch {
|
|
899
|
+
sliceFailed++;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return { resolved: sliceResolved, failed: sliceFailed, results: sliceResults };
|
|
904
|
+
};
|
|
905
|
+
let resolved = 0;
|
|
906
|
+
let failed = 0;
|
|
907
|
+
if (actualWorkers === 1) {
|
|
908
|
+
// Single process — use the existing service (already started)
|
|
909
|
+
const outcome = await resolveSlice(tsgoService, fileEntries);
|
|
910
|
+
resolved = outcome.resolved;
|
|
911
|
+
failed = outcome.failed;
|
|
912
|
+
for (const [k, v] of outcome.results)
|
|
913
|
+
results.set(k, v);
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
// Parallel — spawn extra services, keep the original for slice 0
|
|
917
|
+
const extraServices = [];
|
|
918
|
+
try {
|
|
919
|
+
// Start extra tsgo processes in parallel
|
|
920
|
+
const startPromises = [];
|
|
921
|
+
for (let i = 1; i < actualWorkers; i++) {
|
|
922
|
+
startPromises.push((async () => {
|
|
923
|
+
const svc = new TsgoService(repoPath);
|
|
924
|
+
if (await svc.start())
|
|
925
|
+
return svc;
|
|
926
|
+
return null;
|
|
927
|
+
})());
|
|
928
|
+
}
|
|
929
|
+
const started = await Promise.all(startPromises);
|
|
930
|
+
for (const svc of started) {
|
|
931
|
+
if (svc)
|
|
932
|
+
extraServices.push(svc);
|
|
933
|
+
}
|
|
934
|
+
// Build final service list: original + extras that started successfully
|
|
935
|
+
const services = [tsgoService, ...extraServices];
|
|
936
|
+
const activeSlices = workerSlices.slice(0, services.length);
|
|
937
|
+
// If some services failed to start, redistribute their slices
|
|
938
|
+
if (services.length < actualWorkers) {
|
|
939
|
+
for (let i = services.length; i < actualWorkers; i++) {
|
|
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
|
+
}
|
|
877
947
|
}
|
|
878
948
|
}
|
|
879
|
-
|
|
880
|
-
|
|
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)));
|
|
952
|
+
for (const outcome of outcomes) {
|
|
953
|
+
resolved += outcome.resolved;
|
|
954
|
+
failed += outcome.failed;
|
|
955
|
+
for (const [k, v] of outcome.results)
|
|
956
|
+
results.set(k, v);
|
|
881
957
|
}
|
|
882
958
|
}
|
|
959
|
+
finally {
|
|
960
|
+
// Stop extra services (the original is stopped by the caller)
|
|
961
|
+
for (const svc of extraServices)
|
|
962
|
+
svc.stop();
|
|
963
|
+
}
|
|
883
964
|
}
|
|
884
965
|
const elapsed = Date.now() - t0;
|
|
885
|
-
console.error(`Code Mapper: tsgo resolved ${resolved}/${eligible.length} calls in ${elapsed}ms (${failed} unresolvable)`);
|
|
966
|
+
console.error(`Code Mapper: tsgo resolved ${resolved}/${eligible.length} calls in ${elapsed}ms (${failed} unresolvable, ${actualWorkers} process${actualWorkers > 1 ? 'es' : ''})`);
|
|
886
967
|
return results;
|
|
887
968
|
}
|
|
888
969
|
/** Generic method names that produce false edges when receiver type is unknown (worker-extracted path) */
|
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.8",
|
|
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",
|