graph-indexer 1.2.0 → 2.1.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.
- package/CHANGELOG.md +854 -0
- package/README.md +258 -595
- package/SECURITY.md +85 -0
- package/TRADEMARK.md +31 -0
- package/cli-ui.mjs +78 -0
- package/colbert.mjs +168 -0
- package/config.mjs +408 -32
- package/daemon-ctl.mjs +175 -0
- package/daemon-lock.mjs +69 -0
- package/embedders/python/mlx_embed_server.py +61 -0
- package/embedders/python/requirements-mlx.txt +5 -0
- package/embedders/setup-mlx.mjs +110 -0
- package/embeddings.mjs +571 -0
- package/engine/binary.mjs +437 -0
- package/{core-engine.mjs → engine/memory.mjs} +321 -441
- package/{sqlite-store.mjs → engine/sqlite.mjs} +413 -45
- package/enrichment.mjs +206 -32
- package/git-signals.mjs +248 -0
- package/indexer.mjs +399 -45
- package/init.mjs +1626 -171
- package/layout.mjs +123 -0
- package/mcp/centrality.mjs +117 -0
- package/mcp/format.mjs +61 -0
- package/mcp/resolver.mjs +116 -0
- package/mcp/symbolgraph.mjs +137 -0
- package/mcp/taint.mjs +225 -0
- package/mcp/tools.mjs +1582 -0
- package/mcp/topology.mjs +676 -0
- package/mcp-server.mjs +80 -25
- package/package.json +66 -27
- package/parse/extractor.mjs +628 -0
- package/parse/imports.mjs +365 -0
- package/parse/interprocedural.mjs +134 -0
- package/parse/languages.mjs +253 -0
- package/parse/metadata.mjs +701 -0
- package/parse/routes.mjs +327 -0
- package/parse/scip.mjs +391 -0
- package/parse/taint-patterns.mjs +119 -0
- package/prompts/CORE.md +91 -0
- package/prompts/DOMAIN_TEMPLATE.md +46 -0
- package/prompts/INTEGRATION.md +123 -0
- package/prompts/frameworks/ANDROID.md +17 -0
- package/prompts/frameworks/ASPNET_CORE.md +19 -0
- package/prompts/frameworks/FASTAPI_DJANGO.md +20 -0
- package/prompts/frameworks/LARAVEL_SYMFONY.md +22 -0
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +19 -0
- package/prompts/frameworks/RAILS.md +20 -0
- package/prompts/frameworks/REACT.md +20 -0
- package/prompts/frameworks/SPRING_BOOT.md +20 -0
- package/prompts/languages/BASH.md +22 -0
- package/prompts/languages/C.md +21 -0
- package/prompts/languages/CSHARP.md +24 -0
- package/prompts/languages/CSS_SCSS.md +22 -0
- package/prompts/languages/GO.md +24 -0
- package/prompts/languages/JAVA.md +23 -0
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +24 -0
- package/prompts/languages/KOTLIN.md +24 -0
- package/prompts/languages/PHP.md +23 -0
- package/prompts/languages/PYTHON.md +23 -0
- package/prompts/languages/RUBY.md +23 -0
- package/prompts/languages/RUST.md +21 -0
- package/prompts/languages/SWIFT.md +21 -0
- package/seal.mjs +400 -0
- package/search-core.mjs +405 -44
- package/search-sparse.mjs +183 -0
- package/storage.mjs +33 -3
- package/watch-daemon.mjs +75 -24
- package/PROMPT.md +0 -182
- package/mcp-tools.mjs +0 -545
- package/parser-utils.mjs +0 -1170
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file sqlite
|
|
2
|
+
* @file engine/sqlite.mjs
|
|
3
3
|
* @description Disk-backed graph store for enterprise-scale monorepos. Implements
|
|
4
4
|
* the same read contract as MemoryGraphIndex (see storage.mjs) over
|
|
5
5
|
* `node:sqlite` — built into Node, so this adds ZERO dependencies.
|
|
@@ -28,19 +28,21 @@
|
|
|
28
28
|
import fs from 'fs';
|
|
29
29
|
import {
|
|
30
30
|
tokenize, okapiIdf, bm25Score, fuseAndRank, buildLexicalDocument, embeddingKeyFor,
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
LEXICAL_FUSION_CAP, VECTOR_SCAN_RAW_N, finalizeVectorCandidates,
|
|
32
|
+
isNaturalLanguageQuery, baseEmbeddingKey, scoreSparseExpanded,
|
|
33
|
+
} from '../search-core.mjs';
|
|
34
|
+
import { expandSparseQuery } from '../search-sparse.mjs';
|
|
33
35
|
import {
|
|
34
36
|
writeEmbeddingBinary, appendEmbeddingBinary, scanEmbeddingBinary,
|
|
35
|
-
updateVectorSketch, searchVectorSketch,
|
|
36
|
-
} from './
|
|
37
|
+
updateVectorSketch, searchVectorSketch, SKETCH_THRESHOLD,
|
|
38
|
+
} from './binary.mjs';
|
|
37
39
|
|
|
38
40
|
// node:sqlite is loaded lazily so the default in-memory path never requires it.
|
|
39
41
|
let DatabaseSync = null;
|
|
40
42
|
try { ({ DatabaseSync } = await import('node:sqlite')); } catch { /* reported in ctor */ }
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
// SKETCH_THRESHOLD (shared with the memory store via binary.mjs) — build the binary
|
|
45
|
+
// sketch above this many vectors. Below it the exact streaming scan stays under ~20 ms
|
|
44
46
|
// AND both backends rank identically (the sketch is
|
|
45
47
|
// approximate in the candidate tail); above it the
|
|
46
48
|
// sketch caps latency at ~5–15 ms regardless of size.
|
|
@@ -53,12 +55,28 @@ CREATE TABLE IF NOT EXISTS chunks (
|
|
|
53
55
|
start_line INTEGER, end_line INTEGER,
|
|
54
56
|
calls TEXT, params TEXT, return_type TEXT, class_context TEXT,
|
|
55
57
|
type_refs TEXT, decorators TEXT, extends_ TEXT, hyde TEXT, summary TEXT, concepts TEXT,
|
|
56
|
-
doc_len INTEGER, path_tokens TEXT, vec_key TEXT, vec_offset INTEGER, vec_dim INTEGER
|
|
58
|
+
doc_len INTEGER, path_tokens TEXT, vec_key TEXT, vec_offset INTEGER, vec_dim INTEGER,
|
|
59
|
+
call_sites TEXT
|
|
57
60
|
);
|
|
58
61
|
CREATE TABLE IF NOT EXISTS postings (term TEXT, chunk_id TEXT, tf INTEGER);
|
|
59
62
|
CREATE TABLE IF NOT EXISTS terms (term TEXT PRIMARY KEY, df INTEGER);
|
|
60
63
|
CREATE TABLE IF NOT EXISTS call_edges (callee TEXT, chunk_id TEXT);
|
|
64
|
+
CREATE TABLE IF NOT EXISTS edges (from_chunk_id TEXT, to_chunk_id TEXT, kind TEXT, confidence TEXT);
|
|
65
|
+
CREATE TABLE IF NOT EXISTS centrality (chunk_id TEXT, score REAL, rank INTEGER);
|
|
66
|
+
CREATE TABLE IF NOT EXISTS taint (id INTEGER PRIMARY KEY, payload TEXT);
|
|
67
|
+
CREATE TABLE IF NOT EXISTS scip_refs (id INTEGER PRIMARY KEY, payload TEXT);
|
|
68
|
+
CREATE TABLE IF NOT EXISTS sparse_model (id INTEGER PRIMARY KEY, payload TEXT);
|
|
61
69
|
CREATE TABLE IF NOT EXISTS deps (file TEXT, dep TEXT);
|
|
70
|
+
CREATE TABLE IF NOT EXISTS routes (
|
|
71
|
+
id INTEGER PRIMARY KEY,
|
|
72
|
+
method TEXT NOT NULL,
|
|
73
|
+
path TEXT NOT NULL,
|
|
74
|
+
handler_name TEXT,
|
|
75
|
+
handler_chunk_id TEXT,
|
|
76
|
+
file_path TEXT NOT NULL,
|
|
77
|
+
line INTEGER,
|
|
78
|
+
framework TEXT
|
|
79
|
+
);
|
|
62
80
|
`;
|
|
63
81
|
|
|
64
82
|
const SCHEMA_INDEXES = `
|
|
@@ -69,15 +87,22 @@ CREATE INDEX IF NOT EXISTS idx_chunks_hash ON chunks(content_hash);
|
|
|
69
87
|
CREATE INDEX IF NOT EXISTS idx_postings_term ON postings(term);
|
|
70
88
|
CREATE INDEX IF NOT EXISTS idx_postings_chunk ON postings(chunk_id);
|
|
71
89
|
CREATE INDEX IF NOT EXISTS idx_calledges_callee ON call_edges(callee);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_chunk_id);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_chunk_id);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_centrality_chunk ON centrality(chunk_id);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_centrality_rank ON centrality(rank);
|
|
72
94
|
CREATE INDEX IF NOT EXISTS idx_deps_file ON deps(file);
|
|
73
95
|
CREATE INDEX IF NOT EXISTS idx_deps_dep ON deps(dep);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_routes_method ON routes(method);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_routes_path ON routes(path);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_routes_handler ON routes(handler_chunk_id);
|
|
74
99
|
`;
|
|
75
100
|
|
|
76
101
|
const CHUNK_COLS = [
|
|
77
102
|
'id', 'file_path', 'node_type', 'name', 'name_lower', 'docstring', 'code_snippet',
|
|
78
103
|
'content_hash', 'start_line', 'end_line', 'calls', 'params', 'return_type',
|
|
79
104
|
'class_context', 'type_refs', 'decorators', 'extends_', 'hyde', 'summary', 'concepts',
|
|
80
|
-
'doc_len', 'path_tokens', 'vec_key', 'vec_offset', 'vec_dim',
|
|
105
|
+
'doc_len', 'path_tokens', 'vec_key', 'vec_offset', 'vec_dim', 'call_sites',
|
|
81
106
|
];
|
|
82
107
|
|
|
83
108
|
function jsonArr(v) { return JSON.stringify(Array.isArray(v) ? v : []); }
|
|
@@ -114,6 +139,14 @@ export class SqliteGraphStore {
|
|
|
114
139
|
this._dataVersion = -1;
|
|
115
140
|
this._writeReady = false;
|
|
116
141
|
this._sketch = null; // binary-quantized vector sketch (built above SKETCH_THRESHOLD)
|
|
142
|
+
this._hasEdges = false; // A4: resolved symbol graph present (opt-in)
|
|
143
|
+
this._hasCentrality = false; // A5: symbol centrality present (opt-in, built with the graph)
|
|
144
|
+
this._centralityTotal = 0;
|
|
145
|
+
this._hasTaint = false; // C2: serialized taint flows present (opt-in --taint)
|
|
146
|
+
this._hasScipRefs = false; // A2 v2: SCIP precise references present (opt-in --resolver scip)
|
|
147
|
+
this._scipRefsObj = null; // lazily-parsed `{ defId: [refId,…] }` payload (memoized)
|
|
148
|
+
this._hasSparseModel = false; // B3: learned-sparse model present (opt-in --learned-sparse)
|
|
149
|
+
this._sparseModelObj = null; // lazily-parsed `{ assoc: {…} }` payload (memoized)
|
|
117
150
|
}
|
|
118
151
|
|
|
119
152
|
get backend() { return 'sqlite'; }
|
|
@@ -145,17 +178,73 @@ export class SqliteGraphStore {
|
|
|
145
178
|
// Pre-enrichment-key indexes stored vectors under the plain content hash.
|
|
146
179
|
this.db.exec('UPDATE chunks SET vec_key = content_hash WHERE vec_key IS NULL');
|
|
147
180
|
} catch { /* exists */ }
|
|
181
|
+
// Receiver-aware call graph (precise get_call_graph) landed later; older
|
|
182
|
+
// indexes lack the column. NULL → callers fall back to name-only grouping.
|
|
183
|
+
try { this.db.exec('ALTER TABLE chunks ADD COLUMN call_sites TEXT DEFAULT NULL'); } catch { /* exists */ }
|
|
148
184
|
}
|
|
149
185
|
this.db.exec(SCHEMA_INDEXES);
|
|
150
186
|
this._reloadMeta();
|
|
187
|
+
// Does this corpus carry LLM enrichment? One cheap probe → drives the NL
|
|
188
|
+
// vector-channel weight in fuseAndRank (parity with the in-memory engine).
|
|
189
|
+
try {
|
|
190
|
+
this._corpusEnriched = Boolean(
|
|
191
|
+
this.db.prepare("SELECT 1 FROM chunks WHERE summary IS NOT NULL AND summary != '' LIMIT 1").get()
|
|
192
|
+
);
|
|
193
|
+
} catch { this._corpusEnriched = false; }
|
|
151
194
|
this._loadGraph();
|
|
152
195
|
this._openVecFd();
|
|
153
196
|
this._refreshSketch();
|
|
154
197
|
this._prepare();
|
|
198
|
+
this._refreshHasEdges();
|
|
199
|
+
this._refreshHasCentrality();
|
|
200
|
+
this._refreshHasTaint();
|
|
201
|
+
this._refreshHasScipRefs();
|
|
202
|
+
this._refreshHasSparseModel();
|
|
155
203
|
this._dataVersion = this._readDataVersion();
|
|
156
204
|
return this;
|
|
157
205
|
}
|
|
158
206
|
|
|
207
|
+
/** Whether the opt-in resolved symbol graph (A4) has any edges. */
|
|
208
|
+
_refreshHasEdges() {
|
|
209
|
+
try { this._hasEdges = Boolean(this.db.prepare('SELECT 1 FROM edges LIMIT 1').get()); }
|
|
210
|
+
catch { this._hasEdges = false; }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Whether opt-in symbol centrality (A5) is present, and how many chunks are ranked. */
|
|
214
|
+
_refreshHasCentrality() {
|
|
215
|
+
try {
|
|
216
|
+
const row = this.db.prepare('SELECT COUNT(*) AS n FROM centrality').get();
|
|
217
|
+
this._centralityTotal = row ? row.n : 0;
|
|
218
|
+
this._hasCentrality = this._centralityTotal > 0;
|
|
219
|
+
} catch { this._hasCentrality = false; this._centralityTotal = 0; }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Whether opt-in serialized taint flows (C2 --taint) are present. */
|
|
223
|
+
_refreshHasTaint() {
|
|
224
|
+
try {
|
|
225
|
+
const row = this.db.prepare('SELECT COUNT(*) AS n FROM taint').get();
|
|
226
|
+
this._hasTaint = Boolean(row && row.n > 0);
|
|
227
|
+
} catch { this._hasTaint = false; }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Whether opt-in SCIP precise references (A2 v2 --resolver scip) are present. */
|
|
231
|
+
_refreshHasScipRefs() {
|
|
232
|
+
try {
|
|
233
|
+
const row = this.db.prepare('SELECT COUNT(*) AS n FROM scip_refs').get();
|
|
234
|
+
this._hasScipRefs = Boolean(row && row.n > 0);
|
|
235
|
+
} catch { this._hasScipRefs = false; }
|
|
236
|
+
this._scipRefsObj = null; // drop any memoized payload — the row may have changed
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Whether opt-in learned-sparse associations (B3 --learned-sparse) are present. */
|
|
240
|
+
_refreshHasSparseModel() {
|
|
241
|
+
try {
|
|
242
|
+
const row = this.db.prepare('SELECT COUNT(*) AS n FROM sparse_model').get();
|
|
243
|
+
this._hasSparseModel = Boolean(row && row.n > 0);
|
|
244
|
+
} catch { this._hasSparseModel = false; }
|
|
245
|
+
this._sparseModelObj = null; // drop any memoized payload — the row may have changed
|
|
246
|
+
}
|
|
247
|
+
|
|
159
248
|
_openVecFd() {
|
|
160
249
|
if (this._vecFd >= 0) { try { fs.closeSync(this._vecFd); } catch {} this._vecFd = -1; }
|
|
161
250
|
if (fs.existsSync(this._embeddingPath)) {
|
|
@@ -259,6 +348,12 @@ export class SqliteGraphStore {
|
|
|
259
348
|
this._stmtCallers = this.db.prepare(
|
|
260
349
|
'SELECT c.* FROM chunks c JOIN call_edges e ON e.chunk_id = c.id WHERE e.callee = ?'
|
|
261
350
|
);
|
|
351
|
+
// type_refs / extends_ are JSON arrays of names. LIKE is a cheap prefilter
|
|
352
|
+
// (ASCII case-insensitive in SQLite); findReferers() verifies exact, quoted
|
|
353
|
+
// membership in JS so e.g. "User" never matches "UserProfile".
|
|
354
|
+
this._stmtReferers = this.db.prepare(
|
|
355
|
+
'SELECT * FROM chunks WHERE type_refs LIKE ? ESCAPE \'\\\' OR extends_ LIKE ? ESCAPE \'\\\''
|
|
356
|
+
);
|
|
262
357
|
this._stmtPosting = this.db.prepare(
|
|
263
358
|
'SELECT p.chunk_id AS id, p.tf AS tf, c.doc_len AS doc_len '
|
|
264
359
|
+ 'FROM postings p JOIN chunks c ON c.id = p.chunk_id WHERE p.term = ?'
|
|
@@ -269,6 +364,33 @@ export class SqliteGraphStore {
|
|
|
269
364
|
// ids in a deterministic order keeps both backends rank-identical on ties.
|
|
270
365
|
this._stmtIdsByKey = this.db.prepare('SELECT id FROM chunks WHERE vec_key = ? ORDER BY id');
|
|
271
366
|
this._stmtIdsByHash = this.db.prepare('SELECT id FROM chunks WHERE content_hash = ? ORDER BY id');
|
|
367
|
+
// ORDER BY id (rowid = insertion order) keeps routes in index-build order,
|
|
368
|
+
// matching the in-memory graph.routes array order for cross-backend parity.
|
|
369
|
+
this._stmtRoutes = this.db.prepare(
|
|
370
|
+
'SELECT method, path, handler_name, handler_chunk_id, file_path, line, framework '
|
|
371
|
+
+ 'FROM routes ORDER BY id'
|
|
372
|
+
);
|
|
373
|
+
// A4 symbol graph. ORDER mirrors the in-memory engine's per-endpoint slice of the
|
|
374
|
+
// globally-sorted edge list (fixed `to` → sort by from,kind,confidence; fixed `from`
|
|
375
|
+
// → sort by to,kind,confidence) so getEdges is byte-identical cross-backend.
|
|
376
|
+
this._stmtEdgesIn = this.db.prepare(
|
|
377
|
+
'SELECT from_chunk_id, to_chunk_id, kind, confidence FROM edges WHERE to_chunk_id = ? '
|
|
378
|
+
+ 'ORDER BY from_chunk_id, kind, confidence'
|
|
379
|
+
);
|
|
380
|
+
this._stmtEdgesOut = this.db.prepare(
|
|
381
|
+
'SELECT from_chunk_id, to_chunk_id, kind, confidence FROM edges WHERE from_chunk_id = ? '
|
|
382
|
+
+ 'ORDER BY to_chunk_id, kind, confidence'
|
|
383
|
+
);
|
|
384
|
+
// A5 symbol centrality. rank/score are precomputed at index time; topCentral orders by
|
|
385
|
+
// the stored rank (chunk_id tie-break mirrors the in-memory ranked cache).
|
|
386
|
+
this._stmtCentrality = this.db.prepare('SELECT score, rank FROM centrality WHERE chunk_id = ?');
|
|
387
|
+
this._stmtTopCentral = this.db.prepare(
|
|
388
|
+
'SELECT chunk_id, score, rank FROM centrality ORDER BY rank ASC, chunk_id ASC LIMIT ?'
|
|
389
|
+
);
|
|
390
|
+
// C2 serialized taint flows — a single JSON payload row (id = 0).
|
|
391
|
+
this._stmtTaint = this.db.prepare('SELECT payload FROM taint WHERE id = 0');
|
|
392
|
+
this._stmtScipRefs = this.db.prepare('SELECT payload FROM scip_refs WHERE id = 0');
|
|
393
|
+
this._stmtSparseModel = this.db.prepare('SELECT payload FROM sparse_model WHERE id = 0');
|
|
272
394
|
}
|
|
273
395
|
|
|
274
396
|
_prepareWrite() {
|
|
@@ -312,7 +434,7 @@ export class SqliteGraphStore {
|
|
|
312
434
|
return_type: row.return_type || '', class_context: row.class_context || '',
|
|
313
435
|
type_refs: parseArr(row.type_refs), decorators: parseArr(row.decorators),
|
|
314
436
|
extends: parseArr(row.extends_), hyde: row.hyde || '', summary: row.summary || '',
|
|
315
|
-
concepts: parseArr(row.concepts),
|
|
437
|
+
concepts: parseArr(row.concepts), call_sites: parseArr(row.call_sites),
|
|
316
438
|
};
|
|
317
439
|
}
|
|
318
440
|
|
|
@@ -328,7 +450,166 @@ export class SqliteGraphStore {
|
|
|
328
450
|
return this._stmtByName.all(key).map(r => this._rowToChunk(r));
|
|
329
451
|
}
|
|
330
452
|
|
|
331
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Resolved symbol-graph edges incident to a chunk (A4). Mirrors MemoryGraphIndex.getEdges,
|
|
455
|
+
* including deterministic order. Returns [] unless the opt-in graph was built.
|
|
456
|
+
*/
|
|
457
|
+
/** Whether the opt-in resolved symbol graph (A4) is loaded. */
|
|
458
|
+
hasSymbolGraph() { return this._hasEdges; }
|
|
459
|
+
|
|
460
|
+
getEdges(chunkId, { kind = null, direction = 'in' } = {}) {
|
|
461
|
+
if (!this._hasEdges) return [];
|
|
462
|
+
const rows = direction === 'out' ? this._stmtEdgesOut.all(chunkId) : this._stmtEdgesIn.all(chunkId);
|
|
463
|
+
const out = [];
|
|
464
|
+
for (const e of rows) {
|
|
465
|
+
if (kind && e.kind !== kind) continue;
|
|
466
|
+
const otherId = direction === 'out' ? e.to_chunk_id : e.from_chunk_id;
|
|
467
|
+
out.push({ from_chunk_id: e.from_chunk_id, to_chunk_id: e.to_chunk_id, kind: e.kind, confidence: e.confidence, chunk: this.getChunk(otherId) });
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Whether opt-in symbol centrality (A5) is loaded. */
|
|
473
|
+
hasCentrality() { return this._hasCentrality; }
|
|
474
|
+
|
|
475
|
+
/** Whether opt-in serialized taint flows (C2 --taint) are loaded. */
|
|
476
|
+
hasTaint() { return this._hasTaint; }
|
|
477
|
+
|
|
478
|
+
/** The serialized taint payload `{ flows, meta }` (C2), or null. Mirrors MemoryGraphIndex. */
|
|
479
|
+
getTaintFlows() {
|
|
480
|
+
if (!this._hasTaint) return null;
|
|
481
|
+
const row = this._stmtTaint.get();
|
|
482
|
+
if (!row || !row.payload) return null;
|
|
483
|
+
try { return JSON.parse(row.payload); } catch { return null; }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Whether opt-in SCIP precise references (A2 v2 --resolver scip) are loaded. */
|
|
487
|
+
hasScipRefs() { return this._hasScipRefs; }
|
|
488
|
+
|
|
489
|
+
/** Parse the single JSON payload `{ defId: [refId,…] }` once, then memoize. */
|
|
490
|
+
_loadScipRefs() {
|
|
491
|
+
if (!this._hasScipRefs) return {};
|
|
492
|
+
if (!this._scipRefsObj) {
|
|
493
|
+
const row = this._stmtScipRefs.get();
|
|
494
|
+
try { this._scipRefsObj = row && row.payload ? JSON.parse(row.payload) : {}; }
|
|
495
|
+
catch { this._scipRefsObj = {}; }
|
|
496
|
+
}
|
|
497
|
+
return this._scipRefsObj;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** SCIP-confirmed referer chunk ids for a definition chunk (A2 v2), or []. Mirrors MemoryGraphIndex. */
|
|
501
|
+
getScipReferers(defChunkId) {
|
|
502
|
+
const refs = this._loadScipRefs()[defChunkId];
|
|
503
|
+
return Array.isArray(refs) ? refs : [];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** How many definitions carry SCIP precise references (A2 v2). 0 when not loaded. */
|
|
507
|
+
scipRefCount() { return this._hasScipRefs ? Object.keys(this._loadScipRefs()).length : 0; }
|
|
508
|
+
|
|
509
|
+
/** Whether opt-in learned-sparse associations (B3 --learned-sparse) are loaded. */
|
|
510
|
+
hasSparseModel() { return this._hasSparseModel; }
|
|
511
|
+
|
|
512
|
+
/** Path to the shared `.embeddings.bin` (B4 late-interaction reranker reads candidate vectors from it). */
|
|
513
|
+
embeddingBinPath() { return this._embeddingPath; }
|
|
514
|
+
|
|
515
|
+
/** Parse the single learned-sparse payload once, then memoize. Mirrors MemoryGraphIndex._sparseModel. */
|
|
516
|
+
_loadSparseModel() {
|
|
517
|
+
if (!this._hasSparseModel) return null;
|
|
518
|
+
if (!this._sparseModelObj) {
|
|
519
|
+
const row = this._stmtSparseModel.get();
|
|
520
|
+
try { this._sparseModelObj = row && row.payload ? JSON.parse(row.payload) : null; }
|
|
521
|
+
catch { this._sparseModelObj = null; }
|
|
522
|
+
}
|
|
523
|
+
return this._sparseModelObj;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Symbol centrality (A5) for one chunk. Mirrors MemoryGraphIndex.getCentrality — same
|
|
528
|
+
* { score, rank, total } shape; null when unranked or centrality was not built.
|
|
529
|
+
*/
|
|
530
|
+
getCentrality(chunkId) {
|
|
531
|
+
if (!this._hasCentrality) return null;
|
|
532
|
+
const row = this._stmtCentrality.get(chunkId);
|
|
533
|
+
return row ? { score: row.score, rank: row.rank, total: this._centralityTotal } : null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** The most central symbols (A5), rank-ascending. @returns {Array<{chunk,score,rank}>} */
|
|
537
|
+
topCentral(limit = 20) {
|
|
538
|
+
if (!this._hasCentrality) return [];
|
|
539
|
+
const out = [];
|
|
540
|
+
for (const row of this._stmtTopCentral.all(limit)) {
|
|
541
|
+
const chunk = this.getChunk(row.chunk_id);
|
|
542
|
+
if (chunk) out.push({ chunk, score: row.score, rank: row.rank });
|
|
543
|
+
}
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** From-chunks of edges of a kept kind pointing at any of `defs`, id-sorted. */
|
|
548
|
+
_referrersByEdge(defs, keepKind) {
|
|
549
|
+
const ids = new Set();
|
|
550
|
+
for (const def of defs) for (const e of this._stmtEdgesIn.all(def.id)) if (keepKind(e.kind)) ids.add(e.from_chunk_id);
|
|
551
|
+
const out = [];
|
|
552
|
+
for (const id of [...ids].sort()) { const c = this.getChunk(id); if (c) out.push(c); }
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
findCallers(funcName) {
|
|
557
|
+
if (this._hasEdges) {
|
|
558
|
+
const defs = this.resolveSymbol(funcName);
|
|
559
|
+
if (defs.length > 0) return this._referrersByEdge(defs, k => k === 'calls');
|
|
560
|
+
}
|
|
561
|
+
return this._stmtCallers.all(funcName).map(r => this._rowToChunk(r));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
findReferers(symbol) {
|
|
565
|
+
const key = String(symbol).toLowerCase().trim();
|
|
566
|
+
if (!key) return [];
|
|
567
|
+
if (this._hasEdges) {
|
|
568
|
+
const defs = this.resolveSymbol(symbol);
|
|
569
|
+
if (defs.length > 0) return this._referrersByEdge(defs, k => k === 'extends' || k === 'type');
|
|
570
|
+
}
|
|
571
|
+
// Escape LIKE metacharacters in the symbol, then bracket with JSON quotes so
|
|
572
|
+
// the prefilter only fires on a whole array element.
|
|
573
|
+
const esc = key.replace(/[\\%_]/g, m => '\\' + m);
|
|
574
|
+
const pat = `%"${esc}"%`;
|
|
575
|
+
const out = [];
|
|
576
|
+
for (const row of this._stmtReferers.all(pat, pat)) {
|
|
577
|
+
const c = this._rowToChunk(row);
|
|
578
|
+
// A class/type lists its own name in type_refs — that self-mention is a
|
|
579
|
+
// definition, not a reference; skip it.
|
|
580
|
+
if (c.name && c.name.toLowerCase() === key) continue;
|
|
581
|
+
const hit = c.type_refs.some(t => t.toLowerCase() === key)
|
|
582
|
+
|| c.extends.some(t => t.toLowerCase() === key);
|
|
583
|
+
if (hit) out.push(c);
|
|
584
|
+
}
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* HTTP routes filtered by method and/or path prefix, each augmented with its
|
|
590
|
+
* resolved handler chunk (`chunk`, or null). Filtering logic is byte-identical
|
|
591
|
+
* to MemoryGraphIndex.findRoutes; rows arrive in index-build order (ORDER BY id)
|
|
592
|
+
* so both backends return identical records. An index built before this feature
|
|
593
|
+
* (empty routes table) yields [].
|
|
594
|
+
* @returns {object[]}
|
|
595
|
+
*/
|
|
596
|
+
findRoutes({ method = null, pathPrefix = null } = {}) {
|
|
597
|
+
this._maybeRefresh();
|
|
598
|
+
const m = method ? String(method).toUpperCase().trim() : null;
|
|
599
|
+
const p = pathPrefix ? String(pathPrefix).toLowerCase() : null;
|
|
600
|
+
const out = [];
|
|
601
|
+
for (const r of this._stmtRoutes.all()) {
|
|
602
|
+
if (m && String(r.method || '').toUpperCase() !== m) continue;
|
|
603
|
+
if (p && !String(r.path || '').toLowerCase().startsWith(p)) continue;
|
|
604
|
+
const chunk = r.handler_chunk_id ? this.getChunk(r.handler_chunk_id) : null;
|
|
605
|
+
out.push({
|
|
606
|
+
method: r.method, path: r.path, handler_name: r.handler_name,
|
|
607
|
+
handler_chunk_id: r.handler_chunk_id, file_path: r.file_path,
|
|
608
|
+
line: r.line, framework: r.framework, chunk,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
332
613
|
|
|
333
614
|
*iterateChunks() {
|
|
334
615
|
// Stream rows via a cursor so repo-map / call-graph passes never materialise
|
|
@@ -408,8 +689,9 @@ export class SqliteGraphStore {
|
|
|
408
689
|
searchHybrid(queryText, queryVector, topK = 5, minScore = 0.3, exactBoostName = null) {
|
|
409
690
|
this._maybeRefresh();
|
|
410
691
|
|
|
411
|
-
//
|
|
412
|
-
|
|
692
|
+
// Asymmetric stemming: stem only for natural-language queries so symbolic
|
|
693
|
+
// lookups stay exact — mirrors the in-memory engine for parity.
|
|
694
|
+
const qTokens = tokenize(queryText, isNaturalLanguageQuery(queryText));
|
|
413
695
|
const occ = new Map();
|
|
414
696
|
for (const t of qTokens) occ.set(t, (occ.get(t) || 0) + 1);
|
|
415
697
|
|
|
@@ -429,10 +711,14 @@ export class SqliteGraphStore {
|
|
|
429
711
|
.slice(0, LEXICAL_FUSION_CAP)
|
|
430
712
|
.map(([id, score], i) => ({ id, score, rank: i + 1 }));
|
|
431
713
|
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
714
|
+
// B3 learned-sparse channel: opt-in (model present) + NL-asymmetric. Shares scoreSparseExpanded
|
|
715
|
+
// with the in-memory engine over identically-ordered terms → byte-identical to memory. [] when
|
|
716
|
+
// inert (no model / symbolic query / no association) → fusion byte-identical to the default path.
|
|
717
|
+
const sparseResults = this._searchSparseExpanded(queryText);
|
|
718
|
+
|
|
719
|
+
// Stream the FULL embeddings bin (bounded buffers) so conceptual queries
|
|
720
|
+
// that share no tokens with the code can still match — scoring only lexical
|
|
721
|
+
// candidates silently disabled semantic search where it mattered most.
|
|
436
722
|
let vectorResults = [];
|
|
437
723
|
if (queryVector && this._vecFd >= 0) {
|
|
438
724
|
// Binary sketch above SKETCH_THRESHOLD (Hamming prefilter + exact
|
|
@@ -446,9 +732,9 @@ export class SqliteGraphStore {
|
|
|
446
732
|
});
|
|
447
733
|
const entries = [];
|
|
448
734
|
for (const { key, score } of hits) {
|
|
449
|
-
// Summary-only
|
|
450
|
-
|
|
451
|
-
|
|
735
|
+
// Summary-only (`<key>|s`) and per-window (`<key>|wN`) vectors fold onto
|
|
736
|
+
// the chunk; finalizeVectorCandidates keeps the max-sim score per chunk.
|
|
737
|
+
const baseKey = baseEmbeddingKey(key);
|
|
452
738
|
let rows = this._stmtIdsByKey.all(baseKey);
|
|
453
739
|
if (rows.length === 0) rows = this._stmtIdsByHash.all(baseKey); // legacy bins
|
|
454
740
|
for (const { id } of rows) entries.push({ id, score });
|
|
@@ -456,9 +742,6 @@ export class SqliteGraphStore {
|
|
|
456
742
|
vectorResults = finalizeVectorCandidates(entries);
|
|
457
743
|
}
|
|
458
744
|
|
|
459
|
-
// 3. Fetch candidate rows ONCE (transient, bounded by the caps above), then
|
|
460
|
-
// fuse + boost with the shared ranker. getChunk lazy-fetches stragglers
|
|
461
|
-
// (e.g. exact-boost ids) and caches them for the duration of the call.
|
|
462
745
|
const rowCache = new Map();
|
|
463
746
|
const fetchRow = (id) => {
|
|
464
747
|
if (rowCache.has(id)) return rowCache.get(id);
|
|
@@ -494,12 +777,36 @@ export class SqliteGraphStore {
|
|
|
494
777
|
};
|
|
495
778
|
|
|
496
779
|
return fuseAndRank({
|
|
497
|
-
lexicalResults, vectorResults, getChunk, getPathTokens, getDf,
|
|
780
|
+
lexicalResults, vectorResults, sparseResults, getChunk, getPathTokens, getDf,
|
|
498
781
|
docCount: this._docCount, rrfK: this.rrfK, topK, queryText, exactBoostName,
|
|
782
|
+
corpusEnriched: this._corpusEnriched,
|
|
499
783
|
resolveExact: (term) => this._stmtIdByName.all(term).map(r => r.id),
|
|
500
784
|
});
|
|
501
785
|
}
|
|
502
786
|
|
|
787
|
+
/**
|
|
788
|
+
* B3 — the learned-sparse expansion channel (NL queries, model present). Mirrors
|
|
789
|
+
* MemoryGraphIndex._searchSparseExpanded: identical expansion + the shared scoreSparseExpanded
|
|
790
|
+
* scorer over identically-ordered terms → byte-identical to memory. Returns [] when inert.
|
|
791
|
+
*/
|
|
792
|
+
_searchSparseExpanded(queryText) {
|
|
793
|
+
if (!this._hasSparseModel || !isNaturalLanguageQuery(queryText)) return [];
|
|
794
|
+
const model = this._loadSparseModel();
|
|
795
|
+
const termWeights = model && expandSparseQuery(queryText, model);
|
|
796
|
+
if (!termWeights) return [];
|
|
797
|
+
const avgdl = this._avgdl;
|
|
798
|
+
const docLenCache = new Map();
|
|
799
|
+
return scoreSparseExpanded(termWeights, {
|
|
800
|
+
docCount: this._docCount,
|
|
801
|
+
avgdl,
|
|
802
|
+
getDf: (t) => { const r = this._stmtDf.get(t); return r ? r.df : 0; },
|
|
803
|
+
// .map() materialises the postings AND populates the doc-len cache before scoring iterates,
|
|
804
|
+
// so getDocLen always resolves without a second query.
|
|
805
|
+
getPostings: (t) => this._stmtPosting.all(t).map(r => { docLenCache.set(r.id, r.doc_len); return [r.id, r.tf]; }),
|
|
806
|
+
getDocLen: (id) => docLenCache.get(id) ?? avgdl,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
503
810
|
close() {
|
|
504
811
|
if (this._vecFd >= 0) { try { fs.closeSync(this._vecFd); } catch {} this._vecFd = -1; }
|
|
505
812
|
if (this.db) { try { this.db.close(); } catch {} this.db = null; }
|
|
@@ -509,8 +816,9 @@ export class SqliteGraphStore {
|
|
|
509
816
|
|
|
510
817
|
/** Internal: index one chunk's lexical document + row + call edges. */
|
|
511
818
|
_insertChunk(chunk, imports, vecEntry) {
|
|
512
|
-
const
|
|
513
|
-
const
|
|
819
|
+
const doc = buildLexicalDocument(chunk, imports);
|
|
820
|
+
const tokens = tokenize(doc); // raw + additive stems
|
|
821
|
+
const docLen = tokenize(doc, false).length; // raw-only length for BM25 avgdl (parity w/ memory engine)
|
|
514
822
|
|
|
515
823
|
const termCounts = new Map();
|
|
516
824
|
for (const t of tokens) termCounts.set(t, (termCounts.get(t) || 0) + 1);
|
|
@@ -519,7 +827,7 @@ export class SqliteGraphStore {
|
|
|
519
827
|
this._stmtInsPost.run(term, chunk.id, cnt);
|
|
520
828
|
}
|
|
521
829
|
|
|
522
|
-
const pathTokens = Array.from(new Set(tokenize(chunk.file_path.replace(/[/\-_.]/g, ' '))));
|
|
830
|
+
const pathTokens = Array.from(new Set(tokenize(chunk.file_path.replace(/[/\-_.]/g, ' '), false)));
|
|
523
831
|
this._stmtInsChunk.run(
|
|
524
832
|
chunk.id, chunk.file_path, chunk.node_type ?? null, chunk.name ?? null,
|
|
525
833
|
nameLowerOf(chunk.name), chunk.docstring ?? '', chunk.code_snippet ?? '',
|
|
@@ -529,7 +837,8 @@ export class SqliteGraphStore {
|
|
|
529
837
|
jsonArr(chunk.extends), chunk.hyde ?? null, chunk.summary ?? null, jsonArr(chunk.concepts),
|
|
530
838
|
docLen, JSON.stringify(pathTokens),
|
|
531
839
|
chunk.content_hash ? embeddingKeyFor(chunk) : null,
|
|
532
|
-
vecEntry ? vecEntry.offset : -1, vecEntry ? vecEntry.dim : 0
|
|
840
|
+
vecEntry ? vecEntry.offset : -1, vecEntry ? vecEntry.dim : 0,
|
|
841
|
+
jsonArr(chunk.call_sites)
|
|
533
842
|
);
|
|
534
843
|
for (const callee of (chunk.calls || [])) this._stmtInsEdge.run(callee, chunk.id);
|
|
535
844
|
return docLen;
|
|
@@ -557,8 +866,8 @@ export class SqliteGraphStore {
|
|
|
557
866
|
this._prepareWrite();
|
|
558
867
|
this._maybeRefresh();
|
|
559
868
|
|
|
560
|
-
//
|
|
561
|
-
//
|
|
869
|
+
// Append new vectors first — orphan bytes on a failed commit are harmless
|
|
870
|
+
// and compacted away by the next full index run.
|
|
562
871
|
let appended = new Map();
|
|
563
872
|
const embCount = embeddings ? (embeddings instanceof Map ? embeddings.size : Object.keys(embeddings).length) : 0;
|
|
564
873
|
if (embCount > 0) {
|
|
@@ -574,8 +883,17 @@ export class SqliteGraphStore {
|
|
|
574
883
|
|
|
575
884
|
this.db.exec('BEGIN');
|
|
576
885
|
try {
|
|
577
|
-
//
|
|
578
|
-
//
|
|
886
|
+
// A per-file edit makes the whole-program symbol graph (A4) stale; clear it so
|
|
887
|
+
// findCallers/findReferers fall back to the always-correct scan until the next
|
|
888
|
+
// full `idx-index` rebuilds it (the daemon never rebuilds the graph).
|
|
889
|
+
if (this._hasEdges) { this.db.exec('DELETE FROM edges'); this._hasEdges = false; }
|
|
890
|
+
if (this._hasCentrality) { this.db.exec('DELETE FROM centrality'); this._hasCentrality = false; this._centralityTotal = 0; }
|
|
891
|
+
if (this._hasTaint) { this.db.exec('DELETE FROM taint'); this._hasTaint = false; }
|
|
892
|
+
if (this._hasScipRefs) { this.db.exec('DELETE FROM scip_refs'); this._hasScipRefs = false; this._scipRefsObj = null; }
|
|
893
|
+
if (this._hasSparseModel) { this.db.exec('DELETE FROM sparse_model'); this._hasSparseModel = false; this._sparseModelObj = null; }
|
|
894
|
+
|
|
895
|
+
// Capture reusable vector offsets BEFORE deleting old rows, which may
|
|
896
|
+
// be the only rows that hold those offsets.
|
|
579
897
|
const reuse = new Map();
|
|
580
898
|
for (const chunk of chunks) {
|
|
581
899
|
if (!chunk.content_hash) continue;
|
|
@@ -585,7 +903,6 @@ export class SqliteGraphStore {
|
|
|
585
903
|
if (row) reuse.set(key, { offset: row.vec_offset, dim: row.vec_dim });
|
|
586
904
|
}
|
|
587
905
|
|
|
588
|
-
// 3. Remove the file's old rows with exact BM25 bookkeeping.
|
|
589
906
|
let docCount = this._docCount;
|
|
590
907
|
let totalDocLen = this._totalDocLen;
|
|
591
908
|
for (const row of this._stmtByFile.all(filePath)) {
|
|
@@ -599,7 +916,6 @@ export class SqliteGraphStore {
|
|
|
599
916
|
docCount--;
|
|
600
917
|
}
|
|
601
918
|
|
|
602
|
-
// 4. Insert the new chunks.
|
|
603
919
|
for (const chunk of chunks) {
|
|
604
920
|
const key = chunk.content_hash ? embeddingKeyFor(chunk) : null;
|
|
605
921
|
const vecEntry = key ? (appended.get(key) ?? reuse.get(key) ?? null) : null;
|
|
@@ -608,7 +924,6 @@ export class SqliteGraphStore {
|
|
|
608
924
|
}
|
|
609
925
|
this._stmtPruneTerms.run();
|
|
610
926
|
|
|
611
|
-
// 5. Dependency edges + meta.
|
|
612
927
|
this._stmtDelDeps.run(filePath);
|
|
613
928
|
if (!imports || imports.length === 0) this._stmtInsDep.run(filePath, null);
|
|
614
929
|
else for (const d of imports) this._stmtInsDep.run(filePath, d);
|
|
@@ -629,7 +944,6 @@ export class SqliteGraphStore {
|
|
|
629
944
|
throw err;
|
|
630
945
|
}
|
|
631
946
|
|
|
632
|
-
// 6. Mirror the in-RAM dependency graph (same shape as updateFileGraph).
|
|
633
947
|
const oldDeps = this._graph.dependencies[filePath] || [];
|
|
634
948
|
for (const oldDep of oldDeps) {
|
|
635
949
|
if (this._graph.importedBy[oldDep]) {
|
|
@@ -670,11 +984,10 @@ export class SqliteGraphStore {
|
|
|
670
984
|
* @param {{dependencies:Object<string,string[]>}} payload.graph
|
|
671
985
|
* @param {Map<string,Float32Array>|object} [payload.embeddingCache] Keyed by embeddingKeyFor(chunk).
|
|
672
986
|
*/
|
|
673
|
-
buildFrom({ chunks, graph, embeddingCache }) {
|
|
987
|
+
buildFrom({ chunks, graph, embeddingCache, edges = null, centrality = null, taint = null, scipRefs = null, sparseModel = null }) {
|
|
674
988
|
this._open();
|
|
675
|
-
this.db.exec('PRAGMA synchronous = OFF;'); //
|
|
989
|
+
this.db.exec('PRAGMA synchronous = OFF;'); // safe only for a full rebuild; not used for incremental writes
|
|
676
990
|
|
|
677
|
-
// Write the embedding binary and derive each key's float offset.
|
|
678
991
|
const offsets = new Map();
|
|
679
992
|
let dim = 0;
|
|
680
993
|
const cacheSize = embeddingCache instanceof Map ? embeddingCache.size : Object.keys(embeddingCache || {}).length;
|
|
@@ -697,12 +1010,15 @@ export class SqliteGraphStore {
|
|
|
697
1010
|
let totalDocLen = 0, docCount = 0;
|
|
698
1011
|
|
|
699
1012
|
this.db.exec('BEGIN');
|
|
700
|
-
//
|
|
701
|
-
//
|
|
1013
|
+
// Drop inside the transaction so concurrent readers see the old index
|
|
1014
|
+
// until COMMIT, never briefly-empty tables.
|
|
702
1015
|
this.db.exec(`
|
|
703
1016
|
DROP TABLE IF EXISTS chunks; DROP TABLE IF EXISTS postings;
|
|
704
1017
|
DROP TABLE IF EXISTS terms; DROP TABLE IF EXISTS call_edges;
|
|
705
1018
|
DROP TABLE IF EXISTS deps; DROP TABLE IF EXISTS meta;
|
|
1019
|
+
DROP TABLE IF EXISTS routes; DROP TABLE IF EXISTS edges;
|
|
1020
|
+
DROP TABLE IF EXISTS centrality; DROP TABLE IF EXISTS taint;
|
|
1021
|
+
DROP TABLE IF EXISTS scip_refs; DROP TABLE IF EXISTS sparse_model;
|
|
706
1022
|
`);
|
|
707
1023
|
this.db.exec(SCHEMA_TABLES);
|
|
708
1024
|
this.db.exec(SCHEMA_INDEXES);
|
|
@@ -716,8 +1032,9 @@ export class SqliteGraphStore {
|
|
|
716
1032
|
|
|
717
1033
|
for (const chunk of chunks) {
|
|
718
1034
|
const deps = (graph?.dependencies?.[chunk.file_path]) || [];
|
|
719
|
-
const
|
|
720
|
-
const
|
|
1035
|
+
const doc = buildLexicalDocument(chunk, deps);
|
|
1036
|
+
const tokens = tokenize(doc); // raw + additive stems
|
|
1037
|
+
const docLen = tokenize(doc, false).length; // raw-only length for BM25 avgdl (parity w/ memory engine)
|
|
721
1038
|
totalDocLen += docLen; docCount++;
|
|
722
1039
|
|
|
723
1040
|
const termCounts = new Map();
|
|
@@ -727,7 +1044,7 @@ export class SqliteGraphStore {
|
|
|
727
1044
|
insPost.run(term, chunk.id, cnt);
|
|
728
1045
|
}
|
|
729
1046
|
|
|
730
|
-
const pathTokens = Array.from(new Set(tokenize(chunk.file_path.replace(/[/\-_.]/g, ' '))));
|
|
1047
|
+
const pathTokens = Array.from(new Set(tokenize(chunk.file_path.replace(/[/\-_.]/g, ' '), false)));
|
|
731
1048
|
const vecKey = chunk.content_hash ? embeddingKeyFor(chunk) : null;
|
|
732
1049
|
const vec = vecKey ? (offsets.get(vecKey) ?? offsets.get(chunk.content_hash)) : null;
|
|
733
1050
|
|
|
@@ -739,7 +1056,8 @@ export class SqliteGraphStore {
|
|
|
739
1056
|
chunk.class_context ?? '', jsonArr(chunk.type_refs), jsonArr(chunk.decorators),
|
|
740
1057
|
jsonArr(chunk.extends), chunk.hyde ?? null, chunk.summary ?? null, jsonArr(chunk.concepts),
|
|
741
1058
|
docLen, JSON.stringify(pathTokens), vecKey,
|
|
742
|
-
vec ? vec.offset : -1, vec ? vec.dim : 0
|
|
1059
|
+
vec ? vec.offset : -1, vec ? vec.dim : 0,
|
|
1060
|
+
jsonArr(chunk.call_sites)
|
|
743
1061
|
);
|
|
744
1062
|
for (const callee of (chunk.calls || [])) insEdge.run(callee, chunk.id);
|
|
745
1063
|
}
|
|
@@ -753,6 +1071,56 @@ export class SqliteGraphStore {
|
|
|
753
1071
|
else for (const d of list) insDep.run(file, d);
|
|
754
1072
|
}
|
|
755
1073
|
|
|
1074
|
+
// Inserted in array order so the autoincrement id mirrors the in-memory
|
|
1075
|
+
// graph.routes order, keeping both backends rank-identical.
|
|
1076
|
+
const insRoute = this.db.prepare(
|
|
1077
|
+
'INSERT INTO routes (method, path, handler_name, handler_chunk_id, file_path, line, framework) '
|
|
1078
|
+
+ 'VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
1079
|
+
);
|
|
1080
|
+
for (const r of (graph?.routes || [])) {
|
|
1081
|
+
insRoute.run(
|
|
1082
|
+
String(r.method || ''), String(r.path ?? ''), r.handler_name ?? null,
|
|
1083
|
+
r.handler_chunk_id ?? null, String(r.file_path || ''),
|
|
1084
|
+
r.line ?? null, r.framework ?? null
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// A4 resolved symbol graph (opt-in). Inserted in the deterministic order produced
|
|
1089
|
+
// by buildSymbolGraph so the table mirrors the in-memory edge list.
|
|
1090
|
+
if (Array.isArray(edges) && edges.length) {
|
|
1091
|
+
const insEdge = this.db.prepare(
|
|
1092
|
+
'INSERT INTO edges (from_chunk_id, to_chunk_id, kind, confidence) VALUES (?, ?, ?, ?)'
|
|
1093
|
+
);
|
|
1094
|
+
for (const e of edges) insEdge.run(e.from_chunk_id, e.to_chunk_id, e.kind, e.confidence);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// A5 symbol centrality (opt-in). Precomputed score/rank per chunk id; both backends
|
|
1098
|
+
// read these exact numbers → parity-free. Inserted in rank order for tidy storage.
|
|
1099
|
+
if (centrality && typeof centrality === 'object') {
|
|
1100
|
+
const insCen = this.db.prepare('INSERT INTO centrality (chunk_id, score, rank) VALUES (?, ?, ?)');
|
|
1101
|
+
const rows = Object.entries(centrality).sort((a, b) => a[1].rank - b[1].rank);
|
|
1102
|
+
for (const [id, v] of rows) insCen.run(id, v.score, v.rank);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// C2 serialized taint flows (opt-in). A single JSON payload (id = 0); both backends parse
|
|
1106
|
+
// the identical bytes → parity-free, exactly like edges/centrality.
|
|
1107
|
+
if (taint && typeof taint === 'object') {
|
|
1108
|
+
this.db.prepare('INSERT INTO taint (id, payload) VALUES (0, ?)').run(JSON.stringify(taint));
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// A2 v2 SCIP precise references (opt-in --resolver scip). A single JSON payload (id = 0) of
|
|
1112
|
+
// `{ defChunkId: [refererChunkId, …] }`; both backends parse identical bytes → parity-free,
|
|
1113
|
+
// exactly like taint. find_references reads it for the cross-file `resolved` reference tier.
|
|
1114
|
+
if (scipRefs && typeof scipRefs === 'object' && Object.keys(scipRefs).length) {
|
|
1115
|
+
this.db.prepare('INSERT INTO scip_refs (id, payload) VALUES (0, ?)').run(JSON.stringify(scipRefs));
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// B3 learned-sparse model (opt-in --learned-sparse). A single JSON payload (id = 0); both
|
|
1119
|
+
// backends parse identical bytes → parity-free, exactly like taint/scip_refs.
|
|
1120
|
+
if (sparseModel && typeof sparseModel === 'object' && sparseModel.assoc) {
|
|
1121
|
+
this.db.prepare('INSERT INTO sparse_model (id, payload) VALUES (0, ?)').run(JSON.stringify(sparseModel));
|
|
1122
|
+
}
|
|
1123
|
+
|
|
756
1124
|
const insMeta = this.db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)');
|
|
757
1125
|
insMeta.run('doc_count', String(docCount));
|
|
758
1126
|
insMeta.run('total_doc_len', String(totalDocLen));
|