@zuvia-software-solutions/code-mapper 2.3.6 → 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) => {
|
|
@@ -764,20 +765,39 @@ async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPat
|
|
|
764
765
|
}
|
|
765
766
|
list.push(call);
|
|
766
767
|
}
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
|
|
768
|
+
// Built-in receiver names that resolve to external types, not project code.
|
|
769
|
+
// tsgo always fails on these — skip them to avoid wasted LSP round-trips.
|
|
770
|
+
const BUILTIN_RECEIVERS = new Set([
|
|
771
|
+
'console', 'Math', 'JSON', 'Object', 'Array', 'String', 'Number', 'Boolean',
|
|
772
|
+
'Date', 'RegExp', 'Error', 'Promise', 'Map', 'Set', 'WeakMap', 'WeakSet',
|
|
773
|
+
'Buffer', 'process', 'globalThis', 'window', 'document', 'navigator',
|
|
774
|
+
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
|
|
775
|
+
'require', 'module', 'exports', '__dirname', '__filename',
|
|
776
|
+
'fs', 'path', 'os', 'url', 'util', 'crypto', 'http', 'https', 'net',
|
|
777
|
+
'child_process', 'stream', 'events', 'assert', 'zlib',
|
|
778
|
+
]);
|
|
779
|
+
// Pre-filter calls where tsgo won't add value:
|
|
780
|
+
// A. Free-form calls with unambiguous name — heuristic resolves perfectly
|
|
781
|
+
// B. Member calls with known receiver type AND unambiguous method — heuristic handles
|
|
782
|
+
// C. Member calls on built-in receivers — tsgo always fails on these
|
|
770
783
|
const tsgoEligible = [];
|
|
771
|
-
let
|
|
784
|
+
let skippedUnambiguous = 0;
|
|
785
|
+
const skippedKnownType = 0;
|
|
786
|
+
let skippedBuiltin = 0;
|
|
772
787
|
for (const call of eligible) {
|
|
788
|
+
// A. Free-form, unique name match
|
|
773
789
|
if (call.callForm === 'free' || call.callForm === undefined) {
|
|
774
790
|
const resolved = ctx.resolve(call.calledName, call.filePath);
|
|
775
|
-
// Unique match — heuristic handles this at high confidence
|
|
776
791
|
if (resolved && resolved.candidates.length === 1) {
|
|
777
|
-
|
|
792
|
+
skippedUnambiguous++;
|
|
778
793
|
continue;
|
|
779
794
|
}
|
|
780
795
|
}
|
|
796
|
+
// B. Built-in receiver — tsgo resolves to node_modules/lib.d.ts, never project code
|
|
797
|
+
if (call.callForm === 'member' && call.receiverName && BUILTIN_RECEIVERS.has(call.receiverName)) {
|
|
798
|
+
skippedBuiltin++;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
781
801
|
tsgoEligible.push(call);
|
|
782
802
|
}
|
|
783
803
|
// Regroup filtered calls by file
|
|
@@ -790,79 +810,160 @@ async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPat
|
|
|
790
810
|
}
|
|
791
811
|
list.push(call);
|
|
792
812
|
}
|
|
793
|
-
let resolved = 0;
|
|
794
|
-
let failed = 0;
|
|
795
813
|
const t0 = Date.now();
|
|
796
|
-
|
|
797
|
-
|
|
814
|
+
const skippedTotal = skippedUnambiguous + skippedKnownType + skippedBuiltin;
|
|
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;
|
|
798
835
|
const tsgoTotalFiles = tsgoByFile.size;
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
failed++;
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
// Match by exact startLine, then by range containment
|
|
820
|
-
let bestMatch;
|
|
821
|
-
for (const sym of targetSymbols) {
|
|
822
|
-
const node = graph.getNode(toNodeId(sym.nodeId));
|
|
823
|
-
if (node && node.properties.startLine === def.line) {
|
|
824
|
-
bestMatch = sym;
|
|
825
|
-
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;
|
|
826
853
|
}
|
|
827
|
-
|
|
828
|
-
|
|
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;
|
|
829
861
|
for (const sym of targetSymbols) {
|
|
830
862
|
const node = graph.getNode(toNodeId(sym.nodeId));
|
|
831
|
-
if (node) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
+
}
|
|
837
878
|
}
|
|
838
879
|
}
|
|
839
880
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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++;
|
|
846
896
|
}
|
|
847
|
-
const callKey = `${call.sourceId}\0${call.calledName}\0${call.callLine}`;
|
|
848
|
-
results.set(callKey, {
|
|
849
|
-
nodeId: bestMatch.nodeId,
|
|
850
|
-
confidence: TIER_CONFIDENCE['tsgo-resolved'],
|
|
851
|
-
reason: 'tsgo-lsp',
|
|
852
|
-
});
|
|
853
|
-
resolved++;
|
|
854
897
|
}
|
|
855
|
-
|
|
856
|
-
|
|
898
|
+
catch {
|
|
899
|
+
sliceFailed++;
|
|
857
900
|
}
|
|
858
901
|
}
|
|
859
|
-
|
|
860
|
-
|
|
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
|
+
}
|
|
947
|
+
}
|
|
861
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)));
|
|
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);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
finally {
|
|
960
|
+
// Stop extra services (the original is stopped by the caller)
|
|
961
|
+
for (const svc of extraServices)
|
|
962
|
+
svc.stop();
|
|
862
963
|
}
|
|
863
964
|
}
|
|
864
965
|
const elapsed = Date.now() - t0;
|
|
865
|
-
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' : ''})`);
|
|
866
967
|
return results;
|
|
867
968
|
}
|
|
868
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",
|