@dreb/coding-agent 1.18.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/dist/core/tools/search.d.ts.map +1 -1
- package/dist/core/tools/search.js +14 -36
- package/dist/core/tools/search.js.map +1 -1
- package/package.json +2 -1
- package/dist/core/search/chunker.d.ts +0 -21
- package/dist/core/search/chunker.d.ts.map +0 -1
- package/dist/core/search/chunker.js +0 -51
- package/dist/core/search/chunker.js.map +0 -1
- package/dist/core/search/db.d.ts +0 -89
- package/dist/core/search/db.d.ts.map +0 -1
- package/dist/core/search/db.js +0 -406
- package/dist/core/search/db.js.map +0 -1
- package/dist/core/search/embedder.d.ts +0 -52
- package/dist/core/search/embedder.d.ts.map +0 -1
- package/dist/core/search/embedder.js +0 -158
- package/dist/core/search/embedder.js.map +0 -1
- package/dist/core/search/index-manager.d.ts +0 -55
- package/dist/core/search/index-manager.d.ts.map +0 -1
- package/dist/core/search/index-manager.js +0 -311
- package/dist/core/search/index-manager.js.map +0 -1
- package/dist/core/search/metrics/bm25.d.ts +0 -10
- package/dist/core/search/metrics/bm25.d.ts.map +0 -1
- package/dist/core/search/metrics/bm25.js +0 -32
- package/dist/core/search/metrics/bm25.js.map +0 -1
- package/dist/core/search/metrics/git-recency.d.ts +0 -14
- package/dist/core/search/metrics/git-recency.d.ts.map +0 -1
- package/dist/core/search/metrics/git-recency.js +0 -123
- package/dist/core/search/metrics/git-recency.js.map +0 -1
- package/dist/core/search/metrics/import-graph.d.ts +0 -15
- package/dist/core/search/metrics/import-graph.d.ts.map +0 -1
- package/dist/core/search/metrics/import-graph.js +0 -115
- package/dist/core/search/metrics/import-graph.js.map +0 -1
- package/dist/core/search/metrics/path-match.d.ts +0 -13
- package/dist/core/search/metrics/path-match.d.ts.map +0 -1
- package/dist/core/search/metrics/path-match.js +0 -54
- package/dist/core/search/metrics/path-match.js.map +0 -1
- package/dist/core/search/metrics/symbol-match.d.ts +0 -12
- package/dist/core/search/metrics/symbol-match.d.ts.map +0 -1
- package/dist/core/search/metrics/symbol-match.js +0 -62
- package/dist/core/search/metrics/symbol-match.js.map +0 -1
- package/dist/core/search/metrics/tokenize.d.ts +0 -12
- package/dist/core/search/metrics/tokenize.d.ts.map +0 -1
- package/dist/core/search/metrics/tokenize.js +0 -29
- package/dist/core/search/metrics/tokenize.js.map +0 -1
- package/dist/core/search/poem.d.ts +0 -38
- package/dist/core/search/poem.d.ts.map +0 -1
- package/dist/core/search/poem.js +0 -214
- package/dist/core/search/poem.js.map +0 -1
- package/dist/core/search/query-classifier.d.ts +0 -17
- package/dist/core/search/query-classifier.d.ts.map +0 -1
- package/dist/core/search/query-classifier.js +0 -54
- package/dist/core/search/query-classifier.js.map +0 -1
- package/dist/core/search/scanner.d.ts +0 -30
- package/dist/core/search/scanner.d.ts.map +0 -1
- package/dist/core/search/scanner.js +0 -344
- package/dist/core/search/scanner.js.map +0 -1
- package/dist/core/search/search.d.ts +0 -51
- package/dist/core/search/search.d.ts.map +0 -1
- package/dist/core/search/search.js +0 -381
- package/dist/core/search/search.js.map +0 -1
- package/dist/core/search/text-chunker.d.ts +0 -15
- package/dist/core/search/text-chunker.d.ts.map +0 -1
- package/dist/core/search/text-chunker.js +0 -580
- package/dist/core/search/text-chunker.js.map +0 -1
- package/dist/core/search/tree-sitter-chunker.d.ts +0 -25
- package/dist/core/search/tree-sitter-chunker.d.ts.map +0 -1
- package/dist/core/search/tree-sitter-chunker.js +0 -357
- package/dist/core/search/tree-sitter-chunker.js.map +0 -1
- package/dist/core/search/types.d.ts +0 -96
- package/dist/core/search/types.d.ts.map +0 -1
- package/dist/core/search/types.js +0 -6
- package/dist/core/search/types.js.map +0 -1
- package/dist/core/search/vector-store.d.ts +0 -43
- package/dist/core/search/vector-store.d.ts.map +0 -1
- package/dist/core/search/vector-store.js +0 -73
- package/dist/core/search/vector-store.js.map +0 -1
package/dist/core/search/db.js
DELETED
|
@@ -1,406 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SQLite database abstraction for the search index.
|
|
3
|
-
*
|
|
4
|
-
* Uses `node:sqlite` (built-in Node 22+). Feature-gated — callers must check
|
|
5
|
-
* availability via `isSqliteAvailable()` before constructing a SearchDatabase.
|
|
6
|
-
*/
|
|
7
|
-
import { createRequire } from "node:module";
|
|
8
|
-
import { unpackVector } from "./vector-store.js";
|
|
9
|
-
// ============================================================================
|
|
10
|
-
// Availability Check
|
|
11
|
-
// ============================================================================
|
|
12
|
-
const require = createRequire(import.meta.url);
|
|
13
|
-
let _sqliteAvailable = null;
|
|
14
|
-
/** Check whether `node:sqlite` is available in this Node.js runtime. */
|
|
15
|
-
export function isSqliteAvailable() {
|
|
16
|
-
if (_sqliteAvailable !== null)
|
|
17
|
-
return _sqliteAvailable;
|
|
18
|
-
try {
|
|
19
|
-
require("node:sqlite");
|
|
20
|
-
_sqliteAvailable = true;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
_sqliteAvailable = false;
|
|
24
|
-
}
|
|
25
|
-
return _sqliteAvailable;
|
|
26
|
-
}
|
|
27
|
-
// ============================================================================
|
|
28
|
-
// Schema Version
|
|
29
|
-
// ============================================================================
|
|
30
|
-
const SCHEMA_VERSION = 1;
|
|
31
|
-
// ============================================================================
|
|
32
|
-
// Database
|
|
33
|
-
// ============================================================================
|
|
34
|
-
/** Wrapper around `node:sqlite` DatabaseSync for the search index. */
|
|
35
|
-
export class SearchDatabase {
|
|
36
|
-
db; // DatabaseSync from node:sqlite
|
|
37
|
-
constructor(dbPath) {
|
|
38
|
-
// Import synchronously — caller must have verified availability
|
|
39
|
-
const { DatabaseSync } = require("node:sqlite");
|
|
40
|
-
this.db = new DatabaseSync(dbPath);
|
|
41
|
-
this.db.exec("PRAGMA journal_mode=WAL");
|
|
42
|
-
this.db.exec("PRAGMA synchronous=NORMAL");
|
|
43
|
-
this.db.exec("PRAGMA foreign_keys=ON");
|
|
44
|
-
this.initSchema();
|
|
45
|
-
}
|
|
46
|
-
/** Create or migrate the database schema. */
|
|
47
|
-
initSchema() {
|
|
48
|
-
// Check schema version
|
|
49
|
-
this.db.exec("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)");
|
|
50
|
-
const versionRow = this.db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
51
|
-
const currentVersion = versionRow ? Number.parseInt(versionRow.value, 10) : 0;
|
|
52
|
-
if (currentVersion >= SCHEMA_VERSION)
|
|
53
|
-
return;
|
|
54
|
-
// Files table
|
|
55
|
-
this.db.exec(`
|
|
56
|
-
CREATE TABLE IF NOT EXISTS files (
|
|
57
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
-
file_path TEXT NOT NULL UNIQUE,
|
|
59
|
-
mtime REAL NOT NULL,
|
|
60
|
-
file_type TEXT NOT NULL
|
|
61
|
-
)
|
|
62
|
-
`);
|
|
63
|
-
// Chunks table
|
|
64
|
-
this.db.exec(`
|
|
65
|
-
CREATE TABLE IF NOT EXISTS chunks (
|
|
66
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
-
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
68
|
-
file_path TEXT NOT NULL,
|
|
69
|
-
start_line INTEGER NOT NULL,
|
|
70
|
-
end_line INTEGER NOT NULL,
|
|
71
|
-
kind TEXT NOT NULL,
|
|
72
|
-
name TEXT,
|
|
73
|
-
content TEXT NOT NULL,
|
|
74
|
-
file_type TEXT NOT NULL
|
|
75
|
-
)
|
|
76
|
-
`);
|
|
77
|
-
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_file_id ON chunks(file_id)");
|
|
78
|
-
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_file_path ON chunks(file_path)");
|
|
79
|
-
// FTS5 virtual table (content-synced with chunks)
|
|
80
|
-
this.db.exec(`
|
|
81
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
|
|
82
|
-
content,
|
|
83
|
-
name,
|
|
84
|
-
file_path,
|
|
85
|
-
content='chunks',
|
|
86
|
-
content_rowid='id',
|
|
87
|
-
tokenize='porter unicode61'
|
|
88
|
-
)
|
|
89
|
-
`);
|
|
90
|
-
// FTS triggers for incremental updates
|
|
91
|
-
this.db.exec(`
|
|
92
|
-
CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN
|
|
93
|
-
INSERT INTO chunks_fts(rowid, content, name, file_path)
|
|
94
|
-
VALUES (new.id, new.content, new.name, new.file_path);
|
|
95
|
-
END
|
|
96
|
-
`);
|
|
97
|
-
this.db.exec(`
|
|
98
|
-
CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
|
|
99
|
-
INSERT INTO chunks_fts(chunks_fts, rowid, content, name, file_path)
|
|
100
|
-
VALUES ('delete', old.id, old.content, old.name, old.file_path);
|
|
101
|
-
END
|
|
102
|
-
`);
|
|
103
|
-
this.db.exec(`
|
|
104
|
-
CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN
|
|
105
|
-
INSERT INTO chunks_fts(chunks_fts, rowid, content, name, file_path)
|
|
106
|
-
VALUES ('delete', old.id, old.content, old.name, old.file_path);
|
|
107
|
-
INSERT INTO chunks_fts(rowid, content, name, file_path)
|
|
108
|
-
VALUES (new.id, new.content, new.name, new.file_path);
|
|
109
|
-
END
|
|
110
|
-
`);
|
|
111
|
-
// Embeddings table (keyed by model name for multi-model support)
|
|
112
|
-
this.db.exec(`
|
|
113
|
-
CREATE TABLE IF NOT EXISTS embeddings (
|
|
114
|
-
chunk_id INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,
|
|
115
|
-
model_name TEXT NOT NULL,
|
|
116
|
-
vector BLOB NOT NULL,
|
|
117
|
-
PRIMARY KEY (chunk_id, model_name)
|
|
118
|
-
)
|
|
119
|
-
`);
|
|
120
|
-
// Imports table (import graph edges between files)
|
|
121
|
-
this.db.exec(`
|
|
122
|
-
CREATE TABLE IF NOT EXISTS imports (
|
|
123
|
-
source_file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
124
|
-
target_file_path TEXT NOT NULL,
|
|
125
|
-
PRIMARY KEY (source_file_id, target_file_path)
|
|
126
|
-
)
|
|
127
|
-
`);
|
|
128
|
-
this.db.exec("CREATE INDEX IF NOT EXISTS idx_imports_target ON imports(target_file_path)");
|
|
129
|
-
// Symbols table (symbol names extracted from chunks)
|
|
130
|
-
this.db.exec(`
|
|
131
|
-
CREATE TABLE IF NOT EXISTS symbols (
|
|
132
|
-
chunk_id INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,
|
|
133
|
-
name TEXT NOT NULL,
|
|
134
|
-
kind TEXT NOT NULL
|
|
135
|
-
)
|
|
136
|
-
`);
|
|
137
|
-
this.db.exec("CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)");
|
|
138
|
-
// Set schema version
|
|
139
|
-
this.db
|
|
140
|
-
.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)")
|
|
141
|
-
.run(String(SCHEMA_VERSION));
|
|
142
|
-
}
|
|
143
|
-
// ========================================================================
|
|
144
|
-
// Files
|
|
145
|
-
// ========================================================================
|
|
146
|
-
/** Insert or update a file record. Returns the file ID. */
|
|
147
|
-
upsertFile(filePath, mtime, fileType) {
|
|
148
|
-
const existing = this.db.prepare("SELECT id FROM files WHERE file_path = ?").get(filePath);
|
|
149
|
-
if (existing) {
|
|
150
|
-
this.db.prepare("UPDATE files SET mtime = ?, file_type = ? WHERE id = ?").run(mtime, fileType, existing.id);
|
|
151
|
-
return existing.id;
|
|
152
|
-
}
|
|
153
|
-
const result = this.db
|
|
154
|
-
.prepare("INSERT INTO files (file_path, mtime, file_type) VALUES (?, ?, ?)")
|
|
155
|
-
.run(filePath, mtime, fileType);
|
|
156
|
-
return Number(result.lastInsertRowid);
|
|
157
|
-
}
|
|
158
|
-
/** Get a file by path. */
|
|
159
|
-
getFile(filePath) {
|
|
160
|
-
const row = this.db
|
|
161
|
-
.prepare("SELECT id, file_path, mtime, file_type FROM files WHERE file_path = ?")
|
|
162
|
-
.get(filePath);
|
|
163
|
-
if (!row)
|
|
164
|
-
return null;
|
|
165
|
-
return { id: row.id, filePath: row.file_path, mtime: row.mtime, fileType: row.file_type };
|
|
166
|
-
}
|
|
167
|
-
/** Get all indexed files. */
|
|
168
|
-
getAllFiles() {
|
|
169
|
-
const rows = this.db.prepare("SELECT id, file_path, mtime, file_type FROM files").all();
|
|
170
|
-
return rows.map((r) => ({
|
|
171
|
-
id: r.id,
|
|
172
|
-
filePath: r.file_path,
|
|
173
|
-
mtime: r.mtime,
|
|
174
|
-
fileType: r.file_type,
|
|
175
|
-
}));
|
|
176
|
-
}
|
|
177
|
-
/** Delete a file and all its chunks/embeddings/symbols (cascading). */
|
|
178
|
-
deleteFile(fileId) {
|
|
179
|
-
this.db.prepare("DELETE FROM files WHERE id = ?").run(fileId);
|
|
180
|
-
}
|
|
181
|
-
// ========================================================================
|
|
182
|
-
// Chunks
|
|
183
|
-
// ========================================================================
|
|
184
|
-
/** Insert a chunk. Returns the chunk ID. */
|
|
185
|
-
insertChunk(fileId, filePath, startLine, endLine, kind, name, content, fileType) {
|
|
186
|
-
const result = this.db
|
|
187
|
-
.prepare("INSERT INTO chunks (file_id, file_path, start_line, end_line, kind, name, content, file_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
188
|
-
.run(fileId, filePath, startLine, endLine, kind, name, content, fileType);
|
|
189
|
-
return Number(result.lastInsertRowid);
|
|
190
|
-
}
|
|
191
|
-
/** Delete all chunks for a file. */
|
|
192
|
-
deleteChunksForFile(fileId) {
|
|
193
|
-
this.db.prepare("DELETE FROM chunks WHERE file_id = ?").run(fileId);
|
|
194
|
-
}
|
|
195
|
-
/** Get all chunks. */
|
|
196
|
-
getAllChunks() {
|
|
197
|
-
const rows = this.db
|
|
198
|
-
.prepare("SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks")
|
|
199
|
-
.all();
|
|
200
|
-
return rows.map((r) => this.rowToChunk(r));
|
|
201
|
-
}
|
|
202
|
-
/** Get chunks by file ID. */
|
|
203
|
-
getChunksByFileId(fileId) {
|
|
204
|
-
const rows = this.db
|
|
205
|
-
.prepare("SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks WHERE file_id = ?")
|
|
206
|
-
.all(fileId);
|
|
207
|
-
return rows.map((r) => this.rowToChunk(r));
|
|
208
|
-
}
|
|
209
|
-
/** Get a chunk by ID. */
|
|
210
|
-
getChunk(chunkId) {
|
|
211
|
-
const row = this.db
|
|
212
|
-
.prepare("SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks WHERE id = ?")
|
|
213
|
-
.get(chunkId);
|
|
214
|
-
if (!row)
|
|
215
|
-
return null;
|
|
216
|
-
return this.rowToChunk(row);
|
|
217
|
-
}
|
|
218
|
-
/** Get multiple chunks by IDs. Batches queries to avoid exceeding SQLite's bind variable limit. */
|
|
219
|
-
getChunksById(chunkIds) {
|
|
220
|
-
if (chunkIds.length === 0)
|
|
221
|
-
return [];
|
|
222
|
-
const BATCH_SIZE = 500;
|
|
223
|
-
const results = [];
|
|
224
|
-
for (let i = 0; i < chunkIds.length; i += BATCH_SIZE) {
|
|
225
|
-
const batch = chunkIds.slice(i, i + BATCH_SIZE);
|
|
226
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
227
|
-
const rows = this.db
|
|
228
|
-
.prepare(`SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks WHERE id IN (${placeholders})`)
|
|
229
|
-
.all(...batch);
|
|
230
|
-
for (const r of rows) {
|
|
231
|
-
results.push(this.rowToChunk(r));
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return results;
|
|
235
|
-
}
|
|
236
|
-
rowToChunk(r) {
|
|
237
|
-
return {
|
|
238
|
-
id: r.id,
|
|
239
|
-
fileId: r.file_id,
|
|
240
|
-
filePath: r.file_path,
|
|
241
|
-
startLine: r.start_line,
|
|
242
|
-
endLine: r.end_line,
|
|
243
|
-
kind: r.kind,
|
|
244
|
-
name: r.name,
|
|
245
|
-
content: r.content,
|
|
246
|
-
fileType: r.file_type,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
// ========================================================================
|
|
250
|
-
// Embeddings
|
|
251
|
-
// ========================================================================
|
|
252
|
-
/** Store an embedding vector for a chunk. */
|
|
253
|
-
upsertEmbedding(chunkId, modelName, vector) {
|
|
254
|
-
const blob = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
255
|
-
this.db
|
|
256
|
-
.prepare("INSERT OR REPLACE INTO embeddings (chunk_id, model_name, vector) VALUES (?, ?, ?)")
|
|
257
|
-
.run(chunkId, modelName, blob);
|
|
258
|
-
}
|
|
259
|
-
/** Batch insert embeddings. Uses a transaction for performance. */
|
|
260
|
-
batchUpsertEmbeddings(items) {
|
|
261
|
-
const stmt = this.db.prepare("INSERT OR REPLACE INTO embeddings (chunk_id, model_name, vector) VALUES (?, ?, ?)");
|
|
262
|
-
this.transaction(() => {
|
|
263
|
-
for (const item of items) {
|
|
264
|
-
const blob = Buffer.from(item.vector.buffer, item.vector.byteOffset, item.vector.byteLength);
|
|
265
|
-
stmt.run(item.chunkId, item.modelName, blob);
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
/** Get the embedding for a chunk. */
|
|
270
|
-
getEmbedding(chunkId, modelName) {
|
|
271
|
-
const row = this.db
|
|
272
|
-
.prepare("SELECT vector FROM embeddings WHERE chunk_id = ? AND model_name = ?")
|
|
273
|
-
.get(chunkId, modelName);
|
|
274
|
-
if (!row)
|
|
275
|
-
return null;
|
|
276
|
-
return unpackVector(row.vector);
|
|
277
|
-
}
|
|
278
|
-
/** Get all embeddings for a model. Returns map of chunkId → vector. */
|
|
279
|
-
getAllEmbeddings(modelName) {
|
|
280
|
-
const rows = this.db.prepare("SELECT chunk_id, vector FROM embeddings WHERE model_name = ?").all(modelName);
|
|
281
|
-
const map = new Map();
|
|
282
|
-
for (const row of rows) {
|
|
283
|
-
map.set(row.chunk_id, unpackVector(row.vector));
|
|
284
|
-
}
|
|
285
|
-
return map;
|
|
286
|
-
}
|
|
287
|
-
/** Get chunk IDs that have no embedding for a given model. */
|
|
288
|
-
getChunkIdsWithoutEmbedding(modelName) {
|
|
289
|
-
const rows = this.db
|
|
290
|
-
.prepare(`SELECT c.id FROM chunks c
|
|
291
|
-
LEFT JOIN embeddings e ON c.id = e.chunk_id AND e.model_name = ?
|
|
292
|
-
WHERE e.chunk_id IS NULL`)
|
|
293
|
-
.all(modelName);
|
|
294
|
-
return rows.map((r) => r.id);
|
|
295
|
-
}
|
|
296
|
-
// ========================================================================
|
|
297
|
-
// FTS5
|
|
298
|
-
// ========================================================================
|
|
299
|
-
/** Rebuild the FTS5 index (use after bulk operations). */
|
|
300
|
-
rebuildFts() {
|
|
301
|
-
this.db.exec("INSERT INTO chunks_fts(chunks_fts) VALUES ('rebuild')");
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Search via FTS5 with BM25 ranking.
|
|
305
|
-
* Returns chunk IDs with their BM25 scores (negated so higher = better).
|
|
306
|
-
*/
|
|
307
|
-
ftsSearch(query, limit) {
|
|
308
|
-
try {
|
|
309
|
-
const rows = this.db
|
|
310
|
-
.prepare(`SELECT chunks_fts.rowid as chunk_id, -bm25(chunks_fts, 1.0, 10.0, 5.0) as score
|
|
311
|
-
FROM chunks_fts
|
|
312
|
-
WHERE chunks_fts MATCH ?
|
|
313
|
-
ORDER BY score DESC
|
|
314
|
-
LIMIT ?`)
|
|
315
|
-
.all(query, limit);
|
|
316
|
-
return rows.map((r) => ({ chunkId: r.chunk_id, score: r.score }));
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
// FTS5 MATCH can fail on malformed queries
|
|
320
|
-
return [];
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
// ========================================================================
|
|
324
|
-
// Imports
|
|
325
|
-
// ========================================================================
|
|
326
|
-
/** Record an import edge. */
|
|
327
|
-
insertImport(sourceFileId, targetFilePath) {
|
|
328
|
-
this.db
|
|
329
|
-
.prepare("INSERT OR IGNORE INTO imports (source_file_id, target_file_path) VALUES (?, ?)")
|
|
330
|
-
.run(sourceFileId, targetFilePath);
|
|
331
|
-
}
|
|
332
|
-
/** Delete all imports for a source file. */
|
|
333
|
-
deleteImportsForFile(sourceFileId) {
|
|
334
|
-
this.db.prepare("DELETE FROM imports WHERE source_file_id = ?").run(sourceFileId);
|
|
335
|
-
}
|
|
336
|
-
/** Get files imported by a given source file. */
|
|
337
|
-
getImportsFrom(sourceFileId) {
|
|
338
|
-
const rows = this.db.prepare("SELECT target_file_path FROM imports WHERE source_file_id = ?").all(sourceFileId);
|
|
339
|
-
return rows.map((r) => r.target_file_path);
|
|
340
|
-
}
|
|
341
|
-
/** Get file IDs that import a given target path. */
|
|
342
|
-
getImportersOf(targetFilePath) {
|
|
343
|
-
const rows = this.db.prepare("SELECT source_file_id FROM imports WHERE target_file_path = ?").all(targetFilePath);
|
|
344
|
-
return rows.map((r) => r.source_file_id);
|
|
345
|
-
}
|
|
346
|
-
/** Get all import edges. */
|
|
347
|
-
getAllImports() {
|
|
348
|
-
const rows = this.db.prepare("SELECT source_file_id, target_file_path FROM imports").all();
|
|
349
|
-
return rows.map((r) => ({ sourceFileId: r.source_file_id, targetFilePath: r.target_file_path }));
|
|
350
|
-
}
|
|
351
|
-
// ========================================================================
|
|
352
|
-
// Symbols
|
|
353
|
-
// ========================================================================
|
|
354
|
-
/** Insert a symbol. */
|
|
355
|
-
insertSymbol(chunkId, name, kind) {
|
|
356
|
-
this.db.prepare("INSERT INTO symbols (chunk_id, name, kind) VALUES (?, ?, ?)").run(chunkId, name, kind);
|
|
357
|
-
}
|
|
358
|
-
/** Delete symbols for a chunk. */
|
|
359
|
-
deleteSymbolsForChunk(chunkId) {
|
|
360
|
-
this.db.prepare("DELETE FROM symbols WHERE chunk_id = ?").run(chunkId);
|
|
361
|
-
}
|
|
362
|
-
/** Get all symbols. Returns map of chunkId → symbol names. */
|
|
363
|
-
getAllSymbols() {
|
|
364
|
-
const rows = this.db.prepare("SELECT chunk_id, name FROM symbols").all();
|
|
365
|
-
const map = new Map();
|
|
366
|
-
for (const row of rows) {
|
|
367
|
-
const existing = map.get(row.chunk_id);
|
|
368
|
-
if (existing)
|
|
369
|
-
existing.push(row.name);
|
|
370
|
-
else
|
|
371
|
-
map.set(row.chunk_id, [row.name]);
|
|
372
|
-
}
|
|
373
|
-
return map;
|
|
374
|
-
}
|
|
375
|
-
// ========================================================================
|
|
376
|
-
// Transaction Helpers
|
|
377
|
-
// ========================================================================
|
|
378
|
-
/** Run a function inside a transaction. */
|
|
379
|
-
transaction(fn) {
|
|
380
|
-
this.db.exec("BEGIN");
|
|
381
|
-
try {
|
|
382
|
-
const result = fn();
|
|
383
|
-
this.db.exec("COMMIT");
|
|
384
|
-
return result;
|
|
385
|
-
}
|
|
386
|
-
catch (err) {
|
|
387
|
-
this.db.exec("ROLLBACK");
|
|
388
|
-
throw err;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
/** Get the total number of chunks. */
|
|
392
|
-
getChunkCount() {
|
|
393
|
-
const row = this.db.prepare("SELECT COUNT(*) as count FROM chunks").get();
|
|
394
|
-
return row.count;
|
|
395
|
-
}
|
|
396
|
-
/** Get the total number of files. */
|
|
397
|
-
getFileCount() {
|
|
398
|
-
const row = this.db.prepare("SELECT COUNT(*) as count FROM files").get();
|
|
399
|
-
return row.count;
|
|
400
|
-
}
|
|
401
|
-
/** Close the database connection. */
|
|
402
|
-
close() {
|
|
403
|
-
this.db.close();
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
//# sourceMappingURL=db.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../../src/core/search/db.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SQLite database abstraction for the search index.\n *\n * Uses `node:sqlite` (built-in Node 22+). Feature-gated — callers must check\n * availability via `isSqliteAvailable()` before constructing a SearchDatabase.\n */\n\nimport { createRequire } from \"node:module\";\nimport type { ChunkKind, FileType, IndexedFile, StoredChunk } from \"./types.js\";\nimport { unpackVector } from \"./vector-store.js\";\n\n// ============================================================================\n// Availability Check\n// ============================================================================\n\nconst require = createRequire(import.meta.url);\nlet _sqliteAvailable: boolean | null = null;\n\n/** Check whether `node:sqlite` is available in this Node.js runtime. */\nexport function isSqliteAvailable(): boolean {\n\tif (_sqliteAvailable !== null) return _sqliteAvailable;\n\ttry {\n\t\trequire(\"node:sqlite\");\n\t\t_sqliteAvailable = true;\n\t} catch {\n\t\t_sqliteAvailable = false;\n\t}\n\treturn _sqliteAvailable;\n}\n\n// ============================================================================\n// Schema Version\n// ============================================================================\n\nconst SCHEMA_VERSION = 1;\n\n// ============================================================================\n// Database\n// ============================================================================\n\n/** Wrapper around `node:sqlite` DatabaseSync for the search index. */\nexport class SearchDatabase {\n\tprivate db: any; // DatabaseSync from node:sqlite\n\n\tconstructor(dbPath: string) {\n\t\t// Import synchronously — caller must have verified availability\n\t\tconst { DatabaseSync } = require(\"node:sqlite\");\n\t\tthis.db = new DatabaseSync(dbPath);\n\t\tthis.db.exec(\"PRAGMA journal_mode=WAL\");\n\t\tthis.db.exec(\"PRAGMA synchronous=NORMAL\");\n\t\tthis.db.exec(\"PRAGMA foreign_keys=ON\");\n\t\tthis.initSchema();\n\t}\n\n\t/** Create or migrate the database schema. */\n\tprivate initSchema(): void {\n\t\t// Check schema version\n\t\tthis.db.exec(\"CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)\");\n\t\tconst versionRow = this.db.prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\").get();\n\t\tconst currentVersion = versionRow ? Number.parseInt(versionRow.value, 10) : 0;\n\n\t\tif (currentVersion >= SCHEMA_VERSION) return;\n\n\t\t// Files table\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS files (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tfile_path TEXT NOT NULL UNIQUE,\n\t\t\t\tmtime REAL NOT NULL,\n\t\t\t\tfile_type TEXT NOT NULL\n\t\t\t)\n\t\t`);\n\n\t\t// Chunks table\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS chunks (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tfile_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n\t\t\t\tfile_path TEXT NOT NULL,\n\t\t\t\tstart_line INTEGER NOT NULL,\n\t\t\t\tend_line INTEGER NOT NULL,\n\t\t\t\tkind TEXT NOT NULL,\n\t\t\t\tname TEXT,\n\t\t\t\tcontent TEXT NOT NULL,\n\t\t\t\tfile_type TEXT NOT NULL\n\t\t\t)\n\t\t`);\n\t\tthis.db.exec(\"CREATE INDEX IF NOT EXISTS idx_chunks_file_id ON chunks(file_id)\");\n\t\tthis.db.exec(\"CREATE INDEX IF NOT EXISTS idx_chunks_file_path ON chunks(file_path)\");\n\n\t\t// FTS5 virtual table (content-synced with chunks)\n\t\tthis.db.exec(`\n\t\t\tCREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(\n\t\t\t\tcontent,\n\t\t\t\tname,\n\t\t\t\tfile_path,\n\t\t\t\tcontent='chunks',\n\t\t\t\tcontent_rowid='id',\n\t\t\t\ttokenize='porter unicode61'\n\t\t\t)\n\t\t`);\n\n\t\t// FTS triggers for incremental updates\n\t\tthis.db.exec(`\n\t\t\tCREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN\n\t\t\t\tINSERT INTO chunks_fts(rowid, content, name, file_path)\n\t\t\t\tVALUES (new.id, new.content, new.name, new.file_path);\n\t\t\tEND\n\t\t`);\n\t\tthis.db.exec(`\n\t\t\tCREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN\n\t\t\t\tINSERT INTO chunks_fts(chunks_fts, rowid, content, name, file_path)\n\t\t\t\tVALUES ('delete', old.id, old.content, old.name, old.file_path);\n\t\t\tEND\n\t\t`);\n\t\tthis.db.exec(`\n\t\t\tCREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN\n\t\t\t\tINSERT INTO chunks_fts(chunks_fts, rowid, content, name, file_path)\n\t\t\t\tVALUES ('delete', old.id, old.content, old.name, old.file_path);\n\t\t\t\tINSERT INTO chunks_fts(rowid, content, name, file_path)\n\t\t\t\tVALUES (new.id, new.content, new.name, new.file_path);\n\t\t\tEND\n\t\t`);\n\n\t\t// Embeddings table (keyed by model name for multi-model support)\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS embeddings (\n\t\t\t\tchunk_id INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,\n\t\t\t\tmodel_name TEXT NOT NULL,\n\t\t\t\tvector BLOB NOT NULL,\n\t\t\t\tPRIMARY KEY (chunk_id, model_name)\n\t\t\t)\n\t\t`);\n\n\t\t// Imports table (import graph edges between files)\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS imports (\n\t\t\t\tsource_file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n\t\t\t\ttarget_file_path TEXT NOT NULL,\n\t\t\t\tPRIMARY KEY (source_file_id, target_file_path)\n\t\t\t)\n\t\t`);\n\t\tthis.db.exec(\"CREATE INDEX IF NOT EXISTS idx_imports_target ON imports(target_file_path)\");\n\n\t\t// Symbols table (symbol names extracted from chunks)\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS symbols (\n\t\t\t\tchunk_id INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,\n\t\t\t\tname TEXT NOT NULL,\n\t\t\t\tkind TEXT NOT NULL\n\t\t\t)\n\t\t`);\n\t\tthis.db.exec(\"CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)\");\n\n\t\t// Set schema version\n\t\tthis.db\n\t\t\t.prepare(\"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)\")\n\t\t\t.run(String(SCHEMA_VERSION));\n\t}\n\n\t// ========================================================================\n\t// Files\n\t// ========================================================================\n\n\t/** Insert or update a file record. Returns the file ID. */\n\tupsertFile(filePath: string, mtime: number, fileType: FileType): number {\n\t\tconst existing = this.db.prepare(\"SELECT id FROM files WHERE file_path = ?\").get(filePath);\n\t\tif (existing) {\n\t\t\tthis.db.prepare(\"UPDATE files SET mtime = ?, file_type = ? WHERE id = ?\").run(mtime, fileType, existing.id);\n\t\t\treturn existing.id;\n\t\t}\n\t\tconst result = this.db\n\t\t\t.prepare(\"INSERT INTO files (file_path, mtime, file_type) VALUES (?, ?, ?)\")\n\t\t\t.run(filePath, mtime, fileType);\n\t\treturn Number(result.lastInsertRowid);\n\t}\n\n\t/** Get a file by path. */\n\tgetFile(filePath: string): IndexedFile | null {\n\t\tconst row = this.db\n\t\t\t.prepare(\"SELECT id, file_path, mtime, file_type FROM files WHERE file_path = ?\")\n\t\t\t.get(filePath);\n\t\tif (!row) return null;\n\t\treturn { id: row.id, filePath: row.file_path, mtime: row.mtime, fileType: row.file_type as FileType };\n\t}\n\n\t/** Get all indexed files. */\n\tgetAllFiles(): IndexedFile[] {\n\t\tconst rows = this.db.prepare(\"SELECT id, file_path, mtime, file_type FROM files\").all();\n\t\treturn rows.map((r: any) => ({\n\t\t\tid: r.id,\n\t\t\tfilePath: r.file_path,\n\t\t\tmtime: r.mtime,\n\t\t\tfileType: r.file_type as FileType,\n\t\t}));\n\t}\n\n\t/** Delete a file and all its chunks/embeddings/symbols (cascading). */\n\tdeleteFile(fileId: number): void {\n\t\tthis.db.prepare(\"DELETE FROM files WHERE id = ?\").run(fileId);\n\t}\n\n\t// ========================================================================\n\t// Chunks\n\t// ========================================================================\n\n\t/** Insert a chunk. Returns the chunk ID. */\n\tinsertChunk(\n\t\tfileId: number,\n\t\tfilePath: string,\n\t\tstartLine: number,\n\t\tendLine: number,\n\t\tkind: ChunkKind,\n\t\tname: string | null,\n\t\tcontent: string,\n\t\tfileType: FileType,\n\t): number {\n\t\tconst result = this.db\n\t\t\t.prepare(\n\t\t\t\t\"INSERT INTO chunks (file_id, file_path, start_line, end_line, kind, name, content, file_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\",\n\t\t\t)\n\t\t\t.run(fileId, filePath, startLine, endLine, kind, name, content, fileType);\n\t\treturn Number(result.lastInsertRowid);\n\t}\n\n\t/** Delete all chunks for a file. */\n\tdeleteChunksForFile(fileId: number): void {\n\t\tthis.db.prepare(\"DELETE FROM chunks WHERE file_id = ?\").run(fileId);\n\t}\n\n\t/** Get all chunks. */\n\tgetAllChunks(): StoredChunk[] {\n\t\tconst rows = this.db\n\t\t\t.prepare(\"SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks\")\n\t\t\t.all();\n\t\treturn rows.map((r: any) => this.rowToChunk(r));\n\t}\n\n\t/** Get chunks by file ID. */\n\tgetChunksByFileId(fileId: number): StoredChunk[] {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks WHERE file_id = ?\",\n\t\t\t)\n\t\t\t.all(fileId);\n\t\treturn rows.map((r: any) => this.rowToChunk(r));\n\t}\n\n\t/** Get a chunk by ID. */\n\tgetChunk(chunkId: number): StoredChunk | null {\n\t\tconst row = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks WHERE id = ?\",\n\t\t\t)\n\t\t\t.get(chunkId);\n\t\tif (!row) return null;\n\t\treturn this.rowToChunk(row);\n\t}\n\n\t/** Get multiple chunks by IDs. Batches queries to avoid exceeding SQLite's bind variable limit. */\n\tgetChunksById(chunkIds: number[]): StoredChunk[] {\n\t\tif (chunkIds.length === 0) return [];\n\n\t\tconst BATCH_SIZE = 500;\n\t\tconst results: StoredChunk[] = [];\n\n\t\tfor (let i = 0; i < chunkIds.length; i += BATCH_SIZE) {\n\t\t\tconst batch = chunkIds.slice(i, i + BATCH_SIZE);\n\t\t\tconst placeholders = batch.map(() => \"?\").join(\",\");\n\t\t\tconst rows = this.db\n\t\t\t\t.prepare(\n\t\t\t\t\t`SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks WHERE id IN (${placeholders})`,\n\t\t\t\t)\n\t\t\t\t.all(...batch);\n\t\t\tfor (const r of rows) {\n\t\t\t\tresults.push(this.rowToChunk(r));\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tprivate rowToChunk(r: any): StoredChunk {\n\t\treturn {\n\t\t\tid: r.id,\n\t\t\tfileId: r.file_id,\n\t\t\tfilePath: r.file_path,\n\t\t\tstartLine: r.start_line,\n\t\t\tendLine: r.end_line,\n\t\t\tkind: r.kind as ChunkKind,\n\t\t\tname: r.name,\n\t\t\tcontent: r.content,\n\t\t\tfileType: r.file_type as FileType,\n\t\t};\n\t}\n\n\t// ========================================================================\n\t// Embeddings\n\t// ========================================================================\n\n\t/** Store an embedding vector for a chunk. */\n\tupsertEmbedding(chunkId: number, modelName: string, vector: Float32Array): void {\n\t\tconst blob = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);\n\t\tthis.db\n\t\t\t.prepare(\"INSERT OR REPLACE INTO embeddings (chunk_id, model_name, vector) VALUES (?, ?, ?)\")\n\t\t\t.run(chunkId, modelName, blob);\n\t}\n\n\t/** Batch insert embeddings. Uses a transaction for performance. */\n\tbatchUpsertEmbeddings(items: Array<{ chunkId: number; modelName: string; vector: Float32Array }>): void {\n\t\tconst stmt = this.db.prepare(\"INSERT OR REPLACE INTO embeddings (chunk_id, model_name, vector) VALUES (?, ?, ?)\");\n\t\tthis.transaction(() => {\n\t\t\tfor (const item of items) {\n\t\t\t\tconst blob = Buffer.from(item.vector.buffer, item.vector.byteOffset, item.vector.byteLength);\n\t\t\t\tstmt.run(item.chunkId, item.modelName, blob);\n\t\t\t}\n\t\t});\n\t}\n\n\t/** Get the embedding for a chunk. */\n\tgetEmbedding(chunkId: number, modelName: string): Float32Array | null {\n\t\tconst row = this.db\n\t\t\t.prepare(\"SELECT vector FROM embeddings WHERE chunk_id = ? AND model_name = ?\")\n\t\t\t.get(chunkId, modelName);\n\t\tif (!row) return null;\n\t\treturn unpackVector(row.vector);\n\t}\n\n\t/** Get all embeddings for a model. Returns map of chunkId → vector. */\n\tgetAllEmbeddings(modelName: string): Map<number, Float32Array> {\n\t\tconst rows = this.db.prepare(\"SELECT chunk_id, vector FROM embeddings WHERE model_name = ?\").all(modelName);\n\t\tconst map = new Map<number, Float32Array>();\n\t\tfor (const row of rows) {\n\t\t\tmap.set(row.chunk_id, unpackVector(row.vector));\n\t\t}\n\t\treturn map;\n\t}\n\n\t/** Get chunk IDs that have no embedding for a given model. */\n\tgetChunkIdsWithoutEmbedding(modelName: string): number[] {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT c.id FROM chunks c\n\t\t\t\t LEFT JOIN embeddings e ON c.id = e.chunk_id AND e.model_name = ?\n\t\t\t\t WHERE e.chunk_id IS NULL`,\n\t\t\t)\n\t\t\t.all(modelName);\n\t\treturn rows.map((r: any) => r.id);\n\t}\n\n\t// ========================================================================\n\t// FTS5\n\t// ========================================================================\n\n\t/** Rebuild the FTS5 index (use after bulk operations). */\n\trebuildFts(): void {\n\t\tthis.db.exec(\"INSERT INTO chunks_fts(chunks_fts) VALUES ('rebuild')\");\n\t}\n\n\t/**\n\t * Search via FTS5 with BM25 ranking.\n\t * Returns chunk IDs with their BM25 scores (negated so higher = better).\n\t */\n\tftsSearch(query: string, limit: number): Array<{ chunkId: number; score: number }> {\n\t\ttry {\n\t\t\tconst rows = this.db\n\t\t\t\t.prepare(\n\t\t\t\t\t`SELECT chunks_fts.rowid as chunk_id, -bm25(chunks_fts, 1.0, 10.0, 5.0) as score\n\t\t\t\t\t FROM chunks_fts\n\t\t\t\t\t WHERE chunks_fts MATCH ?\n\t\t\t\t\t ORDER BY score DESC\n\t\t\t\t\t LIMIT ?`,\n\t\t\t\t)\n\t\t\t\t.all(query, limit);\n\t\t\treturn rows.map((r: any) => ({ chunkId: r.chunk_id, score: r.score }));\n\t\t} catch {\n\t\t\t// FTS5 MATCH can fail on malformed queries\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// ========================================================================\n\t// Imports\n\t// ========================================================================\n\n\t/** Record an import edge. */\n\tinsertImport(sourceFileId: number, targetFilePath: string): void {\n\t\tthis.db\n\t\t\t.prepare(\"INSERT OR IGNORE INTO imports (source_file_id, target_file_path) VALUES (?, ?)\")\n\t\t\t.run(sourceFileId, targetFilePath);\n\t}\n\n\t/** Delete all imports for a source file. */\n\tdeleteImportsForFile(sourceFileId: number): void {\n\t\tthis.db.prepare(\"DELETE FROM imports WHERE source_file_id = ?\").run(sourceFileId);\n\t}\n\n\t/** Get files imported by a given source file. */\n\tgetImportsFrom(sourceFileId: number): string[] {\n\t\tconst rows = this.db.prepare(\"SELECT target_file_path FROM imports WHERE source_file_id = ?\").all(sourceFileId);\n\t\treturn rows.map((r: any) => r.target_file_path);\n\t}\n\n\t/** Get file IDs that import a given target path. */\n\tgetImportersOf(targetFilePath: string): number[] {\n\t\tconst rows = this.db.prepare(\"SELECT source_file_id FROM imports WHERE target_file_path = ?\").all(targetFilePath);\n\t\treturn rows.map((r: any) => r.source_file_id);\n\t}\n\n\t/** Get all import edges. */\n\tgetAllImports(): Array<{ sourceFileId: number; targetFilePath: string }> {\n\t\tconst rows = this.db.prepare(\"SELECT source_file_id, target_file_path FROM imports\").all();\n\t\treturn rows.map((r: any) => ({ sourceFileId: r.source_file_id, targetFilePath: r.target_file_path }));\n\t}\n\n\t// ========================================================================\n\t// Symbols\n\t// ========================================================================\n\n\t/** Insert a symbol. */\n\tinsertSymbol(chunkId: number, name: string, kind: string): void {\n\t\tthis.db.prepare(\"INSERT INTO symbols (chunk_id, name, kind) VALUES (?, ?, ?)\").run(chunkId, name, kind);\n\t}\n\n\t/** Delete symbols for a chunk. */\n\tdeleteSymbolsForChunk(chunkId: number): void {\n\t\tthis.db.prepare(\"DELETE FROM symbols WHERE chunk_id = ?\").run(chunkId);\n\t}\n\n\t/** Get all symbols. Returns map of chunkId → symbol names. */\n\tgetAllSymbols(): Map<number, string[]> {\n\t\tconst rows = this.db.prepare(\"SELECT chunk_id, name FROM symbols\").all();\n\t\tconst map = new Map<number, string[]>();\n\t\tfor (const row of rows) {\n\t\t\tconst existing = map.get(row.chunk_id);\n\t\t\tif (existing) existing.push(row.name);\n\t\t\telse map.set(row.chunk_id, [row.name]);\n\t\t}\n\t\treturn map;\n\t}\n\n\t// ========================================================================\n\t// Transaction Helpers\n\t// ========================================================================\n\n\t/** Run a function inside a transaction. */\n\ttransaction<T>(fn: () => T): T {\n\t\tthis.db.exec(\"BEGIN\");\n\t\ttry {\n\t\t\tconst result = fn();\n\t\t\tthis.db.exec(\"COMMIT\");\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\tthis.db.exec(\"ROLLBACK\");\n\t\t\tthrow err;\n\t\t}\n\t}\n\n\t/** Get the total number of chunks. */\n\tgetChunkCount(): number {\n\t\tconst row = this.db.prepare(\"SELECT COUNT(*) as count FROM chunks\").get();\n\t\treturn row.count;\n\t}\n\n\t/** Get the total number of files. */\n\tgetFileCount(): number {\n\t\tconst row = this.db.prepare(\"SELECT COUNT(*) as count FROM files\").get();\n\t\treturn row.count;\n\t}\n\n\t/** Close the database connection. */\n\tclose(): void {\n\t\tthis.db.close();\n\t}\n}\n"]}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Embedding pipeline using @huggingface/transformers.
|
|
3
|
-
*
|
|
4
|
-
* Generates normalized embeddings for semantic search. The embedding
|
|
5
|
-
* dimension is derived at runtime from the model's actual output.
|
|
6
|
-
* First use downloads the model (~23MB) to the configured cache directory.
|
|
7
|
-
*/
|
|
8
|
-
import type { IndexProgressCallback } from "./types.js";
|
|
9
|
-
export interface EmbedderOptions {
|
|
10
|
-
/** Absolute path to the model cache directory (e.g. ~/.dreb/agent/models/). */
|
|
11
|
-
modelCacheDir: string;
|
|
12
|
-
/** HuggingFace model name. Default: 'Xenova/all-MiniLM-L6-v2'. */
|
|
13
|
-
modelName?: string;
|
|
14
|
-
/** Number of texts to embed per batch. Default: 32. */
|
|
15
|
-
batchSize?: number;
|
|
16
|
-
}
|
|
17
|
-
export declare class Embedder {
|
|
18
|
-
private readonly modelCacheDir;
|
|
19
|
-
private readonly modelName;
|
|
20
|
-
private readonly batchSize;
|
|
21
|
-
private extractor;
|
|
22
|
-
private initPromise;
|
|
23
|
-
private resolvedDimension;
|
|
24
|
-
constructor(options: EmbedderOptions);
|
|
25
|
-
/**
|
|
26
|
-
* Initialize the model pipeline. Must be called before embedding.
|
|
27
|
-
*
|
|
28
|
-
* On first use this downloads the ONNX model to `modelCacheDir`.
|
|
29
|
-
* Subsequent calls reuse the cached model.
|
|
30
|
-
*/
|
|
31
|
-
initialize(): Promise<void>;
|
|
32
|
-
/**
|
|
33
|
-
* Embed documents for indexing.
|
|
34
|
-
*
|
|
35
|
-
* Applies model-specific prefixes if required, then processes texts
|
|
36
|
-
* in batches of `batchSize` for memory efficiency.
|
|
37
|
-
*/
|
|
38
|
-
embedDocuments(texts: string[], onProgress?: IndexProgressCallback): Promise<Float32Array[]>;
|
|
39
|
-
/**
|
|
40
|
-
* Embed a query for search.
|
|
41
|
-
*
|
|
42
|
-
* Applies model-specific query prefix if required.
|
|
43
|
-
*/
|
|
44
|
-
embedQuery(query: string): Promise<Float32Array>;
|
|
45
|
-
/** Get the embedding dimension. Returns the model's actual dimension once known, or 384 as default. */
|
|
46
|
-
get dimension(): number;
|
|
47
|
-
/** Dispose the pipeline to free memory. */
|
|
48
|
-
dispose(): void;
|
|
49
|
-
/** Throw if initialize() hasn't been called yet. */
|
|
50
|
-
private ensureInitialized;
|
|
51
|
-
}
|
|
52
|
-
//# sourceMappingURL=embedder.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"embedder.d.ts","sourceRoot":"","sources":["../../../src/core/search/embedder.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAMxD,MAAM,WAAW,eAAe;IAC/B,+EAA+E;IAC/E,aAAa,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAsBD,qBAAa,QAAQ;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,iBAAiB,CAAuB;IAEhD,YAAY,OAAO,EAAE,eAAe,EAInC;IAED;;;;;OAKG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAuChC;IAED;;;;;OAKG;IACG,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAiCjG;IAED;;;;OAIG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAgBrD;IAED,uGAAuG;IACvG,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,2CAA2C;IAC3C,OAAO,IAAI,IAAI,CASd;IAED,oDAAoD;IACpD,OAAO,CAAC,iBAAiB;CAKzB","sourcesContent":["/**\n * Embedding pipeline using @huggingface/transformers.\n *\n * Generates normalized embeddings for semantic search. The embedding\n * dimension is derived at runtime from the model's actual output.\n * First use downloads the model (~23MB) to the configured cache directory.\n */\n\nimport type { IndexProgressCallback } from \"./types.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface EmbedderOptions {\n\t/** Absolute path to the model cache directory (e.g. ~/.dreb/agent/models/). */\n\tmodelCacheDir: string;\n\t/** HuggingFace model name. Default: 'Xenova/all-MiniLM-L6-v2'. */\n\tmodelName?: string;\n\t/** Number of texts to embed per batch. Default: 32. */\n\tbatchSize?: number;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst DEFAULT_MODEL_NAME = \"Xenova/all-MiniLM-L6-v2\";\nconst DEFAULT_BATCH_SIZE = 32;\nconst DEFAULT_DIMENSION = 384;\n\n/**\n * Model-specific prefixes for document vs query embeddings.\n * nomic-embed-text-v1.5 requires these; most other models don't.\n */\nconst MODEL_PREFIXES: Record<string, { document: string; query: string }> = {\n\t\"nomic-ai/nomic-embed-text-v1.5\": { document: \"search_document: \", query: \"search_query: \" },\n};\n\n// ============================================================================\n// Embedder\n// ============================================================================\n\nexport class Embedder {\n\tprivate readonly modelCacheDir: string;\n\tprivate readonly modelName: string;\n\tprivate readonly batchSize: number;\n\tprivate extractor: any | null = null;\n\tprivate initPromise: Promise<void> | null = null;\n\tprivate resolvedDimension: number | null = null;\n\n\tconstructor(options: EmbedderOptions) {\n\t\tthis.modelCacheDir = options.modelCacheDir;\n\t\tthis.modelName = options.modelName ?? DEFAULT_MODEL_NAME;\n\t\tthis.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;\n\t}\n\n\t/**\n\t * Initialize the model pipeline. Must be called before embedding.\n\t *\n\t * On first use this downloads the ONNX model to `modelCacheDir`.\n\t * Subsequent calls reuse the cached model.\n\t */\n\tasync initialize(): Promise<void> {\n\t\tif (this.extractor) return;\n\t\tif (this.initPromise) return this.initPromise;\n\n\t\tthis.initPromise = (async () => {\n\t\t\ttry {\n\t\t\t\t// Dynamic import — @huggingface/transformers is a heavy dependency\n\t\t\t\tconst { pipeline, env } = await import(\"@huggingface/transformers\");\n\n\t\t\t\t// Direct the model cache to our managed directory\n\t\t\t\tenv.cacheDir = this.modelCacheDir;\n\n\t\t\t\t// Suppress the onnxruntime native addon warning — WASM fallback is fine\n\t\t\t\t// The library tries to load native onnxruntime first and logs a warning\n\t\t\t\t// when it falls back to WASM. We suppress this to avoid confusing users.\n\t\t\t\tconst originalWarn = console.warn;\n\t\t\t\tconsole.warn = (...args: any[]) => {\n\t\t\t\t\tconst msg = typeof args[0] === \"string\" ? args[0] : \"\";\n\t\t\t\t\tif (msg.includes(\"onnxruntime\") || msg.includes(\"ONNX\")) return;\n\t\t\t\t\toriginalWarn.apply(console, args);\n\t\t\t\t};\n\n\t\t\t\ttry {\n\t\t\t\t\tthis.extractor = await pipeline(\"feature-extraction\", this.modelName, {\n\t\t\t\t\t\tdtype: \"q8\" as any,\n\t\t\t\t\t\tdevice: \"cpu\" as any,\n\t\t\t\t\t});\n\t\t\t\t} finally {\n\t\t\t\t\tconsole.warn = originalWarn;\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// Reset so subsequent calls can retry instead of returning\n\t\t\t\t// the same rejected promise forever\n\t\t\t\tthis.initPromise = null;\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t})();\n\n\t\treturn this.initPromise;\n\t}\n\n\t/**\n\t * Embed documents for indexing.\n\t *\n\t * Applies model-specific prefixes if required, then processes texts\n\t * in batches of `batchSize` for memory efficiency.\n\t */\n\tasync embedDocuments(texts: string[], onProgress?: IndexProgressCallback): Promise<Float32Array[]> {\n\t\tthis.ensureInitialized();\n\n\t\tconst results: Float32Array[] = [];\n\t\tconst total = texts.length;\n\t\tconst prefix = MODEL_PREFIXES[this.modelName]?.document ?? \"\";\n\n\t\tfor (let i = 0; i < total; i += this.batchSize) {\n\t\t\tconst batch = texts.slice(i, i + this.batchSize);\n\t\t\tconst prefixed = prefix ? batch.map((t) => prefix + t) : batch;\n\n\t\t\tconst output = await this.extractor!(prefixed, {\n\t\t\t\tpooling: \"mean\",\n\t\t\t\tnormalize: true,\n\t\t\t});\n\n\t\t\t// output.data is a flat Float32Array of shape [batchLen, dim]\n\t\t\tconst data: Float32Array = output.data;\n\t\t\tconst dim = data.length / batch.length;\n\t\t\tif (this.resolvedDimension === null) {\n\t\t\t\tthis.resolvedDimension = dim;\n\t\t\t}\n\t\t\tfor (let j = 0; j < batch.length; j++) {\n\t\t\t\tconst start = j * dim;\n\t\t\t\tresults.push(data.slice(start, start + dim));\n\t\t\t}\n\n\t\t\tif (onProgress) {\n\t\t\t\tonProgress(\"embedding\", Math.min(i + batch.length, total), total);\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * Embed a query for search.\n\t *\n\t * Applies model-specific query prefix if required.\n\t */\n\tasync embedQuery(query: string): Promise<Float32Array> {\n\t\tthis.ensureInitialized();\n\n\t\tconst prefix = MODEL_PREFIXES[this.modelName]?.query ?? \"\";\n\t\tconst output = await this.extractor!(prefix + query, {\n\t\t\tpooling: \"mean\",\n\t\t\tnormalize: true,\n\t\t});\n\n\t\t// Single input — derive dimension and slice to exactly one vector\n\t\tconst data: Float32Array = output.data;\n\t\tconst dim = data.length;\n\t\tif (this.resolvedDimension === null) {\n\t\t\tthis.resolvedDimension = dim;\n\t\t}\n\t\treturn data.slice(0, dim);\n\t}\n\n\t/** Get the embedding dimension. Returns the model's actual dimension once known, or 384 as default. */\n\tget dimension(): number {\n\t\treturn this.resolvedDimension ?? DEFAULT_DIMENSION;\n\t}\n\n\t/** Dispose the pipeline to free memory. */\n\tdispose(): void {\n\t\tif (this.extractor) {\n\t\t\t// The pipeline object may have a dispose method depending on the version\n\t\t\tif (typeof this.extractor.dispose === \"function\") {\n\t\t\t\tthis.extractor.dispose();\n\t\t\t}\n\t\t\tthis.extractor = null;\n\t\t}\n\t\tthis.initPromise = null;\n\t}\n\n\t/** Throw if initialize() hasn't been called yet. */\n\tprivate ensureInitialized(): void {\n\t\tif (!this.extractor) {\n\t\t\tthrow new Error(\"Embedder not initialized. Call initialize() first.\");\n\t\t}\n\t}\n}\n"]}
|