graph-indexer 2.0.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 +442 -0
- package/README.md +54 -11
- package/SECURITY.md +17 -0
- package/colbert.mjs +168 -0
- package/config.mjs +246 -21
- package/embeddings.mjs +90 -8
- package/engine/binary.mjs +38 -0
- package/engine/memory.mjs +214 -1
- package/engine/sqlite.mjs +278 -5
- package/enrichment.mjs +95 -0
- package/indexer.mjs +254 -9
- package/init.mjs +25 -2
- package/mcp/centrality.mjs +117 -0
- package/mcp/resolver.mjs +116 -0
- package/mcp/symbolgraph.mjs +137 -0
- package/mcp/taint.mjs +225 -0
- package/mcp/tools.mjs +444 -19
- package/mcp/topology.mjs +138 -13
- package/mcp-server.mjs +26 -2
- package/package.json +29 -21
- package/parse/extractor.mjs +17 -6
- package/parse/interprocedural.mjs +134 -0
- package/parse/languages.mjs +221 -57
- package/parse/metadata.mjs +46 -0
- package/parse/scip.mjs +391 -0
- package/parse/taint-patterns.mjs +119 -0
- package/prompts/CORE.md +9 -4
- package/prompts/frameworks/FASTAPI_DJANGO.md +1 -0
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +1 -0
- package/prompts/frameworks/REACT.md +1 -1
- package/prompts/frameworks/SPRING_BOOT.md +1 -0
- package/prompts/languages/GO.md +1 -0
- package/prompts/languages/JAVA.md +1 -1
- package/prompts/languages/PYTHON.md +1 -1
- package/prompts/languages/RUST.md +1 -1
- package/seal.mjs +400 -0
- package/search-core.mjs +151 -1
- package/search-sparse.mjs +183 -0
- package/storage.mjs +5 -0
- package/watch-daemon.mjs +7 -2
package/engine/memory.mjs
CHANGED
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
tokenize, okapiIdf, bm25Score, fuseAndRank, buildLexicalDocument, embeddingKeyFor,
|
|
11
11
|
SUMMARY_VEC_SUFFIX, LEXICAL_FUSION_CAP, VECTOR_SCAN_RAW_N, finalizeVectorCandidates,
|
|
12
12
|
isNaturalLanguageQuery, WINDOW_VEC_SUFFIX, EMBEDDING_MAX_WINDOWS, baseEmbeddingKey,
|
|
13
|
+
scoreSparseExpanded,
|
|
13
14
|
} from '../search-core.mjs';
|
|
15
|
+
import { expandSparseQuery } from '../search-sparse.mjs';
|
|
14
16
|
import {
|
|
15
17
|
HierarchicalNSW, HNSW_THRESHOLD, LAZY_VEC_THRESHOLD, SKETCH_THRESHOLD,
|
|
16
18
|
writeEmbeddingBinary, scanEmbeddingBinary, updateVectorSketch, searchVectorSketch,
|
|
@@ -70,6 +72,72 @@ export class MemoryGraphIndex {
|
|
|
70
72
|
|
|
71
73
|
this.rrfK = rrfK;
|
|
72
74
|
this._saveTimer = null;
|
|
75
|
+
|
|
76
|
+
// ── A4: persistent resolved symbol graph (opt-in; null when not built) ──
|
|
77
|
+
// _edges is the serialized edge list; the two maps index it by endpoint for
|
|
78
|
+
// O(1) getEdges and edge-backed findCallers/findReferers. Cleared on any
|
|
79
|
+
// incremental update (daemon) so a stale graph never serves wrong callers.
|
|
80
|
+
this._edges = null;
|
|
81
|
+
this._edgesByTo = new Map(); // to_chunk_id → edge[]
|
|
82
|
+
this._edgesByFrom = new Map(); // from_chunk_id → edge[]
|
|
83
|
+
|
|
84
|
+
// ── A5: symbol centrality (PageRank over the edges; built with the graph) ──
|
|
85
|
+
// _centrality maps chunk id → { score, rank }; precomputed at index time and
|
|
86
|
+
// serialized, so both backends serve identical numbers. Cleared with the edges.
|
|
87
|
+
this._centrality = null;
|
|
88
|
+
this._centralityTotal = 0;
|
|
89
|
+
this._centralRanked = null; // lazy id-by-rank cache for topCentral
|
|
90
|
+
|
|
91
|
+
// ── C2: serialized taint flows ({ flows, meta }); precomputed at index time with --taint,
|
|
92
|
+
// so the taint tools serve instantly and both backends serve identical flows. Cleared
|
|
93
|
+
// on any incremental file update (a code change invalidates the flow set).
|
|
94
|
+
this._taint = null;
|
|
95
|
+
|
|
96
|
+
// ── A2 v2: SCIP precise references — `{ defChunkId: [refererChunkId, …] }`, built at index
|
|
97
|
+
// time from a locally-generated SCIP index (--resolver scip) and serialized as an
|
|
98
|
+
// isolated artifact so it cannot perturb A4 edges / A5 centrality. find_references reads
|
|
99
|
+
// it for a binding-precise, cross-file `resolved` reference tier. Cleared with the graph.
|
|
100
|
+
this._scipRefs = null;
|
|
101
|
+
|
|
102
|
+
// ── B3: learned-sparse association model — `{ assoc: { term: [[assocTerm, weight], …] } }`,
|
|
103
|
+
// learned from corpus co-occurrence at index time with --learned-sparse and serialized as
|
|
104
|
+
// an isolated artifact. searchHybrid uses it to add an NL-only vocabulary-EXPANSION
|
|
105
|
+
// channel; absent on default indexes → channel inert → byte-identical. Whole-program →
|
|
106
|
+
// cleared on any incremental file update.
|
|
107
|
+
this._sparseModel = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Index the (already deterministically-ordered) edge list by endpoint. */
|
|
111
|
+
_indexEdges(edges) {
|
|
112
|
+
this._edges = edges;
|
|
113
|
+
this._edgesByTo = new Map();
|
|
114
|
+
this._edgesByFrom = new Map();
|
|
115
|
+
for (const e of edges) {
|
|
116
|
+
if (!this._edgesByTo.has(e.to_chunk_id)) this._edgesByTo.set(e.to_chunk_id, []);
|
|
117
|
+
this._edgesByTo.get(e.to_chunk_id).push(e);
|
|
118
|
+
if (!this._edgesByFrom.has(e.from_chunk_id)) this._edgesByFrom.set(e.from_chunk_id, []);
|
|
119
|
+
this._edgesByFrom.get(e.from_chunk_id).push(e);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Load the precomputed symbol-centrality map (A5). */
|
|
124
|
+
_indexCentrality(centrality) {
|
|
125
|
+
this._centrality = new Map(Object.entries(centrality));
|
|
126
|
+
this._centralityTotal = this._centrality.size;
|
|
127
|
+
this._centralRanked = null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Drop the symbol graph + its centrality + serialized taint + SCIP references (an incremental update makes them stale). */
|
|
131
|
+
_clearEdges() {
|
|
132
|
+
this._edges = null;
|
|
133
|
+
this._edgesByTo = new Map();
|
|
134
|
+
this._edgesByFrom = new Map();
|
|
135
|
+
this._centrality = null;
|
|
136
|
+
this._centralityTotal = 0;
|
|
137
|
+
this._centralRanked = null;
|
|
138
|
+
this._taint = null;
|
|
139
|
+
this._scipRefs = null;
|
|
140
|
+
this._sparseModel = null;
|
|
73
141
|
}
|
|
74
142
|
|
|
75
143
|
// ─── Load ─────────────────────────────────────────────────────────────────
|
|
@@ -78,6 +146,16 @@ export class MemoryGraphIndex {
|
|
|
78
146
|
if (!fs.existsSync(this.indexPath)) return;
|
|
79
147
|
const data = JSON.parse(fs.readFileSync(this.indexPath, 'utf-8'));
|
|
80
148
|
this.graph = data.graph || { dependencies: {}, importedBy: {} };
|
|
149
|
+
// A4 resolved symbol graph (opt-in; absent on default indexes → no-op).
|
|
150
|
+
if (Array.isArray(data.edges)) this._indexEdges(data.edges);
|
|
151
|
+
// A5 symbol centrality (built with the graph; absent on default indexes → no-op).
|
|
152
|
+
if (data.centrality && typeof data.centrality === 'object') this._indexCentrality(data.centrality);
|
|
153
|
+
// C2 serialized taint flows (opt-in --taint; absent on default indexes → no-op).
|
|
154
|
+
if (data.taint && typeof data.taint === 'object') this._taint = data.taint;
|
|
155
|
+
// A2 v2 SCIP precise references (opt-in --resolver scip; absent on default indexes → no-op).
|
|
156
|
+
if (data.scip_refs && typeof data.scip_refs === 'object') this._scipRefs = data.scip_refs;
|
|
157
|
+
// B3 learned-sparse model (opt-in --learned-sparse; absent on default indexes → no-op).
|
|
158
|
+
if (data.sparse_model && typeof data.sparse_model === 'object') this._sparseModel = data.sparse_model;
|
|
81
159
|
// Does this corpus carry LLM enrichment? Drives the NL vector-channel weight
|
|
82
160
|
// in fuseAndRank (strong summary vectors earn full weight; plain code vectors
|
|
83
161
|
// stay a low-weight rescue). Set during the chunk load loop below.
|
|
@@ -492,9 +570,14 @@ export class MemoryGraphIndex {
|
|
|
492
570
|
vectorResults = [];
|
|
493
571
|
}
|
|
494
572
|
|
|
573
|
+
// B3 learned-sparse channel: opt-in (model present) + NL-asymmetric (symbolic queries skip
|
|
574
|
+
// it → byte-identical). expandSparseQuery returns null when nothing fires → no channel.
|
|
575
|
+
const sparseResults = this._searchSparseExpanded(queryText);
|
|
576
|
+
|
|
495
577
|
return fuseAndRank({
|
|
496
578
|
lexicalResults,
|
|
497
579
|
vectorResults,
|
|
580
|
+
sparseResults,
|
|
498
581
|
getChunk: (id) => this.chunks.get(id),
|
|
499
582
|
getPathTokens: (id) => this.pathTokens.get(id),
|
|
500
583
|
getDf: (t) => this.df.get(t) || 0,
|
|
@@ -509,6 +592,31 @@ export class MemoryGraphIndex {
|
|
|
509
592
|
});
|
|
510
593
|
}
|
|
511
594
|
|
|
595
|
+
/**
|
|
596
|
+
* B3 — the learned-sparse expansion channel: NL queries only, model present only. Returns the
|
|
597
|
+
* ranked sparse candidates, or [] when the channel is inert (no model / symbolic query / no
|
|
598
|
+
* association fired) so fusion is byte-identical to the default path.
|
|
599
|
+
*/
|
|
600
|
+
_searchSparseExpanded(queryText) {
|
|
601
|
+
if (!this._sparseModel || !isNaturalLanguageQuery(queryText)) return [];
|
|
602
|
+
const termWeights = expandSparseQuery(queryText, this._sparseModel);
|
|
603
|
+
if (!termWeights) return [];
|
|
604
|
+
const avgdl = this.docCount > 0 ? this.totalDocLen / this.docCount : 1;
|
|
605
|
+
return scoreSparseExpanded(termWeights, {
|
|
606
|
+
docCount: this.docCount,
|
|
607
|
+
avgdl,
|
|
608
|
+
getDf: (t) => this.df.get(t) || 0,
|
|
609
|
+
getPostings:(t) => this.invertedIndex.get(t) || [],
|
|
610
|
+
getDocLen: (id) => this.docLens.get(id),
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Whether opt-in learned-sparse associations (B3 --learned-sparse) are loaded. */
|
|
615
|
+
hasSparseModel() { return this._sparseModel != null; }
|
|
616
|
+
|
|
617
|
+
/** Path to the shared `.embeddings.bin` (B4 late-interaction reranker reads candidate vectors from it). */
|
|
618
|
+
embeddingBinPath() { return this._embeddingPath; }
|
|
619
|
+
|
|
512
620
|
// ─── Persistence ───────────────────────────────────────────────────────────
|
|
513
621
|
|
|
514
622
|
async save() {
|
|
@@ -522,7 +630,14 @@ export class MemoryGraphIndex {
|
|
|
522
630
|
extends: c.extends || [],
|
|
523
631
|
hyde: c.hyde || '', summary: c.summary || '', concepts: c.concepts || [],
|
|
524
632
|
}));
|
|
525
|
-
const payload = JSON.stringify({
|
|
633
|
+
const payload = JSON.stringify({
|
|
634
|
+
chunks: chunksData, graph: this.graph,
|
|
635
|
+
...(this._edges ? { edges: this._edges } : {}),
|
|
636
|
+
...(this._centrality ? { centrality: Object.fromEntries(this._centrality) } : {}),
|
|
637
|
+
...(this._taint ? { taint: this._taint } : {}),
|
|
638
|
+
...(this._scipRefs ? { scip_refs: this._scipRefs } : {}),
|
|
639
|
+
...(this._sparseModel ? { sparse_model: this._sparseModel } : {}),
|
|
640
|
+
});
|
|
526
641
|
const tmpPath = `${this.indexPath}.tmp`;
|
|
527
642
|
const tmpBinPath = `${this._embeddingPath}.tmp`;
|
|
528
643
|
await Promise.all([
|
|
@@ -571,6 +686,10 @@ export class MemoryGraphIndex {
|
|
|
571
686
|
* @param {Map<string, Float32Array|number[]>} [p.embeddings] New vectors keyed by embeddingKeyFor(chunk).
|
|
572
687
|
*/
|
|
573
688
|
applyFileUpdate(filePath, { chunks = [], imports = [], embeddings = null } = {}) {
|
|
689
|
+
// A per-file edit makes the whole-program symbol graph + its centrality stale; drop
|
|
690
|
+
// both so findCallers/findReferers fall back to the always-correct scan until the next
|
|
691
|
+
// full `idx-index` rebuilds them (the daemon never rebuilds the graph).
|
|
692
|
+
if (this._edges || this._centrality || this._taint || this._scipRefs || this._sparseModel) this._clearEdges();
|
|
574
693
|
this.updateFileGraph(filePath, imports);
|
|
575
694
|
|
|
576
695
|
for (const [id, chunk] of Array.from(this.chunks.entries())) {
|
|
@@ -680,13 +799,103 @@ export class MemoryGraphIndex {
|
|
|
680
799
|
return out;
|
|
681
800
|
}
|
|
682
801
|
|
|
802
|
+
/**
|
|
803
|
+
* Resolved symbol-graph edges incident to a chunk (A4). Returns [] unless the
|
|
804
|
+
* opt-in graph was built. Deterministically ordered (the edge list is pre-sorted).
|
|
805
|
+
* @param {string} chunkId
|
|
806
|
+
* @param {object} [opts]
|
|
807
|
+
* @param {string|null} [opts.kind] Filter to 'calls' | 'extends' | 'type'.
|
|
808
|
+
* @param {'in'|'out'} [opts.direction] 'in' = referrers of chunkId (default), 'out' = its referents.
|
|
809
|
+
* @returns {Array<{from_chunk_id:string,to_chunk_id:string,kind:string,confidence:string,chunk:object|null}>}
|
|
810
|
+
*/
|
|
811
|
+
/** Whether the opt-in resolved symbol graph (A4) is loaded. */
|
|
812
|
+
hasSymbolGraph() { return this._edges != null; }
|
|
813
|
+
|
|
814
|
+
getEdges(chunkId, { kind = null, direction = 'in' } = {}) {
|
|
815
|
+
if (!this._edges) return [];
|
|
816
|
+
const src = direction === 'out' ? this._edgesByFrom : this._edgesByTo;
|
|
817
|
+
const list = src.get(chunkId) || [];
|
|
818
|
+
const out = [];
|
|
819
|
+
for (const e of list) {
|
|
820
|
+
if (kind && e.kind !== kind) continue;
|
|
821
|
+
const otherId = direction === 'out' ? e.to_chunk_id : e.from_chunk_id;
|
|
822
|
+
out.push({ ...e, chunk: this.chunks.get(otherId) ?? null });
|
|
823
|
+
}
|
|
824
|
+
return out;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** Whether opt-in symbol centrality (A5) is loaded. */
|
|
828
|
+
hasCentrality() { return this._centrality != null; }
|
|
829
|
+
|
|
830
|
+
/** Whether opt-in serialized taint flows (C2 --taint) are loaded. */
|
|
831
|
+
hasTaint() { return this._taint != null; }
|
|
832
|
+
|
|
833
|
+
/** The serialized taint payload `{ flows, meta }` (C2), or null. */
|
|
834
|
+
getTaintFlows() { return this._taint; }
|
|
835
|
+
|
|
836
|
+
/** Whether opt-in SCIP precise references (A2 v2 --resolver scip) are loaded. */
|
|
837
|
+
hasScipRefs() { return this._scipRefs != null; }
|
|
838
|
+
|
|
839
|
+
/** SCIP-confirmed referer chunk ids for a definition chunk (A2 v2), or [] when none / not loaded. */
|
|
840
|
+
getScipReferers(defChunkId) {
|
|
841
|
+
const refs = this._scipRefs && this._scipRefs[defChunkId];
|
|
842
|
+
return Array.isArray(refs) ? refs : [];
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** How many definitions carry SCIP precise references (A2 v2). 0 when not loaded. */
|
|
846
|
+
scipRefCount() { return this._scipRefs ? Object.keys(this._scipRefs).length : 0; }
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Symbol centrality (A5) for one chunk: its PageRank score, dense rank (1 = most central),
|
|
850
|
+
* and the total number of ranked (connected) chunks. Null when the chunk is unranked
|
|
851
|
+
* (isolated) or centrality was not built. @returns {{score:number,rank:number,total:number}|null}
|
|
852
|
+
*/
|
|
853
|
+
getCentrality(chunkId) {
|
|
854
|
+
if (!this._centrality) return null;
|
|
855
|
+
const e = this._centrality.get(chunkId);
|
|
856
|
+
return e ? { score: e.score, rank: e.rank, total: this._centralityTotal } : null;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/** The most central symbols (A5), rank-ascending. @returns {Array<{chunk,score,rank}>} */
|
|
860
|
+
topCentral(limit = 20) {
|
|
861
|
+
if (!this._centrality) return [];
|
|
862
|
+
if (!this._centralRanked) {
|
|
863
|
+
this._centralRanked = [...this._centrality.entries()]
|
|
864
|
+
.map(([id, v]) => ({ id, score: v.score, rank: v.rank }))
|
|
865
|
+
.sort((a, b) => a.rank - b.rank || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
866
|
+
}
|
|
867
|
+
const out = [];
|
|
868
|
+
for (const { id, score, rank } of this._centralRanked) {
|
|
869
|
+
const chunk = this.chunks.get(id);
|
|
870
|
+
if (!chunk) continue;
|
|
871
|
+
out.push({ chunk, score, rank });
|
|
872
|
+
if (out.length >= limit) break;
|
|
873
|
+
}
|
|
874
|
+
return out;
|
|
875
|
+
}
|
|
876
|
+
|
|
683
877
|
/** Chunks that call the given function name. @returns {object[]} */
|
|
684
878
|
findCallers(funcName) {
|
|
879
|
+
// Edge-backed (A4) when the resolved graph is present and the name is defined —
|
|
880
|
+
// returns the SAME set as the scan below (every name-match caller → each def).
|
|
881
|
+
if (this._edges) {
|
|
882
|
+
const defs = this.resolveSymbol(funcName);
|
|
883
|
+
if (defs.length > 0) return this._referrersByEdge(defs, e => e.kind === 'calls');
|
|
884
|
+
}
|
|
685
885
|
const out = [];
|
|
686
886
|
for (const c of this.chunks.values()) if (c.calls?.includes(funcName)) out.push(c);
|
|
687
887
|
return out;
|
|
688
888
|
}
|
|
689
889
|
|
|
890
|
+
/** From-chunks of edges of a given kind that point at any of `defs`, id-sorted. */
|
|
891
|
+
_referrersByEdge(defs, keep) {
|
|
892
|
+
const ids = new Set();
|
|
893
|
+
for (const def of defs) for (const e of (this._edgesByTo.get(def.id) || [])) if (keep(e)) ids.add(e.from_chunk_id);
|
|
894
|
+
const out = [];
|
|
895
|
+
for (const id of [...ids].sort()) { const c = this.chunks.get(id); if (c) out.push(c); }
|
|
896
|
+
return out;
|
|
897
|
+
}
|
|
898
|
+
|
|
690
899
|
/**
|
|
691
900
|
* Chunks that reference a symbol *by name* as a type (`type_refs`) or as a
|
|
692
901
|
* base class/interface (`extends`) — the non-call half of symbol-level
|
|
@@ -697,6 +906,10 @@ export class MemoryGraphIndex {
|
|
|
697
906
|
findReferers(symbol) {
|
|
698
907
|
const key = String(symbol).toLowerCase().trim();
|
|
699
908
|
if (!key) return [];
|
|
909
|
+
if (this._edges) {
|
|
910
|
+
const defs = this.resolveSymbol(symbol);
|
|
911
|
+
if (defs.length > 0) return this._referrersByEdge(defs, e => e.kind === 'extends' || e.kind === 'type');
|
|
912
|
+
}
|
|
700
913
|
const out = [];
|
|
701
914
|
for (const c of this.chunks.values()) {
|
|
702
915
|
// A class/type lists its own name in type_refs (the name node is a
|
package/engine/sqlite.mjs
CHANGED
|
@@ -29,8 +29,9 @@ import fs from 'fs';
|
|
|
29
29
|
import {
|
|
30
30
|
tokenize, okapiIdf, bm25Score, fuseAndRank, buildLexicalDocument, embeddingKeyFor,
|
|
31
31
|
LEXICAL_FUSION_CAP, VECTOR_SCAN_RAW_N, finalizeVectorCandidates,
|
|
32
|
-
isNaturalLanguageQuery, baseEmbeddingKey,
|
|
32
|
+
isNaturalLanguageQuery, baseEmbeddingKey, scoreSparseExpanded,
|
|
33
33
|
} from '../search-core.mjs';
|
|
34
|
+
import { expandSparseQuery } from '../search-sparse.mjs';
|
|
34
35
|
import {
|
|
35
36
|
writeEmbeddingBinary, appendEmbeddingBinary, scanEmbeddingBinary,
|
|
36
37
|
updateVectorSketch, searchVectorSketch, SKETCH_THRESHOLD,
|
|
@@ -60,6 +61,11 @@ CREATE TABLE IF NOT EXISTS chunks (
|
|
|
60
61
|
CREATE TABLE IF NOT EXISTS postings (term TEXT, chunk_id TEXT, tf INTEGER);
|
|
61
62
|
CREATE TABLE IF NOT EXISTS terms (term TEXT PRIMARY KEY, df INTEGER);
|
|
62
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);
|
|
63
69
|
CREATE TABLE IF NOT EXISTS deps (file TEXT, dep TEXT);
|
|
64
70
|
CREATE TABLE IF NOT EXISTS routes (
|
|
65
71
|
id INTEGER PRIMARY KEY,
|
|
@@ -81,6 +87,10 @@ CREATE INDEX IF NOT EXISTS idx_chunks_hash ON chunks(content_hash);
|
|
|
81
87
|
CREATE INDEX IF NOT EXISTS idx_postings_term ON postings(term);
|
|
82
88
|
CREATE INDEX IF NOT EXISTS idx_postings_chunk ON postings(chunk_id);
|
|
83
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);
|
|
84
94
|
CREATE INDEX IF NOT EXISTS idx_deps_file ON deps(file);
|
|
85
95
|
CREATE INDEX IF NOT EXISTS idx_deps_dep ON deps(dep);
|
|
86
96
|
CREATE INDEX IF NOT EXISTS idx_routes_method ON routes(method);
|
|
@@ -129,6 +139,14 @@ export class SqliteGraphStore {
|
|
|
129
139
|
this._dataVersion = -1;
|
|
130
140
|
this._writeReady = false;
|
|
131
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)
|
|
132
150
|
}
|
|
133
151
|
|
|
134
152
|
get backend() { return 'sqlite'; }
|
|
@@ -177,10 +195,56 @@ export class SqliteGraphStore {
|
|
|
177
195
|
this._openVecFd();
|
|
178
196
|
this._refreshSketch();
|
|
179
197
|
this._prepare();
|
|
198
|
+
this._refreshHasEdges();
|
|
199
|
+
this._refreshHasCentrality();
|
|
200
|
+
this._refreshHasTaint();
|
|
201
|
+
this._refreshHasScipRefs();
|
|
202
|
+
this._refreshHasSparseModel();
|
|
180
203
|
this._dataVersion = this._readDataVersion();
|
|
181
204
|
return this;
|
|
182
205
|
}
|
|
183
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
|
+
|
|
184
248
|
_openVecFd() {
|
|
185
249
|
if (this._vecFd >= 0) { try { fs.closeSync(this._vecFd); } catch {} this._vecFd = -1; }
|
|
186
250
|
if (fs.existsSync(this._embeddingPath)) {
|
|
@@ -306,6 +370,27 @@ export class SqliteGraphStore {
|
|
|
306
370
|
'SELECT method, path, handler_name, handler_chunk_id, file_path, line, framework '
|
|
307
371
|
+ 'FROM routes ORDER BY id'
|
|
308
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');
|
|
309
394
|
}
|
|
310
395
|
|
|
311
396
|
_prepareWrite() {
|
|
@@ -365,11 +450,124 @@ export class SqliteGraphStore {
|
|
|
365
450
|
return this._stmtByName.all(key).map(r => this._rowToChunk(r));
|
|
366
451
|
}
|
|
367
452
|
|
|
368
|
-
|
|
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
|
+
}
|
|
369
563
|
|
|
370
564
|
findReferers(symbol) {
|
|
371
565
|
const key = String(symbol).toLowerCase().trim();
|
|
372
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
|
+
}
|
|
373
571
|
// Escape LIKE metacharacters in the symbol, then bracket with JSON quotes so
|
|
374
572
|
// the prefilter only fires on a whole array element.
|
|
375
573
|
const esc = key.replace(/[\\%_]/g, m => '\\' + m);
|
|
@@ -513,6 +711,11 @@ export class SqliteGraphStore {
|
|
|
513
711
|
.slice(0, LEXICAL_FUSION_CAP)
|
|
514
712
|
.map(([id, score], i) => ({ id, score, rank: i + 1 }));
|
|
515
713
|
|
|
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
|
+
|
|
516
719
|
// Stream the FULL embeddings bin (bounded buffers) so conceptual queries
|
|
517
720
|
// that share no tokens with the code can still match — scoring only lexical
|
|
518
721
|
// candidates silently disabled semantic search where it mattered most.
|
|
@@ -574,13 +777,36 @@ export class SqliteGraphStore {
|
|
|
574
777
|
};
|
|
575
778
|
|
|
576
779
|
return fuseAndRank({
|
|
577
|
-
lexicalResults, vectorResults, getChunk, getPathTokens, getDf,
|
|
780
|
+
lexicalResults, vectorResults, sparseResults, getChunk, getPathTokens, getDf,
|
|
578
781
|
docCount: this._docCount, rrfK: this.rrfK, topK, queryText, exactBoostName,
|
|
579
782
|
corpusEnriched: this._corpusEnriched,
|
|
580
783
|
resolveExact: (term) => this._stmtIdByName.all(term).map(r => r.id),
|
|
581
784
|
});
|
|
582
785
|
}
|
|
583
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
|
+
|
|
584
810
|
close() {
|
|
585
811
|
if (this._vecFd >= 0) { try { fs.closeSync(this._vecFd); } catch {} this._vecFd = -1; }
|
|
586
812
|
if (this.db) { try { this.db.close(); } catch {} this.db = null; }
|
|
@@ -657,6 +883,15 @@ export class SqliteGraphStore {
|
|
|
657
883
|
|
|
658
884
|
this.db.exec('BEGIN');
|
|
659
885
|
try {
|
|
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
|
+
|
|
660
895
|
// Capture reusable vector offsets BEFORE deleting old rows, which may
|
|
661
896
|
// be the only rows that hold those offsets.
|
|
662
897
|
const reuse = new Map();
|
|
@@ -749,7 +984,7 @@ export class SqliteGraphStore {
|
|
|
749
984
|
* @param {{dependencies:Object<string,string[]>}} payload.graph
|
|
750
985
|
* @param {Map<string,Float32Array>|object} [payload.embeddingCache] Keyed by embeddingKeyFor(chunk).
|
|
751
986
|
*/
|
|
752
|
-
buildFrom({ chunks, graph, embeddingCache }) {
|
|
987
|
+
buildFrom({ chunks, graph, embeddingCache, edges = null, centrality = null, taint = null, scipRefs = null, sparseModel = null }) {
|
|
753
988
|
this._open();
|
|
754
989
|
this.db.exec('PRAGMA synchronous = OFF;'); // safe only for a full rebuild; not used for incremental writes
|
|
755
990
|
|
|
@@ -781,7 +1016,9 @@ export class SqliteGraphStore {
|
|
|
781
1016
|
DROP TABLE IF EXISTS chunks; DROP TABLE IF EXISTS postings;
|
|
782
1017
|
DROP TABLE IF EXISTS terms; DROP TABLE IF EXISTS call_edges;
|
|
783
1018
|
DROP TABLE IF EXISTS deps; DROP TABLE IF EXISTS meta;
|
|
784
|
-
DROP TABLE IF EXISTS routes;
|
|
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;
|
|
785
1022
|
`);
|
|
786
1023
|
this.db.exec(SCHEMA_TABLES);
|
|
787
1024
|
this.db.exec(SCHEMA_INDEXES);
|
|
@@ -848,6 +1085,42 @@ export class SqliteGraphStore {
|
|
|
848
1085
|
);
|
|
849
1086
|
}
|
|
850
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
|
+
|
|
851
1124
|
const insMeta = this.db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)');
|
|
852
1125
|
insMeta.run('doc_count', String(docCount));
|
|
853
1126
|
insMeta.run('total_doc_len', String(totalDocLen));
|