@dreb/coding-agent 1.16.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/README.md +19 -9
- package/agents/code-reviewer.md +1 -1
- package/agents/completeness-checker.md +1 -1
- package/agents/error-auditor.md +1 -1
- package/agents/explore.md +1 -1
- package/agents/feature-dev.md +1 -1
- package/agents/independent-assessor.md +1 -1
- package/agents/simplifier.md +1 -1
- package/agents/test-reviewer.md +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +7 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/dreb-paths.d.ts +17 -0
- package/dist/core/tools/dreb-paths.d.ts.map +1 -0
- package/dist/core/tools/dreb-paths.js +43 -0
- package/dist/core/tools/dreb-paths.js.map +1 -0
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +8 -0
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +8 -0
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/search.d.ts +19 -1
- package/dist/core/tools/search.d.ts.map +1 -1
- package/dist/core/tools/search.js +50 -44
- 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 -51
- package/dist/core/search/embedder.d.ts.map +0 -1
- package/dist/core/search/embedder.js +0 -143
- 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 -335
- package/dist/core/search/scanner.js.map +0 -1
- package/dist/core/search/search.d.ts +0 -42
- package/dist/core/search/search.d.ts.map +0 -1
- package/dist/core/search/search.js +0 -337
- 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
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chunking coordinator for the semantic search subsystem.
|
|
3
|
-
*
|
|
4
|
-
* Dispatches to the tree-sitter AST chunker for code files and the
|
|
5
|
-
* text chunker for non-code files (markdown, YAML, JSON, etc.).
|
|
6
|
-
*/
|
|
7
|
-
import type { Chunk, FileType } from "./types.js";
|
|
8
|
-
/**
|
|
9
|
-
* Chunk a file's content into semantically meaningful pieces.
|
|
10
|
-
*
|
|
11
|
-
* For code files, uses tree-sitter to parse the AST and extract functions,
|
|
12
|
-
* classes, methods, etc. For text files, uses format-specific splitting rules.
|
|
13
|
-
*
|
|
14
|
-
* If tree-sitter parsing fails for a code file, falls back to plaintext chunking.
|
|
15
|
-
*
|
|
16
|
-
* @param content - Raw file content
|
|
17
|
-
* @param filePath - Relative file path (stored in chunk metadata)
|
|
18
|
-
* @param fileType - Detected file type
|
|
19
|
-
*/
|
|
20
|
-
export declare function chunkFile(content: string, filePath: string, fileType: FileType): Promise<Chunk[]>;
|
|
21
|
-
//# sourceMappingURL=chunker.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../../../src/core/search/chunker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAoC,MAAM,YAAY,CAAC;AAsBpF;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAYvG","sourcesContent":["/**\n * Chunking coordinator for the semantic search subsystem.\n *\n * Dispatches to the tree-sitter AST chunker for code files and the\n * text chunker for non-code files (markdown, YAML, JSON, etc.).\n */\n\nimport { chunkTextFile } from \"./text-chunker.js\";\nimport { chunkWithTreeSitter, initTreeSitter } from \"./tree-sitter-chunker.js\";\nimport type { Chunk, FileType, TextFileType, TreeSitterLanguage } from \"./types.js\";\n\n// ============================================================================\n// Language Sets\n// ============================================================================\n\nconst TREE_SITTER_LANGUAGES: Set<string> = new Set([\n\t\"typescript\",\n\t\"tsx\",\n\t\"javascript\",\n\t\"python\",\n\t\"go\",\n\t\"rust\",\n\t\"java\",\n\t\"c\",\n\t\"cpp\",\n]);\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Chunk a file's content into semantically meaningful pieces.\n *\n * For code files, uses tree-sitter to parse the AST and extract functions,\n * classes, methods, etc. For text files, uses format-specific splitting rules.\n *\n * If tree-sitter parsing fails for a code file, falls back to plaintext chunking.\n *\n * @param content - Raw file content\n * @param filePath - Relative file path (stored in chunk metadata)\n * @param fileType - Detected file type\n */\nexport async function chunkFile(content: string, filePath: string, fileType: FileType): Promise<Chunk[]> {\n\tif (TREE_SITTER_LANGUAGES.has(fileType)) {\n\t\ttry {\n\t\t\tawait initTreeSitter();\n\t\t\treturn await chunkWithTreeSitter(content, filePath, fileType as TreeSitterLanguage);\n\t\t} catch {\n\t\t\t// Tree-sitter failed — fall back to plaintext chunking\n\t\t\treturn chunkTextFile(content, filePath, \"plaintext\");\n\t\t}\n\t}\n\n\treturn chunkTextFile(content, filePath, fileType as TextFileType);\n}\n"]}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chunking coordinator for the semantic search subsystem.
|
|
3
|
-
*
|
|
4
|
-
* Dispatches to the tree-sitter AST chunker for code files and the
|
|
5
|
-
* text chunker for non-code files (markdown, YAML, JSON, etc.).
|
|
6
|
-
*/
|
|
7
|
-
import { chunkTextFile } from "./text-chunker.js";
|
|
8
|
-
import { chunkWithTreeSitter, initTreeSitter } from "./tree-sitter-chunker.js";
|
|
9
|
-
// ============================================================================
|
|
10
|
-
// Language Sets
|
|
11
|
-
// ============================================================================
|
|
12
|
-
const TREE_SITTER_LANGUAGES = new Set([
|
|
13
|
-
"typescript",
|
|
14
|
-
"tsx",
|
|
15
|
-
"javascript",
|
|
16
|
-
"python",
|
|
17
|
-
"go",
|
|
18
|
-
"rust",
|
|
19
|
-
"java",
|
|
20
|
-
"c",
|
|
21
|
-
"cpp",
|
|
22
|
-
]);
|
|
23
|
-
// ============================================================================
|
|
24
|
-
// Public API
|
|
25
|
-
// ============================================================================
|
|
26
|
-
/**
|
|
27
|
-
* Chunk a file's content into semantically meaningful pieces.
|
|
28
|
-
*
|
|
29
|
-
* For code files, uses tree-sitter to parse the AST and extract functions,
|
|
30
|
-
* classes, methods, etc. For text files, uses format-specific splitting rules.
|
|
31
|
-
*
|
|
32
|
-
* If tree-sitter parsing fails for a code file, falls back to plaintext chunking.
|
|
33
|
-
*
|
|
34
|
-
* @param content - Raw file content
|
|
35
|
-
* @param filePath - Relative file path (stored in chunk metadata)
|
|
36
|
-
* @param fileType - Detected file type
|
|
37
|
-
*/
|
|
38
|
-
export async function chunkFile(content, filePath, fileType) {
|
|
39
|
-
if (TREE_SITTER_LANGUAGES.has(fileType)) {
|
|
40
|
-
try {
|
|
41
|
-
await initTreeSitter();
|
|
42
|
-
return await chunkWithTreeSitter(content, filePath, fileType);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
// Tree-sitter failed — fall back to plaintext chunking
|
|
46
|
-
return chunkTextFile(content, filePath, "plaintext");
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return chunkTextFile(content, filePath, fileType);
|
|
50
|
-
}
|
|
51
|
-
//# sourceMappingURL=chunker.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chunker.js","sourceRoot":"","sources":["../../../src/core/search/chunker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/E,+EAA+E;AAC/E,gBAAgB;AAChB,+EAA+E;AAE/E,MAAM,qBAAqB,GAAgB,IAAI,GAAG,CAAC;IAClD,YAAY;IACZ,KAAK;IACL,YAAY;IACZ,QAAQ;IACR,IAAI;IACJ,MAAM;IACN,MAAM;IACN,GAAG;IACH,KAAK;CACL,CAAC,CAAC;AAEH,+EAA+E;AAC/E,aAAa;AACb,+EAA+E;AAE/E;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAe,EAAE,QAAgB,EAAE,QAAkB,EAAoB;IACxG,IAAI,qBAAqB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC;YACJ,MAAM,cAAc,EAAE,CAAC;YACvB,OAAO,MAAM,mBAAmB,CAAC,OAAO,EAAE,QAAQ,EAAE,QAA8B,CAAC,CAAC;QACrF,CAAC;QAAC,MAAM,CAAC;YACR,yDAAuD;YACvD,OAAO,aAAa,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;QACtD,CAAC;IACF,CAAC;IAED,OAAO,aAAa,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAwB,CAAC,CAAC;AAAA,CAClE","sourcesContent":["/**\n * Chunking coordinator for the semantic search subsystem.\n *\n * Dispatches to the tree-sitter AST chunker for code files and the\n * text chunker for non-code files (markdown, YAML, JSON, etc.).\n */\n\nimport { chunkTextFile } from \"./text-chunker.js\";\nimport { chunkWithTreeSitter, initTreeSitter } from \"./tree-sitter-chunker.js\";\nimport type { Chunk, FileType, TextFileType, TreeSitterLanguage } from \"./types.js\";\n\n// ============================================================================\n// Language Sets\n// ============================================================================\n\nconst TREE_SITTER_LANGUAGES: Set<string> = new Set([\n\t\"typescript\",\n\t\"tsx\",\n\t\"javascript\",\n\t\"python\",\n\t\"go\",\n\t\"rust\",\n\t\"java\",\n\t\"c\",\n\t\"cpp\",\n]);\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Chunk a file's content into semantically meaningful pieces.\n *\n * For code files, uses tree-sitter to parse the AST and extract functions,\n * classes, methods, etc. For text files, uses format-specific splitting rules.\n *\n * If tree-sitter parsing fails for a code file, falls back to plaintext chunking.\n *\n * @param content - Raw file content\n * @param filePath - Relative file path (stored in chunk metadata)\n * @param fileType - Detected file type\n */\nexport async function chunkFile(content: string, filePath: string, fileType: FileType): Promise<Chunk[]> {\n\tif (TREE_SITTER_LANGUAGES.has(fileType)) {\n\t\ttry {\n\t\t\tawait initTreeSitter();\n\t\t\treturn await chunkWithTreeSitter(content, filePath, fileType as TreeSitterLanguage);\n\t\t} catch {\n\t\t\t// Tree-sitter failed — fall back to plaintext chunking\n\t\t\treturn chunkTextFile(content, filePath, \"plaintext\");\n\t\t}\n\t}\n\n\treturn chunkTextFile(content, filePath, fileType as TextFileType);\n}\n"]}
|
package/dist/core/search/db.d.ts
DELETED
|
@@ -1,89 +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 type { ChunkKind, FileType, IndexedFile, StoredChunk } from "./types.js";
|
|
8
|
-
/** Check whether `node:sqlite` is available in this Node.js runtime. */
|
|
9
|
-
export declare function isSqliteAvailable(): boolean;
|
|
10
|
-
/** Wrapper around `node:sqlite` DatabaseSync for the search index. */
|
|
11
|
-
export declare class SearchDatabase {
|
|
12
|
-
private db;
|
|
13
|
-
constructor(dbPath: string);
|
|
14
|
-
/** Create or migrate the database schema. */
|
|
15
|
-
private initSchema;
|
|
16
|
-
/** Insert or update a file record. Returns the file ID. */
|
|
17
|
-
upsertFile(filePath: string, mtime: number, fileType: FileType): number;
|
|
18
|
-
/** Get a file by path. */
|
|
19
|
-
getFile(filePath: string): IndexedFile | null;
|
|
20
|
-
/** Get all indexed files. */
|
|
21
|
-
getAllFiles(): IndexedFile[];
|
|
22
|
-
/** Delete a file and all its chunks/embeddings/symbols (cascading). */
|
|
23
|
-
deleteFile(fileId: number): void;
|
|
24
|
-
/** Insert a chunk. Returns the chunk ID. */
|
|
25
|
-
insertChunk(fileId: number, filePath: string, startLine: number, endLine: number, kind: ChunkKind, name: string | null, content: string, fileType: FileType): number;
|
|
26
|
-
/** Delete all chunks for a file. */
|
|
27
|
-
deleteChunksForFile(fileId: number): void;
|
|
28
|
-
/** Get all chunks. */
|
|
29
|
-
getAllChunks(): StoredChunk[];
|
|
30
|
-
/** Get chunks by file ID. */
|
|
31
|
-
getChunksByFileId(fileId: number): StoredChunk[];
|
|
32
|
-
/** Get a chunk by ID. */
|
|
33
|
-
getChunk(chunkId: number): StoredChunk | null;
|
|
34
|
-
/** Get multiple chunks by IDs. Batches queries to avoid exceeding SQLite's bind variable limit. */
|
|
35
|
-
getChunksById(chunkIds: number[]): StoredChunk[];
|
|
36
|
-
private rowToChunk;
|
|
37
|
-
/** Store an embedding vector for a chunk. */
|
|
38
|
-
upsertEmbedding(chunkId: number, modelName: string, vector: Float32Array): void;
|
|
39
|
-
/** Batch insert embeddings. Uses a transaction for performance. */
|
|
40
|
-
batchUpsertEmbeddings(items: Array<{
|
|
41
|
-
chunkId: number;
|
|
42
|
-
modelName: string;
|
|
43
|
-
vector: Float32Array;
|
|
44
|
-
}>): void;
|
|
45
|
-
/** Get the embedding for a chunk. */
|
|
46
|
-
getEmbedding(chunkId: number, modelName: string): Float32Array | null;
|
|
47
|
-
/** Get all embeddings for a model. Returns map of chunkId → vector. */
|
|
48
|
-
getAllEmbeddings(modelName: string): Map<number, Float32Array>;
|
|
49
|
-
/** Get chunk IDs that have no embedding for a given model. */
|
|
50
|
-
getChunkIdsWithoutEmbedding(modelName: string): number[];
|
|
51
|
-
/** Rebuild the FTS5 index (use after bulk operations). */
|
|
52
|
-
rebuildFts(): void;
|
|
53
|
-
/**
|
|
54
|
-
* Search via FTS5 with BM25 ranking.
|
|
55
|
-
* Returns chunk IDs with their BM25 scores (negated so higher = better).
|
|
56
|
-
*/
|
|
57
|
-
ftsSearch(query: string, limit: number): Array<{
|
|
58
|
-
chunkId: number;
|
|
59
|
-
score: number;
|
|
60
|
-
}>;
|
|
61
|
-
/** Record an import edge. */
|
|
62
|
-
insertImport(sourceFileId: number, targetFilePath: string): void;
|
|
63
|
-
/** Delete all imports for a source file. */
|
|
64
|
-
deleteImportsForFile(sourceFileId: number): void;
|
|
65
|
-
/** Get files imported by a given source file. */
|
|
66
|
-
getImportsFrom(sourceFileId: number): string[];
|
|
67
|
-
/** Get file IDs that import a given target path. */
|
|
68
|
-
getImportersOf(targetFilePath: string): number[];
|
|
69
|
-
/** Get all import edges. */
|
|
70
|
-
getAllImports(): Array<{
|
|
71
|
-
sourceFileId: number;
|
|
72
|
-
targetFilePath: string;
|
|
73
|
-
}>;
|
|
74
|
-
/** Insert a symbol. */
|
|
75
|
-
insertSymbol(chunkId: number, name: string, kind: string): void;
|
|
76
|
-
/** Delete symbols for a chunk. */
|
|
77
|
-
deleteSymbolsForChunk(chunkId: number): void;
|
|
78
|
-
/** Get all symbols. Returns map of chunkId → symbol names. */
|
|
79
|
-
getAllSymbols(): Map<number, string[]>;
|
|
80
|
-
/** Run a function inside a transaction. */
|
|
81
|
-
transaction<T>(fn: () => T): T;
|
|
82
|
-
/** Get the total number of chunks. */
|
|
83
|
-
getChunkCount(): number;
|
|
84
|
-
/** Get the total number of files. */
|
|
85
|
-
getFileCount(): number;
|
|
86
|
-
/** Close the database connection. */
|
|
87
|
-
close(): void;
|
|
88
|
-
}
|
|
89
|
-
//# sourceMappingURL=db.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","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"]}
|
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
|