@zuvia-software-solutions/code-mapper 2.6.0 → 2.6.2

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.
@@ -273,59 +273,73 @@ export const analyzeCommand = async (inputPath, options) => {
273
273
  recordPhase('refs');
274
274
  updateBar(85, 'Building refs index...');
275
275
  {
276
- const { clearRefs, insertRefsBatch, clearFileWords, upsertFileWords } = await import('../core/db/adapter.js');
277
- const fsRef = await import('fs/promises');
276
+ const { clearRefs, clearFileWords, upsertFileWords } = await import('../core/db/adapter.js');
277
+ const fsSync = await import('fs');
278
278
  clearRefs(db);
279
279
  clearFileWords(db);
280
- // Scan all source files for identifier occurrences
281
280
  const STOP_WORDS = new Set(['the', 'and', 'for', 'from', 'with', 'this', 'that', 'have', 'has', 'not', 'are', 'was', 'were', 'been', 'being', 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'does', 'did', 'let', 'var', 'const', 'new', 'return', 'function', 'class', 'import', 'export', 'default', 'void', 'null', 'undefined', 'true', 'false', 'else', 'case', 'break', 'continue', 'while', 'throw', 'catch', 'try', 'finally', 'async', 'await', 'yield', 'typeof', 'instanceof', 'delete', 'switch', 'interface', 'type', 'enum', 'extends', 'implements', 'static', 'private', 'public', 'protected', 'abstract', 'readonly', 'override', 'declare', 'module', 'namespace', 'require', 'string', 'number', 'boolean', 'object', 'any', 'never', 'unknown', 'symbol']);
282
281
  const SRC_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java', '.c', '.h', '.cpp', '.hpp', '.cs', '.rb', '.php', '.kt', '.swift', '.mts', '.mjs', '.cts', '.cjs']);
283
282
  const identRegex = /\b[a-zA-Z_]\w{2,}\b/g;
284
283
  const wordRegex = /\b[a-zA-Z]\w{2,}\b/g;
285
- // Get all file paths from the nodes table
286
284
  const fileRows = db.prepare("SELECT DISTINCT filePath FROM nodes WHERE label = 'File'").all();
287
- let refsBuilt = 0;
288
- for (const { filePath } of fileRows) {
289
- const ext = path.extname(filePath).toLowerCase();
290
- if (!SRC_EXTENSIONS.has(ext))
291
- continue;
292
- let content;
293
- try {
294
- content = await fsRef.readFile(path.resolve(repoPath, filePath), 'utf-8');
295
- }
296
- catch {
297
- continue;
298
- }
299
- // Build refs (identifier occurrences — skip language keywords)
300
- const refs = [];
301
- const lines = content.split('\n');
302
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
303
- let match;
285
+ // Single transaction for all refs + file_words — avoids per-file transaction overhead
286
+ const refsStmt = db.prepare('INSERT INTO refs (symbol, filePath, line) VALUES (?, ?, ?)');
287
+ const tx = db.transaction(() => {
288
+ let refsBuilt = 0;
289
+ for (const { filePath } of fileRows) {
290
+ const ext = path.extname(filePath).toLowerCase();
291
+ if (!SRC_EXTENSIONS.has(ext))
292
+ continue;
293
+ let content;
294
+ try {
295
+ content = fsSync.readFileSync(path.resolve(repoPath, filePath), 'utf-8');
296
+ }
297
+ catch {
298
+ continue;
299
+ }
300
+ // Pre-build line offset table for O(1) line lookups
301
+ const lineOffsets = [0];
302
+ for (let i = 0; i < content.length; i++) {
303
+ if (content.charCodeAt(i) === 10)
304
+ lineOffsets.push(i + 1);
305
+ }
306
+ const getLine = (offset) => {
307
+ let lo = 0, hi = lineOffsets.length - 1;
308
+ while (lo < hi) {
309
+ const mid = (lo + hi + 1) >> 1;
310
+ if (lineOffsets[mid] <= offset)
311
+ lo = mid;
312
+ else
313
+ hi = mid - 1;
314
+ }
315
+ return lo;
316
+ };
317
+ // Refs: regex over whole content with binary-search line lookup
304
318
  identRegex.lastIndex = 0;
305
- while ((match = identRegex.exec(lines[lineIdx])) !== null) {
319
+ let match;
320
+ while ((match = identRegex.exec(content)) !== null) {
306
321
  if (!STOP_WORDS.has(match[0].toLowerCase())) {
307
- refs.push({ symbol: match[0], filePath, line: lineIdx });
322
+ refsStmt.run(match[0], filePath, getLine(match.index));
308
323
  }
309
324
  }
325
+ // File words for conceptual search
326
+ const wordSet = new Set();
327
+ wordRegex.lastIndex = 0;
328
+ let wMatch;
329
+ while ((wMatch = wordRegex.exec(content)) !== null) {
330
+ const w = wMatch[0].toLowerCase();
331
+ if (!STOP_WORDS.has(w))
332
+ wordSet.add(w);
333
+ }
334
+ if (wordSet.size > 0)
335
+ upsertFileWords(db, filePath, [...wordSet].join(' '));
336
+ refsBuilt++;
337
+ if (refsBuilt % 500 === 0) {
338
+ updateBar(85, `Building refs index... (${refsBuilt}/${fileRows.length})`);
339
+ }
310
340
  }
311
- if (refs.length > 0)
312
- insertRefsBatch(db, refs);
313
- // Build file_words (conceptual search)
314
- const wordSet = new Set();
315
- let wMatch;
316
- wordRegex.lastIndex = 0;
317
- while ((wMatch = wordRegex.exec(content)) !== null) {
318
- const w = wMatch[0].toLowerCase();
319
- if (!STOP_WORDS.has(w))
320
- wordSet.add(w);
321
- }
322
- if (wordSet.size > 0)
323
- upsertFileWords(db, filePath, [...wordSet].join(' '));
324
- refsBuilt++;
325
- if (refsBuilt % 500 === 0) {
326
- updateBar(85, `Building refs index... (${refsBuilt}/${fileRows.length})`);
327
- }
328
- }
341
+ });
342
+ tx();
329
343
  }
330
344
  // Phase 3: FTS (85-90%)
331
345
  // FTS5 is auto-created by schema triggers — no manual index creation needed
@@ -766,6 +766,42 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
766
766
  }
767
767
  list.push(call);
768
768
  }
769
+ // Pre-build indexes for O(1) lookups in the hot loop.
770
+ // Without these, interface dispatch does graph.relationships.filter() per call = O(calls × edges).
771
+ const implementsIndex = new Map(); // interfaceNodeId → [classNodeId, ...]
772
+ const hasMethodIndex = new Map(); // classNodeId → methods
773
+ const reverseImportIndex = new Map(); // importedFile → [importingFile, ...]
774
+ for (const rel of graph.iterRelationships()) {
775
+ if (rel.type === 'IMPLEMENTS') {
776
+ let arr = implementsIndex.get(rel.targetId);
777
+ if (!arr) {
778
+ arr = [];
779
+ implementsIndex.set(rel.targetId, arr);
780
+ }
781
+ arr.push(rel.sourceId);
782
+ }
783
+ else if (rel.type === 'HAS_METHOD') {
784
+ const methodNode = graph.getNode(rel.targetId);
785
+ if (methodNode) {
786
+ let arr = hasMethodIndex.get(rel.sourceId);
787
+ if (!arr) {
788
+ arr = [];
789
+ hasMethodIndex.set(rel.sourceId, arr);
790
+ }
791
+ arr.push({ targetId: rel.targetId, name: methodNode.properties.name });
792
+ }
793
+ }
794
+ }
795
+ for (const [file, importedFiles] of ctx.importMap) {
796
+ for (const imported of importedFiles) {
797
+ let arr = reverseImportIndex.get(imported);
798
+ if (!arr) {
799
+ arr = [];
800
+ reverseImportIndex.set(imported, arr);
801
+ }
802
+ arr.push(file);
803
+ }
804
+ }
769
805
  const totalFiles = byFile.size;
770
806
  let filesProcessed = 0;
771
807
  for (const [filePath, calls] of byFile) {
@@ -825,38 +861,29 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
825
861
  const interfaceDefs = receiverResolved.candidates.filter(d => d.type === 'Interface' || d.type === 'Trait');
826
862
  if (interfaceDefs.length > 0) {
827
863
  for (const ifaceDef of interfaceDefs) {
828
- // Strategy 1: Class-based — find IMPLEMENTS edges
829
- const implRows = graph.relationships.filter(r => r.type === 'IMPLEMENTS' && r.targetId === toNodeId(ifaceDef.nodeId));
830
- for (const implRel of implRows) {
831
- const methodEdges = graph.relationships.filter(r => r.type === 'HAS_METHOD' && r.sourceId === implRel.sourceId);
832
- for (const methodEdge of methodEdges) {
833
- const methodNode = graph.getNode(methodEdge.targetId);
834
- if (methodNode && methodNode.properties.name === effectiveCall.calledName) {
835
- resolved = { nodeId: methodEdge.targetId, confidence: 0.85, reason: 'interface-dispatch' };
836
- break;
864
+ // Strategy 1: Class-based — find IMPLEMENTS edges via pre-built index
865
+ const implClassIds = implementsIndex.get(toNodeId(ifaceDef.nodeId));
866
+ if (implClassIds) {
867
+ for (const classId of implClassIds) {
868
+ const methods = hasMethodIndex.get(classId);
869
+ if (methods) {
870
+ const match = methods.find(m => m.name === effectiveCall.calledName);
871
+ if (match) {
872
+ resolved = { nodeId: match.targetId, confidence: 0.85, reason: 'interface-dispatch' };
873
+ break;
874
+ }
837
875
  }
838
876
  }
839
- if (resolved)
840
- break;
841
877
  }
842
878
  if (resolved)
843
879
  break;
844
880
  // Strategy 2: Factory/closure pattern — find functions returning this interface
845
- // e.g. createEventBus(): EventBus → the closure has emit/subscribe as inner functions
846
- // Look for the method name in files that import the interface's file
847
881
  if (!resolved) {
848
882
  const methodName = effectiveCall.calledName;
849
883
  const ifaceFile = ifaceDef.filePath;
850
- // Check: which files import the interface's file?
851
- const importingFiles = [];
852
- for (const [file, importedFiles] of ctx.importMap) {
853
- if (importedFiles.has(ifaceFile)) {
854
- importingFiles.push(file);
855
- }
856
- }
884
+ const importingFiles = reverseImportIndex.get(ifaceFile) ?? [];
857
885
  // Also check the interface's own file
858
- importingFiles.push(ifaceFile);
859
- for (const file of importingFiles) {
886
+ for (const file of [...importingFiles, ifaceFile]) {
860
887
  const method = ctx.symbols.lookupExactFull(file, methodName);
861
888
  if (method && (method.type === 'Function' || method.type === 'Method')) {
862
889
  resolved = { nodeId: method.nodeId, confidence: 0.75, reason: 'interface-factory-dispatch' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuvia-software-solutions/code-mapper",
3
- "version": "2.6.0",
3
+ "version": "2.6.2",
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",