graph-indexer 1.2.0 → 2.0.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 +412 -0
- package/README.md +216 -596
- package/SECURITY.md +68 -0
- package/TRADEMARK.md +31 -0
- package/cli-ui.mjs +78 -0
- package/config.mjs +179 -28
- 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 +489 -0
- package/engine/binary.mjs +399 -0
- package/{core-engine.mjs → engine/memory.mjs} +107 -440
- package/{sqlite-store.mjs → engine/sqlite.mjs} +137 -42
- package/enrichment.mjs +111 -32
- package/git-signals.mjs +248 -0
- package/indexer.mjs +151 -42
- package/init.mjs +1603 -171
- package/layout.mjs +123 -0
- package/mcp/format.mjs +61 -0
- package/mcp/tools.mjs +1157 -0
- package/mcp/topology.mjs +551 -0
- package/mcp-server.mjs +55 -24
- package/package.json +44 -13
- package/parse/extractor.mjs +617 -0
- package/parse/imports.mjs +365 -0
- package/parse/languages.mjs +89 -0
- package/parse/metadata.mjs +655 -0
- package/parse/routes.mjs +327 -0
- package/prompts/CORE.md +86 -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 +19 -0
- package/prompts/frameworks/LARAVEL_SYMFONY.md +22 -0
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +18 -0
- package/prompts/frameworks/RAILS.md +20 -0
- package/prompts/frameworks/REACT.md +20 -0
- package/prompts/frameworks/SPRING_BOOT.md +19 -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 +23 -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/search-core.mjs +254 -43
- package/storage.mjs +28 -3
- package/watch-daemon.mjs +69 -23
- 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
|
|
2
|
+
* @file engine/memory.mjs
|
|
3
3
|
* @description In-Memory Graph Indexer Core Engine.
|
|
4
4
|
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
5
5
|
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
@@ -7,405 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
okapiIdf, bm25Score, fuseAndRank, buildLexicalDocument, embeddingKeyFor,
|
|
10
|
+
tokenize, okapiIdf, bm25Score, fuseAndRank, buildLexicalDocument, embeddingKeyFor,
|
|
12
11
|
SUMMARY_VEC_SUFFIX, LEXICAL_FUSION_CAP, VECTOR_SCAN_RAW_N, finalizeVectorCandidates,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Thresholds for adaptive vector loading strategy
|
|
20
|
-
const HNSW_THRESHOLD = 5000; // Build HNSW index above this (eager mode only)
|
|
21
|
-
const LAZY_VEC_THRESHOLD = 10000; // Switch to lazy (disk-backed) vector access above this
|
|
22
|
-
|
|
23
|
-
// Optional HNSW accelerator for medium corpora (eager mode only)
|
|
24
|
-
let HierarchicalNSW = null;
|
|
25
|
-
try {
|
|
26
|
-
const mod = await import('hnswlib-node');
|
|
27
|
-
HierarchicalNSW = mod.HierarchicalNSW ?? mod.default?.HierarchicalNSW ?? null;
|
|
28
|
-
} catch { /* not installed — flat scan used */ }
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Serializes embeddingCache to compact binary.
|
|
32
|
-
* Format per entry: [uint32 hashLen][utf8 hash][uint32 dim][float32 × dim]
|
|
33
|
-
*/
|
|
34
|
-
export function writeEmbeddingBinary(embeddingCache) {
|
|
35
|
-
const entries = embeddingCache instanceof Map
|
|
36
|
-
? Array.from(embeddingCache.entries())
|
|
37
|
-
: Object.entries(embeddingCache);
|
|
38
|
-
let size = 4;
|
|
39
|
-
for (const [hash, vec] of entries) {
|
|
40
|
-
size += 4 + Buffer.byteLength(hash, 'utf8') + 4 + vec.length * 4;
|
|
41
|
-
}
|
|
42
|
-
const buf = Buffer.allocUnsafe(size);
|
|
43
|
-
let off = 0;
|
|
44
|
-
buf.writeUInt32LE(entries.length, off); off += 4;
|
|
45
|
-
for (const [hash, vec] of entries) {
|
|
46
|
-
const hashBytes = Buffer.from(hash, 'utf8');
|
|
47
|
-
buf.writeUInt32LE(hashBytes.length, off); off += 4;
|
|
48
|
-
hashBytes.copy(buf, off); off += hashBytes.length;
|
|
49
|
-
buf.writeUInt32LE(vec.length, off); off += 4;
|
|
50
|
-
for (let d = 0; d < vec.length; d++) { buf.writeFloatLE(vec[d], off); off += 4; }
|
|
51
|
-
}
|
|
52
|
-
return buf;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Decode an `.embeddings.bin` file into a Map<content_hash, Float32Array>.
|
|
57
|
-
* Standalone (mirrors the instance loader) so the indexer can reuse cached
|
|
58
|
-
* vectors across runs regardless of the active storage backend.
|
|
59
|
-
*/
|
|
60
|
-
export function readEmbeddingBinary(filePath) {
|
|
61
|
-
const cache = new Map();
|
|
62
|
-
if (!fs.existsSync(filePath)) return cache;
|
|
63
|
-
const buf = fs.readFileSync(filePath);
|
|
64
|
-
let off = 0;
|
|
65
|
-
const count = buf.readUInt32LE(off); off += 4;
|
|
66
|
-
for (let i = 0; i < count; i++) {
|
|
67
|
-
const hashLen = buf.readUInt32LE(off); off += 4;
|
|
68
|
-
const hash = buf.subarray(off, off + hashLen).toString('utf8'); off += hashLen;
|
|
69
|
-
const dim = buf.readUInt32LE(off); off += 4;
|
|
70
|
-
const vec = new Float32Array(dim);
|
|
71
|
-
for (let d = 0; d < dim; d++) { vec[d] = buf.readFloatLE(off); off += 4; }
|
|
72
|
-
cache.set(hash, vec);
|
|
73
|
-
}
|
|
74
|
-
return cache;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Append embedding entries to an `.embeddings.bin` file in place (creating it if
|
|
79
|
-
* absent) and return the absolute float-data offset of each appended entry.
|
|
80
|
-
* The leading uint32 entry count is updated so readEmbeddingBinary stays valid.
|
|
81
|
-
*
|
|
82
|
-
* Used by the watch daemon for incremental updates: a full bin rewrite on every
|
|
83
|
-
* file change would be O(corpus) — appending is O(changed chunks). Dead entries
|
|
84
|
-
* from replaced chunks are compacted away on the next full index run.
|
|
85
|
-
*
|
|
86
|
-
* @param {string} filePath
|
|
87
|
-
* @param {Map<string, Float32Array|number[]>|object} entries key → vector
|
|
88
|
-
* @returns {Map<string, {offset:number, dim:number}>}
|
|
89
|
-
*/
|
|
90
|
-
export function appendEmbeddingBinary(filePath, entries) {
|
|
91
|
-
const list = entries instanceof Map ? Array.from(entries.entries()) : Object.entries(entries || {});
|
|
92
|
-
const offsets = new Map();
|
|
93
|
-
if (list.length === 0) return offsets;
|
|
94
|
-
|
|
95
|
-
if (!fs.existsSync(filePath)) {
|
|
96
|
-
const hdr = Buffer.alloc(4);
|
|
97
|
-
hdr.writeUInt32LE(0, 0);
|
|
98
|
-
fs.writeFileSync(filePath, hdr);
|
|
99
|
-
}
|
|
100
|
-
const fd = fs.openSync(filePath, 'r+');
|
|
101
|
-
try {
|
|
102
|
-
const pos = fs.fstatSync(fd).size;
|
|
103
|
-
let size = 0;
|
|
104
|
-
for (const [key, vec] of list) size += 4 + Buffer.byteLength(key, 'utf8') + 4 + vec.length * 4;
|
|
105
|
-
const buf = Buffer.allocUnsafe(size);
|
|
106
|
-
let off = 0;
|
|
107
|
-
for (const [key, vec] of list) {
|
|
108
|
-
const kb = Buffer.from(key, 'utf8');
|
|
109
|
-
buf.writeUInt32LE(kb.length, off); off += 4;
|
|
110
|
-
kb.copy(buf, off); off += kb.length;
|
|
111
|
-
buf.writeUInt32LE(vec.length, off); off += 4;
|
|
112
|
-
offsets.set(key, { offset: pos + off, dim: vec.length });
|
|
113
|
-
for (let d = 0; d < vec.length; d++) { buf.writeFloatLE(vec[d], off); off += 4; }
|
|
114
|
-
}
|
|
115
|
-
fs.writeSync(fd, buf, 0, buf.length, pos);
|
|
116
|
-
const hdr = Buffer.allocUnsafe(4);
|
|
117
|
-
fs.readSync(fd, hdr, 0, 4, 0);
|
|
118
|
-
hdr.writeUInt32LE(hdr.readUInt32LE(0) + list.length, 0);
|
|
119
|
-
fs.writeSync(fd, hdr, 0, 4, 0);
|
|
120
|
-
} finally {
|
|
121
|
-
fs.closeSync(fd);
|
|
122
|
-
}
|
|
123
|
-
return offsets;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Cosine-score EVERY vector in an `.embeddings.bin` against a query vector,
|
|
128
|
-
* streaming from disk in bounded buffers, and return the top-N entry keys.
|
|
129
|
-
*
|
|
130
|
-
* This is the semantic channel for the disk-backed stores. The previous design
|
|
131
|
-
* scored vectors only for chunks that already matched lexically, which silently
|
|
132
|
-
* disabled semantic search exactly where it matters: conceptual queries that
|
|
133
|
-
* share no tokens with the code. A sequential scan of the bin is fast (the OS
|
|
134
|
-
* page cache holds it after the first query) and keeps resident RAM flat.
|
|
135
|
-
*
|
|
136
|
-
* @param {{fd?: number, buffer?: Buffer}} source Open fd or in-memory buffer of the bin.
|
|
137
|
-
* @param {Float32Array|number[]} queryVector
|
|
138
|
-
* @param {{topN?: number, minScore?: number}} [opts]
|
|
139
|
-
* @returns {Array<{key: string, score: number}>} Sorted by score, descending.
|
|
140
|
-
*/
|
|
141
|
-
export function scanEmbeddingBinary({ fd = -1, buffer = null }, queryVector, { topN = 200, minScore = 0 } = {}) {
|
|
142
|
-
const qDim = queryVector.length;
|
|
143
|
-
let qNorm = 0;
|
|
144
|
-
for (let d = 0; d < qDim; d++) qNorm += queryVector[d] * queryVector[d];
|
|
145
|
-
qNorm = Math.sqrt(qNorm);
|
|
146
|
-
if (qNorm === 0) return [];
|
|
147
|
-
|
|
148
|
-
const top = []; // sorted desc, capped at topN
|
|
149
|
-
let worst = -Infinity;
|
|
150
|
-
const push = (key, score) => {
|
|
151
|
-
if (score <= minScore) return;
|
|
152
|
-
if (top.length >= topN && score <= worst) return;
|
|
153
|
-
let lo = 0, hi = top.length;
|
|
154
|
-
while (lo < hi) { const mid = (lo + hi) >> 1; if (top[mid].score >= score) lo = mid + 1; else hi = mid; }
|
|
155
|
-
top.splice(lo, 0, { key, score });
|
|
156
|
-
if (top.length > topN) top.pop();
|
|
157
|
-
worst = top[top.length - 1].score;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
// Parses complete entries in buf starting at startOff; returns the offset of
|
|
161
|
-
// the first incomplete entry (callers carry the remainder into the next read).
|
|
162
|
-
const processEntries = (buf, startOff) => {
|
|
163
|
-
let off = startOff;
|
|
164
|
-
for (;;) {
|
|
165
|
-
if (off + 4 > buf.length) return off;
|
|
166
|
-
const keyLen = buf.readUInt32LE(off);
|
|
167
|
-
if (off + 4 + keyLen + 4 > buf.length) return off;
|
|
168
|
-
const dim = buf.readUInt32LE(off + 4 + keyLen);
|
|
169
|
-
const entryEnd = off + 4 + keyLen + 4 + dim * 4;
|
|
170
|
-
if (entryEnd > buf.length) return off;
|
|
171
|
-
if (dim === qDim) {
|
|
172
|
-
const key = buf.toString('utf8', off + 4, off + 4 + keyLen);
|
|
173
|
-
let dp = 0, nv = 0;
|
|
174
|
-
let p = off + 4 + keyLen + 4;
|
|
175
|
-
for (let d = 0; d < dim; d++, p += 4) {
|
|
176
|
-
const v = buf.readFloatLE(p);
|
|
177
|
-
dp += queryVector[d] * v;
|
|
178
|
-
nv += v * v;
|
|
179
|
-
}
|
|
180
|
-
const n = Math.sqrt(nv);
|
|
181
|
-
if (n > 0) push(key, dp / (qNorm * n));
|
|
182
|
-
}
|
|
183
|
-
off = entryEnd;
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
if (buffer) { processEntries(buffer, 4); return top; }
|
|
188
|
-
if (fd < 0) return [];
|
|
189
|
-
|
|
190
|
-
const CHUNK = 4 * 1024 * 1024;
|
|
191
|
-
let filePos = 0;
|
|
192
|
-
let carry = null;
|
|
193
|
-
let first = true;
|
|
194
|
-
for (;;) {
|
|
195
|
-
const buf = Buffer.allocUnsafe(CHUNK);
|
|
196
|
-
const read = fs.readSync(fd, buf, 0, CHUNK, filePos);
|
|
197
|
-
if (read <= 0) break;
|
|
198
|
-
filePos += read;
|
|
199
|
-
const work = carry && carry.length
|
|
200
|
-
? Buffer.concat([carry, buf.subarray(0, read)])
|
|
201
|
-
: buf.subarray(0, read);
|
|
202
|
-
const consumed = processEntries(work, first ? 4 : 0);
|
|
203
|
-
first = false;
|
|
204
|
-
carry = work.subarray(consumed);
|
|
205
|
-
if (read < CHUNK) break;
|
|
206
|
-
}
|
|
207
|
-
return top;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ─── Binary-quantized vector sketch ─────────────────────────────────────────────
|
|
211
|
-
// The streaming full scan above is exact but O(corpus × dim) per query: ~104 ms
|
|
212
|
-
// at 50k chunks and ~519 ms at 200k (measured warm). The sketch removes that
|
|
213
|
-
// ceiling while keeping resident RAM bounded: each vector is reduced to its SIGN
|
|
214
|
-
// BITS (768 dims → 96 bytes, 0.1% of the float32 data). A query is answered by
|
|
215
|
-
// (1) a Hamming-distance pass over the packed bits (XOR + popcount over Uint32
|
|
216
|
-
// words — a few ms even at 200k rows), keeping the best `oversample × topN`
|
|
217
|
-
// candidates, then (2) an exact cosine rescore of only those candidates, pread
|
|
218
|
-
// from the bin. Sign quantization of normalized embeddings preserves cosine
|
|
219
|
-
// ordering well enough that a 4× oversampled rescore recovers the exact top-N
|
|
220
|
-
// (validated in test/unit.mjs against the exhaustive scan).
|
|
221
|
-
//
|
|
222
|
-
// The sketch is APPEND-AWARE: the bin only ever grows between full rebuilds
|
|
223
|
-
// (the daemon appends), so `updateVectorSketch` re-reads just the tail beyond
|
|
224
|
-
// what it has already consumed, keeping refresh O(changed chunks).
|
|
225
|
-
|
|
226
|
-
function popcnt32(x) {
|
|
227
|
-
x -= (x >> 1) & 0x55555555;
|
|
228
|
-
x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
|
|
229
|
-
x = (x + (x >> 4)) & 0x0f0f0f0f;
|
|
230
|
-
return (x * 0x01010101) >> 24;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Pack a float vector's sign bits into `words` Uint32 words. */
|
|
234
|
-
function quantizeToBits(vec, out, base, words) {
|
|
235
|
-
for (let w = 0; w < words; w++) {
|
|
236
|
-
let bits = 0;
|
|
237
|
-
const d0 = w * 32;
|
|
238
|
-
const dMax = Math.min(d0 + 32, vec.length);
|
|
239
|
-
for (let d = d0; d < dMax; d++) {
|
|
240
|
-
if (vec[d] > 0) bits |= (1 << (d - d0));
|
|
241
|
-
}
|
|
242
|
-
out[base + w] = bits;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Build or incrementally extend a binary sketch of an `.embeddings.bin`.
|
|
248
|
-
*
|
|
249
|
-
* @param {object|null} sketch Existing sketch to extend, or null to create.
|
|
250
|
-
* @param {{fd?: number, buffer?: Buffer}} source
|
|
251
|
-
* @returns {object} sketch { dim, words, n, keys[], offsets[], bits: Uint32Array,
|
|
252
|
-
* consumed, headerCount, firstKey }
|
|
253
|
-
*/
|
|
254
|
-
export function updateVectorSketch(sketch, { fd = -1, buffer = null }) {
|
|
255
|
-
const readAt = (buf, pos, len) => {
|
|
256
|
-
if (buffer) return buffer.subarray(pos, pos + len);
|
|
257
|
-
const b = Buffer.allocUnsafe(len);
|
|
258
|
-
const r = fs.readSync(fd, b, 0, len, pos);
|
|
259
|
-
return r < len ? b.subarray(0, r) : b;
|
|
260
|
-
};
|
|
261
|
-
const fileSize = buffer ? buffer.length : (fd >= 0 ? fs.fstatSync(fd).size : 0);
|
|
262
|
-
if (fileSize < 4) return sketch;
|
|
263
|
-
|
|
264
|
-
const headerCount = readAt(null, 0, 4).readUInt32LE(0);
|
|
265
|
-
if (!sketch) {
|
|
266
|
-
sketch = {
|
|
267
|
-
dim: 0, words: 0, n: 0, keys: [], offsets: [],
|
|
268
|
-
bits: new Uint32Array(0), consumed: 4, headerCount: 0, firstKey: null,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
if (fileSize <= sketch.consumed) return sketch;
|
|
272
|
-
|
|
273
|
-
// Read the unseen tail in large blocks. No carry buffers / Buffer.concat —
|
|
274
|
-
// the next block simply re-reads from the last complete entry boundary
|
|
275
|
-
// (at most one partial entry of overlap), so the build leaves no transient
|
|
276
|
-
// garbage behind. Sign bits are quantized straight off the block buffer.
|
|
277
|
-
const CHUNK = 8 * 1024 * 1024;
|
|
278
|
-
const reusable = buffer ? null : Buffer.allocUnsafe(CHUNK);
|
|
279
|
-
let pos = sketch.consumed;
|
|
280
|
-
while (pos < fileSize) {
|
|
281
|
-
const len = Math.min(CHUNK, fileSize - pos);
|
|
282
|
-
let work;
|
|
283
|
-
if (buffer) {
|
|
284
|
-
work = buffer.subarray(pos, pos + len);
|
|
285
|
-
} else {
|
|
286
|
-
const r = fs.readSync(fd, reusable, 0, len, pos);
|
|
287
|
-
work = r < len ? reusable.subarray(0, r) : (len < CHUNK ? reusable.subarray(0, len) : reusable);
|
|
288
|
-
}
|
|
289
|
-
let off = 0;
|
|
290
|
-
let parsedAny = false;
|
|
291
|
-
for (;;) {
|
|
292
|
-
if (off + 4 > work.length) break;
|
|
293
|
-
const keyLen = work.readUInt32LE(off);
|
|
294
|
-
if (off + 4 + keyLen + 4 > work.length) break;
|
|
295
|
-
const dim = work.readUInt32LE(off + 4 + keyLen);
|
|
296
|
-
const entryEnd = off + 4 + keyLen + 4 + dim * 4;
|
|
297
|
-
if (entryEnd > work.length) break;
|
|
298
|
-
|
|
299
|
-
const key = work.toString('utf8', off + 4, off + 4 + keyLen);
|
|
300
|
-
const floatBase = off + 4 + keyLen + 4;
|
|
301
|
-
if (sketch.dim === 0) {
|
|
302
|
-
sketch.dim = dim;
|
|
303
|
-
sketch.words = Math.ceil(dim / 32);
|
|
304
|
-
}
|
|
305
|
-
if (dim === sketch.dim) {
|
|
306
|
-
if ((sketch.n + 1) * sketch.words > sketch.bits.length) {
|
|
307
|
-
const grown = new Uint32Array(Math.max(1024 * sketch.words, sketch.bits.length * 2));
|
|
308
|
-
grown.set(sketch.bits);
|
|
309
|
-
sketch.bits = grown;
|
|
310
|
-
}
|
|
311
|
-
// Quantize sign bits directly from the block buffer.
|
|
312
|
-
const bitBase = sketch.n * sketch.words;
|
|
313
|
-
for (let w = 0; w < sketch.words; w++) {
|
|
314
|
-
let bits = 0;
|
|
315
|
-
const d0 = w * 32;
|
|
316
|
-
const dMax = Math.min(d0 + 32, dim);
|
|
317
|
-
for (let d = d0; d < dMax; d++) {
|
|
318
|
-
if (work.readFloatLE(floatBase + d * 4) > 0) bits |= (1 << (d - d0));
|
|
319
|
-
}
|
|
320
|
-
sketch.bits[bitBase + w] = bits;
|
|
321
|
-
}
|
|
322
|
-
sketch.keys.push(key);
|
|
323
|
-
sketch.offsets.push(pos + floatBase);
|
|
324
|
-
if (sketch.firstKey === null) sketch.firstKey = key;
|
|
325
|
-
sketch.n++;
|
|
326
|
-
}
|
|
327
|
-
off = entryEnd;
|
|
328
|
-
parsedAny = true;
|
|
329
|
-
}
|
|
330
|
-
if (!parsedAny) break; // partial entry larger than the block — corrupt tail
|
|
331
|
-
pos += off; // resume exactly at the last complete entry boundary
|
|
332
|
-
sketch.consumed = pos;
|
|
333
|
-
}
|
|
334
|
-
sketch.headerCount = headerCount;
|
|
335
|
-
return sketch;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Approximate top-N search via the sketch + exact cosine rescore from the bin.
|
|
340
|
-
* Drop-in replacement for scanEmbeddingBinary (same return shape), ~20–50×
|
|
341
|
-
* faster at scale.
|
|
342
|
-
*
|
|
343
|
-
* @returns {Array<{key: string, score: number}>} sorted by exact cosine, desc.
|
|
344
|
-
*/
|
|
345
|
-
export function searchVectorSketch(sketch, { fd = -1, buffer = null }, queryVector, {
|
|
346
|
-
topN = 200, minScore = 0, oversample = 4,
|
|
347
|
-
} = {}) {
|
|
348
|
-
if (!sketch || sketch.n === 0 || queryVector.length !== sketch.dim) return [];
|
|
349
|
-
const words = sketch.words;
|
|
350
|
-
|
|
351
|
-
// 1. Quantize the query and rank rows by Hamming distance (bounded top-M).
|
|
352
|
-
const qbits = new Uint32Array(words);
|
|
353
|
-
quantizeToBits(queryVector, qbits, 0, words);
|
|
354
|
-
|
|
355
|
-
const M = Math.min(sketch.n, Math.max(topN * oversample, 64));
|
|
356
|
-
const candIdx = new Int32Array(M);
|
|
357
|
-
const candHam = new Int32Array(M);
|
|
358
|
-
let count = 0, worst = -1;
|
|
359
|
-
const bits = sketch.bits;
|
|
360
|
-
for (let i = 0; i < sketch.n; i++) {
|
|
361
|
-
const base = i * words;
|
|
362
|
-
let ham = 0;
|
|
363
|
-
for (let w = 0; w < words; w++) ham += popcnt32((bits[base + w] ^ qbits[w]) >>> 0);
|
|
364
|
-
if (count >= M && ham >= worst) continue;
|
|
365
|
-
// bounded insertion sort (ascending hamming)
|
|
366
|
-
let lo = 0, hi = count;
|
|
367
|
-
while (lo < hi) { const mid = (lo + hi) >> 1; if (candHam[mid] <= ham) lo = mid + 1; else hi = mid; }
|
|
368
|
-
const end = Math.min(count, M - 1);
|
|
369
|
-
for (let j = end; j > lo; j--) { candHam[j] = candHam[j - 1]; candIdx[j] = candIdx[j - 1]; }
|
|
370
|
-
candHam[lo] = ham; candIdx[lo] = i;
|
|
371
|
-
if (count < M) count++;
|
|
372
|
-
worst = candHam[count - 1];
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// 2. Exact cosine rescore of the candidates only (pread per candidate).
|
|
376
|
-
let qNorm = 0;
|
|
377
|
-
for (let d = 0; d < queryVector.length; d++) qNorm += queryVector[d] * queryVector[d];
|
|
378
|
-
qNorm = Math.sqrt(qNorm);
|
|
379
|
-
if (qNorm === 0) return [];
|
|
380
|
-
|
|
381
|
-
const byteLen = sketch.dim * 4;
|
|
382
|
-
const raw = Buffer.allocUnsafe(byteLen);
|
|
383
|
-
const scored = [];
|
|
384
|
-
for (let c = 0; c < count; c++) {
|
|
385
|
-
const i = candIdx[c];
|
|
386
|
-
let view;
|
|
387
|
-
if (buffer) {
|
|
388
|
-
view = buffer.subarray(sketch.offsets[i], sketch.offsets[i] + byteLen);
|
|
389
|
-
if (view.length < byteLen) continue;
|
|
390
|
-
} else {
|
|
391
|
-
const read = fs.readSync(fd, raw, 0, byteLen, sketch.offsets[i]);
|
|
392
|
-
if (read < byteLen) continue;
|
|
393
|
-
view = raw;
|
|
394
|
-
}
|
|
395
|
-
let dp = 0, nv = 0;
|
|
396
|
-
for (let d = 0; d < sketch.dim; d++) {
|
|
397
|
-
const v = view.readFloatLE(d * 4);
|
|
398
|
-
dp += queryVector[d] * v;
|
|
399
|
-
nv += v * v;
|
|
400
|
-
}
|
|
401
|
-
const n = Math.sqrt(nv);
|
|
402
|
-
if (n === 0) continue;
|
|
403
|
-
const score = dp / (qNorm * n);
|
|
404
|
-
if (score > minScore) scored.push({ key: sketch.keys[i], score });
|
|
405
|
-
}
|
|
406
|
-
scored.sort((a, b) => b.score - a.score);
|
|
407
|
-
return scored.slice(0, topN);
|
|
408
|
-
}
|
|
12
|
+
isNaturalLanguageQuery, WINDOW_VEC_SUFFIX, EMBEDDING_MAX_WINDOWS, baseEmbeddingKey,
|
|
13
|
+
} from '../search-core.mjs';
|
|
14
|
+
import {
|
|
15
|
+
HierarchicalNSW, HNSW_THRESHOLD, LAZY_VEC_THRESHOLD, SKETCH_THRESHOLD,
|
|
16
|
+
writeEmbeddingBinary, scanEmbeddingBinary, updateVectorSketch, searchVectorSketch,
|
|
17
|
+
} from './binary.mjs';
|
|
409
18
|
|
|
410
19
|
export class MemoryGraphIndex {
|
|
411
20
|
/**
|
|
@@ -469,10 +78,13 @@ export class MemoryGraphIndex {
|
|
|
469
78
|
if (!fs.existsSync(this.indexPath)) return;
|
|
470
79
|
const data = JSON.parse(fs.readFileSync(this.indexPath, 'utf-8'));
|
|
471
80
|
this.graph = data.graph || { dependencies: {}, importedBy: {} };
|
|
81
|
+
// Does this corpus carry LLM enrichment? Drives the NL vector-channel weight
|
|
82
|
+
// in fuseAndRank (strong summary vectors earn full weight; plain code vectors
|
|
83
|
+
// stay a low-weight rescue). Set during the chunk load loop below.
|
|
84
|
+
this._corpusEnriched = false;
|
|
472
85
|
|
|
473
86
|
const chunkCount = (data.chunks || []).length;
|
|
474
87
|
|
|
475
|
-
// Decide loading strategy based on corpus size and cacheEmbeddings flag
|
|
476
88
|
this._lazyMode = (!this._cacheEmbeddings) && chunkCount >= LAZY_VEC_THRESHOLD;
|
|
477
89
|
|
|
478
90
|
if (fs.existsSync(this._embeddingPath)) {
|
|
@@ -482,13 +94,10 @@ export class MemoryGraphIndex {
|
|
|
482
94
|
// For very large corpora: open persistent fd, release buffer from heap
|
|
483
95
|
if (chunkCount >= 50000) {
|
|
484
96
|
try { this._vecFd = fs.openSync(this._embeddingPath, 'r'); } catch { this._vecFd = -1; }
|
|
485
|
-
// binBuf goes out of scope → GC can collect it
|
|
486
97
|
} else {
|
|
487
|
-
// Medium-large: keep buffer for zero-copy slice access
|
|
488
98
|
this._embeddingBuf = binBuf;
|
|
489
99
|
}
|
|
490
100
|
} else {
|
|
491
|
-
// Eager: fill embeddingCache (needed by indexer + small-corpus MCP server)
|
|
492
101
|
this._loadEmbeddingBinary(binBuf);
|
|
493
102
|
}
|
|
494
103
|
} else if (data.embeddingCache) {
|
|
@@ -499,6 +108,9 @@ export class MemoryGraphIndex {
|
|
|
499
108
|
|
|
500
109
|
for (const chunk of (data.chunks || [])) {
|
|
501
110
|
this.chunks.set(chunk.id, chunk);
|
|
111
|
+
if (!this._corpusEnriched && (chunk.summary || (chunk.concepts && chunk.concepts.length))) {
|
|
112
|
+
this._corpusEnriched = true;
|
|
113
|
+
}
|
|
502
114
|
|
|
503
115
|
// Eager vector population. Vectors are keyed by embeddingKeyFor(chunk)
|
|
504
116
|
// (content_hash + enrichment digest); plain content_hash is the
|
|
@@ -507,10 +119,16 @@ export class MemoryGraphIndex {
|
|
|
507
119
|
const vecKey = embeddingKeyFor(chunk);
|
|
508
120
|
if (chunk.content_hash && this.embeddingCache.has(vecKey)) {
|
|
509
121
|
this.vectors.set(chunk.id, this.embeddingCache.get(vecKey));
|
|
510
|
-
// Summary-only
|
|
511
|
-
// pseudo row
|
|
122
|
+
// Summary-only and per-window vectors (enriched / oversized chunks):
|
|
123
|
+
// stored under pseudo row ids; searchHybrid folds hits back onto the
|
|
124
|
+
// chunk id (max-sim across all of the chunk's vectors).
|
|
512
125
|
const sVec = this.embeddingCache.get(vecKey + SUMMARY_VEC_SUFFIX);
|
|
513
126
|
if (sVec) this.vectors.set(chunk.id + SUMMARY_VEC_SUFFIX, sVec);
|
|
127
|
+
for (let i = 1; i < EMBEDDING_MAX_WINDOWS; i++) {
|
|
128
|
+
const wVec = this.embeddingCache.get(vecKey + WINDOW_VEC_SUFFIX + i);
|
|
129
|
+
if (!wVec) break;
|
|
130
|
+
this.vectors.set(chunk.id + WINDOW_VEC_SUFFIX + i, wVec);
|
|
131
|
+
}
|
|
514
132
|
} else if (chunk.content_hash && this.embeddingCache.has(chunk.content_hash)) {
|
|
515
133
|
this.vectors.set(chunk.id, this.embeddingCache.get(chunk.content_hash));
|
|
516
134
|
} else if (chunk.embedding) {
|
|
@@ -520,12 +138,9 @@ export class MemoryGraphIndex {
|
|
|
520
138
|
}
|
|
521
139
|
}
|
|
522
140
|
|
|
523
|
-
// Build inverted lexical index from the shared document builder
|
|
524
|
-
// (search-core.buildLexicalDocument) — identical text across backends.
|
|
525
141
|
const deps = this.graph.dependencies[chunk.file_path] || [];
|
|
526
142
|
this._indexLexical(chunk.id, buildLexicalDocument(chunk, deps), chunk.file_path);
|
|
527
143
|
|
|
528
|
-
// Build symbol table (Frontier 2)
|
|
529
144
|
if (chunk.name && chunk.name !== 'anonymous') {
|
|
530
145
|
const n = chunk.name.toLowerCase();
|
|
531
146
|
if (!this.symbolTable.has(n)) this.symbolTable.set(n, new Set());
|
|
@@ -606,7 +221,7 @@ export class MemoryGraphIndex {
|
|
|
606
221
|
// ─── Lexical index (TRUE inverted index) ──────────────────────────────────
|
|
607
222
|
|
|
608
223
|
_indexLexical(chunkId, text, filePath = '') {
|
|
609
|
-
const tokens = tokenize(text);
|
|
224
|
+
const tokens = tokenize(text); // raw + additive Porter stems
|
|
610
225
|
if (tokens.length === 0) return;
|
|
611
226
|
|
|
612
227
|
const termCounts = new Map();
|
|
@@ -614,16 +229,16 @@ export class MemoryGraphIndex {
|
|
|
614
229
|
termCounts.set(token, (termCounts.get(token) || 0) + 1);
|
|
615
230
|
}
|
|
616
231
|
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
this.
|
|
232
|
+
// Additive stem tokens earn their own postings but must NOT inflate docLen —
|
|
233
|
+
// inflated length would perturb BM25 normalisation for exact symbolic matches.
|
|
234
|
+
// raw-based docLen keeps raw-term BM25 byte-identical to the pre-stem index.
|
|
235
|
+
// Path tokens are tallied separately in searchHybrid, not here.
|
|
236
|
+
const rawLen = tokenize(text, false).length;
|
|
237
|
+
this.docLens.set(chunkId, rawLen);
|
|
238
|
+
this.totalDocLen += rawLen;
|
|
623
239
|
|
|
624
240
|
const chunkTokenSet = new Set();
|
|
625
241
|
for (const [term, count] of termCounts) {
|
|
626
|
-
// Store raw term count — BM25 applies saturation at search time
|
|
627
242
|
this.df.set(term, (this.df.get(term) || 0) + 1);
|
|
628
243
|
let posting = this.invertedIndex.get(term);
|
|
629
244
|
if (!posting) { posting = new Map(); this.invertedIndex.set(term, posting); }
|
|
@@ -638,7 +253,7 @@ export class MemoryGraphIndex {
|
|
|
638
253
|
// Path matching is done multiplicatively in searchHybrid, which avoids
|
|
639
254
|
// amplifying short stubs that happen to share a path with a long function.
|
|
640
255
|
if (filePath) {
|
|
641
|
-
const pathTokenSet = new Set(tokenize(filePath.replace(/[/\-_.]/g, ' ')));
|
|
256
|
+
const pathTokenSet = new Set(tokenize(filePath.replace(/[/\-_.]/g, ' '), false));
|
|
642
257
|
if (!this.pathTokens) this.pathTokens = new Map();
|
|
643
258
|
this.pathTokens.set(chunkId, pathTokenSet);
|
|
644
259
|
}
|
|
@@ -659,7 +274,6 @@ export class MemoryGraphIndex {
|
|
|
659
274
|
else this.df.set(term, freq - 1);
|
|
660
275
|
}
|
|
661
276
|
}
|
|
662
|
-
// Update BM25 length accounting
|
|
663
277
|
const dl = this.docLens.get(chunkId);
|
|
664
278
|
if (dl !== undefined) {
|
|
665
279
|
this.totalDocLen = Math.max(0, this.totalDocLen - dl);
|
|
@@ -671,7 +285,10 @@ export class MemoryGraphIndex {
|
|
|
671
285
|
}
|
|
672
286
|
|
|
673
287
|
_searchLexical(queryText) {
|
|
674
|
-
|
|
288
|
+
// Asymmetric stemming: the index always carries stems, but only stem the
|
|
289
|
+
// QUERY for natural-language/behavioural queries — symbolic name lookups
|
|
290
|
+
// stay exact so stem collisions never dilute a precise symbol match.
|
|
291
|
+
const queryTokens = tokenize(queryText, isNaturalLanguageQuery(queryText));
|
|
675
292
|
const scores = new Map();
|
|
676
293
|
const avgdl = this.docCount > 0 ? this.totalDocLen / this.docCount : 1;
|
|
677
294
|
|
|
@@ -739,7 +356,6 @@ export class MemoryGraphIndex {
|
|
|
739
356
|
this._vecNorms = norms;
|
|
740
357
|
this._matrixDirty = false;
|
|
741
358
|
|
|
742
|
-
// HNSW for large eager corpora
|
|
743
359
|
if (HierarchicalNSW && n >= HNSW_THRESHOLD) {
|
|
744
360
|
try {
|
|
745
361
|
const hnsw = new HierarchicalNSW('cosine', dim);
|
|
@@ -753,7 +369,7 @@ export class MemoryGraphIndex {
|
|
|
753
369
|
}
|
|
754
370
|
this._hnsw = hnsw;
|
|
755
371
|
} catch (e) {
|
|
756
|
-
process.stderr.write(`[
|
|
372
|
+
process.stderr.write(`[engine/memory] HNSW build failed: ${e.message}\n`);
|
|
757
373
|
this._hnsw = null;
|
|
758
374
|
}
|
|
759
375
|
}
|
|
@@ -773,7 +389,6 @@ export class MemoryGraphIndex {
|
|
|
773
389
|
qNorm = Math.sqrt(qNorm);
|
|
774
390
|
if (qNorm === 0) return [];
|
|
775
391
|
|
|
776
|
-
// HNSW fast path
|
|
777
392
|
if (this._hnsw) {
|
|
778
393
|
const topK = Math.min(200, n);
|
|
779
394
|
try {
|
|
@@ -788,7 +403,6 @@ export class MemoryGraphIndex {
|
|
|
788
403
|
} catch { /* fall through to flat scan */ }
|
|
789
404
|
}
|
|
790
405
|
|
|
791
|
-
// Exact flat scan
|
|
792
406
|
const results = [];
|
|
793
407
|
for (let i = 0; i < n; i++) {
|
|
794
408
|
const base = i * dim;
|
|
@@ -813,9 +427,14 @@ export class MemoryGraphIndex {
|
|
|
813
427
|
if (!this._embeddingBuf && this._vecFd < 0) return [];
|
|
814
428
|
// Large corpora: binary sketch (built once on first vector query) replaces
|
|
815
429
|
// the exact O(corpus) scan with a Hamming prefilter + bounded rescore.
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
|
|
430
|
+
// SKETCH_THRESHOLD is shared with the SQLite store so both switch to the
|
|
431
|
+
// sketch at the same vector count. NOTE: this lazy path only runs when the
|
|
432
|
+
// store is in lazy mode (cacheEmbeddings off AND chunkCount ≥ LAZY_VEC_THRESHOLD);
|
|
433
|
+
// a corpus with < LAZY_VEC_THRESHOLD chunks but ≥ SKETCH_THRESHOLD vectors
|
|
434
|
+
// (heavy enrichment/oversized windows) still scans exactly here while SQLite
|
|
435
|
+
// sketches — a narrow band worth a dedicated scale parity test before relying
|
|
436
|
+
// on byte-identical top-5 above the threshold.
|
|
437
|
+
if (!this._sketch && this._vecOffsets.size >= SKETCH_THRESHOLD) {
|
|
819
438
|
try { this._sketch = updateVectorSketch(null, source); } catch { this._sketch = null; }
|
|
820
439
|
}
|
|
821
440
|
const hits = this._sketch
|
|
@@ -834,6 +453,7 @@ export class MemoryGraphIndex {
|
|
|
834
453
|
const key = embeddingKeyFor(c);
|
|
835
454
|
put(key, c.id);
|
|
836
455
|
put(key + SUMMARY_VEC_SUFFIX, c.id); // summary-only vector → same chunk
|
|
456
|
+
for (let i = 1; i < EMBEDDING_MAX_WINDOWS; i++) put(key + WINDOW_VEC_SUFFIX + i, c.id); // window vectors → same chunk
|
|
837
457
|
if (key !== c.content_hash) put(c.content_hash, c.id); // legacy bins
|
|
838
458
|
}
|
|
839
459
|
this._keyToIds = map;
|
|
@@ -858,12 +478,12 @@ export class MemoryGraphIndex {
|
|
|
858
478
|
// zero lexical overlap, which the old TF-IDF prefilter never could.
|
|
859
479
|
vectorResults = this._scanVectorsLazy(queryVector, minScore);
|
|
860
480
|
} else {
|
|
861
|
-
// Eager rows include summary-only pseudo ids
|
|
862
|
-
// back onto its chunk via the shared
|
|
863
|
-
// chunk, deterministic order, same
|
|
481
|
+
// Eager rows include summary-only and per-window pseudo ids
|
|
482
|
+
// (`<id>|s`, `<id>|wN`) — fold each back onto its chunk via the shared
|
|
483
|
+
// finalizer (best/max-sim score per chunk, deterministic order, same
|
|
484
|
+
// cap as the disk-backed paths).
|
|
864
485
|
const entries = this._searchVector(queryVector, minScore).map(r => ({
|
|
865
|
-
id: r.id
|
|
866
|
-
? r.id.slice(0, -SUMMARY_VEC_SUFFIX.length) : r.id,
|
|
486
|
+
id: baseEmbeddingKey(r.id),
|
|
867
487
|
score: r.score,
|
|
868
488
|
}));
|
|
869
489
|
vectorResults = finalizeVectorCandidates(entries);
|
|
@@ -872,9 +492,6 @@ export class MemoryGraphIndex {
|
|
|
872
492
|
vectorResults = [];
|
|
873
493
|
}
|
|
874
494
|
|
|
875
|
-
// Fusion + boost ladder lives in search-core.mjs so the in-memory engine
|
|
876
|
-
// and the SQLite store rank identically. Backend state is reached through
|
|
877
|
-
// accessors; the math is measured once and shared.
|
|
878
495
|
return fuseAndRank({
|
|
879
496
|
lexicalResults,
|
|
880
497
|
vectorResults,
|
|
@@ -886,8 +503,8 @@ export class MemoryGraphIndex {
|
|
|
886
503
|
topK,
|
|
887
504
|
queryText,
|
|
888
505
|
exactBoostName,
|
|
889
|
-
|
|
890
|
-
//
|
|
506
|
+
corpusEnriched: this._corpusEnriched,
|
|
507
|
+
// O(1) vs the old full-scan; same contract implemented by the SQLite backend.
|
|
891
508
|
resolveExact: (term) => this.symbolTable.get(term) || [],
|
|
892
509
|
});
|
|
893
510
|
}
|
|
@@ -899,7 +516,7 @@ export class MemoryGraphIndex {
|
|
|
899
516
|
id: c.id, file_path: c.file_path, node_type: c.node_type,
|
|
900
517
|
name: c.name, docstring: c.docstring || '', code_snippet: c.code_snippet,
|
|
901
518
|
content_hash: c.content_hash, start_line: c.start_line, end_line: c.end_line,
|
|
902
|
-
calls: c.calls || [], params: c.params || [],
|
|
519
|
+
calls: c.calls || [], call_sites: c.call_sites || [], params: c.params || [],
|
|
903
520
|
return_type: c.return_type || '', class_context: c.class_context || '',
|
|
904
521
|
type_refs: c.type_refs || [], decorators: c.decorators || [],
|
|
905
522
|
extends: c.extends || [],
|
|
@@ -923,7 +540,7 @@ export class MemoryGraphIndex {
|
|
|
923
540
|
this._saveTimer = setTimeout(async () => {
|
|
924
541
|
this._saveTimer = null;
|
|
925
542
|
try { await this.save(); }
|
|
926
|
-
catch (err) { process.stderr.write(`[
|
|
543
|
+
catch (err) { process.stderr.write(`[engine/memory] ❌ Async save failed: ${err.message}\n`); }
|
|
927
544
|
}, delayMs);
|
|
928
545
|
}
|
|
929
546
|
|
|
@@ -961,6 +578,7 @@ export class MemoryGraphIndex {
|
|
|
961
578
|
this._removeLexical(id);
|
|
962
579
|
this.removeVector(id);
|
|
963
580
|
this.removeVector(id + SUMMARY_VEC_SUFFIX);
|
|
581
|
+
for (let i = 1; i < EMBEDDING_MAX_WINDOWS; i++) this.removeVector(id + WINDOW_VEC_SUFFIX + i);
|
|
964
582
|
this._removeSymbol(chunk);
|
|
965
583
|
this.chunks.delete(id);
|
|
966
584
|
}
|
|
@@ -977,6 +595,10 @@ export class MemoryGraphIndex {
|
|
|
977
595
|
if (vec) this.addVector(chunk.id, vec);
|
|
978
596
|
const sVec = this.embeddingCache.get(vecKey + SUMMARY_VEC_SUFFIX);
|
|
979
597
|
if (sVec) this.addVector(chunk.id + SUMMARY_VEC_SUFFIX, sVec);
|
|
598
|
+
for (let i = 1; i < EMBEDDING_MAX_WINDOWS; i++) {
|
|
599
|
+
const wVec = this.embeddingCache.get(vecKey + WINDOW_VEC_SUFFIX + i);
|
|
600
|
+
if (wVec) this.addVector(chunk.id + WINDOW_VEC_SUFFIX + i, wVec);
|
|
601
|
+
}
|
|
980
602
|
this._indexLexical(chunk.id, buildLexicalDocument(chunk, imports), chunk.file_path);
|
|
981
603
|
this.chunks.set(chunk.id, chunk);
|
|
982
604
|
if (chunk.name && chunk.name !== 'anonymous') {
|
|
@@ -1065,6 +687,51 @@ export class MemoryGraphIndex {
|
|
|
1065
687
|
return out;
|
|
1066
688
|
}
|
|
1067
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Chunks that reference a symbol *by name* as a type (`type_refs`) or as a
|
|
692
|
+
* base class/interface (`extends`) — the non-call half of symbol-level
|
|
693
|
+
* references. Calls are served by findCallers; mcp/topology.mjs findReferences fuses
|
|
694
|
+
* the two and classifies each by confidence. Case-insensitive exact match.
|
|
695
|
+
* @returns {object[]}
|
|
696
|
+
*/
|
|
697
|
+
findReferers(symbol) {
|
|
698
|
+
const key = String(symbol).toLowerCase().trim();
|
|
699
|
+
if (!key) return [];
|
|
700
|
+
const out = [];
|
|
701
|
+
for (const c of this.chunks.values()) {
|
|
702
|
+
// A class/type lists its own name in type_refs (the name node is a
|
|
703
|
+
// type_identifier) — that self-mention is a definition, not a reference.
|
|
704
|
+
if (c.name && c.name.toLowerCase() === key) continue;
|
|
705
|
+
const hit = (c.type_refs && c.type_refs.some(t => t.toLowerCase() === key))
|
|
706
|
+
|| (c.extends && c.extends.some(t => t.toLowerCase() === key));
|
|
707
|
+
if (hit) out.push(c);
|
|
708
|
+
}
|
|
709
|
+
return out;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* HTTP routes filtered by method and/or path prefix, each augmented with its
|
|
714
|
+
* resolved handler chunk (`chunk`, looked up by handler_chunk_id, or null).
|
|
715
|
+
* `graph.routes` round-trips verbatim through load(); a route-less index yields
|
|
716
|
+
* []. Method is matched case-insensitively (HTTP verbs are upper-cased); the
|
|
717
|
+
* path prefix match is case-insensitive. Mirrors SqliteGraphStore.findRoutes so
|
|
718
|
+
* both backends return identical records.
|
|
719
|
+
* @returns {object[]}
|
|
720
|
+
*/
|
|
721
|
+
findRoutes({ method = null, pathPrefix = null } = {}) {
|
|
722
|
+
const routes = (this.graph && this.graph.routes) || [];
|
|
723
|
+
const m = method ? String(method).toUpperCase().trim() : null;
|
|
724
|
+
const p = pathPrefix ? String(pathPrefix).toLowerCase() : null;
|
|
725
|
+
const out = [];
|
|
726
|
+
for (const r of routes) {
|
|
727
|
+
if (m && String(r.method || '').toUpperCase() !== m) continue;
|
|
728
|
+
if (p && !String(r.path || '').toLowerCase().startsWith(p)) continue;
|
|
729
|
+
const chunk = r.handler_chunk_id ? (this.chunks.get(r.handler_chunk_id) || null) : null;
|
|
730
|
+
out.push({ ...r, chunk });
|
|
731
|
+
}
|
|
732
|
+
return out;
|
|
733
|
+
}
|
|
734
|
+
|
|
1068
735
|
/** Lazily iterate every chunk (cursor-friendly parity with the SQLite store). */
|
|
1069
736
|
*iterateChunks() { yield* this.chunks.values(); }
|
|
1070
737
|
|
|
@@ -1078,11 +745,11 @@ export class MemoryGraphIndex {
|
|
|
1078
745
|
vectorCount() {
|
|
1079
746
|
if (this._lazyMode) {
|
|
1080
747
|
let n = 0;
|
|
1081
|
-
for (const k of this._vecOffsets.keys()) if (
|
|
748
|
+
for (const k of this._vecOffsets.keys()) if (baseEmbeddingKey(k) === k) n++;
|
|
1082
749
|
return n;
|
|
1083
750
|
}
|
|
1084
751
|
let n = 0;
|
|
1085
|
-
for (const k of this.vectors.keys()) if (
|
|
752
|
+
for (const k of this.vectors.keys()) if (baseEmbeddingKey(k) === k) n++;
|
|
1086
753
|
return n;
|
|
1087
754
|
}
|
|
1088
755
|
|