simt-dev 0.3.1 → 0.5.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 +157 -0
- package/README.md +271 -62
- package/package.json +6 -2
- package/rules/core.md +1 -1
- package/src/advice/advice.mjs +229 -0
- package/src/agenthooks/policy.mjs +10 -2
- package/src/agenthooks/prompt.mjs +81 -0
- package/src/agenthooks/render.mjs +138 -17
- package/src/argv.mjs +28 -1
- package/src/bench/grade.mjs +3 -0
- package/src/bench/run.mjs +88 -54
- package/src/bench/tasks.mjs +38 -0
- package/src/bench/variants.mjs +456 -0
- package/src/brief/assemble.mjs +368 -0
- package/src/brief/kind.mjs +134 -0
- package/src/brief/packet.mjs +200 -0
- package/src/commands/agenthook.mjs +84 -5
- package/src/commands/bench.mjs +72 -15
- package/src/commands/brief.mjs +78 -0
- package/src/commands/compare.mjs +10 -2
- package/src/commands/decide.mjs +116 -0
- package/src/commands/distill.mjs +36 -0
- package/src/commands/help.mjs +62 -3
- package/src/commands/index.mjs +29 -1
- package/src/commands/lessons.mjs +127 -0
- package/src/commands/local.mjs +97 -0
- package/src/commands/map.mjs +42 -7
- package/src/commands/repoindex.mjs +4 -1
- package/src/commands/review.mjs +65 -0
- package/src/commands/rules.mjs +17 -2
- package/src/commands/skills.mjs +5 -50
- package/src/commands/skillscompile.mjs +255 -0
- package/src/commands/sync.mjs +15 -0
- package/src/commands/tests.mjs +2 -49
- package/src/commands/why.mjs +23 -25
- package/src/compare/learn.mjs +680 -0
- package/src/decisions/index.mjs +116 -65
- package/src/distill/distill.mjs +246 -0
- package/src/distill/engine.mjs +187 -0
- package/src/federation/client.mjs +188 -0
- package/src/federation/config.mjs +134 -0
- package/src/federation/query.mjs +48 -0
- package/src/graph/compile.mjs +475 -0
- package/src/index/gate.mjs +1 -13
- package/src/index/govern.mjs +23 -11
- package/src/index/map.mjs +251 -17
- package/src/index/persist.mjs +74 -7
- package/src/index/store.mjs +222 -8
- package/src/kernel/variants.mjs +353 -0
- package/src/ledger/index.mjs +36 -1
- package/src/local/client.mjs +482 -0
- package/src/local/config.mjs +269 -0
- package/src/mcp/tools.mjs +138 -1
- package/src/memory/decide.mjs +462 -0
- package/src/memory/lessons.mjs +858 -0
- package/src/projection/lock.mjs +39 -7
- package/src/projection/rules.mjs +32 -8
- package/src/resolve/body.mjs +46 -0
- package/src/review/critic.mjs +509 -0
- package/src/toolwords.mjs +6 -0
package/src/index/map.mjs
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
// Ranked repo map (R22, ADR-091).
|
|
1
|
+
// Ranked repo map and hybrid retrieval (R22, R28, ADR-091).
|
|
2
2
|
//
|
|
3
|
-
// PageRank over import and call edges, personalised to the task
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// PageRank over import and call edges, personalised to the task, fused with
|
|
4
|
+
// FTS5 BM25 (`bm25()`) and resolver signals via reciprocal-rank fusion (RRF).
|
|
5
|
+
// Optional local vector embeddings (stored as SQLite blobs via R37) are scored
|
|
6
|
+
// by brute-force cosine similarity and stay off by default unless they raise
|
|
7
|
+
// gold-file recall@10 by at least 10 percentage points (+0.10).
|
|
8
8
|
|
|
9
9
|
import { readdir, readFile } from 'node:fs/promises';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
|
|
12
|
-
import { resolveVaultDir } from '../bench/mine.mjs';
|
|
13
|
-
import {
|
|
12
|
+
import { isTestPath, resolveVaultDir } from '../bench/mine.mjs';
|
|
13
|
+
import { readLocalConfig } from '../local/config.mjs';
|
|
14
|
+
import { localEmbed } from '../local/client.mjs';
|
|
15
|
+
import {
|
|
16
|
+
decodeVectorBlob,
|
|
17
|
+
readEdges,
|
|
18
|
+
readEmbeddings,
|
|
19
|
+
readFiles,
|
|
20
|
+
readFts,
|
|
21
|
+
readSymbols,
|
|
22
|
+
scoreFtsBm25,
|
|
23
|
+
searchFtsBm25,
|
|
24
|
+
} from './store.mjs';
|
|
14
25
|
|
|
15
26
|
export const ITERATIONS = 20;
|
|
16
27
|
export const DAMPING = 0.85;
|
|
28
|
+
export const RRF_K = 40;
|
|
29
|
+
export const EMBED_RECALL_GAIN_BAR = 0.1;
|
|
17
30
|
const TOP = 10;
|
|
18
31
|
export const DEFAULT_BUDGET = 2048;
|
|
19
32
|
|
|
@@ -26,13 +39,106 @@ const STOP = new Set([
|
|
|
26
39
|
]);
|
|
27
40
|
|
|
28
41
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
42
|
+
* Reciprocal-rank fusion (RRF) over multiple ranked lists.
|
|
43
|
+
*
|
|
44
|
+
* Each list entry may be a path string or `{ path }`. Ties break by path ascending
|
|
45
|
+
* so fusion is strictly deterministic.
|
|
31
46
|
*
|
|
32
|
-
* @param {{
|
|
47
|
+
* @param {{items: ({path: string}|string)[], weight?: number}[]} lists
|
|
48
|
+
* @param {number} [k]
|
|
33
49
|
* @returns {{path: string, rank: number}[]}
|
|
34
50
|
*/
|
|
35
|
-
export function
|
|
51
|
+
export function rrfFuse(lists, k = RRF_K) {
|
|
52
|
+
const fused = new Map();
|
|
53
|
+
const bestScore = new Map();
|
|
54
|
+
for (const { items = [], weight = 1 } of lists) {
|
|
55
|
+
if (!Array.isArray(items) || weight <= 0) continue;
|
|
56
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
57
|
+
const entry = items[i];
|
|
58
|
+
const path = typeof entry === 'string' ? entry : entry?.path;
|
|
59
|
+
if (!path) continue;
|
|
60
|
+
fused.set(path, (fused.get(path) ?? 0) + weight / (k + i + 1));
|
|
61
|
+
if (typeof entry === 'object' && typeof entry?.score === 'number') {
|
|
62
|
+
bestScore.set(path, Math.max(bestScore.get(path) ?? 0, entry.score));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return [...fused.entries()]
|
|
67
|
+
.map(([path, rank]) => ({ path, rank, score: bestScore.get(path) ?? 0 }))
|
|
68
|
+
.sort((a, b) => (a.rank !== b.rank ? (a.rank < b.rank ? 1 : -1) : cmp(a.path, b.path)));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Brute-force cosine similarity between two numeric / Float32Array vectors.
|
|
73
|
+
*/
|
|
74
|
+
export function cosineSimilarity(a, b) {
|
|
75
|
+
if (!a || !b) return 0;
|
|
76
|
+
const len = Math.min(a.length ?? 0, b.length ?? 0);
|
|
77
|
+
if (len === 0) return 0;
|
|
78
|
+
let dot = 0;
|
|
79
|
+
let normA = 0;
|
|
80
|
+
let normB = 0;
|
|
81
|
+
for (let i = 0; i < len; i += 1) {
|
|
82
|
+
const va = Number(a[i]) || 0;
|
|
83
|
+
const vb = Number(b[i]) || 0;
|
|
84
|
+
dot += va * vb;
|
|
85
|
+
normA += va * va;
|
|
86
|
+
normB += vb * vb;
|
|
87
|
+
}
|
|
88
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
89
|
+
if (denom === 0) return 0;
|
|
90
|
+
return dot / denom;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Rank stored vector embeddings against a query vector by brute-force cosine similarity.
|
|
95
|
+
*
|
|
96
|
+
* @param {{path: string, vec: Float32Array|number[]|Buffer}[]} embeddings
|
|
97
|
+
* @param {Float32Array|number[]|Buffer} queryVector
|
|
98
|
+
* @returns {{path: string, score: number}[]}
|
|
99
|
+
*/
|
|
100
|
+
export function rankByCosine(embeddings, queryVector) {
|
|
101
|
+
const qVec = queryVector instanceof Float32Array ? queryVector : decodeVectorBlob(queryVector);
|
|
102
|
+
if (!Array.isArray(embeddings) || embeddings.length === 0 || qVec.length === 0) return [];
|
|
103
|
+
const scored = [];
|
|
104
|
+
for (const row of embeddings) {
|
|
105
|
+
if (!row?.path) continue;
|
|
106
|
+
const vec = row.vec instanceof Float32Array ? row.vec : decodeVectorBlob(row.vec);
|
|
107
|
+
if (vec.length === 0) continue;
|
|
108
|
+
const sim = cosineSimilarity(vec, qVec);
|
|
109
|
+
if (sim > 0) scored.push({ path: row.path, score: sim });
|
|
110
|
+
}
|
|
111
|
+
return scored.sort((a, b) => (a.score !== b.score ? (a.score < b.score ? 1 : -1) : cmp(a.path, b.path)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Rank files for one task. No task text ranks the whole graph. A task ranks the
|
|
116
|
+
* files it mentions and the files one import or call away from those, and when
|
|
117
|
+
* FTS / BM25 rows or optional vector embeddings are supplied, fuses BM25 +
|
|
118
|
+
* graph proximity + resolver signals via reciprocal-rank fusion (R28).
|
|
119
|
+
*
|
|
120
|
+
* @param {{
|
|
121
|
+
* files: {path: string}[],
|
|
122
|
+
* edges: {src: string, dst: string, kind: string, weight?: number}[],
|
|
123
|
+
* symbols: {path: string, name: string}[],
|
|
124
|
+
* fts?: {kind?: string, ref?: string, path?: string, body?: string}[],
|
|
125
|
+
* bm25?: {path: string, score?: number}[],
|
|
126
|
+
* embeddings?: {path: string, vec: Float32Array|number[]|Buffer}[],
|
|
127
|
+
* queryVector?: Float32Array|number[]|Buffer|null,
|
|
128
|
+
* task?: string
|
|
129
|
+
* }} input
|
|
130
|
+
* @returns {{path: string, rank: number, score: number}[]}
|
|
131
|
+
*/
|
|
132
|
+
export function rankFiles({
|
|
133
|
+
files,
|
|
134
|
+
edges,
|
|
135
|
+
symbols,
|
|
136
|
+
fts = undefined,
|
|
137
|
+
bm25 = undefined,
|
|
138
|
+
embeddings = undefined,
|
|
139
|
+
queryVector = null,
|
|
140
|
+
task = '',
|
|
141
|
+
}) {
|
|
36
142
|
const nodes = [...new Set(files.map((file) => file.path))].sort();
|
|
37
143
|
const symbolsByPath = groupSymbols(symbols);
|
|
38
144
|
const scores = scoreFiles(nodes, symbolsByPath, task);
|
|
@@ -43,9 +149,44 @@ export function rankFiles({ files, edges, symbols, task = '' }) {
|
|
|
43
149
|
// A file the task mentions stays ahead of a file it does not. PageRank on
|
|
44
150
|
// this graph otherwise returns the file every module imports, and the files
|
|
45
151
|
// the task named never reach the top 10.
|
|
46
|
-
|
|
152
|
+
const baseRanked = kept
|
|
47
153
|
.map((path, i) => ({ path, rank: rank[i], score: scores.get(path) ?? 0 }))
|
|
48
154
|
.sort(byRank);
|
|
155
|
+
|
|
156
|
+
if (String(task ?? '').trim().length === 0) return baseRanked;
|
|
157
|
+
|
|
158
|
+
const knownPaths = new Set(nodes);
|
|
159
|
+
const rawBm25 = Array.isArray(bm25)
|
|
160
|
+
? bm25
|
|
161
|
+
: Array.isArray(fts) && fts.length > 0
|
|
162
|
+
? scoreFtsBm25(fts, task)
|
|
163
|
+
: [];
|
|
164
|
+
const bm25Ranked = rawBm25
|
|
165
|
+
.filter((row) => knownPaths.has(row.path) && !isTestPath(row.path))
|
|
166
|
+
.slice(0, 30);
|
|
167
|
+
|
|
168
|
+
const embedRanked =
|
|
169
|
+
queryVector && Array.isArray(embeddings) && embeddings.length > 0
|
|
170
|
+
? rankByCosine(embeddings, queryVector)
|
|
171
|
+
.filter((row) => knownPaths.has(row.path) && !isTestPath(row.path))
|
|
172
|
+
.slice(0, 30)
|
|
173
|
+
: [];
|
|
174
|
+
|
|
175
|
+
if (bm25Ranked.length === 0 && embedRanked.length === 0) {
|
|
176
|
+
return baseRanked;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const resolverRanked = baseRanked.filter((row) => (row.score ?? 0) > 0);
|
|
180
|
+
const graphRanked = [...baseRanked].sort((a, b) => (a.rank !== b.rank ? (a.rank < b.rank ? 1 : -1) : cmp(a.path, b.path)));
|
|
181
|
+
const lists = [
|
|
182
|
+
{ items: resolverRanked, weight: 2.0 },
|
|
183
|
+
{ items: graphRanked, weight: 1.0 },
|
|
184
|
+
{ items: bm25Ranked, weight: 1.0 },
|
|
185
|
+
];
|
|
186
|
+
if (embedRanked.length > 0) {
|
|
187
|
+
lists.push({ items: embedRanked, weight: 1.5 });
|
|
188
|
+
}
|
|
189
|
+
return rrfFuse(lists, RRF_K);
|
|
49
190
|
}
|
|
50
191
|
|
|
51
192
|
/**
|
|
@@ -101,13 +242,47 @@ export function goldPaths(patch) {
|
|
|
101
242
|
return [...new Set(out)].sort();
|
|
102
243
|
}
|
|
103
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Embed a task query using the repository's configured `local.embed` endpoint.
|
|
247
|
+
*
|
|
248
|
+
* Returns `null` without opening a socket when `embed` is false, `local.embed`
|
|
249
|
+
* is unset, or a non-loopback URL is refused.
|
|
250
|
+
*/
|
|
251
|
+
export async function embedTaskQuery(root, task, { embed = false, allowRemote = false, fetchImpl = undefined } = {}) {
|
|
252
|
+
if (!embed || String(task ?? '').trim().length === 0) return null;
|
|
253
|
+
const localCfg = await readLocalConfig(root, { allowRemote });
|
|
254
|
+
if (!localCfg.embed || localCfg.embed.blocked) return null;
|
|
255
|
+
const res = await localEmbed(localCfg.embed, {
|
|
256
|
+
input: String(task).slice(0, 4096),
|
|
257
|
+
allowRemote,
|
|
258
|
+
fetchImpl,
|
|
259
|
+
});
|
|
260
|
+
if (!res.ok || !Array.isArray(res.embeddings?.[0]) || res.embeddings[0].length === 0) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
return Float32Array.from(res.embeddings[0], (v) => Number(v) || 0);
|
|
264
|
+
}
|
|
265
|
+
|
|
104
266
|
/**
|
|
105
267
|
* Mean recall@10 over mined tasks whose gold patch is in the vault.
|
|
106
268
|
* A missing vault is unmeasured, not a zero: the absence of a hidden patch is
|
|
107
269
|
* not evidence the map missed the file (ADR-076).
|
|
108
270
|
*
|
|
271
|
+
* When `embed: true` (or stored embeddings are provided), also evaluates recall@10
|
|
272
|
+
* with embeddings fused in and enforces the R28 acceptance gate: embeddings stay off
|
|
273
|
+
* by default unless they raise gold-file recall@10 by at least 10 percentage points
|
|
274
|
+
* (`EMBED_RECALL_GAIN_BAR = 0.1`).
|
|
275
|
+
*
|
|
109
276
|
* @param {string} root
|
|
110
|
-
* @param {{
|
|
277
|
+
* @param {{
|
|
278
|
+
* vaultRoot?: string,
|
|
279
|
+
* backend?: string,
|
|
280
|
+
* embed?: boolean,
|
|
281
|
+
* allowRemote?: boolean,
|
|
282
|
+
* fetchImpl?: typeof fetch,
|
|
283
|
+
* embeddings?: {path: string, vec: Float32Array|number[]|Buffer}[],
|
|
284
|
+
* taskVectors?: Record<string, Float32Array|number[]>|Map<string, Float32Array|number[]>
|
|
285
|
+
* }} [opts]
|
|
111
286
|
*/
|
|
112
287
|
export async function measureRecall(root, opts = {}) {
|
|
113
288
|
const tasksDir = join(root, '.simt-dev', 'bench', 'tasks');
|
|
@@ -121,9 +296,19 @@ export async function measureRecall(root, opts = {}) {
|
|
|
121
296
|
if (files.length === 0) {
|
|
122
297
|
return { measured: false, reason: 'no index', tasks: 0, missing: 0, recall: null };
|
|
123
298
|
}
|
|
124
|
-
const [symbols, edges] = await Promise.all([
|
|
299
|
+
const [symbols, edges, fts] = await Promise.all([
|
|
300
|
+
readSymbols(root, opts),
|
|
301
|
+
readEdges(root, opts),
|
|
302
|
+
readFts(root, opts),
|
|
303
|
+
]);
|
|
304
|
+
const storedEmbeddings =
|
|
305
|
+
opts.embeddings ?? (opts.embed ? await readEmbeddings(root, opts) : []);
|
|
306
|
+
const checkEmbed = storedEmbeddings.length > 0 || opts.embed === true;
|
|
307
|
+
|
|
125
308
|
const vault = opts.vaultRoot ?? resolveVaultDir(root);
|
|
126
309
|
let sum = 0;
|
|
310
|
+
let embedSum = 0;
|
|
311
|
+
let embedEvaluated = 0;
|
|
127
312
|
let tasks = 0;
|
|
128
313
|
let missing = 0;
|
|
129
314
|
for (const name of names) {
|
|
@@ -140,12 +325,61 @@ export async function measureRecall(root, opts = {}) {
|
|
|
140
325
|
missing += 1;
|
|
141
326
|
continue;
|
|
142
327
|
}
|
|
143
|
-
const
|
|
328
|
+
const prompt = task.prompt ?? '';
|
|
329
|
+
const bm25 = await searchFtsBm25(root, prompt, { ...opts, fts });
|
|
330
|
+
const ranked = rankFiles({ files, edges, symbols, fts, bm25, task: prompt });
|
|
144
331
|
sum += recallAt10(ranked, gold);
|
|
145
332
|
tasks += 1;
|
|
333
|
+
|
|
334
|
+
if (checkEmbed && storedEmbeddings.length > 0) {
|
|
335
|
+
const providedVec =
|
|
336
|
+
opts.taskVectors instanceof Map
|
|
337
|
+
? opts.taskVectors.get(task.id)
|
|
338
|
+
: opts.taskVectors?.[task.id];
|
|
339
|
+
const queryVector =
|
|
340
|
+
providedVec ??
|
|
341
|
+
(await embedTaskQuery(root, prompt, {
|
|
342
|
+
embed: opts.embed === true,
|
|
343
|
+
allowRemote: opts.allowRemote === true,
|
|
344
|
+
fetchImpl: opts.fetchImpl,
|
|
345
|
+
}));
|
|
346
|
+
if (queryVector) {
|
|
347
|
+
const embedRanked = rankFiles({
|
|
348
|
+
files,
|
|
349
|
+
edges,
|
|
350
|
+
symbols,
|
|
351
|
+
fts,
|
|
352
|
+
bm25,
|
|
353
|
+
embeddings: storedEmbeddings,
|
|
354
|
+
queryVector,
|
|
355
|
+
task: prompt,
|
|
356
|
+
});
|
|
357
|
+
embedSum += recallAt10(embedRanked, gold);
|
|
358
|
+
embedEvaluated += 1;
|
|
359
|
+
} else {
|
|
360
|
+
embedSum += recallAt10(ranked, gold);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
146
363
|
}
|
|
147
364
|
if (tasks === 0) return { measured: false, reason: 'no gold patches', tasks: 0, missing, recall: null };
|
|
148
|
-
|
|
365
|
+
const baseRecall = sum / tasks;
|
|
366
|
+
const embedRecall = checkEmbed && embedEvaluated > 0 ? embedSum / tasks : null;
|
|
367
|
+
const embedDelta = embedRecall === null ? null : embedRecall - baseRecall;
|
|
368
|
+
// R28 acceptance gate: embeddings stay off by default unless they raise
|
|
369
|
+
// gold-file recall@10 by at least 10 points (+0.10).
|
|
370
|
+
const embedAccepted = embedDelta !== null && embedDelta + 1e-9 >= EMBED_RECALL_GAIN_BAR;
|
|
371
|
+
return {
|
|
372
|
+
measured: true,
|
|
373
|
+
reason: '',
|
|
374
|
+
tasks,
|
|
375
|
+
missing,
|
|
376
|
+
recall: embedAccepted ? embedRecall : baseRecall,
|
|
377
|
+
baseRecall,
|
|
378
|
+
embedRecall,
|
|
379
|
+
embedDelta,
|
|
380
|
+
embedAccepted,
|
|
381
|
+
embeddingsDefault: 'off',
|
|
382
|
+
};
|
|
149
383
|
}
|
|
150
384
|
|
|
151
385
|
export function formatRecall(result) {
|
package/src/index/persist.mjs
CHANGED
|
@@ -9,17 +9,42 @@ import { dirname, join } from 'node:path';
|
|
|
9
9
|
|
|
10
10
|
export const SQLITE_FILE = '.simt-dev/index.sqlite';
|
|
11
11
|
export const JSONL_FILE = '.simt-dev/index.jsonl';
|
|
12
|
-
export const SCHEMA_V = '
|
|
12
|
+
export const SCHEMA_V = '3';
|
|
13
13
|
|
|
14
14
|
const SCHEMA = `
|
|
15
15
|
create table if not exists files(path text primary key, blob text, lang text, bytes int, pkg text);
|
|
16
16
|
create table if not exists symbols(id integer primary key, path text, name text, kind text, line int, sig text, exported int);
|
|
17
17
|
create table if not exists edges(src text, dst text, kind text, weight real);
|
|
18
18
|
create virtual table if not exists fts using fts5(kind, ref, body);
|
|
19
|
+
create table if not exists embeddings(path text primary key, blob text, model text, dims int, vec blob);
|
|
19
20
|
create table if not exists meta(key text primary key, value text);
|
|
20
21
|
`;
|
|
21
22
|
|
|
22
|
-
export
|
|
23
|
+
export function encodeVectorBlob(vec) {
|
|
24
|
+
if (!Array.isArray(vec) && !(vec instanceof Float32Array)) return Buffer.alloc(0);
|
|
25
|
+
const f32 = vec instanceof Float32Array ? vec : Float32Array.from(vec, (v) => Number(v) || 0);
|
|
26
|
+
return Buffer.from(f32.buffer, f32.byteOffset, f32.byteLength);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function decodeVectorBlob(blob) {
|
|
30
|
+
if (!blob) return new Float32Array(0);
|
|
31
|
+
if (blob instanceof Float32Array) return blob;
|
|
32
|
+
if (Array.isArray(blob)) return Float32Array.from(blob, (v) => Number(v) || 0);
|
|
33
|
+
const buf = Buffer.isBuffer(blob)
|
|
34
|
+
? blob
|
|
35
|
+
: blob instanceof Uint8Array
|
|
36
|
+
? Buffer.from(blob.buffer, blob.byteOffset, blob.byteLength)
|
|
37
|
+
: typeof blob === 'string'
|
|
38
|
+
? Buffer.from(blob, 'base64')
|
|
39
|
+
: Buffer.alloc(0);
|
|
40
|
+
const count = Math.floor(buf.byteLength / 4);
|
|
41
|
+
if (count === 0) return new Float32Array(0);
|
|
42
|
+
const copy = new Uint8Array(count * 4);
|
|
43
|
+
copy.set(new Uint8Array(buf.buffer, buf.byteOffset, count * 4));
|
|
44
|
+
return new Float32Array(copy.buffer);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function writeSqlite(root, next, removed, symbols, edges, fts, embeddings = []) {
|
|
23
48
|
const { DatabaseSync } = await import('node:sqlite');
|
|
24
49
|
const file = join(root, SQLITE_FILE);
|
|
25
50
|
await mkdir(dirname(file), { recursive: true });
|
|
@@ -32,25 +57,35 @@ export async function writeSqlite(root, next, removed, symbols, edges, fts) {
|
|
|
32
57
|
const dropSymbols = db.prepare('delete from symbols where path = ?');
|
|
33
58
|
const dropEdges = db.prepare('delete from edges where src = ? or dst = ?');
|
|
34
59
|
const dropFts = db.prepare('delete from fts where ref = ?');
|
|
60
|
+
const dropEmbed = db.prepare('delete from embeddings where path = ?');
|
|
35
61
|
const dropFile = db.prepare('delete from files where path = ?');
|
|
36
62
|
const addSymbol = db.prepare('insert into symbols(path, name, kind, line, sig, exported) values (?, ?, ?, ?, ?, ?)');
|
|
37
63
|
const addEdge = db.prepare('insert into edges(src, dst, kind, weight) values (?, ?, ?, ?)');
|
|
38
64
|
const addFts = db.prepare('insert into fts(kind, ref, body) values (?, ?, ?)');
|
|
65
|
+
const upsertEmbed = db.prepare(`insert into embeddings(path, blob, model, dims, vec) values (?, ?, ?, ?, ?)
|
|
66
|
+
on conflict(path) do update set blob = excluded.blob, model = excluded.model, dims = excluded.dims, vec = excluded.vec`);
|
|
39
67
|
for (const row of next) {
|
|
40
68
|
upsert.run(row.path, row.blob, row.lang, row.bytes, row.pkg);
|
|
41
69
|
dropSymbols.run(row.path);
|
|
42
70
|
dropEdges.run(row.path, row.path);
|
|
43
71
|
dropFts.run(row.path);
|
|
72
|
+
dropEmbed.run(row.path);
|
|
44
73
|
}
|
|
45
74
|
for (const path of removed) {
|
|
46
75
|
dropFile.run(path);
|
|
47
76
|
dropSymbols.run(path);
|
|
48
77
|
dropEdges.run(path, path);
|
|
49
78
|
dropFts.run(path);
|
|
79
|
+
dropEmbed.run(path);
|
|
50
80
|
}
|
|
51
81
|
for (const row of symbols) addSymbol.run(row.path, row.name, row.kind, row.line, row.sig, row.exported);
|
|
52
82
|
for (const row of edges) addEdge.run(row.src, row.dst, row.kind, row.weight);
|
|
53
83
|
for (const row of fts) addFts.run(row.kind, row.ref, row.body);
|
|
84
|
+
for (const row of embeddings) {
|
|
85
|
+
const vecBlob = Buffer.isBuffer(row.vec) || row.vec instanceof Uint8Array ? row.vec : encodeVectorBlob(row.vec);
|
|
86
|
+
const dims = Number.isInteger(row.dims) ? row.dims : Math.floor(vecBlob.byteLength / 4);
|
|
87
|
+
upsertEmbed.run(row.path, row.blob ?? '', row.model ?? '', dims, vecBlob);
|
|
88
|
+
}
|
|
54
89
|
db.prepare('insert into meta(key, value) values (?, ?) on conflict(key) do update set value = excluded.value').run('schema', SCHEMA_V);
|
|
55
90
|
db.exec('commit');
|
|
56
91
|
} catch (error) {
|
|
@@ -61,7 +96,7 @@ export async function writeSqlite(root, next, removed, symbols, edges, fts) {
|
|
|
61
96
|
}
|
|
62
97
|
}
|
|
63
98
|
|
|
64
|
-
export async function writeJsonl(root, previous, next, removed, symbols, edges, fts, force) {
|
|
99
|
+
export async function writeJsonl(root, previous, next, removed, symbols, edges, fts, force, embeddings = []) {
|
|
65
100
|
const gone = new Set(removed);
|
|
66
101
|
const replaced = new Map(next.map((row) => [row.path, row]));
|
|
67
102
|
const drop = new Set([...replaced.keys(), ...gone]);
|
|
@@ -76,9 +111,14 @@ export async function writeJsonl(root, previous, next, removed, symbols, edges,
|
|
|
76
111
|
const keptSymbols = force ? [] : (await readJsonl(root, 'symbols')).filter((row) => !drop.has(row.path));
|
|
77
112
|
const keptEdges = force ? [] : (await readJsonl(root, 'edges')).filter((row) => !drop.has(row.src) && !drop.has(row.dst));
|
|
78
113
|
const keptFts = force ? [] : (await readJsonl(root, 'fts')).filter((row) => !drop.has(row.ref));
|
|
114
|
+
const newEmbedPaths = new Set(embeddings.map((row) => row.path));
|
|
115
|
+
const keptEmbeddings = force
|
|
116
|
+
? []
|
|
117
|
+
: (await readJsonl(root, 'embeddings')).filter((row) => !drop.has(row.path) && !newEmbedPaths.has(row.path));
|
|
79
118
|
const allSymbols = [...keptSymbols, ...symbols].sort(bySymbol);
|
|
80
119
|
const allEdges = [...keptEdges, ...edges].sort(byEdge);
|
|
81
120
|
const allFts = [...keptFts, ...fts].sort((a, b) => cmp(a.ref, b.ref));
|
|
121
|
+
const allEmbeddings = [...keptEmbeddings, ...embeddings].sort(byPath);
|
|
82
122
|
const file = join(root, JSONL_FILE);
|
|
83
123
|
await mkdir(dirname(file), { recursive: true });
|
|
84
124
|
const lines = [
|
|
@@ -87,9 +127,21 @@ export async function writeJsonl(root, previous, next, removed, symbols, edges,
|
|
|
87
127
|
...allSymbols.map((row) => JSON.stringify({ table: 'symbols', path: row.path, name: row.name, kind: row.kind, line: row.line, sig: row.sig, exported: row.exported })),
|
|
88
128
|
...allEdges.map((row) => JSON.stringify({ table: 'edges', src: row.src, dst: row.dst, kind: row.kind, weight: row.weight })),
|
|
89
129
|
...allFts.map((row) => JSON.stringify({ table: 'fts', kind: row.kind, ref: row.ref, body: row.body })),
|
|
130
|
+
...allEmbeddings.map((row) => {
|
|
131
|
+
const vecBlob = Buffer.isBuffer(row.vec) || row.vec instanceof Uint8Array ? Buffer.from(row.vec.buffer, row.vec.byteOffset, row.vec.byteLength) : encodeVectorBlob(row.vec);
|
|
132
|
+
const dims = Number.isInteger(row.dims) ? row.dims : Math.floor(vecBlob.byteLength / 4);
|
|
133
|
+
return JSON.stringify({
|
|
134
|
+
table: 'embeddings',
|
|
135
|
+
path: row.path,
|
|
136
|
+
blob: row.blob ?? '',
|
|
137
|
+
model: row.model ?? '',
|
|
138
|
+
dims,
|
|
139
|
+
vec: vecBlob.toString('base64'),
|
|
140
|
+
});
|
|
141
|
+
}),
|
|
90
142
|
];
|
|
91
143
|
await writeFile(file, `${lines.join('\n')}\n`);
|
|
92
|
-
return { symbols: allSymbols.length, edges: allEdges.length };
|
|
144
|
+
return { symbols: allSymbols.length, edges: allEdges.length, embeddings: allEmbeddings.length };
|
|
93
145
|
}
|
|
94
146
|
|
|
95
147
|
export async function readJsonl(root, table) {
|
|
@@ -109,6 +161,16 @@ export async function readJsonl(root, table) {
|
|
|
109
161
|
else if (table === 'symbols') rows.push({ path: row.path, name: row.name, kind: row.kind, line: row.line, sig: row.sig, exported: row.exported });
|
|
110
162
|
else if (table === 'edges') rows.push({ src: row.src, dst: row.dst, kind: row.kind, weight: row.weight });
|
|
111
163
|
else if (table === 'fts') rows.push({ kind: row.kind, ref: row.ref, body: row.body });
|
|
164
|
+
else if (table === 'embeddings') {
|
|
165
|
+
const vec = decodeVectorBlob(row.vec);
|
|
166
|
+
rows.push({
|
|
167
|
+
path: row.path,
|
|
168
|
+
blob: row.blob ?? '',
|
|
169
|
+
model: row.model ?? '',
|
|
170
|
+
dims: Number.isInteger(row.dims) ? row.dims : vec.length,
|
|
171
|
+
vec,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
112
174
|
}
|
|
113
175
|
if (table === 'symbols') rows.sort(bySymbol);
|
|
114
176
|
else if (table === 'edges') rows.sort(byEdge);
|
|
@@ -117,7 +179,7 @@ export async function readJsonl(root, table) {
|
|
|
117
179
|
return rows;
|
|
118
180
|
}
|
|
119
181
|
|
|
120
|
-
export async function readSqliteRows(root, sql) {
|
|
182
|
+
export async function readSqliteRows(root, sql, params = []) {
|
|
121
183
|
let DatabaseSync;
|
|
122
184
|
try {
|
|
123
185
|
({ DatabaseSync } = await import('node:sqlite'));
|
|
@@ -132,7 +194,7 @@ export async function readSqliteRows(root, sql) {
|
|
|
132
194
|
}
|
|
133
195
|
const db = new DatabaseSync(file);
|
|
134
196
|
try {
|
|
135
|
-
return db.prepare(sql).all();
|
|
197
|
+
return db.prepare(sql).all(...params);
|
|
136
198
|
} catch {
|
|
137
199
|
return [];
|
|
138
200
|
} finally {
|
|
@@ -152,7 +214,12 @@ export async function readSchema(root, backend) {
|
|
|
152
214
|
export async function countSqlite(root) {
|
|
153
215
|
const symbols = await readSqliteRows(root, 'select count(*) as n from symbols');
|
|
154
216
|
const edges = await readSqliteRows(root, 'select count(*) as n from edges');
|
|
155
|
-
|
|
217
|
+
const embeddings = await readSqliteRows(root, 'select count(*) as n from embeddings');
|
|
218
|
+
return {
|
|
219
|
+
symbols: Number(symbols[0]?.n ?? 0),
|
|
220
|
+
edges: Number(edges[0]?.n ?? 0),
|
|
221
|
+
embeddings: Number(embeddings[0]?.n ?? 0),
|
|
222
|
+
};
|
|
156
223
|
}
|
|
157
224
|
|
|
158
225
|
function byPath(a, b) {
|