@zuvia-software-solutions/code-mapper 2.2.3 → 2.3.0

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.
@@ -5,7 +5,7 @@ import { execFileSync } from 'child_process';
5
5
  import v8 from 'v8';
6
6
  import cliProgress from 'cli-progress';
7
7
  import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
8
- import { openDb, closeDb, resetDb, getStats, insertEmbeddingsBatch, countEmbeddings } from '../core/db/adapter.js';
8
+ import { openDb, closeDb, resetDb, getStats, insertEmbeddingsBatch, countEmbeddings, populateSearchText } from '../core/db/adapter.js';
9
9
  import { loadGraphToDb } from '../core/db/graph-loader.js';
10
10
  import { stitchRoutes } from '../core/ingestion/route-stitcher.js';
11
11
  import { toNodeId } from '../core/db/schema.js';
@@ -200,6 +200,10 @@ export const analyzeCommand = async (inputPath, options) => {
200
200
  const dbWarnings = dbResult.warnings;
201
201
  // Phase 2.5: HTTP route stitching (post-DB-load, needs content field)
202
202
  stitchRoutes(db);
203
+ // Phase 2.6: Populate searchText for BM25 concept matching
204
+ // Uses first comment + callers + module — must run after edges are loaded
205
+ updateBar(84, 'Building search index...');
206
+ populateSearchText(db);
203
207
  // Phase 3: FTS (85-90%)
204
208
  // FTS5 is auto-created by schema triggers — no manual index creation needed
205
209
  updateBar(85, 'Search indexes ready');
@@ -90,6 +90,12 @@ export declare function getStats(db: Database.Database): {
90
90
  export declare function insertNodesBatch(db: Database.Database, nodes: readonly NodeInsert[]): void;
91
91
  /** Batch insert edges in a single transaction. */
92
92
  export declare function insertEdgesBatch(db: Database.Database, edges: readonly EdgeInsert[]): void;
93
+ /**
94
+ * Populate the searchText column for all nodes with semantic summaries.
95
+ * Uses first comment + callers + module to enable BM25 concept matching.
96
+ * Call AFTER edges are loaded (needs CALLS and MEMBER_OF edges).
97
+ */
98
+ export declare function populateSearchText(db: Database.Database): void;
93
99
  /** Batch insert embeddings in a single transaction. */
94
100
  export declare function insertEmbeddingsBatch(db: Database.Database, items: readonly {
95
101
  nodeId: NodeId;
@@ -145,12 +145,12 @@ const INSERT_NODE_SQL = `
145
145
  id, label, name, filePath, startLine, endLine, isExported, content, description,
146
146
  heuristicLabel, cohesion, symbolCount, keywords, enrichedBy,
147
147
  processType, stepCount, communities, entryPointId, terminalId,
148
- parameterCount, returnType, nameExpanded
148
+ parameterCount, returnType, nameExpanded, searchText
149
149
  ) VALUES (
150
150
  @id, @label, @name, @filePath, @startLine, @endLine, @isExported, @content, @description,
151
151
  @heuristicLabel, @cohesion, @symbolCount, @keywords, @enrichedBy,
152
152
  @processType, @stepCount, @communities, @entryPointId, @terminalId,
153
- @parameterCount, @returnType, @nameExpanded
153
+ @parameterCount, @returnType, @nameExpanded, @searchText
154
154
  )
155
155
  `;
156
156
  /** Insert or replace a node. Automatically expands name for FTS natural language matching. */
@@ -178,6 +178,7 @@ export function insertNode(db, node) {
178
178
  parameterCount: node.parameterCount ?? null,
179
179
  returnType: node.returnType ?? null,
180
180
  nameExpanded: node.nameExpanded ?? expandIdentifier(node.name ?? ''),
181
+ searchText: node.searchText ?? '',
181
182
  });
182
183
  }
183
184
  /** Get a node by ID. Returns undefined if not found. */
@@ -373,6 +374,7 @@ export function insertNodesBatch(db, nodes) {
373
374
  terminalId: node.terminalId ?? null, parameterCount: node.parameterCount ?? null,
374
375
  returnType: node.returnType ?? null,
375
376
  nameExpanded: node.nameExpanded ?? expandIdentifier(node.name ?? ''),
377
+ searchText: node.searchText ?? '',
376
378
  });
377
379
  }
378
380
  });
@@ -392,6 +394,104 @@ export function insertEdgesBatch(db, edges) {
392
394
  });
393
395
  txn(edges);
394
396
  }
397
+ /**
398
+ * Populate the searchText column for all nodes with semantic summaries.
399
+ * Uses first comment + callers + module to enable BM25 concept matching.
400
+ * Call AFTER edges are loaded (needs CALLS and MEMBER_OF edges).
401
+ */
402
+ export function populateSearchText(db) {
403
+ // Extract first comment from content
404
+ function extractComment(content) {
405
+ if (!content)
406
+ return '';
407
+ const lines = content.split('\n');
408
+ const out = [];
409
+ let inBlock = false;
410
+ for (const l of lines) {
411
+ const t = l.trim();
412
+ if (t.startsWith('/**') || t.startsWith('/*')) {
413
+ inBlock = true;
414
+ const inner = t.replace(/^\/\*\*?\s*/, '').replace(/\*\/\s*$/, '').trim();
415
+ if (inner && !inner.startsWith('@'))
416
+ out.push(inner);
417
+ if (t.includes('*/'))
418
+ inBlock = false;
419
+ continue;
420
+ }
421
+ if (inBlock) {
422
+ if (t.includes('*/')) {
423
+ inBlock = false;
424
+ continue;
425
+ }
426
+ const inner = t.replace(/^\*\s?/, '').trim();
427
+ if (inner && !inner.startsWith('@'))
428
+ out.push(inner);
429
+ if (out.length >= 3)
430
+ break;
431
+ continue;
432
+ }
433
+ if (t.startsWith('//')) {
434
+ const inner = t.slice(2).trim();
435
+ if (inner)
436
+ out.push(inner);
437
+ if (out.length >= 3)
438
+ break;
439
+ continue;
440
+ }
441
+ if (out.length > 0)
442
+ break;
443
+ }
444
+ return out.join(' ');
445
+ }
446
+ const nodes = db.prepare("SELECT id, name, nameExpanded, content FROM nodes WHERE label IN ('Function','Class','Method','Interface','Const','TypeAlias','Enum')").all();
447
+ if (nodes.length === 0)
448
+ return;
449
+ // Batch fetch callers + module
450
+ const callerMap = new Map();
451
+ const moduleMap = new Map();
452
+ const ids = nodes.map(n => n.id);
453
+ for (let i = 0; i < ids.length; i += 900) {
454
+ const chunk = ids.slice(i, i + 900);
455
+ const ph = chunk.map(() => '?').join(',');
456
+ const callerRows = db.prepare(`SELECT e.targetId AS nid, n.name FROM edges e JOIN nodes n ON n.id = e.sourceId WHERE e.targetId IN (${ph}) AND e.type = 'CALLS' AND e.confidence >= 0.7`).all(...chunk);
457
+ for (const r of callerRows) {
458
+ if (!callerMap.has(r.nid))
459
+ callerMap.set(r.nid, []);
460
+ callerMap.get(r.nid).push(r.name);
461
+ }
462
+ const modRows = db.prepare(`SELECT e.sourceId AS nid, c.heuristicLabel AS module FROM edges e JOIN nodes c ON c.id = e.targetId WHERE e.sourceId IN (${ph}) AND e.type = 'MEMBER_OF' AND c.label = 'Community'`).all(...chunk);
463
+ for (const r of modRows)
464
+ moduleMap.set(r.nid, r.module);
465
+ }
466
+ // Build searchText and update
467
+ // Drop FTS triggers temporarily to avoid column-count issues during bulk update,
468
+ // then rebuild the FTS index in one pass (faster than per-row trigger updates)
469
+ db.exec("DROP TRIGGER IF EXISTS nodes_fts_au");
470
+ const txn = db.transaction(() => {
471
+ for (const node of nodes) {
472
+ const parts = [];
473
+ if (node.nameExpanded)
474
+ parts.push(node.nameExpanded);
475
+ const comment = extractComment(node.content);
476
+ if (comment)
477
+ parts.push(comment);
478
+ const callers = callerMap.get(node.id)?.slice(0, 5);
479
+ if (callers && callers.length > 0)
480
+ parts.push(callers.map(c => expandIdentifier(c)).join(' '));
481
+ const mod = moduleMap.get(node.id);
482
+ if (mod)
483
+ parts.push(mod);
484
+ db.prepare('UPDATE nodes SET searchText = ? WHERE id = ?').run(parts.join(' | '), node.id);
485
+ }
486
+ });
487
+ txn();
488
+ // Rebuild FTS index from scratch and recreate the trigger
489
+ db.exec("INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild')");
490
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_fts_au AFTER UPDATE ON nodes BEGIN
491
+ INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, searchText, filePath, content) VALUES ('delete', old.rowid, old.name, old.nameExpanded, old.searchText, old.filePath, old.content);
492
+ INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, searchText, filePath, content) VALUES (new.rowid, new.name, new.nameExpanded, new.searchText, new.filePath, new.content);
493
+ END`);
494
+ }
395
495
  /** Batch insert embeddings in a single transaction. */
396
496
  export function insertEmbeddingsBatch(db, items) {
397
497
  const stmt = db.prepare('INSERT OR REPLACE INTO embeddings (nodeId, embedding, textHash) VALUES (?, ?, ?)');
@@ -49,6 +49,7 @@ export interface NodeRow {
49
49
  readonly parameterCount: number | null;
50
50
  readonly returnType: string | null;
51
51
  readonly nameExpanded: string;
52
+ readonly searchText: string;
52
53
  }
53
54
  /** An edge row as stored in the `edges` table */
54
55
  export interface EdgeRow {
@@ -91,6 +92,7 @@ export interface NodeInsert {
91
92
  readonly parameterCount?: number | null;
92
93
  readonly returnType?: string | null;
93
94
  readonly nameExpanded?: string;
95
+ readonly searchText?: string;
94
96
  }
95
97
  /** Fields required to insert an edge */
96
98
  export interface EdgeInsert {
@@ -105,4 +107,4 @@ export interface EdgeInsert {
105
107
  }
106
108
  /** Legacy edge table name constant (kept for compatibility) */
107
109
  export declare const REL_TABLE_NAME = "CodeRelation";
108
- export declare const SCHEMA_SQL = "\n-- Nodes: unified table for all code elements\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n label TEXT NOT NULL,\n name TEXT NOT NULL DEFAULT '',\n filePath TEXT NOT NULL DEFAULT '',\n startLine INTEGER,\n endLine INTEGER,\n isExported INTEGER,\n content TEXT NOT NULL DEFAULT '',\n description TEXT NOT NULL DEFAULT '',\n heuristicLabel TEXT,\n cohesion REAL,\n symbolCount INTEGER,\n keywords TEXT,\n enrichedBy TEXT,\n processType TEXT,\n stepCount INTEGER,\n communities TEXT,\n entryPointId TEXT,\n terminalId TEXT,\n parameterCount INTEGER,\n returnType TEXT,\n nameExpanded TEXT DEFAULT ''\n);\n\nCREATE INDEX IF NOT EXISTS idx_nodes_label ON nodes(label);\nCREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);\nCREATE INDEX IF NOT EXISTS idx_nodes_filePath ON nodes(filePath);\nCREATE INDEX IF NOT EXISTS idx_nodes_label_name ON nodes(label, name);\nCREATE INDEX IF NOT EXISTS idx_nodes_filePath_lines ON nodes(filePath, startLine, endLine);\n\n-- Edges: single table for all relationships\nCREATE TABLE IF NOT EXISTS edges (\n id TEXT PRIMARY KEY,\n sourceId TEXT NOT NULL,\n targetId TEXT NOT NULL,\n type TEXT NOT NULL,\n confidence REAL NOT NULL DEFAULT 1.0,\n reason TEXT NOT NULL DEFAULT '',\n step INTEGER NOT NULL DEFAULT 0,\n callLine INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_edges_sourceId ON edges(sourceId);\nCREATE INDEX IF NOT EXISTS idx_edges_targetId ON edges(targetId);\nCREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type);\nCREATE INDEX IF NOT EXISTS idx_edges_source_type ON edges(sourceId, type);\nCREATE INDEX IF NOT EXISTS idx_edges_target_type ON edges(targetId, type);\n\n-- Embeddings: vector storage\nCREATE TABLE IF NOT EXISTS embeddings (\n nodeId TEXT PRIMARY KEY,\n embedding BLOB NOT NULL,\n textHash TEXT\n);\n\n-- FTS5 virtual table (auto-updated via triggers)\nCREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(\n name,\n nameExpanded,\n filePath,\n content,\n content='nodes',\n content_rowid='rowid'\n);\n\nCREATE TRIGGER IF NOT EXISTS nodes_fts_ai AFTER INSERT ON nodes BEGIN\n INSERT INTO nodes_fts(rowid, name, nameExpanded, filePath, content) VALUES (new.rowid, new.name, new.nameExpanded, new.filePath, new.content);\nEND;\nCREATE TRIGGER IF NOT EXISTS nodes_fts_ad AFTER DELETE ON nodes BEGIN\n INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, filePath, content) VALUES ('delete', old.rowid, old.name, old.nameExpanded, old.filePath, old.content);\nEND;\nCREATE TRIGGER IF NOT EXISTS nodes_fts_au AFTER UPDATE ON nodes BEGIN\n INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, filePath, content) VALUES ('delete', old.rowid, old.name, old.nameExpanded, old.filePath, old.content);\n INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, filePath, content) VALUES (new.rowid, new.name, new.nameExpanded, new.filePath, new.content);\nEND;\n";
110
+ export declare const SCHEMA_SQL = "\n-- Nodes: unified table for all code elements\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n label TEXT NOT NULL,\n name TEXT NOT NULL DEFAULT '',\n filePath TEXT NOT NULL DEFAULT '',\n startLine INTEGER,\n endLine INTEGER,\n isExported INTEGER,\n content TEXT NOT NULL DEFAULT '',\n description TEXT NOT NULL DEFAULT '',\n heuristicLabel TEXT,\n cohesion REAL,\n symbolCount INTEGER,\n keywords TEXT,\n enrichedBy TEXT,\n processType TEXT,\n stepCount INTEGER,\n communities TEXT,\n entryPointId TEXT,\n terminalId TEXT,\n parameterCount INTEGER,\n returnType TEXT,\n nameExpanded TEXT DEFAULT '',\n searchText TEXT DEFAULT ''\n);\n\nCREATE INDEX IF NOT EXISTS idx_nodes_label ON nodes(label);\nCREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);\nCREATE INDEX IF NOT EXISTS idx_nodes_filePath ON nodes(filePath);\nCREATE INDEX IF NOT EXISTS idx_nodes_label_name ON nodes(label, name);\nCREATE INDEX IF NOT EXISTS idx_nodes_filePath_lines ON nodes(filePath, startLine, endLine);\n\n-- Edges: single table for all relationships\nCREATE TABLE IF NOT EXISTS edges (\n id TEXT PRIMARY KEY,\n sourceId TEXT NOT NULL,\n targetId TEXT NOT NULL,\n type TEXT NOT NULL,\n confidence REAL NOT NULL DEFAULT 1.0,\n reason TEXT NOT NULL DEFAULT '',\n step INTEGER NOT NULL DEFAULT 0,\n callLine INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_edges_sourceId ON edges(sourceId);\nCREATE INDEX IF NOT EXISTS idx_edges_targetId ON edges(targetId);\nCREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type);\nCREATE INDEX IF NOT EXISTS idx_edges_source_type ON edges(sourceId, type);\nCREATE INDEX IF NOT EXISTS idx_edges_target_type ON edges(targetId, type);\n\n-- Embeddings: vector storage\nCREATE TABLE IF NOT EXISTS embeddings (\n nodeId TEXT PRIMARY KEY,\n embedding BLOB NOT NULL,\n textHash TEXT\n);\n\n-- FTS5 virtual table (auto-updated via triggers)\nCREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(\n name,\n nameExpanded,\n searchText,\n filePath,\n content,\n content='nodes',\n content_rowid='rowid'\n);\n\nCREATE TRIGGER IF NOT EXISTS nodes_fts_ai AFTER INSERT ON nodes BEGIN\n INSERT INTO nodes_fts(rowid, name, nameExpanded, searchText, filePath, content) VALUES (new.rowid, new.name, new.nameExpanded, new.searchText, new.filePath, new.content);\nEND;\nCREATE TRIGGER IF NOT EXISTS nodes_fts_ad AFTER DELETE ON nodes BEGIN\n INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, searchText, filePath, content) VALUES ('delete', old.rowid, old.name, old.nameExpanded, old.searchText, old.filePath, old.content);\nEND;\nCREATE TRIGGER IF NOT EXISTS nodes_fts_au AFTER UPDATE ON nodes BEGIN\n INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, searchText, filePath, content) VALUES ('delete', old.rowid, old.name, old.nameExpanded, old.searchText, old.filePath, old.content);\n INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, searchText, filePath, content) VALUES (new.rowid, new.name, new.nameExpanded, new.searchText, new.filePath, new.content);\nEND;\n";
@@ -79,7 +79,8 @@ CREATE TABLE IF NOT EXISTS nodes (
79
79
  terminalId TEXT,
80
80
  parameterCount INTEGER,
81
81
  returnType TEXT,
82
- nameExpanded TEXT DEFAULT ''
82
+ nameExpanded TEXT DEFAULT '',
83
+ searchText TEXT DEFAULT ''
83
84
  );
84
85
 
85
86
  CREATE INDEX IF NOT EXISTS idx_nodes_label ON nodes(label);
@@ -117,6 +118,7 @@ CREATE TABLE IF NOT EXISTS embeddings (
117
118
  CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
118
119
  name,
119
120
  nameExpanded,
121
+ searchText,
120
122
  filePath,
121
123
  content,
122
124
  content='nodes',
@@ -124,13 +126,13 @@ CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
124
126
  );
125
127
 
126
128
  CREATE TRIGGER IF NOT EXISTS nodes_fts_ai AFTER INSERT ON nodes BEGIN
127
- INSERT INTO nodes_fts(rowid, name, nameExpanded, filePath, content) VALUES (new.rowid, new.name, new.nameExpanded, new.filePath, new.content);
129
+ INSERT INTO nodes_fts(rowid, name, nameExpanded, searchText, filePath, content) VALUES (new.rowid, new.name, new.nameExpanded, new.searchText, new.filePath, new.content);
128
130
  END;
129
131
  CREATE TRIGGER IF NOT EXISTS nodes_fts_ad AFTER DELETE ON nodes BEGIN
130
- INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, filePath, content) VALUES ('delete', old.rowid, old.name, old.nameExpanded, old.filePath, old.content);
132
+ INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, searchText, filePath, content) VALUES ('delete', old.rowid, old.name, old.nameExpanded, old.searchText, old.filePath, old.content);
131
133
  END;
132
134
  CREATE TRIGGER IF NOT EXISTS nodes_fts_au AFTER UPDATE ON nodes BEGIN
133
- INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, filePath, content) VALUES ('delete', old.rowid, old.name, old.nameExpanded, old.filePath, old.content);
134
- INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, filePath, content) VALUES (new.rowid, new.name, new.nameExpanded, new.filePath, new.content);
135
+ INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, searchText, filePath, content) VALUES ('delete', old.rowid, old.name, old.nameExpanded, old.searchText, old.filePath, old.content);
136
+ INSERT INTO nodes_fts(nodes_fts, rowid, name, nameExpanded, searchText, filePath, content) VALUES (new.rowid, new.name, new.nameExpanded, new.searchText, new.filePath, new.content);
135
137
  END;
136
138
  `;
@@ -27,7 +27,7 @@ import mlx.core as mx
27
27
  import mlx.nn as nn
28
28
  from tokenizers import Tokenizer
29
29
 
30
- MODEL_DIR = os.path.dirname(os.path.abspath(__file__)) + "/jina-v5-small-mlx"
30
+ MODEL_DIR = os.path.dirname(os.path.abspath(__file__)) + "/jina-code-0.5b-mlx"
31
31
 
32
32
 
33
33
 
@@ -64,7 +64,10 @@ def load_model():
64
64
  ensure_model_downloaded()
65
65
 
66
66
  sys.path.insert(0, MODEL_DIR)
67
- from model import JinaEmbeddingModel
67
+ import importlib
68
+ model_module = importlib.import_module("model")
69
+ # Support both model class names (v5 = JinaEmbeddingModel, code-0.5b = JinaCodeEmbeddingModel)
70
+ JinaEmbeddingModel = getattr(model_module, "JinaEmbeddingModel", None) or getattr(model_module, "JinaCodeEmbeddingModel")
68
71
 
69
72
  with open(os.path.join(MODEL_DIR, "config.json")) as f:
70
73
  config = json.load(f)
@@ -99,8 +102,17 @@ def embed_tiered(model, tokenizer, texts, task_type="retrieval.passage", truncat
99
102
  if not texts:
100
103
  return []
101
104
 
102
- # Add task prefix
103
- prefix_map = {"retrieval.query": "Query: ", "retrieval.passage": "Document: "}
105
+ # Add task prefix — auto-detect based on model type
106
+ # v5 (Qwen3): "Query: " / "Document: "
107
+ # code-0.5b (Qwen2): "Find the most relevant code snippet...\n" / "Candidate code snippet:\n"
108
+ is_code_model = "jina-code" in MODEL_DIR
109
+ if is_code_model:
110
+ prefix_map = {
111
+ "retrieval.query": "Find the most relevant code snippet given the following query:\n",
112
+ "retrieval.passage": "Candidate code snippet:\n",
113
+ }
114
+ else:
115
+ prefix_map = {"retrieval.query": "Query: ", "retrieval.passage": "Document: "}
104
116
  prefix = prefix_map.get(task_type, "")
105
117
  prefixed = [prefix + t for t in texts] if prefix else texts
106
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuvia-software-solutions/code-mapper",
3
- "version": "2.2.3",
3
+ "version": "2.3.0",
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",