@velvetmonkey/vault-core 2.0.136 → 2.0.138

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/sqlite.js CHANGED
@@ -11,639 +11,17 @@
11
11
  */
12
12
  import Database from 'better-sqlite3';
13
13
  import * as fs from 'fs';
14
- import * as path from 'path';
14
+ // Re-export constants from schema
15
+ export { SCHEMA_VERSION, STATE_DB_FILENAME, FLYWHEEL_DIR, SCHEMA_SQL } from './schema.js';
16
+ // Re-export migrations
17
+ export { getStateDbPath, initSchema, deleteStateDbFiles, backupStateDb, preserveCorruptedDb } from './migrations.js';
18
+ // Re-export all query functions
19
+ export { searchEntities, searchEntitiesPrefix, getEntityByName, getAllEntitiesFromDb, getEntityIndexFromDb, getEntitiesByAlias, recordEntityMention, getEntityRecency, getAllRecency, setWriteState, getWriteState, deleteWriteState, setFlywheelConfig, getFlywheelConfig, getAllFlywheelConfig, deleteFlywheelConfig, saveFlywheelConfigToDb, loadFlywheelConfigFromDb, recordMergeDismissal, getDismissedMergePairs, getStateDbMetadata, isEntityDataStale, escapeFts5Query, rebuildEntitiesFts, stateDbExists, deleteStateDb, saveVaultIndexCache, loadVaultIndexCache, getVaultIndexCacheInfo, clearVaultIndexCache, isVaultIndexCacheValid, loadContentHashes, saveContentHashBatch, renameContentHash, } from './queries.js';
20
+ // Import for use in openStateDb
21
+ import { getStateDbPath, initSchema, deleteStateDbFiles, backupStateDb, preserveCorruptedDb } from './migrations.js';
15
22
  // =============================================================================
16
- // Constants
23
+ // Factory
17
24
  // =============================================================================
18
- /** Current schema version - bump when schema changes */
19
- export const SCHEMA_VERSION = 31;
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
- description TEXT
51
- );
52
- CREATE INDEX IF NOT EXISTS idx_entities_name_lower ON entities(name_lower);
53
- CREATE INDEX IF NOT EXISTS idx_entities_category ON entities(category);
54
-
55
- -- FTS5 for entity search with porter stemmer
56
- CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
57
- name, aliases, category,
58
- content='entities', content_rowid='id',
59
- tokenize='porter unicode61'
60
- );
61
-
62
- -- Auto-sync triggers for entities_fts
63
- CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
64
- INSERT INTO entities_fts(rowid, name, aliases, category)
65
- VALUES (
66
- new.id,
67
- new.name,
68
- COALESCE((SELECT group_concat(value, ' ') FROM json_each(new.aliases_json)), ''),
69
- new.category
70
- );
71
- END;
72
-
73
- CREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN
74
- INSERT INTO entities_fts(entities_fts, rowid, name, aliases, category)
75
- VALUES (
76
- 'delete',
77
- old.id,
78
- old.name,
79
- COALESCE((SELECT group_concat(value, ' ') FROM json_each(old.aliases_json)), ''),
80
- old.category
81
- );
82
- END;
83
-
84
- CREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN
85
- INSERT INTO entities_fts(entities_fts, rowid, name, aliases, category)
86
- VALUES (
87
- 'delete',
88
- old.id,
89
- old.name,
90
- COALESCE((SELECT group_concat(value, ' ') FROM json_each(old.aliases_json)), ''),
91
- old.category
92
- );
93
- INSERT INTO entities_fts(rowid, name, aliases, category)
94
- VALUES (
95
- new.id,
96
- new.name,
97
- COALESCE((SELECT group_concat(value, ' ') FROM json_each(new.aliases_json)), ''),
98
- new.category
99
- );
100
- END;
101
-
102
- -- Recency tracking (replaces entity-recency.json)
103
- CREATE TABLE IF NOT EXISTS recency (
104
- entity_name_lower TEXT PRIMARY KEY,
105
- last_mentioned_at INTEGER NOT NULL,
106
- mention_count INTEGER DEFAULT 1
107
- );
108
-
109
- -- Write state (replaces last-commit.json and other write state)
110
- CREATE TABLE IF NOT EXISTS write_state (
111
- key TEXT PRIMARY KEY,
112
- value TEXT NOT NULL,
113
- updated_at TEXT DEFAULT (datetime('now'))
114
- );
115
-
116
- -- Content search FTS5 (migrated from vault-search.db)
117
- -- v11: Added frontmatter column for weighted search (path, title, frontmatter, content)
118
- CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
119
- path, title, frontmatter, content,
120
- tokenize='porter'
121
- );
122
-
123
- -- FTS5 build metadata (consolidated from vault-search.db)
124
- CREATE TABLE IF NOT EXISTS fts_metadata (
125
- key TEXT PRIMARY KEY,
126
- value TEXT
127
- );
128
-
129
- -- Vault index cache (for fast startup)
130
- -- Stores serialized VaultIndex to avoid full rebuild on startup
131
- CREATE TABLE IF NOT EXISTS vault_index_cache (
132
- id INTEGER PRIMARY KEY CHECK (id = 1),
133
- data BLOB NOT NULL,
134
- built_at INTEGER NOT NULL,
135
- note_count INTEGER NOT NULL,
136
- version INTEGER DEFAULT 1
137
- );
138
-
139
- -- Flywheel configuration (replaces .flywheel.json)
140
- CREATE TABLE IF NOT EXISTS flywheel_config (
141
- key TEXT PRIMARY KEY,
142
- value TEXT NOT NULL,
143
- updated_at TEXT DEFAULT (datetime('now'))
144
- );
145
-
146
- -- Vault metrics (v4: growth tracking)
147
- CREATE TABLE IF NOT EXISTS vault_metrics (
148
- id INTEGER PRIMARY KEY AUTOINCREMENT,
149
- timestamp INTEGER NOT NULL,
150
- metric TEXT NOT NULL,
151
- value REAL NOT NULL
152
- );
153
- CREATE INDEX IF NOT EXISTS idx_vault_metrics_ts ON vault_metrics(timestamp);
154
- CREATE INDEX IF NOT EXISTS idx_vault_metrics_m ON vault_metrics(metric, timestamp);
155
-
156
- -- Wikilink feedback (v4: quality tracking)
157
- CREATE TABLE IF NOT EXISTS wikilink_feedback (
158
- id INTEGER PRIMARY KEY AUTOINCREMENT,
159
- entity TEXT NOT NULL,
160
- context TEXT NOT NULL,
161
- note_path TEXT NOT NULL,
162
- correct INTEGER NOT NULL,
163
- confidence REAL NOT NULL DEFAULT 1.0,
164
- created_at TEXT DEFAULT (datetime('now'))
165
- );
166
- CREATE INDEX IF NOT EXISTS idx_wl_feedback_entity ON wikilink_feedback(entity);
167
- CREATE INDEX IF NOT EXISTS idx_wl_feedback_note_path ON wikilink_feedback(note_path);
168
-
169
- -- Wikilink suppressions (v4: auto-suppress false positives)
170
- CREATE TABLE IF NOT EXISTS wikilink_suppressions (
171
- entity TEXT PRIMARY KEY,
172
- false_positive_rate REAL NOT NULL,
173
- updated_at TEXT DEFAULT (datetime('now'))
174
- );
175
-
176
- -- Wikilink applications tracking (v5: implicit feedback)
177
- CREATE TABLE IF NOT EXISTS wikilink_applications (
178
- id INTEGER PRIMARY KEY AUTOINCREMENT,
179
- entity TEXT NOT NULL,
180
- note_path TEXT NOT NULL,
181
- applied_at TEXT DEFAULT (datetime('now')),
182
- status TEXT DEFAULT 'applied'
183
- );
184
- CREATE UNIQUE INDEX IF NOT EXISTS idx_wl_apps_unique ON wikilink_applications(entity COLLATE NOCASE, note_path);
185
-
186
- -- Index events tracking (v6: index activity history)
187
- CREATE TABLE IF NOT EXISTS index_events (
188
- id INTEGER PRIMARY KEY AUTOINCREMENT,
189
- timestamp INTEGER NOT NULL,
190
- trigger TEXT NOT NULL,
191
- duration_ms INTEGER NOT NULL,
192
- success INTEGER NOT NULL DEFAULT 1,
193
- note_count INTEGER,
194
- files_changed INTEGER,
195
- changed_paths TEXT,
196
- error TEXT,
197
- steps TEXT
198
- );
199
- CREATE INDEX IF NOT EXISTS idx_index_events_ts ON index_events(timestamp);
200
-
201
- -- Tool invocation tracking (v7: usage analytics)
202
- CREATE TABLE IF NOT EXISTS tool_invocations (
203
- id INTEGER PRIMARY KEY AUTOINCREMENT,
204
- timestamp INTEGER NOT NULL,
205
- tool_name TEXT NOT NULL,
206
- session_id TEXT,
207
- note_paths TEXT,
208
- duration_ms INTEGER,
209
- success INTEGER NOT NULL DEFAULT 1,
210
- response_tokens INTEGER,
211
- baseline_tokens INTEGER
212
- );
213
- CREATE INDEX IF NOT EXISTS idx_tool_inv_ts ON tool_invocations(timestamp);
214
- CREATE INDEX IF NOT EXISTS idx_tool_inv_tool ON tool_invocations(tool_name, timestamp);
215
- CREATE INDEX IF NOT EXISTS idx_tool_inv_session ON tool_invocations(session_id, timestamp);
216
-
217
- -- Graph topology snapshots (v8: structural evolution)
218
- CREATE TABLE IF NOT EXISTS graph_snapshots (
219
- id INTEGER PRIMARY KEY AUTOINCREMENT,
220
- timestamp INTEGER NOT NULL,
221
- metric TEXT NOT NULL,
222
- value REAL NOT NULL,
223
- details TEXT
224
- );
225
- CREATE INDEX IF NOT EXISTS idx_graph_snap_ts ON graph_snapshots(timestamp);
226
- CREATE INDEX IF NOT EXISTS idx_graph_snap_m ON graph_snapshots(metric, timestamp);
227
-
228
- -- Note embeddings for semantic search (v9)
229
- CREATE TABLE IF NOT EXISTS note_embeddings (
230
- path TEXT PRIMARY KEY,
231
- embedding BLOB NOT NULL,
232
- content_hash TEXT NOT NULL,
233
- model TEXT NOT NULL,
234
- updated_at INTEGER NOT NULL
235
- );
236
-
237
- -- Entity embeddings for semantic entity search (v10)
238
- CREATE TABLE IF NOT EXISTS entity_embeddings (
239
- entity_name TEXT PRIMARY KEY,
240
- embedding BLOB NOT NULL,
241
- source_hash TEXT NOT NULL,
242
- model TEXT NOT NULL,
243
- updated_at INTEGER NOT NULL
244
- );
245
-
246
- -- Task cache for fast task queries (v12)
247
- CREATE TABLE IF NOT EXISTS tasks (
248
- id INTEGER PRIMARY KEY AUTOINCREMENT,
249
- path TEXT NOT NULL,
250
- line INTEGER NOT NULL,
251
- text TEXT NOT NULL,
252
- status TEXT NOT NULL,
253
- raw TEXT NOT NULL,
254
- context TEXT,
255
- tags_json TEXT,
256
- due_date TEXT,
257
- UNIQUE(path, line)
258
- );
259
- CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
260
- CREATE INDEX IF NOT EXISTS idx_tasks_path ON tasks(path);
261
- CREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due_date);
262
-
263
- -- Merge dismissals (v13: persistent merge pair suppression)
264
- CREATE TABLE IF NOT EXISTS merge_dismissals (
265
- pair_key TEXT PRIMARY KEY,
266
- source_path TEXT NOT NULL,
267
- target_path TEXT NOT NULL,
268
- source_name TEXT NOT NULL,
269
- target_name TEXT NOT NULL,
270
- reason TEXT NOT NULL,
271
- dismissed_at TEXT DEFAULT (datetime('now'))
272
- );
273
-
274
- -- Suggestion events audit log (v15: pipeline observability)
275
- CREATE TABLE IF NOT EXISTS suggestion_events (
276
- id INTEGER PRIMARY KEY AUTOINCREMENT,
277
- timestamp INTEGER NOT NULL,
278
- note_path TEXT NOT NULL,
279
- entity TEXT NOT NULL,
280
- total_score REAL NOT NULL,
281
- breakdown_json TEXT NOT NULL,
282
- threshold REAL NOT NULL,
283
- passed INTEGER NOT NULL,
284
- strictness TEXT NOT NULL,
285
- applied INTEGER DEFAULT 0,
286
- pipeline_event_id INTEGER,
287
- UNIQUE(timestamp, note_path, entity)
288
- );
289
- CREATE INDEX IF NOT EXISTS idx_suggestion_entity ON suggestion_events(entity);
290
- CREATE INDEX IF NOT EXISTS idx_suggestion_note ON suggestion_events(note_path);
291
-
292
- -- Forward-link persistence for diff-based feedback (v16), edge weights (v22)
293
- CREATE TABLE IF NOT EXISTS note_links (
294
- note_path TEXT NOT NULL,
295
- target TEXT NOT NULL,
296
- weight REAL NOT NULL DEFAULT 1.0,
297
- weight_updated_at INTEGER,
298
- PRIMARY KEY (note_path, target)
299
- );
300
-
301
- -- Entity field change audit log (v17)
302
- CREATE TABLE IF NOT EXISTS entity_changes (
303
- entity TEXT NOT NULL,
304
- field TEXT NOT NULL,
305
- old_value TEXT,
306
- new_value TEXT,
307
- changed_at TEXT NOT NULL DEFAULT (datetime('now')),
308
- PRIMARY KEY (entity, field, changed_at)
309
- );
310
-
311
- -- Note tag persistence for diff-based feedback (v18)
312
- CREATE TABLE IF NOT EXISTS note_tags (
313
- note_path TEXT NOT NULL,
314
- tag TEXT NOT NULL,
315
- PRIMARY KEY (note_path, tag)
316
- );
317
-
318
- -- Wikilink survival tracking for positive feedback signals (v19)
319
- CREATE TABLE IF NOT EXISTS note_link_history (
320
- note_path TEXT NOT NULL,
321
- target TEXT NOT NULL,
322
- first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
323
- edits_survived INTEGER NOT NULL DEFAULT 0,
324
- last_positive_at TEXT,
325
- PRIMARY KEY (note_path, target)
326
- );
327
-
328
- -- Note move history (v20): records when files are moved/renamed to a different folder
329
- CREATE TABLE IF NOT EXISTS note_moves (
330
- id INTEGER PRIMARY KEY AUTOINCREMENT,
331
- old_path TEXT NOT NULL,
332
- new_path TEXT NOT NULL,
333
- moved_at TEXT NOT NULL DEFAULT (datetime('now')),
334
- old_folder TEXT,
335
- new_folder TEXT
336
- );
337
- CREATE INDEX IF NOT EXISTS idx_note_moves_old_path ON note_moves(old_path);
338
- CREATE INDEX IF NOT EXISTS idx_note_moves_new_path ON note_moves(new_path);
339
- CREATE INDEX IF NOT EXISTS idx_note_moves_moved_at ON note_moves(moved_at);
340
-
341
- -- Corrections (v24): persistent correction records from user/engine feedback
342
- CREATE TABLE IF NOT EXISTS corrections (
343
- id INTEGER PRIMARY KEY AUTOINCREMENT,
344
- entity TEXT,
345
- note_path TEXT,
346
- correction_type TEXT NOT NULL,
347
- description TEXT NOT NULL,
348
- source TEXT NOT NULL DEFAULT 'user',
349
- status TEXT DEFAULT 'pending',
350
- created_at TEXT DEFAULT (datetime('now')),
351
- resolved_at TEXT
352
- );
353
- CREATE INDEX IF NOT EXISTS idx_corrections_status ON corrections(status);
354
- CREATE INDEX IF NOT EXISTS idx_corrections_entity ON corrections(entity);
355
-
356
- -- Memories (v26): lightweight key-value working memory for agents
357
- CREATE TABLE IF NOT EXISTS memories (
358
- id INTEGER PRIMARY KEY AUTOINCREMENT,
359
- key TEXT NOT NULL,
360
- value TEXT NOT NULL,
361
- memory_type TEXT NOT NULL,
362
- entity TEXT,
363
- entities_json TEXT,
364
- source_agent_id TEXT,
365
- source_session_id TEXT,
366
- confidence REAL NOT NULL DEFAULT 1.0,
367
- created_at INTEGER NOT NULL,
368
- updated_at INTEGER NOT NULL,
369
- accessed_at INTEGER NOT NULL,
370
- ttl_days INTEGER,
371
- superseded_by INTEGER REFERENCES memories(id),
372
- visibility TEXT NOT NULL DEFAULT 'shared'
373
- );
374
- CREATE INDEX IF NOT EXISTS idx_memories_key ON memories(key);
375
- CREATE INDEX IF NOT EXISTS idx_memories_entity ON memories(entity);
376
- CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type);
377
-
378
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
379
- key, value,
380
- content=memories, content_rowid=id,
381
- tokenize='porter unicode61'
382
- );
383
-
384
- -- Auto-sync triggers for memories_fts
385
- CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
386
- INSERT INTO memories_fts(rowid, key, value)
387
- VALUES (new.id, new.key, new.value);
388
- END;
389
-
390
- CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
391
- INSERT INTO memories_fts(memories_fts, rowid, key, value)
392
- VALUES ('delete', old.id, old.key, old.value);
393
- END;
394
-
395
- CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
396
- INSERT INTO memories_fts(memories_fts, rowid, key, value)
397
- VALUES ('delete', old.id, old.key, old.value);
398
- INSERT INTO memories_fts(rowid, key, value)
399
- VALUES (new.id, new.key, new.value);
400
- END;
401
-
402
- -- Co-occurrence cache (v27): persist co-occurrence index to avoid full vault scan on restart
403
- CREATE TABLE IF NOT EXISTS cooccurrence_cache (
404
- id INTEGER PRIMARY KEY CHECK (id = 1),
405
- data TEXT NOT NULL,
406
- built_at INTEGER NOT NULL,
407
- entity_count INTEGER NOT NULL,
408
- association_count INTEGER NOT NULL
409
- );
410
-
411
- -- Content hashes (v28): persist watcher content hashes across restarts
412
- CREATE TABLE IF NOT EXISTS content_hashes (
413
- path TEXT PRIMARY KEY,
414
- hash TEXT NOT NULL,
415
- updated_at INTEGER NOT NULL
416
- );
417
-
418
- -- Session summaries (v26): agent session tracking
419
- CREATE TABLE IF NOT EXISTS session_summaries (
420
- id INTEGER PRIMARY KEY AUTOINCREMENT,
421
- session_id TEXT NOT NULL UNIQUE,
422
- summary TEXT NOT NULL,
423
- topics_json TEXT,
424
- notes_modified_json TEXT,
425
- agent_id TEXT,
426
- started_at INTEGER,
427
- ended_at INTEGER NOT NULL,
428
- tool_count INTEGER
429
- );
430
-
431
- -- Retrieval co-occurrence (v30): notes retrieved together build implicit edges
432
- CREATE TABLE IF NOT EXISTS retrieval_cooccurrence (
433
- id INTEGER PRIMARY KEY AUTOINCREMENT,
434
- note_a TEXT NOT NULL,
435
- note_b TEXT NOT NULL,
436
- session_id TEXT NOT NULL,
437
- timestamp INTEGER NOT NULL,
438
- weight REAL NOT NULL DEFAULT 1.0,
439
- UNIQUE(note_a, note_b, session_id)
440
- );
441
- CREATE INDEX IF NOT EXISTS idx_retcooc_notes ON retrieval_cooccurrence(note_a, note_b);
442
- CREATE INDEX IF NOT EXISTS idx_retcooc_ts ON retrieval_cooccurrence(timestamp);
443
-
444
- -- Deferred proactive linking queue (v31)
445
- CREATE TABLE IF NOT EXISTS proactive_queue (
446
- id INTEGER PRIMARY KEY AUTOINCREMENT,
447
- note_path TEXT NOT NULL,
448
- entity TEXT NOT NULL,
449
- score REAL NOT NULL,
450
- confidence TEXT NOT NULL,
451
- queued_at INTEGER NOT NULL,
452
- expires_at INTEGER NOT NULL,
453
- status TEXT NOT NULL DEFAULT 'pending',
454
- applied_at INTEGER,
455
- UNIQUE(note_path, entity)
456
- );
457
- CREATE INDEX IF NOT EXISTS idx_pq_status ON proactive_queue(status, expires_at);
458
- `;
459
- // =============================================================================
460
- // Database Initialization
461
- // =============================================================================
462
- /**
463
- * Get the database path for a vault
464
- */
465
- export function getStateDbPath(vaultPath) {
466
- const flywheelDir = path.join(vaultPath, FLYWHEEL_DIR);
467
- if (!fs.existsSync(flywheelDir)) {
468
- fs.mkdirSync(flywheelDir, { recursive: true });
469
- }
470
- return path.join(flywheelDir, STATE_DB_FILENAME);
471
- }
472
- /**
473
- * Initialize schema and run migrations
474
- */
475
- function initSchema(db) {
476
- // Enable WAL mode for better concurrent read performance
477
- db.pragma('journal_mode = WAL');
478
- // Enable foreign keys
479
- db.pragma('foreign_keys = ON');
480
- // Performance tuning
481
- db.pragma('synchronous = NORMAL'); // Safe with WAL — fsync only on checkpoint, not every commit
482
- db.pragma('cache_size = -64000'); // 64 MB page cache (default is ~2 MB)
483
- db.pragma('temp_store = MEMORY'); // Temp tables/indices in RAM instead of disk
484
- // Run schema creation
485
- db.exec(SCHEMA_SQL);
486
- // Guard: Verify critical tables were created
487
- // This catches cases where schema execution silently failed (e.g., corrupted db)
488
- const tables = db.prepare(`
489
- SELECT name FROM sqlite_master
490
- WHERE type='table' AND name IN ('entities', 'schema_version', 'metadata')
491
- `).all();
492
- if (tables.length < 3) {
493
- const foundTables = tables.map(t => t.name).join(', ') || 'none';
494
- throw new Error(`[vault-core] Schema validation failed: expected 3 critical tables, found ${tables.length} (${foundTables}). ` +
495
- `Database may be corrupted. Delete ${db.name} and restart.`);
496
- }
497
- // Check and record schema version
498
- const versionRow = db.prepare('SELECT MAX(version) as version FROM schema_version').get();
499
- const currentVersion = versionRow?.version ?? 0;
500
- if (currentVersion < SCHEMA_VERSION) {
501
- // v2: Drop dead notes/links tables if they exist from v1
502
- if (currentVersion < 2) {
503
- db.exec('DROP TABLE IF EXISTS notes');
504
- db.exec('DROP TABLE IF EXISTS links');
505
- }
506
- // v3: Rename crank_state → write_state
507
- if (currentVersion < 3) {
508
- const hasCrankState = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='crank_state'`).get();
509
- const hasWriteState = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='write_state'`).get();
510
- if (hasCrankState && !hasWriteState) {
511
- db.exec('ALTER TABLE crank_state RENAME TO write_state');
512
- }
513
- else if (hasCrankState && hasWriteState) {
514
- // Both exist (stale db) — drop the old one
515
- db.exec('DROP TABLE crank_state');
516
- }
517
- }
518
- // v4: vault_metrics, wikilink_feedback, wikilink_suppressions tables
519
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
520
- // v5: wikilink_applications table (implicit feedback tracking)
521
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
522
- // v6: index_events table (index activity history)
523
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
524
- // v7: tool_invocations table (usage analytics)
525
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
526
- // v8: graph_snapshots table (structural evolution)
527
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
528
- // v9: note_embeddings table (semantic search)
529
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
530
- // v10: entity_embeddings table (semantic entity search)
531
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
532
- // v11: notes_fts gains frontmatter column (4-col: path, title, frontmatter, content)
533
- // Virtual tables can't ALTER, so drop and recreate
534
- if (currentVersion < 11) {
535
- db.exec('DROP TABLE IF EXISTS notes_fts');
536
- db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
537
- path, title, frontmatter, content,
538
- tokenize='porter'
539
- )`);
540
- // Clear FTS metadata to force rebuild with new schema
541
- db.exec(`DELETE FROM fts_metadata WHERE key = 'last_built'`);
542
- }
543
- // v12: tasks cache table (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
544
- // v13: merge_dismissals table (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
545
- // v14: Add steps column to index_events (pipeline observability)
546
- if (currentVersion < 14) {
547
- const hasSteps = db.prepare(`SELECT COUNT(*) as cnt FROM pragma_table_info('index_events') WHERE name = 'steps'`).get();
548
- if (hasSteps.cnt === 0) {
549
- db.exec('ALTER TABLE index_events ADD COLUMN steps TEXT');
550
- }
551
- }
552
- // v15: suggestion_events table (pipeline observability audit log)
553
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
554
- // v16: note_links table (forward-link persistence for diff-based feedback)
555
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
556
- // v17: entity_changes table (entity field change audit log)
557
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
558
- // v18: note_tags table (tag persistence for diff-based feedback)
559
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
560
- // v19: note_link_history table (wikilink survival tracking for positive feedback)
561
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
562
- // v20: note_moves table (records file renames/moves detected by the watcher)
563
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
564
- // v21: description TEXT column on entities table
565
- if (currentVersion < 21) {
566
- const hasDesc = db.prepare(`SELECT COUNT(*) as cnt FROM pragma_table_info('entities') WHERE name = 'description'`).get();
567
- if (hasDesc.cnt === 0) {
568
- db.exec('ALTER TABLE entities ADD COLUMN description TEXT');
569
- }
570
- }
571
- // v22: Edge weight columns on note_links table
572
- if (currentVersion < 22) {
573
- const hasWeight = db.prepare(`SELECT COUNT(*) as cnt FROM pragma_table_info('note_links') WHERE name = 'weight'`).get();
574
- if (hasWeight.cnt === 0) {
575
- db.exec('ALTER TABLE note_links ADD COLUMN weight REAL NOT NULL DEFAULT 1.0');
576
- db.exec('ALTER TABLE note_links ADD COLUMN weight_updated_at INTEGER');
577
- }
578
- }
579
- // v23: Case-insensitive unique index on wikilink_applications
580
- if (currentVersion < 23) {
581
- db.exec('DROP INDEX IF EXISTS idx_wl_apps_unique');
582
- db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_wl_apps_unique ON wikilink_applications(entity COLLATE NOCASE, note_path)');
583
- }
584
- // v24: corrections table (persistent correction records)
585
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
586
- // v25: confidence column on wikilink_feedback (signal quality weighting)
587
- if (currentVersion < 25) {
588
- const hasConfidence = db.prepare(`SELECT COUNT(*) as cnt FROM pragma_table_info('wikilink_feedback') WHERE name = 'confidence'`).get();
589
- if (hasConfidence.cnt === 0) {
590
- db.exec('ALTER TABLE wikilink_feedback ADD COLUMN confidence REAL NOT NULL DEFAULT 1.0');
591
- }
592
- }
593
- // v26: memories table, memories_fts, session_summaries table
594
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
595
- // v27: cooccurrence_cache table (persist co-occurrence index)
596
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
597
- // v28: content_hashes table (persist watcher content hashes across restarts)
598
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
599
- // v29: index on wikilink_feedback(note_path) for temporal analysis queries
600
- // (created by SCHEMA_SQL above via CREATE INDEX IF NOT EXISTS)
601
- // v31: proactive_queue table (deferred proactive linking)
602
- // (created by SCHEMA_SQL above via CREATE TABLE IF NOT EXISTS)
603
- // v30: token economics columns on tool_invocations
604
- if (currentVersion < 30) {
605
- const hasResponseTokens = db.prepare(`SELECT COUNT(*) as cnt FROM pragma_table_info('tool_invocations') WHERE name = 'response_tokens'`).get();
606
- if (hasResponseTokens.cnt === 0) {
607
- db.exec('ALTER TABLE tool_invocations ADD COLUMN response_tokens INTEGER');
608
- db.exec('ALTER TABLE tool_invocations ADD COLUMN baseline_tokens INTEGER');
609
- }
610
- }
611
- db.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(SCHEMA_VERSION);
612
- }
613
- }
614
- function deleteStateDbFiles(dbPath) {
615
- for (const suffix of ['', '-wal', '-shm']) {
616
- const p = dbPath + suffix;
617
- if (fs.existsSync(p))
618
- fs.unlinkSync(p);
619
- }
620
- }
621
- /** Back up state.db before opening (skip if missing or 0 bytes). */
622
- function backupStateDb(dbPath) {
623
- try {
624
- if (!fs.existsSync(dbPath))
625
- return;
626
- const stat = fs.statSync(dbPath);
627
- if (stat.size === 0)
628
- return;
629
- fs.copyFileSync(dbPath, dbPath + '.backup');
630
- }
631
- catch (err) {
632
- console.error(`[vault-core] Failed to back up state.db: ${err instanceof Error ? err.message : err}`);
633
- }
634
- }
635
- /** Preserve a corrupted database for inspection before deleting. */
636
- function preserveCorruptedDb(dbPath) {
637
- try {
638
- if (fs.existsSync(dbPath)) {
639
- fs.copyFileSync(dbPath, dbPath + '.corrupt');
640
- console.error(`[vault-core] Corrupted database preserved at ${dbPath}.corrupt`);
641
- }
642
- }
643
- catch {
644
- // Best effort — don't block recovery
645
- }
646
- }
647
25
  /**
648
26
  * Open or create the state database for a vault
649
27
  *
@@ -693,102 +71,102 @@ export function openStateDb(vaultPath) {
693
71
  vaultPath,
694
72
  dbPath,
695
73
  // Entity operations
696
- insertEntity: db.prepare(`
697
- INSERT INTO entities (name, name_lower, path, category, aliases_json, hub_score, description)
698
- VALUES (?, ?, ?, ?, ?, ?, ?)
74
+ insertEntity: db.prepare(`
75
+ INSERT INTO entities (name, name_lower, path, category, aliases_json, hub_score, description)
76
+ VALUES (?, ?, ?, ?, ?, ?, ?)
699
77
  `),
700
- updateEntity: db.prepare(`
701
- UPDATE entities
702
- SET name = ?, name_lower = ?, path = ?, category = ?, aliases_json = ?, hub_score = ?, description = ?
703
- WHERE id = ?
78
+ updateEntity: db.prepare(`
79
+ UPDATE entities
80
+ SET name = ?, name_lower = ?, path = ?, category = ?, aliases_json = ?, hub_score = ?, description = ?
81
+ WHERE id = ?
704
82
  `),
705
83
  deleteEntity: db.prepare('DELETE FROM entities WHERE id = ?'),
706
- getEntityByName: db.prepare(`
707
- SELECT id, name, name_lower, path, category, aliases_json, hub_score, description
708
- FROM entities WHERE name_lower = ?
84
+ getEntityByName: db.prepare(`
85
+ SELECT id, name, name_lower, path, category, aliases_json, hub_score, description
86
+ FROM entities WHERE name_lower = ?
709
87
  `),
710
- getEntityById: db.prepare(`
711
- SELECT id, name, name_lower, path, category, aliases_json, hub_score, description
712
- FROM entities WHERE id = ?
88
+ getEntityById: db.prepare(`
89
+ SELECT id, name, name_lower, path, category, aliases_json, hub_score, description
90
+ FROM entities WHERE id = ?
713
91
  `),
714
- getAllEntities: db.prepare(`
715
- SELECT id, name, name_lower, path, category, aliases_json, hub_score, description
716
- FROM entities ORDER BY name
92
+ getAllEntities: db.prepare(`
93
+ SELECT id, name, name_lower, path, category, aliases_json, hub_score, description
94
+ FROM entities ORDER BY name
717
95
  `),
718
- getEntitiesByCategory: db.prepare(`
719
- SELECT id, name, name_lower, path, category, aliases_json, hub_score, description
720
- FROM entities WHERE category = ? ORDER BY name
96
+ getEntitiesByCategory: db.prepare(`
97
+ SELECT id, name, name_lower, path, category, aliases_json, hub_score, description
98
+ FROM entities WHERE category = ? ORDER BY name
721
99
  `),
722
- searchEntitiesFts: db.prepare(`
723
- SELECT e.id, e.name, e.name_lower, e.path, e.category, e.aliases_json, e.hub_score, e.description,
724
- bm25(entities_fts) as rank
725
- FROM entities_fts
726
- JOIN entities e ON e.id = entities_fts.rowid
727
- WHERE entities_fts MATCH ?
728
- ORDER BY rank
729
- LIMIT ?
100
+ searchEntitiesFts: db.prepare(`
101
+ SELECT e.id, e.name, e.name_lower, e.path, e.category, e.aliases_json, e.hub_score, e.description,
102
+ bm25(entities_fts) as rank
103
+ FROM entities_fts
104
+ JOIN entities e ON e.id = entities_fts.rowid
105
+ WHERE entities_fts MATCH ?
106
+ ORDER BY rank
107
+ LIMIT ?
730
108
  `),
731
109
  clearEntities: db.prepare('DELETE FROM entities'),
732
110
  // Entity alias lookup
733
- getEntitiesByAlias: db.prepare(`
734
- SELECT e.id, e.name, e.name_lower, e.path, e.category, e.aliases_json, e.hub_score, e.description
735
- FROM entities e
736
- WHERE EXISTS (SELECT 1 FROM json_each(e.aliases_json) WHERE LOWER(value) = ?)
111
+ getEntitiesByAlias: db.prepare(`
112
+ SELECT e.id, e.name, e.name_lower, e.path, e.category, e.aliases_json, e.hub_score, e.description
113
+ FROM entities e
114
+ WHERE EXISTS (SELECT 1 FROM json_each(e.aliases_json) WHERE LOWER(value) = ?)
737
115
  `),
738
116
  // Recency operations
739
- upsertRecency: db.prepare(`
740
- INSERT INTO recency (entity_name_lower, last_mentioned_at, mention_count)
741
- VALUES (?, ?, 1)
742
- ON CONFLICT(entity_name_lower) DO UPDATE SET
743
- last_mentioned_at = excluded.last_mentioned_at,
744
- mention_count = mention_count + 1
117
+ upsertRecency: db.prepare(`
118
+ INSERT INTO recency (entity_name_lower, last_mentioned_at, mention_count)
119
+ VALUES (?, ?, 1)
120
+ ON CONFLICT(entity_name_lower) DO UPDATE SET
121
+ last_mentioned_at = excluded.last_mentioned_at,
122
+ mention_count = mention_count + 1
745
123
  `),
746
- getRecency: db.prepare(`
747
- SELECT entity_name_lower, last_mentioned_at, mention_count
748
- FROM recency WHERE entity_name_lower = ?
124
+ getRecency: db.prepare(`
125
+ SELECT entity_name_lower, last_mentioned_at, mention_count
126
+ FROM recency WHERE entity_name_lower = ?
749
127
  `),
750
- getAllRecency: db.prepare(`
751
- SELECT entity_name_lower, last_mentioned_at, mention_count
752
- FROM recency ORDER BY last_mentioned_at DESC
128
+ getAllRecency: db.prepare(`
129
+ SELECT entity_name_lower, last_mentioned_at, mention_count
130
+ FROM recency ORDER BY last_mentioned_at DESC
753
131
  `),
754
132
  clearRecency: db.prepare('DELETE FROM recency'),
755
133
  // Write state operations
756
- setWriteState: db.prepare(`
757
- INSERT INTO write_state (key, value, updated_at)
758
- VALUES (?, ?, datetime('now'))
759
- ON CONFLICT(key) DO UPDATE SET
760
- value = excluded.value,
761
- updated_at = datetime('now')
134
+ setWriteState: db.prepare(`
135
+ INSERT INTO write_state (key, value, updated_at)
136
+ VALUES (?, ?, datetime('now'))
137
+ ON CONFLICT(key) DO UPDATE SET
138
+ value = excluded.value,
139
+ updated_at = datetime('now')
762
140
  `),
763
141
  getWriteState: db.prepare('SELECT value FROM write_state WHERE key = ?'),
764
142
  deleteWriteState: db.prepare('DELETE FROM write_state WHERE key = ?'),
765
143
  // Flywheel config operations
766
- setFlywheelConfigStmt: db.prepare(`
767
- INSERT INTO flywheel_config (key, value, updated_at)
768
- VALUES (?, ?, datetime('now'))
769
- ON CONFLICT(key) DO UPDATE SET
770
- value = excluded.value,
771
- updated_at = datetime('now')
144
+ setFlywheelConfigStmt: db.prepare(`
145
+ INSERT INTO flywheel_config (key, value, updated_at)
146
+ VALUES (?, ?, datetime('now'))
147
+ ON CONFLICT(key) DO UPDATE SET
148
+ value = excluded.value,
149
+ updated_at = datetime('now')
772
150
  `),
773
151
  getFlywheelConfigStmt: db.prepare('SELECT value FROM flywheel_config WHERE key = ?'),
774
152
  getAllFlywheelConfigStmt: db.prepare('SELECT key, value FROM flywheel_config'),
775
153
  deleteFlywheelConfigStmt: db.prepare('DELETE FROM flywheel_config WHERE key = ?'),
776
154
  // Task cache operations
777
- insertTask: db.prepare(`
778
- INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
779
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
155
+ insertTask: db.prepare(`
156
+ INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
157
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
780
158
  `),
781
159
  deleteTasksForPath: db.prepare('DELETE FROM tasks WHERE path = ?'),
782
160
  clearAllTasks: db.prepare('DELETE FROM tasks'),
783
161
  countTasksByStatus: db.prepare('SELECT status, COUNT(*) as cnt FROM tasks GROUP BY status'),
784
162
  // Metadata operations
785
163
  getMetadataValue: db.prepare('SELECT value FROM metadata WHERE key = ?'),
786
- setMetadataValue: db.prepare(`
787
- INSERT INTO metadata (key, value, updated_at)
788
- VALUES (?, ?, datetime('now'))
789
- ON CONFLICT(key) DO UPDATE SET
790
- value = excluded.value,
791
- updated_at = datetime('now')
164
+ setMetadataValue: db.prepare(`
165
+ INSERT INTO metadata (key, value, updated_at)
166
+ VALUES (?, ?, datetime('now'))
167
+ ON CONFLICT(key) DO UPDATE SET
168
+ value = excluded.value,
169
+ updated_at = datetime('now')
792
170
  `),
793
171
  // Transactions
794
172
  bulkInsertEntities: db.transaction((entities, category) => {
@@ -834,472 +212,4 @@ export function openStateDb(vaultPath) {
834
212
  };
835
213
  return stateDb;
836
214
  }
837
- // =============================================================================
838
- // Entity Operations
839
- // =============================================================================
840
- /**
841
- * Search entities using FTS5 with porter stemming
842
- *
843
- * @param stateDb - State database instance
844
- * @param query - Search query (supports FTS5 syntax)
845
- * @param limit - Maximum results to return
846
- * @returns Array of matching entities with relevance scores
847
- */
848
- export function searchEntities(stateDb, query, limit = 20) {
849
- const escapedQuery = escapeFts5Query(query);
850
- // Handle empty query - return empty results
851
- if (!escapedQuery) {
852
- return [];
853
- }
854
- const rows = stateDb.searchEntitiesFts.all(escapedQuery, limit);
855
- return rows.map(row => ({
856
- id: row.id,
857
- name: row.name,
858
- nameLower: row.name_lower,
859
- path: row.path,
860
- category: row.category,
861
- aliases: row.aliases_json ? JSON.parse(row.aliases_json) : [],
862
- hubScore: row.hub_score,
863
- description: row.description ?? undefined,
864
- rank: row.rank,
865
- }));
866
- }
867
- /**
868
- * Search entities by prefix for autocomplete
869
- *
870
- * @param stateDb - State database instance
871
- * @param prefix - Prefix to search for
872
- * @param limit - Maximum results to return
873
- */
874
- export function searchEntitiesPrefix(stateDb, prefix, limit = 20) {
875
- return searchEntities(stateDb, `${escapeFts5Query(prefix)}*`, limit);
876
- }
877
- /**
878
- * Get entity by exact name (case-insensitive)
879
- */
880
- export function getEntityByName(stateDb, name) {
881
- const row = stateDb.getEntityByName.get(name.toLowerCase());
882
- if (!row)
883
- return null;
884
- return {
885
- id: row.id,
886
- name: row.name,
887
- nameLower: row.name_lower,
888
- path: row.path,
889
- category: row.category,
890
- aliases: row.aliases_json ? JSON.parse(row.aliases_json) : [],
891
- hubScore: row.hub_score,
892
- description: row.description ?? undefined,
893
- rank: 0,
894
- };
895
- }
896
- /**
897
- * Get all entities from the database
898
- */
899
- export function getAllEntitiesFromDb(stateDb) {
900
- const rows = stateDb.getAllEntities.all();
901
- return rows.map(row => ({
902
- id: row.id,
903
- name: row.name,
904
- nameLower: row.name_lower,
905
- path: row.path,
906
- category: row.category,
907
- aliases: row.aliases_json ? JSON.parse(row.aliases_json) : [],
908
- hubScore: row.hub_score,
909
- description: row.description ?? undefined,
910
- rank: 0,
911
- }));
912
- }
913
- /**
914
- * Convert database entities back to EntityIndex format
915
- */
916
- export function getEntityIndexFromDb(stateDb) {
917
- const entities = getAllEntitiesFromDb(stateDb);
918
- const index = {
919
- technologies: [],
920
- acronyms: [],
921
- people: [],
922
- projects: [],
923
- organizations: [],
924
- locations: [],
925
- concepts: [],
926
- animals: [],
927
- media: [],
928
- events: [],
929
- documents: [],
930
- vehicles: [],
931
- health: [],
932
- finance: [],
933
- food: [],
934
- hobbies: [],
935
- periodical: [],
936
- other: [],
937
- _metadata: {
938
- total_entities: entities.length,
939
- generated_at: new Date().toISOString(),
940
- vault_path: stateDb.vaultPath,
941
- source: 'vault-core sqlite',
942
- },
943
- };
944
- for (const entity of entities) {
945
- const entityObj = {
946
- name: entity.name,
947
- path: entity.path,
948
- aliases: entity.aliases,
949
- hubScore: entity.hubScore,
950
- description: entity.description,
951
- };
952
- index[entity.category].push(entityObj);
953
- }
954
- return index;
955
- }
956
- /**
957
- * Get entities that have a given alias (case-insensitive)
958
- *
959
- * @param stateDb - State database instance
960
- * @param alias - Alias to search for (case-insensitive)
961
- * @returns Array of matching entities
962
- */
963
- export function getEntitiesByAlias(stateDb, alias) {
964
- const rows = stateDb.getEntitiesByAlias.all(alias.toLowerCase());
965
- return rows.map(row => ({
966
- id: row.id,
967
- name: row.name,
968
- nameLower: row.name_lower,
969
- path: row.path,
970
- category: row.category,
971
- aliases: row.aliases_json ? JSON.parse(row.aliases_json) : [],
972
- hubScore: row.hub_score,
973
- description: row.description ?? undefined,
974
- rank: 0,
975
- }));
976
- }
977
- // =============================================================================
978
- // Recency Operations
979
- // =============================================================================
980
- /**
981
- * Record a mention of an entity
982
- */
983
- export function recordEntityMention(stateDb, entityName, mentionedAt = new Date()) {
984
- stateDb.upsertRecency.run(entityName.toLowerCase(), mentionedAt.getTime());
985
- }
986
- /**
987
- * Get recency info for an entity
988
- */
989
- export function getEntityRecency(stateDb, entityName) {
990
- const row = stateDb.getRecency.get(entityName.toLowerCase());
991
- if (!row)
992
- return null;
993
- return {
994
- entityNameLower: row.entity_name_lower,
995
- lastMentionedAt: row.last_mentioned_at,
996
- mentionCount: row.mention_count,
997
- };
998
- }
999
- /**
1000
- * Get all recency data ordered by most recent
1001
- */
1002
- export function getAllRecency(stateDb) {
1003
- const rows = stateDb.getAllRecency.all();
1004
- return rows.map(row => ({
1005
- entityNameLower: row.entity_name_lower,
1006
- lastMentionedAt: row.last_mentioned_at,
1007
- mentionCount: row.mention_count,
1008
- }));
1009
- }
1010
- // =============================================================================
1011
- // Write State Operations
1012
- // =============================================================================
1013
- /**
1014
- * Set a write state value
1015
- */
1016
- export function setWriteState(stateDb, key, value) {
1017
- stateDb.setWriteState.run(key, JSON.stringify(value));
1018
- }
1019
- /**
1020
- * Get a write state value
1021
- */
1022
- export function getWriteState(stateDb, key) {
1023
- const row = stateDb.getWriteState.get(key);
1024
- if (!row)
1025
- return null;
1026
- return JSON.parse(row.value);
1027
- }
1028
- /**
1029
- * Delete a write state key
1030
- */
1031
- export function deleteWriteState(stateDb, key) {
1032
- stateDb.deleteWriteState.run(key);
1033
- }
1034
- /**
1035
- * Set a flywheel config value
1036
- */
1037
- export function setFlywheelConfig(stateDb, key, value) {
1038
- stateDb.setFlywheelConfigStmt.run(key, JSON.stringify(value));
1039
- }
1040
- /**
1041
- * Get a flywheel config value
1042
- */
1043
- export function getFlywheelConfig(stateDb, key) {
1044
- const row = stateDb.getFlywheelConfigStmt.get(key);
1045
- if (!row)
1046
- return null;
1047
- return JSON.parse(row.value);
1048
- }
1049
- /**
1050
- * Get all flywheel config values as an object
1051
- */
1052
- export function getAllFlywheelConfig(stateDb) {
1053
- const rows = stateDb.getAllFlywheelConfigStmt.all();
1054
- const config = {};
1055
- for (const row of rows) {
1056
- try {
1057
- config[row.key] = JSON.parse(row.value);
1058
- }
1059
- catch {
1060
- config[row.key] = row.value;
1061
- }
1062
- }
1063
- return config;
1064
- }
1065
- /**
1066
- * Delete a flywheel config key
1067
- */
1068
- export function deleteFlywheelConfig(stateDb, key) {
1069
- stateDb.deleteFlywheelConfigStmt.run(key);
1070
- }
1071
- /**
1072
- * Save entire Flywheel config object to database
1073
- * Stores each top-level key as a separate row
1074
- */
1075
- export function saveFlywheelConfigToDb(stateDb, config) {
1076
- const transaction = stateDb.db.transaction(() => {
1077
- for (const [key, value] of Object.entries(config)) {
1078
- if (value !== undefined) {
1079
- setFlywheelConfig(stateDb, key, value);
1080
- }
1081
- }
1082
- });
1083
- transaction();
1084
- }
1085
- /**
1086
- * Load Flywheel config from database and reconstruct as typed object
1087
- */
1088
- export function loadFlywheelConfigFromDb(stateDb) {
1089
- const config = getAllFlywheelConfig(stateDb);
1090
- if (Object.keys(config).length === 0) {
1091
- return null;
1092
- }
1093
- return config;
1094
- }
1095
- // =============================================================================
1096
- // Merge Dismissal Operations
1097
- // =============================================================================
1098
- /**
1099
- * Record a merge dismissal so the pair never reappears in suggestions.
1100
- */
1101
- export function recordMergeDismissal(db, sourcePath, targetPath, sourceName, targetName, reason) {
1102
- const pairKey = [sourcePath, targetPath].sort().join('::');
1103
- db.db.prepare(`INSERT OR IGNORE INTO merge_dismissals
1104
- (pair_key, source_path, target_path, source_name, target_name, reason)
1105
- VALUES (?, ?, ?, ?, ?, ?)`)
1106
- .run(pairKey, sourcePath, targetPath, sourceName, targetName, reason);
1107
- }
1108
- /**
1109
- * Get all dismissed merge pair keys for filtering.
1110
- */
1111
- export function getDismissedMergePairs(db) {
1112
- const rows = db.db.prepare('SELECT pair_key FROM merge_dismissals').all();
1113
- return new Set(rows.map(r => r.pair_key));
1114
- }
1115
- // =============================================================================
1116
- // Metadata Operations
1117
- // =============================================================================
1118
- /**
1119
- * Get database metadata
1120
- */
1121
- export function getStateDbMetadata(stateDb) {
1122
- const schemaRow = stateDb.db.prepare('SELECT MAX(version) as version FROM schema_version').get();
1123
- const entitiesBuiltRow = stateDb.getMetadataValue.get('entities_built_at');
1124
- const entityCountRow = stateDb.getMetadataValue.get('entity_count');
1125
- const notesBuiltRow = stateDb.getMetadataValue.get('notes_built_at');
1126
- const noteCountRow = stateDb.getMetadataValue.get('note_count');
1127
- return {
1128
- schemaVersion: schemaRow?.version ?? 0,
1129
- entitiesBuiltAt: entitiesBuiltRow?.value ?? null,
1130
- entityCount: entityCountRow ? parseInt(entityCountRow.value, 10) : 0,
1131
- notesBuiltAt: notesBuiltRow?.value ?? null,
1132
- noteCount: noteCountRow ? parseInt(noteCountRow.value, 10) : 0,
1133
- };
1134
- }
1135
- /**
1136
- * Check if entity data is stale (older than threshold)
1137
- */
1138
- export function isEntityDataStale(stateDb, thresholdMs = 60 * 60 * 1000 // 1 hour default
1139
- ) {
1140
- const metadata = getStateDbMetadata(stateDb);
1141
- if (!metadata.entitiesBuiltAt) {
1142
- return true;
1143
- }
1144
- const builtAt = new Date(metadata.entitiesBuiltAt).getTime();
1145
- const age = Date.now() - builtAt;
1146
- return age > thresholdMs;
1147
- }
1148
- // =============================================================================
1149
- // Utility Functions
1150
- // =============================================================================
1151
- /**
1152
- * Escape special FTS5 characters in a query
1153
- */
1154
- export function escapeFts5Query(query) {
1155
- // Handle empty query
1156
- if (!query || !query.trim()) {
1157
- return '';
1158
- }
1159
- // Remove or escape FTS5 special characters
1160
- // Keep * for prefix matching, escape others
1161
- return query
1162
- .replace(/"/g, '""') // Escape quotes
1163
- .replace(/[(){}[\]^~:-]/g, ' ') // Remove special operators including hyphen
1164
- .replace(/\s+/g, ' ') // Normalize whitespace
1165
- .trim();
1166
- }
1167
- /**
1168
- * Rebuild the entities_fts index from the entities table.
1169
- * Uses FTS5's built-in 'rebuild' command to resynchronize.
1170
- * Call this if the FTS index gets out of sync (e.g., T.aliases errors).
1171
- */
1172
- export function rebuildEntitiesFts(stateDb) {
1173
- stateDb.db.exec(`INSERT INTO entities_fts(entities_fts) VALUES('rebuild')`);
1174
- }
1175
- /**
1176
- * Check if the state database exists for a vault
1177
- */
1178
- export function stateDbExists(vaultPath) {
1179
- const dbPath = getStateDbPath(vaultPath);
1180
- return fs.existsSync(dbPath);
1181
- }
1182
- /**
1183
- * Delete the state database (for testing or reset)
1184
- */
1185
- export function deleteStateDb(vaultPath) {
1186
- const dbPath = getStateDbPath(vaultPath);
1187
- if (fs.existsSync(dbPath)) {
1188
- fs.unlinkSync(dbPath);
1189
- }
1190
- // Also remove WAL and SHM files if they exist
1191
- const walPath = dbPath + '-wal';
1192
- const shmPath = dbPath + '-shm';
1193
- if (fs.existsSync(walPath))
1194
- fs.unlinkSync(walPath);
1195
- if (fs.existsSync(shmPath))
1196
- fs.unlinkSync(shmPath);
1197
- }
1198
- /**
1199
- * Save VaultIndex to cache
1200
- *
1201
- * @param stateDb - State database instance
1202
- * @param indexData - Serialized VaultIndex data
1203
- */
1204
- export function saveVaultIndexCache(stateDb, indexData) {
1205
- const data = JSON.stringify(indexData);
1206
- const stmt = stateDb.db.prepare(`
1207
- INSERT OR REPLACE INTO vault_index_cache (id, data, built_at, note_count, version)
1208
- VALUES (1, ?, ?, ?, 1)
1209
- `);
1210
- stmt.run(data, indexData.builtAt, indexData.notes.length);
1211
- }
1212
- /**
1213
- * Load VaultIndex from cache
1214
- *
1215
- * @param stateDb - State database instance
1216
- * @returns Cached VaultIndex data or null if not found
1217
- */
1218
- export function loadVaultIndexCache(stateDb) {
1219
- const stmt = stateDb.db.prepare(`
1220
- SELECT data, built_at, note_count FROM vault_index_cache WHERE id = 1
1221
- `);
1222
- const row = stmt.get();
1223
- if (!row)
1224
- return null;
1225
- try {
1226
- return JSON.parse(row.data);
1227
- }
1228
- catch {
1229
- return null;
1230
- }
1231
- }
1232
- /**
1233
- * Get cache metadata without loading full data
1234
- */
1235
- export function getVaultIndexCacheInfo(stateDb) {
1236
- const stmt = stateDb.db.prepare(`
1237
- SELECT built_at, note_count, version FROM vault_index_cache WHERE id = 1
1238
- `);
1239
- const row = stmt.get();
1240
- if (!row)
1241
- return null;
1242
- return {
1243
- builtAt: new Date(row.built_at),
1244
- noteCount: row.note_count,
1245
- version: row.version,
1246
- };
1247
- }
1248
- /**
1249
- * Clear the vault index cache
1250
- */
1251
- export function clearVaultIndexCache(stateDb) {
1252
- stateDb.db.prepare('DELETE FROM vault_index_cache').run();
1253
- }
1254
- /**
1255
- * Check if cache is valid (not too old and note count matches)
1256
- *
1257
- * @param stateDb - State database instance
1258
- * @param actualNoteCount - Current number of notes in vault
1259
- * @param maxAgeMs - Maximum cache age in milliseconds (default 24 hours)
1260
- */
1261
- export function isVaultIndexCacheValid(stateDb, actualNoteCount, maxAgeMs = 24 * 60 * 60 * 1000) {
1262
- const info = getVaultIndexCacheInfo(stateDb);
1263
- if (!info)
1264
- return false;
1265
- // Check note count matches (quick validation)
1266
- if (info.noteCount !== actualNoteCount)
1267
- return false;
1268
- // Check age
1269
- const age = Date.now() - info.builtAt.getTime();
1270
- if (age > maxAgeMs)
1271
- return false;
1272
- return true;
1273
- }
1274
- // =============================================================================
1275
- // Content Hash Operations
1276
- // =============================================================================
1277
- /** Load all persisted content hashes */
1278
- export function loadContentHashes(stateDb) {
1279
- const rows = stateDb.db.prepare('SELECT path, hash FROM content_hashes').all();
1280
- const map = new Map();
1281
- for (const row of rows) {
1282
- map.set(row.path, row.hash);
1283
- }
1284
- return map;
1285
- }
1286
- /** Persist hash changes from a watcher batch (upserts + deletes in one transaction) */
1287
- export function saveContentHashBatch(stateDb, upserts, deletes) {
1288
- const upsertStmt = stateDb.db.prepare('INSERT OR REPLACE INTO content_hashes (path, hash, updated_at) VALUES (?, ?, ?)');
1289
- const deleteStmt = stateDb.db.prepare('DELETE FROM content_hashes WHERE path = ?');
1290
- const now = Date.now();
1291
- const runBatch = stateDb.db.transaction(() => {
1292
- for (const { path, hash } of upserts) {
1293
- upsertStmt.run(path, hash, now);
1294
- }
1295
- for (const p of deletes) {
1296
- deleteStmt.run(p);
1297
- }
1298
- });
1299
- runBatch();
1300
- }
1301
- /** Rename a hash entry (for file renames) */
1302
- export function renameContentHash(stateDb, oldPath, newPath) {
1303
- stateDb.db.prepare('UPDATE content_hashes SET path = ?, updated_at = ? WHERE path = ?').run(newPath, Date.now(), oldPath);
1304
- }
1305
215
  //# sourceMappingURL=sqlite.js.map