@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.
- package/dist/cli/analyze.js +55 -41
- package/dist/core/ingestion/call-processor.js +49 -22
- package/package.json +1 -1
package/dist/cli/analyze.js
CHANGED
|
@@ -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,
|
|
277
|
-
const
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
319
|
+
let match;
|
|
320
|
+
while ((match = identRegex.exec(content)) !== null) {
|
|
306
321
|
if (!STOP_WORDS.has(match[0].toLowerCase())) {
|
|
307
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|