@velvetmonkey/vault-core 2.0.30 → 2.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/entities.d.ts +50 -0
- package/dist/src/entities.d.ts.map +1 -0
- package/dist/src/entities.js +499 -0
- package/dist/src/entities.js.map +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +23 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logging/index.d.ts +7 -0
- package/dist/src/logging/index.d.ts.map +1 -0
- package/dist/src/logging/index.js +7 -0
- package/dist/src/logging/index.js.map +1 -0
- package/dist/src/logging/operationLogger.d.ts +59 -0
- package/dist/src/logging/operationLogger.d.ts.map +1 -0
- package/dist/src/logging/operationLogger.js +282 -0
- package/dist/src/logging/operationLogger.js.map +1 -0
- package/dist/src/logging/sessionManager.d.ts +35 -0
- package/dist/src/logging/sessionManager.d.ts.map +1 -0
- package/dist/src/logging/sessionManager.js +68 -0
- package/dist/src/logging/sessionManager.js.map +1 -0
- package/dist/src/logging/types.d.ts +123 -0
- package/dist/src/logging/types.d.ts.map +1 -0
- package/dist/src/logging/types.js +23 -0
- package/dist/src/logging/types.js.map +1 -0
- package/dist/src/protectedZones.d.ts +36 -0
- package/dist/src/protectedZones.d.ts.map +1 -0
- package/dist/src/protectedZones.js +114 -0
- package/dist/src/protectedZones.js.map +1 -0
- package/dist/src/sqlite.d.ts +273 -0
- package/dist/src/sqlite.d.ts.map +1 -0
- package/dist/src/sqlite.js +959 -0
- package/dist/src/sqlite.js.map +1 -0
- package/dist/src/types.d.ts +171 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +5 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/wikilinks.d.ts +76 -0
- package/dist/src/wikilinks.d.ts.map +1 -0
- package/dist/src/wikilinks.js +681 -0
- package/dist/src/wikilinks.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SQLite State Management
|
|
3
|
+
*
|
|
4
|
+
* Consolidates scattered JSON files and in-memory state into a single
|
|
5
|
+
* SQLite database with FTS5 for entity search.
|
|
6
|
+
*
|
|
7
|
+
* Target performance:
|
|
8
|
+
* - Startup <100ms for 10k note vault
|
|
9
|
+
* - Entity search <10ms
|
|
10
|
+
* - Single .flywheel/state.db for backup
|
|
11
|
+
*/
|
|
12
|
+
import Database from 'better-sqlite3';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Constants
|
|
17
|
+
// =============================================================================
|
|
18
|
+
/** Current schema version - bump when schema changes */
|
|
19
|
+
export const SCHEMA_VERSION = 14;
|
|
20
|
+
/** State database filename */
|
|
21
|
+
export const STATE_DB_FILENAME = 'state.db';
|
|
22
|
+
/** Directory for flywheel state */
|
|
23
|
+
export const FLYWHEEL_DIR = '.flywheel';
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Schema
|
|
26
|
+
// =============================================================================
|
|
27
|
+
const SCHEMA_SQL = `
|
|
28
|
+
-- Schema version tracking
|
|
29
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
30
|
+
version INTEGER PRIMARY KEY,
|
|
31
|
+
applied_at TEXT DEFAULT (datetime('now'))
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
-- Metadata key-value store
|
|
35
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
36
|
+
key TEXT PRIMARY KEY,
|
|
37
|
+
value TEXT NOT NULL,
|
|
38
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
-- Entity index (replaces wikilink-entities.json)
|
|
42
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
43
|
+
id INTEGER PRIMARY KEY,
|
|
44
|
+
name TEXT NOT NULL,
|
|
45
|
+
name_lower TEXT NOT NULL,
|
|
46
|
+
path TEXT NOT NULL,
|
|
47
|
+
category TEXT NOT NULL,
|
|
48
|
+
aliases_json TEXT,
|
|
49
|
+
hub_score INTEGER DEFAULT 0
|
|
50
|
+
);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name_lower ON entities(name_lower);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_entities_category ON entities(category);
|
|
53
|
+
|
|
54
|
+
-- FTS5 for entity search with porter stemmer
|
|
55
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
56
|
+
name, aliases, category,
|
|
57
|
+
content='entities', content_rowid='id',
|
|
58
|
+
tokenize='porter unicode61'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
-- Auto-sync triggers for entities_fts
|
|
62
|
+
CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
|
|
63
|
+
INSERT INTO entities_fts(rowid, name, aliases, category)
|
|
64
|
+
VALUES (
|
|
65
|
+
new.id,
|
|
66
|
+
new.name,
|
|
67
|
+
COALESCE((SELECT group_concat(value, ' ') FROM json_each(new.aliases_json)), ''),
|
|
68
|
+
new.category
|
|
69
|
+
);
|
|
70
|
+
END;
|
|
71
|
+
|
|
72
|
+
CREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN
|
|
73
|
+
INSERT INTO entities_fts(entities_fts, rowid, name, aliases, category)
|
|
74
|
+
VALUES (
|
|
75
|
+
'delete',
|
|
76
|
+
old.id,
|
|
77
|
+
old.name,
|
|
78
|
+
COALESCE((SELECT group_concat(value, ' ') FROM json_each(old.aliases_json)), ''),
|
|
79
|
+
old.category
|
|
80
|
+
);
|
|
81
|
+
END;
|
|
82
|
+
|
|
83
|
+
CREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN
|
|
84
|
+
INSERT INTO entities_fts(entities_fts, rowid, name, aliases, category)
|
|
85
|
+
VALUES (
|
|
86
|
+
'delete',
|
|
87
|
+
old.id,
|
|
88
|
+
old.name,
|
|
89
|
+
COALESCE((SELECT group_concat(value, ' ') FROM json_each(old.aliases_json)), ''),
|
|
90
|
+
old.category
|
|
91
|
+
);
|
|
92
|
+
INSERT INTO entities_fts(rowid, name, aliases, category)
|
|
93
|
+
VALUES (
|
|
94
|
+
new.id,
|
|
95
|
+
new.name,
|
|
96
|
+
COALESCE((SELECT group_concat(value, ' ') FROM json_each(new.aliases_json)), ''),
|
|
97
|
+
new.category
|
|
98
|
+
);
|
|
99
|
+
END;
|
|
100
|
+
|
|
101
|
+
-- Recency tracking (replaces entity-recency.json)
|
|
102
|
+
CREATE TABLE IF NOT EXISTS recency (
|
|
103
|
+
entity_name_lower TEXT PRIMARY KEY,
|
|
104
|
+
last_mentioned_at INTEGER NOT NULL,
|
|
105
|
+
mention_count INTEGER DEFAULT 1
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
-- Write state (replaces last-commit.json and other write state)
|
|
109
|
+
CREATE TABLE IF NOT EXISTS write_state (
|
|
110
|
+
key TEXT PRIMARY KEY,
|
|
111
|
+
value TEXT NOT NULL,
|
|
112
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
-- Content search FTS5 (migrated from vault-search.db)
|
|
116
|
+
-- v11: Added frontmatter column for weighted search (path, title, frontmatter, content)
|
|
117
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
|
|
118
|
+
path, title, frontmatter, content,
|
|
119
|
+
tokenize='porter'
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
-- FTS5 build metadata (consolidated from vault-search.db)
|
|
123
|
+
CREATE TABLE IF NOT EXISTS fts_metadata (
|
|
124
|
+
key TEXT PRIMARY KEY,
|
|
125
|
+
value TEXT
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
-- Vault index cache (for fast startup)
|
|
129
|
+
-- Stores serialized VaultIndex to avoid full rebuild on startup
|
|
130
|
+
CREATE TABLE IF NOT EXISTS vault_index_cache (
|
|
131
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
132
|
+
data BLOB NOT NULL,
|
|
133
|
+
built_at INTEGER NOT NULL,
|
|
134
|
+
note_count INTEGER NOT NULL,
|
|
135
|
+
version INTEGER DEFAULT 1
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
-- Flywheel configuration (replaces .flywheel.json)
|
|
139
|
+
CREATE TABLE IF NOT EXISTS flywheel_config (
|
|
140
|
+
key TEXT PRIMARY KEY,
|
|
141
|
+
value TEXT NOT NULL,
|
|
142
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
-- Vault metrics (v4: growth tracking)
|
|
146
|
+
CREATE TABLE IF NOT EXISTS vault_metrics (
|
|
147
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
148
|
+
timestamp INTEGER NOT NULL,
|
|
149
|
+
metric TEXT NOT NULL,
|
|
150
|
+
value REAL NOT NULL
|
|
151
|
+
);
|
|
152
|
+
CREATE INDEX IF NOT EXISTS idx_vault_metrics_ts ON vault_metrics(timestamp);
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_vault_metrics_m ON vault_metrics(metric, timestamp);
|
|
154
|
+
|
|
155
|
+
-- Wikilink feedback (v4: quality tracking)
|
|
156
|
+
CREATE TABLE IF NOT EXISTS wikilink_feedback (
|
|
157
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
158
|
+
entity TEXT NOT NULL,
|
|
159
|
+
context TEXT NOT NULL,
|
|
160
|
+
note_path TEXT NOT NULL,
|
|
161
|
+
correct INTEGER NOT NULL,
|
|
162
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
163
|
+
);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_wl_feedback_entity ON wikilink_feedback(entity);
|
|
165
|
+
|
|
166
|
+
-- Wikilink suppressions (v4: auto-suppress false positives)
|
|
167
|
+
CREATE TABLE IF NOT EXISTS wikilink_suppressions (
|
|
168
|
+
entity TEXT PRIMARY KEY,
|
|
169
|
+
false_positive_rate REAL NOT NULL,
|
|
170
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
-- Wikilink applications tracking (v5: implicit feedback)
|
|
174
|
+
CREATE TABLE IF NOT EXISTS wikilink_applications (
|
|
175
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
176
|
+
entity TEXT NOT NULL,
|
|
177
|
+
note_path TEXT NOT NULL,
|
|
178
|
+
applied_at TEXT DEFAULT (datetime('now')),
|
|
179
|
+
status TEXT DEFAULT 'applied'
|
|
180
|
+
);
|
|
181
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_wl_apps_unique ON wikilink_applications(entity, note_path);
|
|
182
|
+
|
|
183
|
+
-- Index events tracking (v6: index activity history)
|
|
184
|
+
CREATE TABLE IF NOT EXISTS index_events (
|
|
185
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
186
|
+
timestamp INTEGER NOT NULL,
|
|
187
|
+
trigger TEXT NOT NULL,
|
|
188
|
+
duration_ms INTEGER NOT NULL,
|
|
189
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
190
|
+
note_count INTEGER,
|
|
191
|
+
files_changed INTEGER,
|
|
192
|
+
changed_paths TEXT,
|
|
193
|
+
error TEXT,
|
|
194
|
+
steps TEXT
|
|
195
|
+
);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_index_events_ts ON index_events(timestamp);
|
|
197
|
+
|
|
198
|
+
-- Tool invocation tracking (v7: usage analytics)
|
|
199
|
+
CREATE TABLE IF NOT EXISTS tool_invocations (
|
|
200
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
201
|
+
timestamp INTEGER NOT NULL,
|
|
202
|
+
tool_name TEXT NOT NULL,
|
|
203
|
+
session_id TEXT,
|
|
204
|
+
note_paths TEXT,
|
|
205
|
+
duration_ms INTEGER,
|
|
206
|
+
success INTEGER NOT NULL DEFAULT 1
|
|
207
|
+
);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_tool_inv_ts ON tool_invocations(timestamp);
|
|
209
|
+
CREATE INDEX IF NOT EXISTS idx_tool_inv_tool ON tool_invocations(tool_name, timestamp);
|
|
210
|
+
CREATE INDEX IF NOT EXISTS idx_tool_inv_session ON tool_invocations(session_id, timestamp);
|
|
211
|
+
|
|
212
|
+
-- Graph topology snapshots (v8: structural evolution)
|
|
213
|
+
CREATE TABLE IF NOT EXISTS graph_snapshots (
|
|
214
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
215
|
+
timestamp INTEGER NOT NULL,
|
|
216
|
+
metric TEXT NOT NULL,
|
|
217
|
+
value REAL NOT NULL,
|
|
218
|
+
details TEXT
|
|
219
|
+
);
|
|
220
|
+
CREATE INDEX IF NOT EXISTS idx_graph_snap_ts ON graph_snapshots(timestamp);
|
|
221
|
+
CREATE INDEX IF NOT EXISTS idx_graph_snap_m ON graph_snapshots(metric, timestamp);
|
|
222
|
+
|
|
223
|
+
-- Note embeddings for semantic search (v9)
|
|
224
|
+
CREATE TABLE IF NOT EXISTS note_embeddings (
|
|
225
|
+
path TEXT PRIMARY KEY,
|
|
226
|
+
embedding BLOB NOT NULL,
|
|
227
|
+
content_hash TEXT NOT NULL,
|
|
228
|
+
model TEXT NOT NULL,
|
|
229
|
+
updated_at INTEGER NOT NULL
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
-- Entity embeddings for semantic entity search (v10)
|
|
233
|
+
CREATE TABLE IF NOT EXISTS entity_embeddings (
|
|
234
|
+
entity_name TEXT PRIMARY KEY,
|
|
235
|
+
embedding BLOB NOT NULL,
|
|
236
|
+
source_hash TEXT NOT NULL,
|
|
237
|
+
model TEXT NOT NULL,
|
|
238
|
+
updated_at INTEGER NOT NULL
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
-- Task cache for fast task queries (v12)
|
|
242
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
243
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
244
|
+
path TEXT NOT NULL,
|
|
245
|
+
line INTEGER NOT NULL,
|
|
246
|
+
text TEXT NOT NULL,
|
|
247
|
+
status TEXT NOT NULL,
|
|
248
|
+
raw TEXT NOT NULL,
|
|
249
|
+
context TEXT,
|
|
250
|
+
tags_json TEXT,
|
|
251
|
+
due_date TEXT,
|
|
252
|
+
UNIQUE(path, line)
|
|
253
|
+
);
|
|
254
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
255
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_path ON tasks(path);
|
|
256
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due_date);
|
|
257
|
+
|
|
258
|
+
-- Merge dismissals (v13: persistent merge pair suppression)
|
|
259
|
+
CREATE TABLE IF NOT EXISTS merge_dismissals (
|
|
260
|
+
pair_key TEXT PRIMARY KEY,
|
|
261
|
+
source_path TEXT NOT NULL,
|
|
262
|
+
target_path TEXT NOT NULL,
|
|
263
|
+
source_name TEXT NOT NULL,
|
|
264
|
+
target_name TEXT NOT NULL,
|
|
265
|
+
reason TEXT NOT NULL,
|
|
266
|
+
dismissed_at TEXT DEFAULT (datetime('now'))
|
|
267
|
+
);
|
|
268
|
+
`;
|
|
269
|
+
// =============================================================================
|
|
270
|
+
// Database Initialization
|
|
271
|
+
// =============================================================================
|
|
272
|
+
/**
|
|
273
|
+
* Get the database path for a vault
|
|
274
|
+
*/
|
|
275
|
+
export function getStateDbPath(vaultPath) {
|
|
276
|
+
const flywheelDir = path.join(vaultPath, FLYWHEEL_DIR);
|
|
277
|
+
if (!fs.existsSync(flywheelDir)) {
|
|
278
|
+
fs.mkdirSync(flywheelDir, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
return path.join(flywheelDir, STATE_DB_FILENAME);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Initialize schema and run migrations
|
|
284
|
+
*/
|
|
285
|
+
function initSchema(db) {
|
|
286
|
+
// Enable WAL mode for better concurrent read performance
|
|
287
|
+
db.pragma('journal_mode = WAL');
|
|
288
|
+
// Enable foreign keys
|
|
289
|
+
db.pragma('foreign_keys = ON');
|
|
290
|
+
// Run schema creation
|
|
291
|
+
db.exec(SCHEMA_SQL);
|
|
292
|
+
// Guard: Verify critical tables were created
|
|
293
|
+
// This catches cases where schema execution silently failed (e.g., corrupted db)
|
|
294
|
+
const tables = db.prepare(`
|
|
295
|
+
SELECT name FROM sqlite_master
|
|
296
|
+
WHERE type='table' AND name IN ('entities', 'schema_version', 'metadata')
|
|
297
|
+
`).all();
|
|
298
|
+
if (tables.length < 3) {
|
|
299
|
+
const foundTables = tables.map(t => t.name).join(', ') || 'none';
|
|
300
|
+
throw new Error(`[vault-core] Schema validation failed: expected 3 critical tables, found ${tables.length} (${foundTables}). ` +
|
|
301
|
+
`Database may be corrupted. Delete ${db.name} and restart.`);
|
|
302
|
+
}
|
|
303
|
+
// Check and record schema version
|
|
304
|
+
const versionRow = db.prepare('SELECT MAX(version) as version FROM schema_version').get();
|
|
305
|
+
const currentVersion = versionRow?.version ?? 0;
|
|
306
|
+
if (currentVersion < SCHEMA_VERSION) {
|
|
307
|
+
// v2: Drop dead notes/links tables if they exist from v1
|
|
308
|
+
if (currentVersion < 2) {
|
|
309
|
+
db.exec('DROP TABLE IF EXISTS notes');
|
|
310
|
+
db.exec('DROP TABLE IF EXISTS links');
|
|
311
|
+
}
|
|
312
|
+
// v3: Rename crank_state → write_state
|
|
313
|
+
if (currentVersion < 3) {
|
|
314
|
+
const hasCrankState = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='crank_state'`).get();
|
|
315
|
+
const hasWriteState = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='write_state'`).get();
|
|
316
|
+
if (hasCrankState && !hasWriteState) {
|
|
317
|
+
db.exec('ALTER TABLE crank_state RENAME TO write_state');
|
|
318
|
+
}
|
|
319
|
+
else if (hasCrankState && hasWriteState) {
|
|
320
|
+
// Both exist (stale db) — drop the old one
|
|
321
|
+
db.exec('DROP TABLE crank_state');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// v4: vault_metrics, wikilink_feedback, wikilink_suppressions tables
|
|
325
|
+
// (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
|
|
326
|
+
// v5: wikilink_applications table (implicit feedback tracking)
|
|
327
|
+
// (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
|
|
328
|
+
// v6: index_events table (index activity history)
|
|
329
|
+
// (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
|
|
330
|
+
// v7: tool_invocations table (usage analytics)
|
|
331
|
+
// (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
|
|
332
|
+
// v8: graph_snapshots table (structural evolution)
|
|
333
|
+
// (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
|
|
334
|
+
// v9: note_embeddings table (semantic search)
|
|
335
|
+
// (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
|
|
336
|
+
// v10: entity_embeddings table (semantic entity search)
|
|
337
|
+
// (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
|
|
338
|
+
// v11: notes_fts gains frontmatter column (4-col: path, title, frontmatter, content)
|
|
339
|
+
// Virtual tables can't ALTER, so drop and recreate
|
|
340
|
+
if (currentVersion < 11) {
|
|
341
|
+
db.exec('DROP TABLE IF EXISTS notes_fts');
|
|
342
|
+
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
|
|
343
|
+
path, title, frontmatter, content,
|
|
344
|
+
tokenize='porter'
|
|
345
|
+
)`);
|
|
346
|
+
// Clear FTS metadata to force rebuild with new schema
|
|
347
|
+
db.exec(`DELETE FROM fts_metadata WHERE key = 'last_built'`);
|
|
348
|
+
}
|
|
349
|
+
// v12: tasks cache table (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
|
|
350
|
+
// v13: merge_dismissals table (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
|
|
351
|
+
// v14: Add steps column to index_events (pipeline observability)
|
|
352
|
+
if (currentVersion < 14) {
|
|
353
|
+
const hasSteps = db.prepare(`SELECT COUNT(*) as cnt FROM pragma_table_info('index_events') WHERE name = 'steps'`).get();
|
|
354
|
+
if (hasSteps.cnt === 0) {
|
|
355
|
+
db.exec('ALTER TABLE index_events ADD COLUMN steps TEXT');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
db.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(SCHEMA_VERSION);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Open or create the state database for a vault
|
|
363
|
+
*
|
|
364
|
+
* @param vaultPath - Absolute path to the vault root
|
|
365
|
+
* @returns StateDb instance with prepared statements
|
|
366
|
+
*/
|
|
367
|
+
export function openStateDb(vaultPath) {
|
|
368
|
+
const dbPath = getStateDbPath(vaultPath);
|
|
369
|
+
// Guard: Delete corrupted 0-byte database files
|
|
370
|
+
// This can happen when better-sqlite3 fails to compile (e.g., Node 24)
|
|
371
|
+
// and creates an empty file instead of a valid SQLite database
|
|
372
|
+
if (fs.existsSync(dbPath)) {
|
|
373
|
+
const stat = fs.statSync(dbPath);
|
|
374
|
+
if (stat.size === 0) {
|
|
375
|
+
console.error(`[vault-core] Deleting corrupted 0-byte state.db at ${dbPath}`);
|
|
376
|
+
fs.unlinkSync(dbPath);
|
|
377
|
+
// Also remove WAL and SHM files if they exist
|
|
378
|
+
const walPath = dbPath + '-wal';
|
|
379
|
+
const shmPath = dbPath + '-shm';
|
|
380
|
+
if (fs.existsSync(walPath))
|
|
381
|
+
fs.unlinkSync(walPath);
|
|
382
|
+
if (fs.existsSync(shmPath))
|
|
383
|
+
fs.unlinkSync(shmPath);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const db = new Database(dbPath);
|
|
387
|
+
// Initialize schema
|
|
388
|
+
initSchema(db);
|
|
389
|
+
// Prepare all statements
|
|
390
|
+
const stateDb = {
|
|
391
|
+
db,
|
|
392
|
+
vaultPath,
|
|
393
|
+
dbPath,
|
|
394
|
+
// Entity operations
|
|
395
|
+
insertEntity: db.prepare(`
|
|
396
|
+
INSERT INTO entities (name, name_lower, path, category, aliases_json, hub_score)
|
|
397
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
398
|
+
`),
|
|
399
|
+
updateEntity: db.prepare(`
|
|
400
|
+
UPDATE entities
|
|
401
|
+
SET name = ?, name_lower = ?, path = ?, category = ?, aliases_json = ?, hub_score = ?
|
|
402
|
+
WHERE id = ?
|
|
403
|
+
`),
|
|
404
|
+
deleteEntity: db.prepare('DELETE FROM entities WHERE id = ?'),
|
|
405
|
+
getEntityByName: db.prepare(`
|
|
406
|
+
SELECT id, name, name_lower, path, category, aliases_json, hub_score
|
|
407
|
+
FROM entities WHERE name_lower = ?
|
|
408
|
+
`),
|
|
409
|
+
getEntityById: db.prepare(`
|
|
410
|
+
SELECT id, name, name_lower, path, category, aliases_json, hub_score
|
|
411
|
+
FROM entities WHERE id = ?
|
|
412
|
+
`),
|
|
413
|
+
getAllEntities: db.prepare(`
|
|
414
|
+
SELECT id, name, name_lower, path, category, aliases_json, hub_score
|
|
415
|
+
FROM entities ORDER BY name
|
|
416
|
+
`),
|
|
417
|
+
getEntitiesByCategory: db.prepare(`
|
|
418
|
+
SELECT id, name, name_lower, path, category, aliases_json, hub_score
|
|
419
|
+
FROM entities WHERE category = ? ORDER BY name
|
|
420
|
+
`),
|
|
421
|
+
searchEntitiesFts: db.prepare(`
|
|
422
|
+
SELECT e.id, e.name, e.name_lower, e.path, e.category, e.aliases_json, e.hub_score,
|
|
423
|
+
bm25(entities_fts) as rank
|
|
424
|
+
FROM entities_fts
|
|
425
|
+
JOIN entities e ON e.id = entities_fts.rowid
|
|
426
|
+
WHERE entities_fts MATCH ?
|
|
427
|
+
ORDER BY rank
|
|
428
|
+
LIMIT ?
|
|
429
|
+
`),
|
|
430
|
+
clearEntities: db.prepare('DELETE FROM entities'),
|
|
431
|
+
// Entity alias lookup
|
|
432
|
+
getEntitiesByAlias: db.prepare(`
|
|
433
|
+
SELECT e.id, e.name, e.name_lower, e.path, e.category, e.aliases_json, e.hub_score
|
|
434
|
+
FROM entities e
|
|
435
|
+
WHERE EXISTS (SELECT 1 FROM json_each(e.aliases_json) WHERE LOWER(value) = ?)
|
|
436
|
+
`),
|
|
437
|
+
// Recency operations
|
|
438
|
+
upsertRecency: db.prepare(`
|
|
439
|
+
INSERT INTO recency (entity_name_lower, last_mentioned_at, mention_count)
|
|
440
|
+
VALUES (?, ?, 1)
|
|
441
|
+
ON CONFLICT(entity_name_lower) DO UPDATE SET
|
|
442
|
+
last_mentioned_at = excluded.last_mentioned_at,
|
|
443
|
+
mention_count = mention_count + 1
|
|
444
|
+
`),
|
|
445
|
+
getRecency: db.prepare(`
|
|
446
|
+
SELECT entity_name_lower, last_mentioned_at, mention_count
|
|
447
|
+
FROM recency WHERE entity_name_lower = ?
|
|
448
|
+
`),
|
|
449
|
+
getAllRecency: db.prepare(`
|
|
450
|
+
SELECT entity_name_lower, last_mentioned_at, mention_count
|
|
451
|
+
FROM recency ORDER BY last_mentioned_at DESC
|
|
452
|
+
`),
|
|
453
|
+
clearRecency: db.prepare('DELETE FROM recency'),
|
|
454
|
+
// Write state operations
|
|
455
|
+
setWriteState: db.prepare(`
|
|
456
|
+
INSERT INTO write_state (key, value, updated_at)
|
|
457
|
+
VALUES (?, ?, datetime('now'))
|
|
458
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
459
|
+
value = excluded.value,
|
|
460
|
+
updated_at = datetime('now')
|
|
461
|
+
`),
|
|
462
|
+
getWriteState: db.prepare('SELECT value FROM write_state WHERE key = ?'),
|
|
463
|
+
deleteWriteState: db.prepare('DELETE FROM write_state WHERE key = ?'),
|
|
464
|
+
// Flywheel config operations
|
|
465
|
+
setFlywheelConfigStmt: db.prepare(`
|
|
466
|
+
INSERT INTO flywheel_config (key, value, updated_at)
|
|
467
|
+
VALUES (?, ?, datetime('now'))
|
|
468
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
469
|
+
value = excluded.value,
|
|
470
|
+
updated_at = datetime('now')
|
|
471
|
+
`),
|
|
472
|
+
getFlywheelConfigStmt: db.prepare('SELECT value FROM flywheel_config WHERE key = ?'),
|
|
473
|
+
getAllFlywheelConfigStmt: db.prepare('SELECT key, value FROM flywheel_config'),
|
|
474
|
+
deleteFlywheelConfigStmt: db.prepare('DELETE FROM flywheel_config WHERE key = ?'),
|
|
475
|
+
// Task cache operations
|
|
476
|
+
insertTask: db.prepare(`
|
|
477
|
+
INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
|
|
478
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
479
|
+
`),
|
|
480
|
+
deleteTasksForPath: db.prepare('DELETE FROM tasks WHERE path = ?'),
|
|
481
|
+
clearAllTasks: db.prepare('DELETE FROM tasks'),
|
|
482
|
+
countTasksByStatus: db.prepare('SELECT status, COUNT(*) as cnt FROM tasks GROUP BY status'),
|
|
483
|
+
// Metadata operations
|
|
484
|
+
getMetadataValue: db.prepare('SELECT value FROM metadata WHERE key = ?'),
|
|
485
|
+
setMetadataValue: db.prepare(`
|
|
486
|
+
INSERT INTO metadata (key, value, updated_at)
|
|
487
|
+
VALUES (?, ?, datetime('now'))
|
|
488
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
489
|
+
value = excluded.value,
|
|
490
|
+
updated_at = datetime('now')
|
|
491
|
+
`),
|
|
492
|
+
// Transactions
|
|
493
|
+
bulkInsertEntities: db.transaction((entities, category) => {
|
|
494
|
+
let count = 0;
|
|
495
|
+
for (const entity of entities) {
|
|
496
|
+
stateDb.insertEntity.run(entity.name, entity.name.toLowerCase(), entity.path, category, JSON.stringify(entity.aliases), entity.hubScore ?? 0);
|
|
497
|
+
count++;
|
|
498
|
+
}
|
|
499
|
+
return count;
|
|
500
|
+
}),
|
|
501
|
+
replaceAllEntities: db.transaction((index) => {
|
|
502
|
+
// Clear existing entities
|
|
503
|
+
stateDb.clearEntities.run();
|
|
504
|
+
// Insert all entities by category
|
|
505
|
+
const categories = [
|
|
506
|
+
'technologies', 'acronyms', 'people', 'projects',
|
|
507
|
+
'organizations', 'locations', 'concepts', 'animals',
|
|
508
|
+
'media', 'events', 'documents', 'vehicles', 'health',
|
|
509
|
+
'finance', 'food', 'hobbies', 'other',
|
|
510
|
+
];
|
|
511
|
+
let total = 0;
|
|
512
|
+
for (const category of categories) {
|
|
513
|
+
const entities = index[category];
|
|
514
|
+
if (!entities?.length)
|
|
515
|
+
continue;
|
|
516
|
+
for (const entity of entities) {
|
|
517
|
+
// Handle both string and EntityWithAliases formats
|
|
518
|
+
const entityObj = typeof entity === 'string'
|
|
519
|
+
? { name: entity, path: '', aliases: [], hubScore: 0 }
|
|
520
|
+
: entity;
|
|
521
|
+
stateDb.insertEntity.run(entityObj.name, entityObj.name.toLowerCase(), entityObj.path, category, JSON.stringify(entityObj.aliases), entityObj.hubScore ?? 0);
|
|
522
|
+
total++;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// Update metadata
|
|
526
|
+
stateDb.setMetadataValue.run('entities_built_at', new Date().toISOString());
|
|
527
|
+
stateDb.setMetadataValue.run('entity_count', String(total));
|
|
528
|
+
return total;
|
|
529
|
+
}),
|
|
530
|
+
close: () => {
|
|
531
|
+
db.close();
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
return stateDb;
|
|
535
|
+
}
|
|
536
|
+
// =============================================================================
|
|
537
|
+
// Entity Operations
|
|
538
|
+
// =============================================================================
|
|
539
|
+
/**
|
|
540
|
+
* Search entities using FTS5 with porter stemming
|
|
541
|
+
*
|
|
542
|
+
* @param stateDb - State database instance
|
|
543
|
+
* @param query - Search query (supports FTS5 syntax)
|
|
544
|
+
* @param limit - Maximum results to return
|
|
545
|
+
* @returns Array of matching entities with relevance scores
|
|
546
|
+
*/
|
|
547
|
+
export function searchEntities(stateDb, query, limit = 20) {
|
|
548
|
+
const escapedQuery = escapeFts5Query(query);
|
|
549
|
+
// Handle empty query - return empty results
|
|
550
|
+
if (!escapedQuery) {
|
|
551
|
+
return [];
|
|
552
|
+
}
|
|
553
|
+
const rows = stateDb.searchEntitiesFts.all(escapedQuery, limit);
|
|
554
|
+
return rows.map(row => ({
|
|
555
|
+
id: row.id,
|
|
556
|
+
name: row.name,
|
|
557
|
+
nameLower: row.name_lower,
|
|
558
|
+
path: row.path,
|
|
559
|
+
category: row.category,
|
|
560
|
+
aliases: row.aliases_json ? JSON.parse(row.aliases_json) : [],
|
|
561
|
+
hubScore: row.hub_score,
|
|
562
|
+
rank: row.rank,
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Search entities by prefix for autocomplete
|
|
567
|
+
*
|
|
568
|
+
* @param stateDb - State database instance
|
|
569
|
+
* @param prefix - Prefix to search for
|
|
570
|
+
* @param limit - Maximum results to return
|
|
571
|
+
*/
|
|
572
|
+
export function searchEntitiesPrefix(stateDb, prefix, limit = 20) {
|
|
573
|
+
return searchEntities(stateDb, `${escapeFts5Query(prefix)}*`, limit);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Get entity by exact name (case-insensitive)
|
|
577
|
+
*/
|
|
578
|
+
export function getEntityByName(stateDb, name) {
|
|
579
|
+
const row = stateDb.getEntityByName.get(name.toLowerCase());
|
|
580
|
+
if (!row)
|
|
581
|
+
return null;
|
|
582
|
+
return {
|
|
583
|
+
id: row.id,
|
|
584
|
+
name: row.name,
|
|
585
|
+
nameLower: row.name_lower,
|
|
586
|
+
path: row.path,
|
|
587
|
+
category: row.category,
|
|
588
|
+
aliases: row.aliases_json ? JSON.parse(row.aliases_json) : [],
|
|
589
|
+
hubScore: row.hub_score,
|
|
590
|
+
rank: 0,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Get all entities from the database
|
|
595
|
+
*/
|
|
596
|
+
export function getAllEntitiesFromDb(stateDb) {
|
|
597
|
+
const rows = stateDb.getAllEntities.all();
|
|
598
|
+
return rows.map(row => ({
|
|
599
|
+
id: row.id,
|
|
600
|
+
name: row.name,
|
|
601
|
+
nameLower: row.name_lower,
|
|
602
|
+
path: row.path,
|
|
603
|
+
category: row.category,
|
|
604
|
+
aliases: row.aliases_json ? JSON.parse(row.aliases_json) : [],
|
|
605
|
+
hubScore: row.hub_score,
|
|
606
|
+
rank: 0,
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Convert database entities back to EntityIndex format
|
|
611
|
+
*/
|
|
612
|
+
export function getEntityIndexFromDb(stateDb) {
|
|
613
|
+
const entities = getAllEntitiesFromDb(stateDb);
|
|
614
|
+
const index = {
|
|
615
|
+
technologies: [],
|
|
616
|
+
acronyms: [],
|
|
617
|
+
people: [],
|
|
618
|
+
projects: [],
|
|
619
|
+
organizations: [],
|
|
620
|
+
locations: [],
|
|
621
|
+
concepts: [],
|
|
622
|
+
animals: [],
|
|
623
|
+
media: [],
|
|
624
|
+
events: [],
|
|
625
|
+
documents: [],
|
|
626
|
+
vehicles: [],
|
|
627
|
+
health: [],
|
|
628
|
+
finance: [],
|
|
629
|
+
food: [],
|
|
630
|
+
hobbies: [],
|
|
631
|
+
other: [],
|
|
632
|
+
_metadata: {
|
|
633
|
+
total_entities: entities.length,
|
|
634
|
+
generated_at: new Date().toISOString(),
|
|
635
|
+
vault_path: stateDb.vaultPath,
|
|
636
|
+
source: 'vault-core sqlite',
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
for (const entity of entities) {
|
|
640
|
+
const entityObj = {
|
|
641
|
+
name: entity.name,
|
|
642
|
+
path: entity.path,
|
|
643
|
+
aliases: entity.aliases,
|
|
644
|
+
hubScore: entity.hubScore,
|
|
645
|
+
};
|
|
646
|
+
index[entity.category].push(entityObj);
|
|
647
|
+
}
|
|
648
|
+
return index;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Get entities that have a given alias (case-insensitive)
|
|
652
|
+
*
|
|
653
|
+
* @param stateDb - State database instance
|
|
654
|
+
* @param alias - Alias to search for (case-insensitive)
|
|
655
|
+
* @returns Array of matching entities
|
|
656
|
+
*/
|
|
657
|
+
export function getEntitiesByAlias(stateDb, alias) {
|
|
658
|
+
const rows = stateDb.getEntitiesByAlias.all(alias.toLowerCase());
|
|
659
|
+
return rows.map(row => ({
|
|
660
|
+
id: row.id,
|
|
661
|
+
name: row.name,
|
|
662
|
+
nameLower: row.name_lower,
|
|
663
|
+
path: row.path,
|
|
664
|
+
category: row.category,
|
|
665
|
+
aliases: row.aliases_json ? JSON.parse(row.aliases_json) : [],
|
|
666
|
+
hubScore: row.hub_score,
|
|
667
|
+
rank: 0,
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
// =============================================================================
|
|
671
|
+
// Recency Operations
|
|
672
|
+
// =============================================================================
|
|
673
|
+
/**
|
|
674
|
+
* Record a mention of an entity
|
|
675
|
+
*/
|
|
676
|
+
export function recordEntityMention(stateDb, entityName, mentionedAt = new Date()) {
|
|
677
|
+
stateDb.upsertRecency.run(entityName.toLowerCase(), mentionedAt.getTime());
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Get recency info for an entity
|
|
681
|
+
*/
|
|
682
|
+
export function getEntityRecency(stateDb, entityName) {
|
|
683
|
+
const row = stateDb.getRecency.get(entityName.toLowerCase());
|
|
684
|
+
if (!row)
|
|
685
|
+
return null;
|
|
686
|
+
return {
|
|
687
|
+
entityNameLower: row.entity_name_lower,
|
|
688
|
+
lastMentionedAt: row.last_mentioned_at,
|
|
689
|
+
mentionCount: row.mention_count,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Get all recency data ordered by most recent
|
|
694
|
+
*/
|
|
695
|
+
export function getAllRecency(stateDb) {
|
|
696
|
+
const rows = stateDb.getAllRecency.all();
|
|
697
|
+
return rows.map(row => ({
|
|
698
|
+
entityNameLower: row.entity_name_lower,
|
|
699
|
+
lastMentionedAt: row.last_mentioned_at,
|
|
700
|
+
mentionCount: row.mention_count,
|
|
701
|
+
}));
|
|
702
|
+
}
|
|
703
|
+
// =============================================================================
|
|
704
|
+
// Write State Operations
|
|
705
|
+
// =============================================================================
|
|
706
|
+
/**
|
|
707
|
+
* Set a write state value
|
|
708
|
+
*/
|
|
709
|
+
export function setWriteState(stateDb, key, value) {
|
|
710
|
+
stateDb.setWriteState.run(key, JSON.stringify(value));
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Get a write state value
|
|
714
|
+
*/
|
|
715
|
+
export function getWriteState(stateDb, key) {
|
|
716
|
+
const row = stateDb.getWriteState.get(key);
|
|
717
|
+
if (!row)
|
|
718
|
+
return null;
|
|
719
|
+
return JSON.parse(row.value);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Delete a write state key
|
|
723
|
+
*/
|
|
724
|
+
export function deleteWriteState(stateDb, key) {
|
|
725
|
+
stateDb.deleteWriteState.run(key);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Set a flywheel config value
|
|
729
|
+
*/
|
|
730
|
+
export function setFlywheelConfig(stateDb, key, value) {
|
|
731
|
+
stateDb.setFlywheelConfigStmt.run(key, JSON.stringify(value));
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Get a flywheel config value
|
|
735
|
+
*/
|
|
736
|
+
export function getFlywheelConfig(stateDb, key) {
|
|
737
|
+
const row = stateDb.getFlywheelConfigStmt.get(key);
|
|
738
|
+
if (!row)
|
|
739
|
+
return null;
|
|
740
|
+
return JSON.parse(row.value);
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Get all flywheel config values as an object
|
|
744
|
+
*/
|
|
745
|
+
export function getAllFlywheelConfig(stateDb) {
|
|
746
|
+
const rows = stateDb.getAllFlywheelConfigStmt.all();
|
|
747
|
+
const config = {};
|
|
748
|
+
for (const row of rows) {
|
|
749
|
+
try {
|
|
750
|
+
config[row.key] = JSON.parse(row.value);
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
config[row.key] = row.value;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return config;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Delete a flywheel config key
|
|
760
|
+
*/
|
|
761
|
+
export function deleteFlywheelConfig(stateDb, key) {
|
|
762
|
+
stateDb.deleteFlywheelConfigStmt.run(key);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Save entire Flywheel config object to database
|
|
766
|
+
* Stores each top-level key as a separate row
|
|
767
|
+
*/
|
|
768
|
+
export function saveFlywheelConfigToDb(stateDb, config) {
|
|
769
|
+
const transaction = stateDb.db.transaction(() => {
|
|
770
|
+
for (const [key, value] of Object.entries(config)) {
|
|
771
|
+
if (value !== undefined) {
|
|
772
|
+
setFlywheelConfig(stateDb, key, value);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
transaction();
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Load Flywheel config from database and reconstruct as typed object
|
|
780
|
+
*/
|
|
781
|
+
export function loadFlywheelConfigFromDb(stateDb) {
|
|
782
|
+
const config = getAllFlywheelConfig(stateDb);
|
|
783
|
+
if (Object.keys(config).length === 0) {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
return config;
|
|
787
|
+
}
|
|
788
|
+
// =============================================================================
|
|
789
|
+
// Merge Dismissal Operations
|
|
790
|
+
// =============================================================================
|
|
791
|
+
/**
|
|
792
|
+
* Record a merge dismissal so the pair never reappears in suggestions.
|
|
793
|
+
*/
|
|
794
|
+
export function recordMergeDismissal(db, sourcePath, targetPath, sourceName, targetName, reason) {
|
|
795
|
+
const pairKey = [sourcePath, targetPath].sort().join('::');
|
|
796
|
+
db.db.prepare(`INSERT OR IGNORE INTO merge_dismissals
|
|
797
|
+
(pair_key, source_path, target_path, source_name, target_name, reason)
|
|
798
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
799
|
+
.run(pairKey, sourcePath, targetPath, sourceName, targetName, reason);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Get all dismissed merge pair keys for filtering.
|
|
803
|
+
*/
|
|
804
|
+
export function getDismissedMergePairs(db) {
|
|
805
|
+
const rows = db.db.prepare('SELECT pair_key FROM merge_dismissals').all();
|
|
806
|
+
return new Set(rows.map(r => r.pair_key));
|
|
807
|
+
}
|
|
808
|
+
// =============================================================================
|
|
809
|
+
// Metadata Operations
|
|
810
|
+
// =============================================================================
|
|
811
|
+
/**
|
|
812
|
+
* Get database metadata
|
|
813
|
+
*/
|
|
814
|
+
export function getStateDbMetadata(stateDb) {
|
|
815
|
+
const schemaRow = stateDb.db.prepare('SELECT MAX(version) as version FROM schema_version').get();
|
|
816
|
+
const entitiesBuiltRow = stateDb.getMetadataValue.get('entities_built_at');
|
|
817
|
+
const entityCountRow = stateDb.getMetadataValue.get('entity_count');
|
|
818
|
+
const notesBuiltRow = stateDb.getMetadataValue.get('notes_built_at');
|
|
819
|
+
const noteCountRow = stateDb.getMetadataValue.get('note_count');
|
|
820
|
+
return {
|
|
821
|
+
schemaVersion: schemaRow?.version ?? 0,
|
|
822
|
+
entitiesBuiltAt: entitiesBuiltRow?.value ?? null,
|
|
823
|
+
entityCount: entityCountRow ? parseInt(entityCountRow.value, 10) : 0,
|
|
824
|
+
notesBuiltAt: notesBuiltRow?.value ?? null,
|
|
825
|
+
noteCount: noteCountRow ? parseInt(noteCountRow.value, 10) : 0,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Check if entity data is stale (older than threshold)
|
|
830
|
+
*/
|
|
831
|
+
export function isEntityDataStale(stateDb, thresholdMs = 60 * 60 * 1000 // 1 hour default
|
|
832
|
+
) {
|
|
833
|
+
const metadata = getStateDbMetadata(stateDb);
|
|
834
|
+
if (!metadata.entitiesBuiltAt) {
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
const builtAt = new Date(metadata.entitiesBuiltAt).getTime();
|
|
838
|
+
const age = Date.now() - builtAt;
|
|
839
|
+
return age > thresholdMs;
|
|
840
|
+
}
|
|
841
|
+
// =============================================================================
|
|
842
|
+
// Utility Functions
|
|
843
|
+
// =============================================================================
|
|
844
|
+
/**
|
|
845
|
+
* Escape special FTS5 characters in a query
|
|
846
|
+
*/
|
|
847
|
+
export function escapeFts5Query(query) {
|
|
848
|
+
// Handle empty query
|
|
849
|
+
if (!query || !query.trim()) {
|
|
850
|
+
return '';
|
|
851
|
+
}
|
|
852
|
+
// Remove or escape FTS5 special characters
|
|
853
|
+
// Keep * for prefix matching, escape others
|
|
854
|
+
return query
|
|
855
|
+
.replace(/"/g, '""') // Escape quotes
|
|
856
|
+
.replace(/[(){}[\]^~:-]/g, ' ') // Remove special operators including hyphen
|
|
857
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
858
|
+
.trim();
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Check if the state database exists for a vault
|
|
862
|
+
*/
|
|
863
|
+
export function stateDbExists(vaultPath) {
|
|
864
|
+
const dbPath = getStateDbPath(vaultPath);
|
|
865
|
+
return fs.existsSync(dbPath);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Delete the state database (for testing or reset)
|
|
869
|
+
*/
|
|
870
|
+
export function deleteStateDb(vaultPath) {
|
|
871
|
+
const dbPath = getStateDbPath(vaultPath);
|
|
872
|
+
if (fs.existsSync(dbPath)) {
|
|
873
|
+
fs.unlinkSync(dbPath);
|
|
874
|
+
}
|
|
875
|
+
// Also remove WAL and SHM files if they exist
|
|
876
|
+
const walPath = dbPath + '-wal';
|
|
877
|
+
const shmPath = dbPath + '-shm';
|
|
878
|
+
if (fs.existsSync(walPath))
|
|
879
|
+
fs.unlinkSync(walPath);
|
|
880
|
+
if (fs.existsSync(shmPath))
|
|
881
|
+
fs.unlinkSync(shmPath);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Save VaultIndex to cache
|
|
885
|
+
*
|
|
886
|
+
* @param stateDb - State database instance
|
|
887
|
+
* @param indexData - Serialized VaultIndex data
|
|
888
|
+
*/
|
|
889
|
+
export function saveVaultIndexCache(stateDb, indexData) {
|
|
890
|
+
const data = JSON.stringify(indexData);
|
|
891
|
+
const stmt = stateDb.db.prepare(`
|
|
892
|
+
INSERT OR REPLACE INTO vault_index_cache (id, data, built_at, note_count, version)
|
|
893
|
+
VALUES (1, ?, ?, ?, 1)
|
|
894
|
+
`);
|
|
895
|
+
stmt.run(data, indexData.builtAt, indexData.notes.length);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Load VaultIndex from cache
|
|
899
|
+
*
|
|
900
|
+
* @param stateDb - State database instance
|
|
901
|
+
* @returns Cached VaultIndex data or null if not found
|
|
902
|
+
*/
|
|
903
|
+
export function loadVaultIndexCache(stateDb) {
|
|
904
|
+
const stmt = stateDb.db.prepare(`
|
|
905
|
+
SELECT data, built_at, note_count FROM vault_index_cache WHERE id = 1
|
|
906
|
+
`);
|
|
907
|
+
const row = stmt.get();
|
|
908
|
+
if (!row)
|
|
909
|
+
return null;
|
|
910
|
+
try {
|
|
911
|
+
return JSON.parse(row.data);
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Get cache metadata without loading full data
|
|
919
|
+
*/
|
|
920
|
+
export function getVaultIndexCacheInfo(stateDb) {
|
|
921
|
+
const stmt = stateDb.db.prepare(`
|
|
922
|
+
SELECT built_at, note_count, version FROM vault_index_cache WHERE id = 1
|
|
923
|
+
`);
|
|
924
|
+
const row = stmt.get();
|
|
925
|
+
if (!row)
|
|
926
|
+
return null;
|
|
927
|
+
return {
|
|
928
|
+
builtAt: new Date(row.built_at),
|
|
929
|
+
noteCount: row.note_count,
|
|
930
|
+
version: row.version,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Clear the vault index cache
|
|
935
|
+
*/
|
|
936
|
+
export function clearVaultIndexCache(stateDb) {
|
|
937
|
+
stateDb.db.prepare('DELETE FROM vault_index_cache').run();
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Check if cache is valid (not too old and note count matches)
|
|
941
|
+
*
|
|
942
|
+
* @param stateDb - State database instance
|
|
943
|
+
* @param actualNoteCount - Current number of notes in vault
|
|
944
|
+
* @param maxAgeMs - Maximum cache age in milliseconds (default 24 hours)
|
|
945
|
+
*/
|
|
946
|
+
export function isVaultIndexCacheValid(stateDb, actualNoteCount, maxAgeMs = 24 * 60 * 60 * 1000) {
|
|
947
|
+
const info = getVaultIndexCacheInfo(stateDb);
|
|
948
|
+
if (!info)
|
|
949
|
+
return false;
|
|
950
|
+
// Check note count matches (quick validation)
|
|
951
|
+
if (info.noteCount !== actualNoteCount)
|
|
952
|
+
return false;
|
|
953
|
+
// Check age
|
|
954
|
+
const age = Date.now() - info.builtAt.getTime();
|
|
955
|
+
if (age > maxAgeMs)
|
|
956
|
+
return false;
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
//# sourceMappingURL=sqlite.js.map
|