@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.
@@ -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 both updateBar and the interval use
168
- // the same format so they don't flicker against each other
169
- let lastPhaseLabel = 'Initializing...';
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
- // Update bar with phase label + elapsed seconds (shown after 3s)
172
- const updateBar = (value, phaseLabel) => {
173
- if (phaseLabel !== lastPhaseLabel) {
174
- lastPhaseLabel = phaseLabel;
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 >= 3 ? `${phaseLabel} (${elapsed}s)` : phaseLabel;
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 >= 3) {
186
- bar.update({ phase: `${lastPhaseLabel} (${elapsed}s)`, resources: getResourceStats() });
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
- let phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
222
- if (progress.stats && progress.stats.totalFiles > 0 &&
223
- (progress.phase === 'parsing' || progress.phase === 'extracting' || progress.phase === 'calls')) {
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 type { TsgoService } from '../semantic/tsgo-service.js';
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
- // Pre-filter: skip free-form calls ONLY when the function name is unambiguous
768
- // in the symbol table. Heuristic resolves unique names perfectly.
769
- // Ambiguous names (multiple symbols with same name) need tsgo for disambiguation.
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 skippedHeuristic = 0;
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
- skippedHeuristic++;
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
- console.error(`Code Mapper: tsgo resolving ${tsgoEligible.length} calls across ${tsgoByFile.size} files (skipped ${skippedHeuristic} heuristic-resolvable)...`);
797
- let tsgoFilesProcessed = 0;
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
- for (const [filePath, calls] of tsgoByFile) {
800
- tsgoFilesProcessed++;
801
- if (tsgoFilesProcessed % 25 === 0) {
802
- onProgress?.(tsgoFilesProcessed, tsgoTotalFiles);
803
- await yieldToEventLoop();
804
- }
805
- const absFilePath = path.resolve(repoPath, filePath);
806
- // Sequential LSP requests tsgo processes over stdio, concurrent floods cause hangs
807
- for (const call of calls) {
808
- try {
809
- const def = await tsgoService.resolveDefinition(absFilePath, call.callLine - 1, call.callColumn);
810
- if (!def) {
811
- failed++;
812
- continue;
813
- }
814
- const targetSymbols = ctx.symbols.lookupAllInFile(def.filePath);
815
- if (targetSymbols.length === 0) {
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
- if (!bestMatch) {
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
- const sl = node.properties.startLine;
833
- const el = node.properties.endLine;
834
- if (sl !== undefined && el !== undefined && def.line >= sl && def.line <= el) {
835
- bestMatch = sym;
836
- break;
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
- if (bestMatch) {
842
- // Drop self-referencing tsgo edges
843
- if (bestMatch.nodeId === call.sourceId) {
844
- failed++;
845
- continue;
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
- else {
856
- failed++;
898
+ catch {
899
+ sliceFailed++;
857
900
  }
858
901
  }
859
- catch {
860
- failed++;
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.6",
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",