codestrate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -0
- package/dist/constants.d.ts +40 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +145 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +134 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/index.d.ts +260 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +173 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/security/guard.d.ts +18 -0
- package/dist/security/guard.d.ts.map +1 -0
- package/dist/security/guard.js +39 -0
- package/dist/security/guard.js.map +1 -0
- package/dist/services/code-searcher.d.ts +22 -0
- package/dist/services/code-searcher.d.ts.map +1 -0
- package/dist/services/code-searcher.js +171 -0
- package/dist/services/code-searcher.js.map +1 -0
- package/dist/services/context-builder.d.ts +22 -0
- package/dist/services/context-builder.d.ts.map +1 -0
- package/dist/services/context-builder.js +232 -0
- package/dist/services/context-builder.js.map +1 -0
- package/dist/services/memory-store.d.ts +31 -0
- package/dist/services/memory-store.d.ts.map +1 -0
- package/dist/services/memory-store.js +27 -0
- package/dist/services/memory-store.js.map +1 -0
- package/dist/services/repository-parser.d.ts +8 -0
- package/dist/services/repository-parser.d.ts.map +1 -0
- package/dist/services/repository-parser.js +255 -0
- package/dist/services/repository-parser.js.map +1 -0
- package/dist/services/symbol-indexer.d.ts +16 -0
- package/dist/services/symbol-indexer.d.ts.map +1 -0
- package/dist/services/symbol-indexer.js +417 -0
- package/dist/services/symbol-indexer.js.map +1 -0
- package/dist/services/tree-sitter.d.ts +22 -0
- package/dist/services/tree-sitter.d.ts.map +1 -0
- package/dist/services/tree-sitter.js +269 -0
- package/dist/services/tree-sitter.js.map +1 -0
- package/dist/services/validation-runner.d.ts +30 -0
- package/dist/services/validation-runner.d.ts.map +1 -0
- package/dist/services/validation-runner.js +159 -0
- package/dist/services/validation-runner.js.map +1 -0
- package/dist/store/index.d.ts +23 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +93 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/interface.d.ts +73 -0
- package/dist/store/interface.d.ts.map +1 -0
- package/dist/store/interface.js +9 -0
- package/dist/store/interface.js.map +1 -0
- package/dist/store/memory.d.ts +21 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +157 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sqlite.d.ts +95 -0
- package/dist/store/sqlite.d.ts.map +1 -0
- package/dist/store/sqlite.js +1018 -0
- package/dist/store/sqlite.js.map +1 -0
- package/dist/tools/analyze-impact.d.ts +3 -0
- package/dist/tools/analyze-impact.d.ts.map +1 -0
- package/dist/tools/analyze-impact.js +110 -0
- package/dist/tools/analyze-impact.js.map +1 -0
- package/dist/tools/explore.d.ts +3 -0
- package/dist/tools/explore.d.ts.map +1 -0
- package/dist/tools/explore.js +96 -0
- package/dist/tools/explore.js.map +1 -0
- package/dist/tools/get-context.d.ts +3 -0
- package/dist/tools/get-context.d.ts.map +1 -0
- package/dist/tools/get-context.js +82 -0
- package/dist/tools/get-context.js.map +1 -0
- package/dist/tools/inspect-symbol.d.ts +3 -0
- package/dist/tools/inspect-symbol.d.ts.map +1 -0
- package/dist/tools/inspect-symbol.js +146 -0
- package/dist/tools/inspect-symbol.js.map +1 -0
- package/dist/tools/recall-experience.d.ts +3 -0
- package/dist/tools/recall-experience.d.ts.map +1 -0
- package/dist/tools/recall-experience.js +141 -0
- package/dist/tools/recall-experience.js.map +1 -0
- package/dist/tools/record-experience.d.ts +6 -0
- package/dist/tools/record-experience.d.ts.map +1 -0
- package/dist/tools/record-experience.js +68 -0
- package/dist/tools/record-experience.js.map +1 -0
- package/dist/tools/reindex.d.ts +8 -0
- package/dist/tools/reindex.d.ts.map +1 -0
- package/dist/tools/reindex.js +74 -0
- package/dist/tools/reindex.js.map +1 -0
- package/dist/tools/repo-overview.d.ts +7 -0
- package/dist/tools/repo-overview.d.ts.map +1 -0
- package/dist/tools/repo-overview.js +102 -0
- package/dist/tools/repo-overview.js.map +1 -0
- package/dist/tools/run-validation.d.ts +6 -0
- package/dist/tools/run-validation.d.ts.map +1 -0
- package/dist/tools/run-validation.js +115 -0
- package/dist/tools/run-validation.js.map +1 -0
- package/dist/tools/search-code.d.ts +3 -0
- package/dist/tools/search-code.d.ts.map +1 -0
- package/dist/tools/search-code.js +98 -0
- package/dist/tools/search-code.js.map +1 -0
- package/dist/tools/search-history.d.ts +6 -0
- package/dist/tools/search-history.d.ts.map +1 -0
- package/dist/tools/search-history.js +69 -0
- package/dist/tools/search-history.js.map +1 -0
- package/dist/tools/status.d.ts +7 -0
- package/dist/tools/status.d.ts.map +1 -0
- package/dist/tools/status.js +98 -0
- package/dist/tools/status.js.map +1 -0
- package/dist/tools/trace-flow.d.ts +3 -0
- package/dist/tools/trace-flow.d.ts.map +1 -0
- package/dist/tools/trace-flow.js +155 -0
- package/dist/tools/trace-flow.js.map +1 -0
- package/dist/types.d.ts +182 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +48 -0
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Store — default production storage backend.
|
|
3
|
+
*
|
|
4
|
+
* Uses better-sqlite3 for synchronous, fast SQLite access.
|
|
5
|
+
* Implements the full Store interface with schema migration support.
|
|
6
|
+
*
|
|
7
|
+
* @module store/sqlite
|
|
8
|
+
*/
|
|
9
|
+
import Database from "better-sqlite3";
|
|
10
|
+
import { existsSync, mkdirSync } from "fs";
|
|
11
|
+
import { join, dirname } from "path";
|
|
12
|
+
// ─── Schema migrations ─────────────────────────────────────
|
|
13
|
+
const MIGRATIONS = [
|
|
14
|
+
{
|
|
15
|
+
version: 1,
|
|
16
|
+
name: "initial-schema",
|
|
17
|
+
sql: [
|
|
18
|
+
`CREATE TABLE IF NOT EXISTS schema_version (
|
|
19
|
+
version INTEGER PRIMARY KEY,
|
|
20
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
21
|
+
)`,
|
|
22
|
+
`CREATE TABLE IF NOT EXISTS repository_snapshot (
|
|
23
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
24
|
+
repo_fingerprint TEXT,
|
|
25
|
+
commit_sha TEXT,
|
|
26
|
+
branch TEXT,
|
|
27
|
+
build_system TEXT,
|
|
28
|
+
repo_path TEXT,
|
|
29
|
+
indexed_at TEXT,
|
|
30
|
+
snapshot_json TEXT
|
|
31
|
+
)`,
|
|
32
|
+
`CREATE TABLE IF NOT EXISTS symbols (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
name TEXT NOT NULL,
|
|
35
|
+
kind TEXT NOT NULL,
|
|
36
|
+
file TEXT NOT NULL,
|
|
37
|
+
line INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
column INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
signature TEXT NOT NULL DEFAULT '',
|
|
40
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
41
|
+
imports TEXT DEFAULT '[]',
|
|
42
|
+
calls TEXT DEFAULT '[]',
|
|
43
|
+
called_by TEXT DEFAULT '[]',
|
|
44
|
+
related_tests TEXT DEFAULT '[]',
|
|
45
|
+
commit_sha TEXT,
|
|
46
|
+
repo_fingerprint TEXT,
|
|
47
|
+
UNIQUE(name, file)
|
|
48
|
+
)`,
|
|
49
|
+
`CREATE TABLE IF NOT EXISTS relations (
|
|
50
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
51
|
+
source_symbol TEXT NOT NULL,
|
|
52
|
+
target_symbol TEXT NOT NULL,
|
|
53
|
+
kind TEXT NOT NULL,
|
|
54
|
+
UNIQUE(source_symbol, target_symbol, kind)
|
|
55
|
+
)`,
|
|
56
|
+
`CREATE TABLE IF NOT EXISTS files_by_language (
|
|
57
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
language TEXT NOT NULL,
|
|
59
|
+
file_path TEXT NOT NULL,
|
|
60
|
+
UNIQUE(language, file_path)
|
|
61
|
+
)`,
|
|
62
|
+
`CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)`,
|
|
63
|
+
`CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file)`,
|
|
64
|
+
`CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind)`,
|
|
65
|
+
`CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_symbol)`,
|
|
66
|
+
`CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_symbol)`,
|
|
67
|
+
`CREATE INDEX IF NOT EXISTS idx_files_language ON files_by_language(language)`,
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
version: 2,
|
|
72
|
+
name: "experiences-and-memories",
|
|
73
|
+
sql: [
|
|
74
|
+
`CREATE TABLE IF NOT EXISTS experiences (
|
|
75
|
+
id TEXT PRIMARY KEY,
|
|
76
|
+
repo TEXT,
|
|
77
|
+
task TEXT NOT NULL,
|
|
78
|
+
diagnosis TEXT NOT NULL,
|
|
79
|
+
solution TEXT NOT NULL,
|
|
80
|
+
changed_files TEXT NOT NULL,
|
|
81
|
+
"commit" TEXT NOT NULL,
|
|
82
|
+
validation TEXT DEFAULT '[]',
|
|
83
|
+
failed_attempts TEXT DEFAULT '[]',
|
|
84
|
+
evidence TEXT,
|
|
85
|
+
quality REAL DEFAULT 0.5,
|
|
86
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
|
87
|
+
type TEXT NOT NULL DEFAULT 'episodic'
|
|
88
|
+
)`,
|
|
89
|
+
`CREATE TABLE IF NOT EXISTS memories (
|
|
90
|
+
id TEXT PRIMARY KEY,
|
|
91
|
+
repo TEXT,
|
|
92
|
+
type TEXT NOT NULL CHECK (type IN ('semantic', 'episodic', 'procedural', 'negative')),
|
|
93
|
+
content TEXT NOT NULL,
|
|
94
|
+
symbols TEXT DEFAULT '[]',
|
|
95
|
+
files TEXT DEFAULT '[]',
|
|
96
|
+
"commit" TEXT,
|
|
97
|
+
quality REAL DEFAULT 0.3,
|
|
98
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
|
99
|
+
source TEXT NOT NULL DEFAULT 'manual'
|
|
100
|
+
)`,
|
|
101
|
+
`CREATE INDEX IF NOT EXISTS idx_experiences_repo ON experiences(repo)`,
|
|
102
|
+
`CREATE INDEX IF NOT EXISTS idx_memories_repo ON memories(repo)`,
|
|
103
|
+
`CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type)`,
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
version: 3,
|
|
108
|
+
name: "validation-results",
|
|
109
|
+
sql: [
|
|
110
|
+
`CREATE TABLE IF NOT EXISTS validation_results (
|
|
111
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
112
|
+
repo TEXT,
|
|
113
|
+
type TEXT NOT NULL CHECK (type IN ('build','test','lint')),
|
|
114
|
+
passed INTEGER NOT NULL,
|
|
115
|
+
command TEXT NOT NULL,
|
|
116
|
+
output TEXT DEFAULT '',
|
|
117
|
+
errors TEXT DEFAULT '[]',
|
|
118
|
+
duration REAL,
|
|
119
|
+
exit_code INTEGER,
|
|
120
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
121
|
+
)`,
|
|
122
|
+
`CREATE INDEX IF NOT EXISTS idx_validation_repo ON validation_results(repo)`,
|
|
123
|
+
`CREATE INDEX IF NOT EXISTS idx_validation_created ON validation_results(created_at DESC)`,
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
version: 4,
|
|
128
|
+
name: "full-text-search",
|
|
129
|
+
sql: [
|
|
130
|
+
`CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(
|
|
131
|
+
name,
|
|
132
|
+
signature,
|
|
133
|
+
summary,
|
|
134
|
+
file,
|
|
135
|
+
content='symbols',
|
|
136
|
+
content_rowid='id',
|
|
137
|
+
tokenize='unicode61'
|
|
138
|
+
)`,
|
|
139
|
+
`CREATE TRIGGER IF NOT EXISTS symbols_fts_ai AFTER INSERT ON symbols BEGIN
|
|
140
|
+
INSERT INTO symbols_fts(rowid, name, signature, summary, file)
|
|
141
|
+
VALUES (NEW.id, NEW.name, NEW.signature, NEW.summary, NEW.file);
|
|
142
|
+
END`,
|
|
143
|
+
`CREATE TRIGGER IF NOT EXISTS symbols_fts_ad AFTER DELETE ON symbols BEGIN
|
|
144
|
+
INSERT INTO symbols_fts(symbols_fts, rowid, name, signature, summary, file)
|
|
145
|
+
VALUES ('delete', OLD.id, OLD.name, OLD.signature, OLD.summary, OLD.file);
|
|
146
|
+
END`,
|
|
147
|
+
`CREATE TRIGGER IF NOT EXISTS symbols_fts_au AFTER UPDATE ON symbols BEGIN
|
|
148
|
+
INSERT INTO symbols_fts(symbols_fts, rowid, name, signature, summary, file)
|
|
149
|
+
VALUES ('delete', OLD.id, OLD.name, OLD.signature, OLD.summary, OLD.file);
|
|
150
|
+
INSERT INTO symbols_fts(rowid, name, signature, summary, file)
|
|
151
|
+
VALUES (NEW.id, NEW.name, NEW.signature, NEW.summary, NEW.file);
|
|
152
|
+
END`,
|
|
153
|
+
// Backfill: index all existing symbols so FTS5 works immediately
|
|
154
|
+
// after migration. INSERT OR IGNORE skips rows already indexed
|
|
155
|
+
// (idempotent on re-run).
|
|
156
|
+
`INSERT OR IGNORE INTO symbols_fts(rowid, name, signature, summary, file)
|
|
157
|
+
SELECT id, name, signature, summary, file FROM symbols`,
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
version: 5,
|
|
162
|
+
name: "graph-nodes-edges",
|
|
163
|
+
sql: [
|
|
164
|
+
`CREATE TABLE IF NOT EXISTS graph_nodes (
|
|
165
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
166
|
+
symbol_id INTEGER REFERENCES symbols(id) ON DELETE CASCADE,
|
|
167
|
+
file TEXT NOT NULL,
|
|
168
|
+
name TEXT NOT NULL,
|
|
169
|
+
kind TEXT NOT NULL,
|
|
170
|
+
signature TEXT NOT NULL DEFAULT '',
|
|
171
|
+
line INTEGER,
|
|
172
|
+
"column" INTEGER,
|
|
173
|
+
UNIQUE(symbol_id)
|
|
174
|
+
)`,
|
|
175
|
+
`CREATE TABLE IF NOT EXISTS graph_edges (
|
|
176
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
177
|
+
source_node_id INTEGER NOT NULL REFERENCES graph_nodes(id) ON DELETE CASCADE,
|
|
178
|
+
target_node_id INTEGER NOT NULL REFERENCES graph_nodes(id) ON DELETE CASCADE,
|
|
179
|
+
kind TEXT NOT NULL,
|
|
180
|
+
line INTEGER,
|
|
181
|
+
"column" INTEGER,
|
|
182
|
+
UNIQUE(source_node_id, target_node_id, kind)
|
|
183
|
+
)`,
|
|
184
|
+
`CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
|
|
185
|
+
name, file, signature,
|
|
186
|
+
content='graph_nodes',
|
|
187
|
+
content_rowid='id',
|
|
188
|
+
tokenize='unicode61'
|
|
189
|
+
)`,
|
|
190
|
+
`CREATE TRIGGER IF NOT EXISTS nodes_fts_ai AFTER INSERT ON graph_nodes BEGIN
|
|
191
|
+
INSERT INTO nodes_fts(rowid, name, file, signature)
|
|
192
|
+
VALUES (NEW.id, NEW.name, NEW.file, NEW.signature);
|
|
193
|
+
END`,
|
|
194
|
+
`CREATE TRIGGER IF NOT EXISTS nodes_fts_ad AFTER DELETE ON graph_nodes BEGIN
|
|
195
|
+
INSERT INTO nodes_fts(nodes_fts, rowid, name, file, signature)
|
|
196
|
+
VALUES ('delete', OLD.id, OLD.name, OLD.file, OLD.signature);
|
|
197
|
+
END`,
|
|
198
|
+
`CREATE TRIGGER IF NOT EXISTS nodes_fts_au AFTER UPDATE ON graph_nodes BEGIN
|
|
199
|
+
INSERT INTO nodes_fts(nodes_fts, rowid, name, file, signature)
|
|
200
|
+
VALUES ('delete', OLD.id, OLD.name, OLD.file, OLD.signature);
|
|
201
|
+
INSERT INTO nodes_fts(rowid, name, file, signature)
|
|
202
|
+
VALUES (NEW.id, NEW.name, NEW.file, NEW.signature);
|
|
203
|
+
END`,
|
|
204
|
+
`INSERT OR IGNORE INTO nodes_fts(rowid, name, file, signature)
|
|
205
|
+
SELECT id, name, file, signature FROM graph_nodes`,
|
|
206
|
+
`CREATE INDEX IF NOT EXISTS idx_graph_edges_source ON graph_edges(source_node_id)`,
|
|
207
|
+
`CREATE INDEX IF NOT EXISTS idx_graph_edges_target ON graph_edges(target_node_id)`,
|
|
208
|
+
`CREATE INDEX IF NOT EXISTS idx_graph_edges_kind ON graph_edges(kind)`,
|
|
209
|
+
`CREATE INDEX IF NOT EXISTS idx_graph_nodes_kind ON graph_nodes(kind)`,
|
|
210
|
+
`CREATE INDEX IF NOT EXISTS idx_graph_nodes_file ON graph_nodes(file)`,
|
|
211
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_graph_nodes_symbol ON graph_nodes(symbol_id)`,
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
export class SQLiteStore {
|
|
216
|
+
db = null;
|
|
217
|
+
dbPath;
|
|
218
|
+
constructor(dbPath) {
|
|
219
|
+
this.dbPath = dbPath ?? join(process.cwd(), ".ci-mcp", "store.db");
|
|
220
|
+
}
|
|
221
|
+
// ── Lifecycle ──────────────────────────────────────────
|
|
222
|
+
open() {
|
|
223
|
+
if (this.db)
|
|
224
|
+
return;
|
|
225
|
+
const dir = dirname(this.dbPath);
|
|
226
|
+
if (!existsSync(dir)) {
|
|
227
|
+
mkdirSync(dir, { recursive: true });
|
|
228
|
+
}
|
|
229
|
+
this.db = new Database(this.dbPath);
|
|
230
|
+
this.db.pragma("journal_mode = WAL");
|
|
231
|
+
this.db.pragma("foreign_keys = ON");
|
|
232
|
+
}
|
|
233
|
+
close() {
|
|
234
|
+
if (this.db) {
|
|
235
|
+
this.db.close();
|
|
236
|
+
this.db = null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
isOpen() {
|
|
240
|
+
return this.db !== null;
|
|
241
|
+
}
|
|
242
|
+
// ── Migration ─────────────────────────────────────────
|
|
243
|
+
getCurrentVersion() {
|
|
244
|
+
this.ensureOpen();
|
|
245
|
+
try {
|
|
246
|
+
const row = this.db.prepare("SELECT MAX(version) as version FROM schema_version").get();
|
|
247
|
+
return row?.version ?? 0;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
migrate(targetVersion) {
|
|
254
|
+
this.ensureOpen();
|
|
255
|
+
const current = this.getCurrentVersion();
|
|
256
|
+
const target = targetVersion ?? MIGRATIONS.length;
|
|
257
|
+
for (const migration of MIGRATIONS) {
|
|
258
|
+
if (migration.version > current && migration.version <= target) {
|
|
259
|
+
const run = this.db.transaction(() => {
|
|
260
|
+
for (const sql of migration.sql) {
|
|
261
|
+
this.db.exec(sql);
|
|
262
|
+
}
|
|
263
|
+
this.db
|
|
264
|
+
.prepare("INSERT INTO schema_version (version) VALUES (?)")
|
|
265
|
+
.run(migration.version);
|
|
266
|
+
});
|
|
267
|
+
run();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// ── Repository snapshot (IndexStore) ──────────────────
|
|
272
|
+
getSnapshot() {
|
|
273
|
+
this.ensureOpen();
|
|
274
|
+
const row = this.db
|
|
275
|
+
.prepare("SELECT snapshot_json FROM repository_snapshot WHERE id = 1")
|
|
276
|
+
.get();
|
|
277
|
+
return row ? JSON.parse(row.snapshot_json) : null;
|
|
278
|
+
}
|
|
279
|
+
setSnapshot(snapshot) {
|
|
280
|
+
this.ensureOpen();
|
|
281
|
+
this.db
|
|
282
|
+
.prepare(`INSERT INTO repository_snapshot (id, repo_fingerprint, commit_sha, branch,
|
|
283
|
+
build_system, repo_path, snapshot_json)
|
|
284
|
+
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
285
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
286
|
+
repo_fingerprint = excluded.repo_fingerprint,
|
|
287
|
+
commit_sha = excluded.commit_sha,
|
|
288
|
+
branch = excluded.branch,
|
|
289
|
+
build_system = excluded.build_system,
|
|
290
|
+
repo_path = excluded.repo_path,
|
|
291
|
+
snapshot_json = excluded.snapshot_json`)
|
|
292
|
+
.run(snapshot.repoFingerprint ?? null, snapshot.commitSha ?? null, snapshot.branch ?? null, snapshot.buildSystem ?? null, snapshot.path ?? null, JSON.stringify(snapshot));
|
|
293
|
+
}
|
|
294
|
+
getRepoFingerprint() {
|
|
295
|
+
this.ensureOpen();
|
|
296
|
+
const row = this.db
|
|
297
|
+
.prepare("SELECT repo_fingerprint FROM repository_snapshot WHERE id = 1")
|
|
298
|
+
.get();
|
|
299
|
+
return row?.repo_fingerprint ?? null;
|
|
300
|
+
}
|
|
301
|
+
setRepoFingerprint(fingerprint) {
|
|
302
|
+
this.ensureOpen();
|
|
303
|
+
this.db
|
|
304
|
+
.prepare(`INSERT INTO repository_snapshot (id, repo_fingerprint)
|
|
305
|
+
VALUES (1, ?)
|
|
306
|
+
ON CONFLICT(id) DO UPDATE SET repo_fingerprint = excluded.repo_fingerprint`)
|
|
307
|
+
.run(fingerprint);
|
|
308
|
+
}
|
|
309
|
+
// ── Symbols (IndexStore) ──────────────────────────────
|
|
310
|
+
getSymbols() {
|
|
311
|
+
this.ensureOpen();
|
|
312
|
+
const rows = this.db.prepare("SELECT * FROM symbols").all();
|
|
313
|
+
return rows.map(this.rowToSymbol);
|
|
314
|
+
}
|
|
315
|
+
getSymbol(name, file) {
|
|
316
|
+
this.ensureOpen();
|
|
317
|
+
let row;
|
|
318
|
+
if (file) {
|
|
319
|
+
row = this.db
|
|
320
|
+
.prepare("SELECT * FROM symbols WHERE name = ? AND file = ?")
|
|
321
|
+
.get(name, file);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
row = this.db
|
|
325
|
+
.prepare("SELECT * FROM symbols WHERE name = ? LIMIT 1")
|
|
326
|
+
.get(name);
|
|
327
|
+
}
|
|
328
|
+
return row ? this.rowToSymbol(row) : null;
|
|
329
|
+
}
|
|
330
|
+
addSymbol(symbol) {
|
|
331
|
+
this.ensureOpen();
|
|
332
|
+
this.db
|
|
333
|
+
.prepare(`INSERT OR REPLACE INTO symbols
|
|
334
|
+
(name, kind, file, line, "column", signature, summary,
|
|
335
|
+
imports, calls, called_by, related_tests,
|
|
336
|
+
commit_sha, repo_fingerprint)
|
|
337
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
338
|
+
.run(symbol.name, symbol.kind, symbol.file, symbol.line, symbol.column, symbol.signature, symbol.summary, JSON.stringify(symbol.imports ?? []), JSON.stringify(symbol.calls ?? []), JSON.stringify(symbol.calledBy ?? []), JSON.stringify(symbol.relatedTests ?? []), symbol.commitSha, symbol.repoFingerprint);
|
|
339
|
+
}
|
|
340
|
+
addSymbols(symbols) {
|
|
341
|
+
const insert = this.db.prepare(`INSERT OR REPLACE INTO symbols
|
|
342
|
+
(name, kind, file, line, "column", signature, summary,
|
|
343
|
+
imports, calls, called_by, related_tests,
|
|
344
|
+
commit_sha, repo_fingerprint)
|
|
345
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
346
|
+
const batch = this.db.transaction((symbols) => {
|
|
347
|
+
for (const s of symbols) {
|
|
348
|
+
insert.run(s.name, s.kind, s.file, s.line, s.column, s.signature, s.summary, JSON.stringify(s.imports ?? []), JSON.stringify(s.calls ?? []), JSON.stringify(s.calledBy ?? []), JSON.stringify(s.relatedTests ?? []), s.commitSha, s.repoFingerprint);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
batch(symbols);
|
|
352
|
+
}
|
|
353
|
+
removeSymbolsByFile(file) {
|
|
354
|
+
this.ensureOpen();
|
|
355
|
+
this.db.prepare("DELETE FROM symbols WHERE file = ?").run(file);
|
|
356
|
+
}
|
|
357
|
+
clearSymbols() {
|
|
358
|
+
this.ensureOpen();
|
|
359
|
+
this.db.exec("DELETE FROM symbols; DELETE FROM relations");
|
|
360
|
+
}
|
|
361
|
+
// ── Full-text search (SQLite-only, requires migration v4) ──
|
|
362
|
+
// These methods use the FTS5 virtual table for fast symbol search.
|
|
363
|
+
// The JSON store doesn't support FTS — use `findSymbols()` there.
|
|
364
|
+
/**
|
|
365
|
+
* Search symbols using FTS5 full-text index.
|
|
366
|
+
* Returns symbols ranked by relevance (BM25).
|
|
367
|
+
* Limited to `limit` results (default 20, max 50).
|
|
368
|
+
* Falls back to name LIKE search when FTS is unavailable
|
|
369
|
+
* (pre-v4 migration databases).
|
|
370
|
+
*/
|
|
371
|
+
searchSymbolsFTS(query, limit = 20) {
|
|
372
|
+
this.ensureOpen();
|
|
373
|
+
const safeLimit = Math.min(Math.max(1, limit), 50);
|
|
374
|
+
try {
|
|
375
|
+
// FTS5 query: escape special chars, use BM25 ranking.
|
|
376
|
+
// `rank` column is auto-generated by FTS5 — lower = more relevant.
|
|
377
|
+
const stmt = this.db.prepare(`
|
|
378
|
+
SELECT s.* FROM symbols s
|
|
379
|
+
JOIN symbols_fts fts ON s.id = fts.rowid
|
|
380
|
+
WHERE symbols_fts MATCH ?
|
|
381
|
+
ORDER BY rank
|
|
382
|
+
LIMIT ?
|
|
383
|
+
`);
|
|
384
|
+
const rows = stmt.all(query, safeLimit);
|
|
385
|
+
return rows.map(r => this.rowToSymbol(r));
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// Fallback: FTS5 table doesn't exist yet (pre-v4).
|
|
389
|
+
// Use LIKE search on name + file as best-effort.
|
|
390
|
+
const likeQuery = `%${query}%`;
|
|
391
|
+
const stmt = this.db.prepare(`
|
|
392
|
+
SELECT * FROM symbols
|
|
393
|
+
WHERE name LIKE ? OR file LIKE ?
|
|
394
|
+
LIMIT ?
|
|
395
|
+
`);
|
|
396
|
+
const rows = stmt.all(likeQuery, likeQuery, safeLimit);
|
|
397
|
+
return rows.map(r => this.rowToSymbol(r));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/** Populate graph nodes and edges from symbol data.
|
|
401
|
+
* Accepts symbols directly to decouple from store backend.
|
|
402
|
+
* Clears previous graph data first. */
|
|
403
|
+
populateGraph(symbols, relations) {
|
|
404
|
+
this.ensureOpen();
|
|
405
|
+
this.db.exec("DELETE FROM graph_edges; DELETE FROM graph_nodes");
|
|
406
|
+
// Insert graph nodes from symbols
|
|
407
|
+
const insertNode = this.db.prepare(`
|
|
408
|
+
INSERT INTO graph_nodes (symbol_id, file, name, kind, signature, line, "column")
|
|
409
|
+
VALUES (NULL, ?, ?, ?, ?, ?, ?)
|
|
410
|
+
`);
|
|
411
|
+
const nodeIds = this.db.transaction(() => {
|
|
412
|
+
const ids = new Map();
|
|
413
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
414
|
+
const s = symbols[i];
|
|
415
|
+
const result = insertNode.run(s.file, s.name, s.kind, s.signature, s.line, s.column);
|
|
416
|
+
ids.set(s.file + ":" + s.name, Number(result.lastInsertRowid));
|
|
417
|
+
}
|
|
418
|
+
return ids;
|
|
419
|
+
})();
|
|
420
|
+
// Build name-based lookup (includes all nodes)
|
|
421
|
+
const nameToId = new Map();
|
|
422
|
+
for (const [key, id] of nodeIds) {
|
|
423
|
+
const name = key.split(":")[1];
|
|
424
|
+
nameToId.set(name, id);
|
|
425
|
+
}
|
|
426
|
+
// Insert edges from call/called_by/imports in symbols
|
|
427
|
+
const insertEdge = this.db.prepare(`
|
|
428
|
+
INSERT OR IGNORE INTO graph_edges (source_node_id, target_node_id, kind)
|
|
429
|
+
VALUES (?, ?, ?)
|
|
430
|
+
`);
|
|
431
|
+
const parseArr = (val) => {
|
|
432
|
+
if (Array.isArray(val))
|
|
433
|
+
return val;
|
|
434
|
+
const s = String(val ?? "[]");
|
|
435
|
+
try {
|
|
436
|
+
return JSON.parse(s);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
this.db.transaction(() => {
|
|
443
|
+
// calls/called_by from SymbolInfo
|
|
444
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
445
|
+
const sym = symbols[i];
|
|
446
|
+
const myNodeId = nodeIds.get(sym.file + ":" + sym.name);
|
|
447
|
+
if (!myNodeId)
|
|
448
|
+
continue;
|
|
449
|
+
for (const targetName of parseArr(sym.calls)) {
|
|
450
|
+
const targetId = nameToId.get(targetName);
|
|
451
|
+
if (targetId && targetId !== myNodeId) {
|
|
452
|
+
insertEdge.run(myNodeId, targetId, "calls");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// relations edges
|
|
457
|
+
for (const rel of relations) {
|
|
458
|
+
const sourceId = nameToId.get(rel.source);
|
|
459
|
+
const targetId = nameToId.get(rel.target);
|
|
460
|
+
if (sourceId && targetId && sourceId !== targetId) {
|
|
461
|
+
insertEdge.run(sourceId, targetId, rel.kind);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
})();
|
|
465
|
+
}
|
|
466
|
+
/** Get a graph node by ID. */
|
|
467
|
+
getGraphNode(id) {
|
|
468
|
+
this.ensureOpen();
|
|
469
|
+
const row = this.db.prepare("SELECT * FROM graph_nodes WHERE id = ?").get(id);
|
|
470
|
+
if (!row)
|
|
471
|
+
return null;
|
|
472
|
+
return {
|
|
473
|
+
id: row.id,
|
|
474
|
+
symbolId: row.symbol_id ?? undefined,
|
|
475
|
+
file: row.file,
|
|
476
|
+
name: row.name,
|
|
477
|
+
kind: row.kind,
|
|
478
|
+
signature: row.signature,
|
|
479
|
+
line: row.line ?? undefined,
|
|
480
|
+
column: row.column ?? undefined,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/** Get edges for a graph node. */
|
|
484
|
+
getGraphEdges(nodeId, direction = "both", kind) {
|
|
485
|
+
this.ensureOpen();
|
|
486
|
+
let query;
|
|
487
|
+
const params = [];
|
|
488
|
+
if (direction === "incoming" || direction === "both") {
|
|
489
|
+
query = "SELECT id, source_node_id, target_node_id, kind, line, \"column\" FROM graph_edges WHERE target_node_id = ?";
|
|
490
|
+
params.push(nodeId);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
query = "SELECT id, source_node_id, target_node_id, kind, line, \"column\" FROM graph_edges WHERE source_node_id = ?";
|
|
494
|
+
params.push(nodeId);
|
|
495
|
+
}
|
|
496
|
+
if (kind) {
|
|
497
|
+
query += " AND kind = ?";
|
|
498
|
+
params.push(kind);
|
|
499
|
+
}
|
|
500
|
+
const rows = this.db.prepare(query).all(...params);
|
|
501
|
+
let edges = rows.map(r => ({
|
|
502
|
+
id: r.id,
|
|
503
|
+
sourceNodeId: r.source_node_id,
|
|
504
|
+
targetNodeId: r.target_node_id,
|
|
505
|
+
kind: r.kind,
|
|
506
|
+
line: r.line ?? undefined,
|
|
507
|
+
column: r.column ?? undefined,
|
|
508
|
+
}));
|
|
509
|
+
// For "both", also query incoming edges
|
|
510
|
+
if (direction === "both") {
|
|
511
|
+
let incomingQuery = "SELECT id, source_node_id, target_node_id, kind, line, \"column\" FROM graph_edges WHERE source_node_id = ?";
|
|
512
|
+
if (kind)
|
|
513
|
+
incomingQuery += " AND kind = ?";
|
|
514
|
+
const incomingParams = [nodeId];
|
|
515
|
+
if (kind)
|
|
516
|
+
incomingParams.push(kind);
|
|
517
|
+
const incomingRows = this.db.prepare(incomingQuery).all(...incomingParams);
|
|
518
|
+
const incomingEdges = incomingRows.map(r => ({
|
|
519
|
+
id: r.id,
|
|
520
|
+
sourceNodeId: r.source_node_id,
|
|
521
|
+
targetNodeId: r.target_node_id,
|
|
522
|
+
kind: r.kind,
|
|
523
|
+
line: r.line ?? undefined,
|
|
524
|
+
column: r.column ?? undefined,
|
|
525
|
+
}));
|
|
526
|
+
edges = [...edges, ...incomingEdges];
|
|
527
|
+
}
|
|
528
|
+
return edges;
|
|
529
|
+
}
|
|
530
|
+
/** Search graph nodes by name (LIKE fallback). */
|
|
531
|
+
searchGraphNodes(query, limit = 20) {
|
|
532
|
+
this.ensureOpen();
|
|
533
|
+
const safeLimit = Math.min(Math.max(1, limit), 50);
|
|
534
|
+
const pattern = `%${query}%`;
|
|
535
|
+
const rows = this.db
|
|
536
|
+
.prepare("SELECT * FROM graph_nodes WHERE name LIKE ? ORDER BY kind, name LIMIT ?")
|
|
537
|
+
.all(pattern, safeLimit);
|
|
538
|
+
return rows.map(r => ({
|
|
539
|
+
id: r.id,
|
|
540
|
+
symbolId: r.symbol_id ?? undefined,
|
|
541
|
+
file: r.file,
|
|
542
|
+
name: r.name,
|
|
543
|
+
kind: r.kind,
|
|
544
|
+
signature: r.signature,
|
|
545
|
+
line: r.line ?? undefined,
|
|
546
|
+
column: r.column ?? undefined,
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
/** Search graph nodes by name using FTS5. */
|
|
550
|
+
searchGraphNodesFTS(query, limit = 20) {
|
|
551
|
+
this.ensureOpen();
|
|
552
|
+
const safeLimit = Math.min(Math.max(1, limit), 50);
|
|
553
|
+
try {
|
|
554
|
+
const stmt = this.db.prepare(`
|
|
555
|
+
SELECT gn.* FROM graph_nodes gn
|
|
556
|
+
JOIN nodes_fts fts ON gn.id = fts.rowid
|
|
557
|
+
WHERE nodes_fts MATCH ?
|
|
558
|
+
ORDER BY rank
|
|
559
|
+
LIMIT ?
|
|
560
|
+
`);
|
|
561
|
+
const rows = stmt.all(query, safeLimit);
|
|
562
|
+
return rows.map(r => ({
|
|
563
|
+
id: r.id,
|
|
564
|
+
symbolId: r.symbol_id ?? undefined,
|
|
565
|
+
file: r.file,
|
|
566
|
+
name: r.name,
|
|
567
|
+
kind: r.kind,
|
|
568
|
+
signature: r.signature,
|
|
569
|
+
line: r.line ?? undefined,
|
|
570
|
+
column: r.column ?? undefined,
|
|
571
|
+
}));
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
const likeQuery = `%${query}%`;
|
|
575
|
+
const stmt = this.db.prepare(`
|
|
576
|
+
SELECT * FROM graph_nodes
|
|
577
|
+
WHERE name LIKE ? OR file LIKE ?
|
|
578
|
+
LIMIT ?
|
|
579
|
+
`);
|
|
580
|
+
const rows = stmt.all(likeQuery, likeQuery, safeLimit);
|
|
581
|
+
return rows.map(r => ({
|
|
582
|
+
id: r.id,
|
|
583
|
+
symbolId: r.symbol_id ?? undefined,
|
|
584
|
+
file: r.file,
|
|
585
|
+
name: r.name,
|
|
586
|
+
kind: r.kind,
|
|
587
|
+
signature: r.signature,
|
|
588
|
+
line: r.line ?? undefined,
|
|
589
|
+
column: r.column ?? undefined,
|
|
590
|
+
}));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/** CamelCase-aware graph search — split query into segments. */
|
|
594
|
+
searchGraphNodesCamel(query, limit = 20) {
|
|
595
|
+
const segments = nameSegmentVocab(query);
|
|
596
|
+
if (segments.length === 0)
|
|
597
|
+
return this.searchGraphNodesFTS(query, limit);
|
|
598
|
+
const pattern = segments.map(s => `%${s}%`).join("");
|
|
599
|
+
const safeLimit = Math.min(Math.max(1, limit), 50);
|
|
600
|
+
try {
|
|
601
|
+
const rows = this.db
|
|
602
|
+
.prepare("SELECT * FROM graph_nodes WHERE name LIKE ? ORDER BY kind, name LIMIT ?")
|
|
603
|
+
.all(pattern, safeLimit);
|
|
604
|
+
return rows.map(r => ({
|
|
605
|
+
id: r.id,
|
|
606
|
+
symbolId: r.symbol_id ?? undefined,
|
|
607
|
+
file: r.file,
|
|
608
|
+
name: r.name,
|
|
609
|
+
kind: r.kind,
|
|
610
|
+
signature: r.signature,
|
|
611
|
+
line: r.line ?? undefined,
|
|
612
|
+
column: r.column ?? undefined,
|
|
613
|
+
}));
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/** Find a path between two graph nodes using BFS. */
|
|
620
|
+
findGraphPath(fromNodeId, toNodeId, maxDepth = 5) {
|
|
621
|
+
this.ensureOpen();
|
|
622
|
+
// BFS on graph_edges
|
|
623
|
+
const visited = new Set();
|
|
624
|
+
const parent = new Map();
|
|
625
|
+
const queue = [fromNodeId];
|
|
626
|
+
visited.add(fromNodeId);
|
|
627
|
+
for (let depth = 0; queue.length > 0 && depth < maxDepth; depth++) {
|
|
628
|
+
const levelSize = queue.length;
|
|
629
|
+
for (let i = 0; i < levelSize; i++) {
|
|
630
|
+
const currentId = queue.shift();
|
|
631
|
+
if (currentId === toNodeId) {
|
|
632
|
+
// Reconstruct path
|
|
633
|
+
const path = [];
|
|
634
|
+
let nodeId = toNodeId;
|
|
635
|
+
while (parent.has(nodeId)) {
|
|
636
|
+
const p = parent.get(nodeId);
|
|
637
|
+
path.unshift(p.edge);
|
|
638
|
+
nodeId = p.parentId;
|
|
639
|
+
}
|
|
640
|
+
return path;
|
|
641
|
+
}
|
|
642
|
+
// Get outgoing edges from current node
|
|
643
|
+
const edges = this.db
|
|
644
|
+
.prepare("SELECT * FROM graph_edges WHERE source_node_id = ?")
|
|
645
|
+
.all(currentId);
|
|
646
|
+
for (const edge of edges) {
|
|
647
|
+
if (!visited.has(edge.target_node_id)) {
|
|
648
|
+
visited.add(edge.target_node_id);
|
|
649
|
+
parent.set(edge.target_node_id, { parentId: currentId, edge });
|
|
650
|
+
queue.push(edge.target_node_id);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// Get incoming edges
|
|
654
|
+
const inEdges = this.db
|
|
655
|
+
.prepare("SELECT * FROM graph_edges WHERE target_node_id = ?")
|
|
656
|
+
.all(currentId);
|
|
657
|
+
for (const edge of inEdges) {
|
|
658
|
+
if (!visited.has(edge.source_node_id)) {
|
|
659
|
+
visited.add(edge.source_node_id);
|
|
660
|
+
parent.set(edge.source_node_id, { parentId: currentId, edge });
|
|
661
|
+
queue.push(edge.source_node_id);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
/** Remove graph entries for a file (cascade via FK — foreign_keys must be ON). */
|
|
669
|
+
removeGraphForFile(file) {
|
|
670
|
+
this.ensureOpen();
|
|
671
|
+
this.db.prepare("DELETE FROM graph_nodes WHERE file = ?").run(file);
|
|
672
|
+
}
|
|
673
|
+
/** Clear all graph data. */
|
|
674
|
+
clearGraph() {
|
|
675
|
+
this.ensureOpen();
|
|
676
|
+
this.db.exec("DELETE FROM graph_edges; DELETE FROM graph_nodes");
|
|
677
|
+
}
|
|
678
|
+
// ── Relations (IndexStore) ────────────────────────────
|
|
679
|
+
getRelations() {
|
|
680
|
+
this.ensureOpen();
|
|
681
|
+
const rows = this.db.prepare("SELECT * FROM relations").all();
|
|
682
|
+
return rows.map((r) => ({
|
|
683
|
+
source: r.source_symbol,
|
|
684
|
+
target: r.target_symbol,
|
|
685
|
+
kind: r.kind,
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
688
|
+
getRelationsForSymbol(symbol) {
|
|
689
|
+
this.ensureOpen();
|
|
690
|
+
const rows = this.db
|
|
691
|
+
.prepare("SELECT * FROM relations WHERE source_symbol = ? OR target_symbol = ?")
|
|
692
|
+
.all(symbol, symbol);
|
|
693
|
+
return rows.map((r) => ({
|
|
694
|
+
source: r.source_symbol,
|
|
695
|
+
target: r.target_symbol,
|
|
696
|
+
kind: r.kind,
|
|
697
|
+
}));
|
|
698
|
+
}
|
|
699
|
+
addRelation(relation) {
|
|
700
|
+
this.ensureOpen();
|
|
701
|
+
this.db
|
|
702
|
+
.prepare("INSERT OR REPLACE INTO relations (source_symbol, target_symbol, kind) VALUES (?, ?, ?)")
|
|
703
|
+
.run(relation.source, relation.target, relation.kind);
|
|
704
|
+
}
|
|
705
|
+
addRelations(relations) {
|
|
706
|
+
const insert = this.db.prepare("INSERT OR REPLACE INTO relations (source_symbol, target_symbol, kind) VALUES (?, ?, ?)");
|
|
707
|
+
const batch = this.db.transaction((rels) => {
|
|
708
|
+
for (const r of rels) {
|
|
709
|
+
insert.run(r.source, r.target, r.kind);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
batch(relations);
|
|
713
|
+
}
|
|
714
|
+
clearRelations() {
|
|
715
|
+
this.ensureOpen();
|
|
716
|
+
this.db.exec("DELETE FROM relations");
|
|
717
|
+
}
|
|
718
|
+
// ── Files by language (IndexStore) ────────────────────
|
|
719
|
+
getFilesByLanguage() {
|
|
720
|
+
this.ensureOpen();
|
|
721
|
+
const rows = this.db
|
|
722
|
+
.prepare("SELECT language, file_path FROM files_by_language ORDER BY language, file_path")
|
|
723
|
+
.all();
|
|
724
|
+
const result = {};
|
|
725
|
+
for (const r of rows) {
|
|
726
|
+
if (!result[r.language])
|
|
727
|
+
result[r.language] = [];
|
|
728
|
+
result[r.language].push(r.file_path);
|
|
729
|
+
}
|
|
730
|
+
return result;
|
|
731
|
+
}
|
|
732
|
+
addFile(language, filePath) {
|
|
733
|
+
this.ensureOpen();
|
|
734
|
+
this.db
|
|
735
|
+
.prepare("INSERT OR IGNORE INTO files_by_language (language, file_path) VALUES (?, ?)")
|
|
736
|
+
.run(language, filePath);
|
|
737
|
+
}
|
|
738
|
+
setFilesByLanguage(files) {
|
|
739
|
+
this.ensureOpen();
|
|
740
|
+
const clear = this.db.prepare("DELETE FROM files_by_language");
|
|
741
|
+
const insert = this.db.prepare("INSERT INTO files_by_language (language, file_path) VALUES (?, ?)");
|
|
742
|
+
const batch = this.db.transaction(() => {
|
|
743
|
+
clear.run();
|
|
744
|
+
for (const [lang, paths] of Object.entries(files)) {
|
|
745
|
+
for (const p of paths) {
|
|
746
|
+
insert.run(lang, p);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
batch();
|
|
751
|
+
}
|
|
752
|
+
// ── Index tracking (IndexStore) ───────────────────────
|
|
753
|
+
getIndexedCommit() {
|
|
754
|
+
this.ensureOpen();
|
|
755
|
+
const row = this.db
|
|
756
|
+
.prepare("SELECT commit_sha FROM repository_snapshot WHERE id = 1")
|
|
757
|
+
.get();
|
|
758
|
+
return row?.commit_sha ?? null;
|
|
759
|
+
}
|
|
760
|
+
setIndexedCommit(sha) {
|
|
761
|
+
this.ensureOpen();
|
|
762
|
+
this.db
|
|
763
|
+
.prepare(`INSERT INTO repository_snapshot (id, commit_sha) VALUES (1, ?)
|
|
764
|
+
ON CONFLICT(id) DO UPDATE SET commit_sha = excluded.commit_sha`)
|
|
765
|
+
.run(sha);
|
|
766
|
+
}
|
|
767
|
+
getIndexedAt() {
|
|
768
|
+
this.ensureOpen();
|
|
769
|
+
const row = this.db
|
|
770
|
+
.prepare("SELECT indexed_at FROM repository_snapshot WHERE id = 1")
|
|
771
|
+
.get();
|
|
772
|
+
return row?.indexed_at ?? null;
|
|
773
|
+
}
|
|
774
|
+
setIndexedAt(ts) {
|
|
775
|
+
this.ensureOpen();
|
|
776
|
+
this.db
|
|
777
|
+
.prepare(`INSERT INTO repository_snapshot (id, indexed_at) VALUES (1, ?)
|
|
778
|
+
ON CONFLICT(id) DO UPDATE SET indexed_at = excluded.indexed_at`)
|
|
779
|
+
.run(ts);
|
|
780
|
+
}
|
|
781
|
+
// ── Purge (IndexStore) ────────────────────────────────
|
|
782
|
+
purge() {
|
|
783
|
+
this.ensureOpen();
|
|
784
|
+
this.db.exec(`DELETE FROM symbols; DELETE FROM relations;
|
|
785
|
+
DELETE FROM files_by_language; DELETE FROM repository_snapshot;
|
|
786
|
+
DELETE FROM experiences; DELETE FROM memories;
|
|
787
|
+
DELETE FROM validation_results`);
|
|
788
|
+
}
|
|
789
|
+
// ── Experiences (MemoryStore) ─────────────────────────
|
|
790
|
+
addExperience(exp) {
|
|
791
|
+
this.ensureOpen();
|
|
792
|
+
this.db
|
|
793
|
+
.prepare(`INSERT OR REPLACE INTO experiences
|
|
794
|
+
(id, repo, task, diagnosis, solution, changed_files,
|
|
795
|
+
"commit", validation, failed_attempts, evidence,
|
|
796
|
+
quality, timestamp, type)
|
|
797
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
798
|
+
.run(exp.id, exp.repo, exp.task, exp.diagnosis, exp.solution, JSON.stringify(exp.changedFiles), exp.commit, JSON.stringify(exp.validation ?? []), JSON.stringify(exp.failedAttempts ?? []), exp.evidence ? JSON.stringify(exp.evidence) : null, exp.quality, exp.timestamp, exp.type);
|
|
799
|
+
}
|
|
800
|
+
getExperience(id) {
|
|
801
|
+
this.ensureOpen();
|
|
802
|
+
const row = this.db
|
|
803
|
+
.prepare("SELECT * FROM experiences WHERE id = ?")
|
|
804
|
+
.get(id);
|
|
805
|
+
return row ? this.rowToExperience(row) : null;
|
|
806
|
+
}
|
|
807
|
+
getAllExperiences(repo) {
|
|
808
|
+
this.ensureOpen();
|
|
809
|
+
let rows;
|
|
810
|
+
if (repo) {
|
|
811
|
+
rows = this.db
|
|
812
|
+
.prepare("SELECT * FROM experiences WHERE repo = ? ORDER BY timestamp DESC")
|
|
813
|
+
.all(repo);
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
rows = this.db
|
|
817
|
+
.prepare("SELECT * FROM experiences ORDER BY timestamp DESC")
|
|
818
|
+
.all();
|
|
819
|
+
}
|
|
820
|
+
return rows.map((r) => this.rowToExperience(r));
|
|
821
|
+
}
|
|
822
|
+
updateExperience(id, updates) {
|
|
823
|
+
this.ensureOpen();
|
|
824
|
+
const existing = this.getExperience(id);
|
|
825
|
+
if (!existing)
|
|
826
|
+
return;
|
|
827
|
+
const merged = { ...existing, ...updates };
|
|
828
|
+
this.addExperience(merged);
|
|
829
|
+
}
|
|
830
|
+
deleteExperience(id) {
|
|
831
|
+
this.ensureOpen();
|
|
832
|
+
this.db.prepare("DELETE FROM experiences WHERE id = ?").run(id);
|
|
833
|
+
}
|
|
834
|
+
// ── Memories (MemoryStore) ────────────────────────────
|
|
835
|
+
addMemory(mem) {
|
|
836
|
+
this.ensureOpen();
|
|
837
|
+
this.db
|
|
838
|
+
.prepare(`INSERT OR REPLACE INTO memories
|
|
839
|
+
(id, repo, type, content, symbols, files,
|
|
840
|
+
"commit", quality, timestamp, source)
|
|
841
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
842
|
+
.run(mem.id, mem.repo, mem.type, mem.content, JSON.stringify(mem.symbols ?? []), JSON.stringify(mem.files ?? []), mem.commit, mem.quality, mem.timestamp, mem.source);
|
|
843
|
+
}
|
|
844
|
+
getMemory(id) {
|
|
845
|
+
this.ensureOpen();
|
|
846
|
+
const row = this.db
|
|
847
|
+
.prepare("SELECT * FROM memories WHERE id = ?")
|
|
848
|
+
.get(id);
|
|
849
|
+
return row ? this.rowToMemory(row) : null;
|
|
850
|
+
}
|
|
851
|
+
getAllMemories(repo) {
|
|
852
|
+
this.ensureOpen();
|
|
853
|
+
let rows;
|
|
854
|
+
if (repo) {
|
|
855
|
+
rows = this.db
|
|
856
|
+
.prepare("SELECT * FROM memories WHERE repo = ? ORDER BY timestamp DESC")
|
|
857
|
+
.all(repo);
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
rows = this.db
|
|
861
|
+
.prepare("SELECT * FROM memories ORDER BY timestamp DESC")
|
|
862
|
+
.all();
|
|
863
|
+
}
|
|
864
|
+
return rows.map((r) => this.rowToMemory(r));
|
|
865
|
+
}
|
|
866
|
+
updateMemory(id, updates) {
|
|
867
|
+
this.ensureOpen();
|
|
868
|
+
const existing = this.getMemory(id);
|
|
869
|
+
if (!existing)
|
|
870
|
+
return;
|
|
871
|
+
const merged = { ...existing, ...updates };
|
|
872
|
+
this.addMemory(merged);
|
|
873
|
+
}
|
|
874
|
+
deleteMemory(id) {
|
|
875
|
+
this.ensureOpen();
|
|
876
|
+
this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
877
|
+
}
|
|
878
|
+
updateQualityScore(id, delta) {
|
|
879
|
+
this.ensureOpen();
|
|
880
|
+
this.db
|
|
881
|
+
.prepare("UPDATE experiences SET quality = MIN(1.0, MAX(0.0, quality + ?)) WHERE id = ?")
|
|
882
|
+
.run(delta, id);
|
|
883
|
+
this.db
|
|
884
|
+
.prepare("UPDATE memories SET quality = MIN(1.0, MAX(0.0, quality + ?)) WHERE id = ?")
|
|
885
|
+
.run(delta, id);
|
|
886
|
+
}
|
|
887
|
+
// ── Validation results ────────────────────────────────
|
|
888
|
+
save(result) {
|
|
889
|
+
this.ensureOpen();
|
|
890
|
+
this.db
|
|
891
|
+
.prepare(`INSERT INTO validation_results
|
|
892
|
+
(repo, type, passed, command, output, errors, duration, exit_code)
|
|
893
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
894
|
+
.run(null, // repo — not in ValidationResult type
|
|
895
|
+
result.type, result.passed ? 1 : 0, result.command, result.output, JSON.stringify(result.errors ?? []), result.duration ?? null, result.exitCode ?? null);
|
|
896
|
+
}
|
|
897
|
+
getLatest(limit = 10) {
|
|
898
|
+
this.ensureOpen();
|
|
899
|
+
const rows = this.db
|
|
900
|
+
.prepare("SELECT * FROM validation_results ORDER BY created_at DESC LIMIT ?")
|
|
901
|
+
.all(limit);
|
|
902
|
+
return rows.map((r) => ({
|
|
903
|
+
type: r.type,
|
|
904
|
+
passed: r.passed === 1,
|
|
905
|
+
command: r.command,
|
|
906
|
+
output: r.output,
|
|
907
|
+
errors: r.errors ? JSON.parse(r.errors) : [],
|
|
908
|
+
duration: r.duration,
|
|
909
|
+
exitCode: r.exit_code ?? null,
|
|
910
|
+
}));
|
|
911
|
+
}
|
|
912
|
+
// ── Maintenance ───────────────────────────────────────
|
|
913
|
+
vacuum() {
|
|
914
|
+
this.ensureOpen();
|
|
915
|
+
this.db.exec("VACUUM");
|
|
916
|
+
}
|
|
917
|
+
backup(destPath) {
|
|
918
|
+
this.ensureOpen();
|
|
919
|
+
this.db.backup(destPath);
|
|
920
|
+
}
|
|
921
|
+
// ── Helpers ──────────────────────────────────────────
|
|
922
|
+
ensureOpen() {
|
|
923
|
+
if (!this.db) {
|
|
924
|
+
throw new Error("Store not opened. Call store.open() first.");
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
rowToSymbol(row) {
|
|
928
|
+
const json = (v, fallback) => {
|
|
929
|
+
const s = String(v ?? fallback);
|
|
930
|
+
try {
|
|
931
|
+
return JSON.parse(s);
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
return [];
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
return {
|
|
938
|
+
name: String(row.name ?? ""),
|
|
939
|
+
kind: String(row.kind ?? "unknown"),
|
|
940
|
+
file: String(row.file ?? ""),
|
|
941
|
+
line: Number(row.line ?? 0),
|
|
942
|
+
column: Number(row.column ?? 0),
|
|
943
|
+
signature: String(row.signature ?? ""),
|
|
944
|
+
summary: String(row.summary ?? ""),
|
|
945
|
+
imports: json(row.imports, "[]"),
|
|
946
|
+
calls: json(row.calls, "[]"),
|
|
947
|
+
calledBy: json(row.called_by, "[]"),
|
|
948
|
+
relatedTests: json(row.related_tests, "[]"),
|
|
949
|
+
commitSha: String(row.commit_sha ?? ""),
|
|
950
|
+
repoFingerprint: String(row.repo_fingerprint ?? ""),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
rowToExperience(row) {
|
|
954
|
+
return {
|
|
955
|
+
id: row.id,
|
|
956
|
+
task: row.task,
|
|
957
|
+
diagnosis: row.diagnosis,
|
|
958
|
+
solution: row.solution,
|
|
959
|
+
changedFiles: JSON.parse(row.changed_files),
|
|
960
|
+
commit: row.commit,
|
|
961
|
+
validation: JSON.parse(row.validation ?? "[]"),
|
|
962
|
+
failedAttempts: JSON.parse(row.failed_attempts ?? "[]"),
|
|
963
|
+
evidence: row.evidence ? JSON.parse(row.evidence) : undefined,
|
|
964
|
+
quality: row.quality,
|
|
965
|
+
timestamp: row.timestamp,
|
|
966
|
+
repo: row.repo,
|
|
967
|
+
type: row.type,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
rowToMemory(row) {
|
|
971
|
+
return {
|
|
972
|
+
id: row.id,
|
|
973
|
+
type: row.type,
|
|
974
|
+
content: row.content,
|
|
975
|
+
symbols: row.symbols ? JSON.parse(row.symbols) : [],
|
|
976
|
+
files: row.files ? JSON.parse(row.files) : [],
|
|
977
|
+
commit: row.commit ?? null,
|
|
978
|
+
quality: row.quality,
|
|
979
|
+
timestamp: row.timestamp,
|
|
980
|
+
repo: row.repo,
|
|
981
|
+
source: row.source,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// ─── Factory ───────────────────────────────────────────────
|
|
986
|
+
let globalStore = null;
|
|
987
|
+
export function getStore(dbPath) {
|
|
988
|
+
if (!globalStore) {
|
|
989
|
+
globalStore = new SQLiteStore(dbPath);
|
|
990
|
+
globalStore.open();
|
|
991
|
+
globalStore.migrate();
|
|
992
|
+
}
|
|
993
|
+
return globalStore;
|
|
994
|
+
}
|
|
995
|
+
export function closeStore() {
|
|
996
|
+
if (globalStore) {
|
|
997
|
+
globalStore.close();
|
|
998
|
+
globalStore = null;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
/** CamelCase / snake_case splitting for fuzzy symbol matching. */
|
|
1002
|
+
export function nameSegmentVocab(name) {
|
|
1003
|
+
if (!name)
|
|
1004
|
+
return [];
|
|
1005
|
+
// Split on camelCase boundaries + non-alphanumeric
|
|
1006
|
+
// "getUserData" → ["get", "User", "Data"]
|
|
1007
|
+
// "SymbolIndexer" → ["Symbol", "Indexer"]
|
|
1008
|
+
// "parse_file_name" → ["parse", "file", "name"]
|
|
1009
|
+
const parts = name
|
|
1010
|
+
.replace(/([A-Z])/g, " $1")
|
|
1011
|
+
.replace(/[_-]+/g, " ")
|
|
1012
|
+
.replace(/[^a-zA-Z0-9 ]/g, "")
|
|
1013
|
+
.split(/\s+/)
|
|
1014
|
+
.filter(Boolean);
|
|
1015
|
+
// Only return segments that are useful (skip single chars)
|
|
1016
|
+
return parts.filter(s => s.length > 1);
|
|
1017
|
+
}
|
|
1018
|
+
//# sourceMappingURL=sqlite.js.map
|