@velvetmonkey/vault-core 1.27.28 → 1.27.30
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/CLAUDE.md +11 -0
- package/dist/entities.d.ts +0 -0
- package/dist/entities.d.ts.map +0 -0
- package/dist/entities.js +0 -0
- package/dist/entities.js.map +0 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logging/index.d.ts +0 -0
- package/dist/logging/index.d.ts.map +0 -0
- package/dist/logging/index.js +0 -0
- package/dist/logging/index.js.map +0 -0
- package/dist/logging/operationLogger.d.ts +0 -0
- package/dist/logging/operationLogger.d.ts.map +0 -0
- package/dist/logging/operationLogger.js +0 -0
- package/dist/logging/operationLogger.js.map +0 -0
- package/dist/logging/sessionManager.d.ts +0 -0
- package/dist/logging/sessionManager.d.ts.map +0 -0
- package/dist/logging/sessionManager.js +0 -0
- package/dist/logging/sessionManager.js.map +0 -0
- package/dist/logging/types.d.ts +0 -0
- package/dist/logging/types.d.ts.map +0 -0
- package/dist/logging/types.js +0 -0
- package/dist/logging/types.js.map +0 -0
- package/dist/protectedZones.d.ts +0 -0
- package/dist/protectedZones.d.ts.map +0 -0
- package/dist/protectedZones.js +0 -0
- package/dist/protectedZones.js.map +0 -0
- package/dist/sqlite.d.ts +243 -0
- package/dist/sqlite.d.ts.map +1 -0
- package/dist/sqlite.js +751 -0
- package/dist/sqlite.js.map +1 -0
- package/dist/types.d.ts +0 -0
- package/dist/types.d.ts.map +0 -0
- package/dist/types.js +0 -0
- package/dist/types.js.map +0 -0
- package/dist/wikilinks.d.ts +0 -0
- package/dist/wikilinks.d.ts.map +1 -1
- package/dist/wikilinks.js +30 -3
- package/dist/wikilinks.js.map +1 -1
- package/package.json +5 -1
package/dist/sqlite.js
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
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 = 1;
|
|
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
|
+
-- Notes metadata
|
|
42
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
43
|
+
id INTEGER PRIMARY KEY,
|
|
44
|
+
path TEXT UNIQUE NOT NULL,
|
|
45
|
+
title TEXT NOT NULL,
|
|
46
|
+
content_hash TEXT,
|
|
47
|
+
modified_at INTEGER NOT NULL,
|
|
48
|
+
aliases_json TEXT,
|
|
49
|
+
tags_json TEXT
|
|
50
|
+
);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_notes_path ON notes(path);
|
|
52
|
+
|
|
53
|
+
-- Entity index (replaces wikilink-entities.json)
|
|
54
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
55
|
+
id INTEGER PRIMARY KEY,
|
|
56
|
+
name TEXT NOT NULL,
|
|
57
|
+
name_lower TEXT NOT NULL,
|
|
58
|
+
path TEXT NOT NULL,
|
|
59
|
+
category TEXT NOT NULL,
|
|
60
|
+
aliases_json TEXT,
|
|
61
|
+
hub_score INTEGER DEFAULT 0
|
|
62
|
+
);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name_lower ON entities(name_lower);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_entities_category ON entities(category);
|
|
65
|
+
|
|
66
|
+
-- FTS5 for entity search with porter stemmer
|
|
67
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
68
|
+
name, aliases, category,
|
|
69
|
+
content='entities', content_rowid='id',
|
|
70
|
+
tokenize='porter unicode61'
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
-- Auto-sync triggers for entities_fts
|
|
74
|
+
CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
|
|
75
|
+
INSERT INTO entities_fts(rowid, name, aliases, category)
|
|
76
|
+
VALUES (
|
|
77
|
+
new.id,
|
|
78
|
+
new.name,
|
|
79
|
+
COALESCE((SELECT group_concat(value, ' ') FROM json_each(new.aliases_json)), ''),
|
|
80
|
+
new.category
|
|
81
|
+
);
|
|
82
|
+
END;
|
|
83
|
+
|
|
84
|
+
CREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE 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
|
+
END;
|
|
94
|
+
|
|
95
|
+
CREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN
|
|
96
|
+
INSERT INTO entities_fts(entities_fts, rowid, name, aliases, category)
|
|
97
|
+
VALUES (
|
|
98
|
+
'delete',
|
|
99
|
+
old.id,
|
|
100
|
+
old.name,
|
|
101
|
+
COALESCE((SELECT group_concat(value, ' ') FROM json_each(old.aliases_json)), ''),
|
|
102
|
+
old.category
|
|
103
|
+
);
|
|
104
|
+
INSERT INTO entities_fts(rowid, name, aliases, category)
|
|
105
|
+
VALUES (
|
|
106
|
+
new.id,
|
|
107
|
+
new.name,
|
|
108
|
+
COALESCE((SELECT group_concat(value, ' ') FROM json_each(new.aliases_json)), ''),
|
|
109
|
+
new.category
|
|
110
|
+
);
|
|
111
|
+
END;
|
|
112
|
+
|
|
113
|
+
-- Links table (replaces in-memory backlinks)
|
|
114
|
+
CREATE TABLE IF NOT EXISTS links (
|
|
115
|
+
id INTEGER PRIMARY KEY,
|
|
116
|
+
source_path TEXT NOT NULL,
|
|
117
|
+
target TEXT NOT NULL,
|
|
118
|
+
target_path TEXT,
|
|
119
|
+
line_number INTEGER
|
|
120
|
+
);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_links_source_path ON links(source_path);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_links_target_path ON links(target_path);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_links_target ON links(target);
|
|
124
|
+
|
|
125
|
+
-- Recency tracking (replaces entity-recency.json)
|
|
126
|
+
CREATE TABLE IF NOT EXISTS recency (
|
|
127
|
+
entity_name_lower TEXT PRIMARY KEY,
|
|
128
|
+
last_mentioned_at INTEGER NOT NULL,
|
|
129
|
+
mention_count INTEGER DEFAULT 1
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
-- Crank state (replaces last-crank-commit.json and other crank state)
|
|
133
|
+
CREATE TABLE IF NOT EXISTS crank_state (
|
|
134
|
+
key TEXT PRIMARY KEY,
|
|
135
|
+
value TEXT NOT NULL,
|
|
136
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
-- Content search FTS5 (migrated from vault-search.db)
|
|
140
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
|
|
141
|
+
path, title, content,
|
|
142
|
+
tokenize='porter'
|
|
143
|
+
);
|
|
144
|
+
`;
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// Database Initialization
|
|
147
|
+
// =============================================================================
|
|
148
|
+
/**
|
|
149
|
+
* Get the database path for a vault
|
|
150
|
+
*/
|
|
151
|
+
export function getStateDbPath(vaultPath) {
|
|
152
|
+
const flywheelDir = path.join(vaultPath, FLYWHEEL_DIR);
|
|
153
|
+
if (!fs.existsSync(flywheelDir)) {
|
|
154
|
+
fs.mkdirSync(flywheelDir, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
return path.join(flywheelDir, STATE_DB_FILENAME);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Initialize schema and run migrations
|
|
160
|
+
*/
|
|
161
|
+
function initSchema(db) {
|
|
162
|
+
// Enable WAL mode for better concurrent read performance
|
|
163
|
+
db.pragma('journal_mode = WAL');
|
|
164
|
+
// Enable foreign keys
|
|
165
|
+
db.pragma('foreign_keys = ON');
|
|
166
|
+
// Run schema creation
|
|
167
|
+
db.exec(SCHEMA_SQL);
|
|
168
|
+
// Check and record schema version
|
|
169
|
+
const versionRow = db.prepare('SELECT MAX(version) as version FROM schema_version').get();
|
|
170
|
+
const currentVersion = versionRow?.version ?? 0;
|
|
171
|
+
if (currentVersion < SCHEMA_VERSION) {
|
|
172
|
+
// Run migrations here when we add new schema versions
|
|
173
|
+
// For now, just record the current version
|
|
174
|
+
db.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(SCHEMA_VERSION);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Open or create the state database for a vault
|
|
179
|
+
*
|
|
180
|
+
* @param vaultPath - Absolute path to the vault root
|
|
181
|
+
* @returns StateDb instance with prepared statements
|
|
182
|
+
*/
|
|
183
|
+
export function openStateDb(vaultPath) {
|
|
184
|
+
const dbPath = getStateDbPath(vaultPath);
|
|
185
|
+
const db = new Database(dbPath);
|
|
186
|
+
// Initialize schema
|
|
187
|
+
initSchema(db);
|
|
188
|
+
// Prepare all statements
|
|
189
|
+
const stateDb = {
|
|
190
|
+
db,
|
|
191
|
+
vaultPath,
|
|
192
|
+
dbPath,
|
|
193
|
+
// Entity operations
|
|
194
|
+
insertEntity: db.prepare(`
|
|
195
|
+
INSERT INTO entities (name, name_lower, path, category, aliases_json, hub_score)
|
|
196
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
197
|
+
`),
|
|
198
|
+
updateEntity: db.prepare(`
|
|
199
|
+
UPDATE entities
|
|
200
|
+
SET name = ?, name_lower = ?, path = ?, category = ?, aliases_json = ?, hub_score = ?
|
|
201
|
+
WHERE id = ?
|
|
202
|
+
`),
|
|
203
|
+
deleteEntity: db.prepare('DELETE FROM entities WHERE id = ?'),
|
|
204
|
+
getEntityByName: db.prepare(`
|
|
205
|
+
SELECT id, name, name_lower, path, category, aliases_json, hub_score
|
|
206
|
+
FROM entities WHERE name_lower = ?
|
|
207
|
+
`),
|
|
208
|
+
getEntityById: db.prepare(`
|
|
209
|
+
SELECT id, name, name_lower, path, category, aliases_json, hub_score
|
|
210
|
+
FROM entities WHERE id = ?
|
|
211
|
+
`),
|
|
212
|
+
getAllEntities: db.prepare(`
|
|
213
|
+
SELECT id, name, name_lower, path, category, aliases_json, hub_score
|
|
214
|
+
FROM entities ORDER BY name
|
|
215
|
+
`),
|
|
216
|
+
getEntitiesByCategory: db.prepare(`
|
|
217
|
+
SELECT id, name, name_lower, path, category, aliases_json, hub_score
|
|
218
|
+
FROM entities WHERE category = ? ORDER BY name
|
|
219
|
+
`),
|
|
220
|
+
searchEntitiesFts: db.prepare(`
|
|
221
|
+
SELECT e.id, e.name, e.name_lower, e.path, e.category, e.aliases_json, e.hub_score,
|
|
222
|
+
bm25(entities_fts) as rank
|
|
223
|
+
FROM entities_fts
|
|
224
|
+
JOIN entities e ON e.id = entities_fts.rowid
|
|
225
|
+
WHERE entities_fts MATCH ?
|
|
226
|
+
ORDER BY rank
|
|
227
|
+
LIMIT ?
|
|
228
|
+
`),
|
|
229
|
+
clearEntities: db.prepare('DELETE FROM entities'),
|
|
230
|
+
// Link operations
|
|
231
|
+
insertLink: db.prepare(`
|
|
232
|
+
INSERT INTO links (source_path, target, target_path, line_number)
|
|
233
|
+
VALUES (?, ?, ?, ?)
|
|
234
|
+
`),
|
|
235
|
+
deleteLinksFromSource: db.prepare('DELETE FROM links WHERE source_path = ?'),
|
|
236
|
+
getBacklinks: db.prepare(`
|
|
237
|
+
SELECT id, source_path, target, target_path, line_number
|
|
238
|
+
FROM links WHERE target_path = ?
|
|
239
|
+
`),
|
|
240
|
+
getOutlinks: db.prepare(`
|
|
241
|
+
SELECT id, source_path, target, target_path, line_number
|
|
242
|
+
FROM links WHERE source_path = ?
|
|
243
|
+
`),
|
|
244
|
+
clearLinks: db.prepare('DELETE FROM links'),
|
|
245
|
+
// Recency operations
|
|
246
|
+
upsertRecency: db.prepare(`
|
|
247
|
+
INSERT INTO recency (entity_name_lower, last_mentioned_at, mention_count)
|
|
248
|
+
VALUES (?, ?, 1)
|
|
249
|
+
ON CONFLICT(entity_name_lower) DO UPDATE SET
|
|
250
|
+
last_mentioned_at = excluded.last_mentioned_at,
|
|
251
|
+
mention_count = mention_count + 1
|
|
252
|
+
`),
|
|
253
|
+
getRecency: db.prepare(`
|
|
254
|
+
SELECT entity_name_lower, last_mentioned_at, mention_count
|
|
255
|
+
FROM recency WHERE entity_name_lower = ?
|
|
256
|
+
`),
|
|
257
|
+
getAllRecency: db.prepare(`
|
|
258
|
+
SELECT entity_name_lower, last_mentioned_at, mention_count
|
|
259
|
+
FROM recency ORDER BY last_mentioned_at DESC
|
|
260
|
+
`),
|
|
261
|
+
clearRecency: db.prepare('DELETE FROM recency'),
|
|
262
|
+
// Crank state operations
|
|
263
|
+
setCrankState: db.prepare(`
|
|
264
|
+
INSERT INTO crank_state (key, value, updated_at)
|
|
265
|
+
VALUES (?, ?, datetime('now'))
|
|
266
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
267
|
+
value = excluded.value,
|
|
268
|
+
updated_at = datetime('now')
|
|
269
|
+
`),
|
|
270
|
+
getCrankState: db.prepare('SELECT value FROM crank_state WHERE key = ?'),
|
|
271
|
+
deleteCrankState: db.prepare('DELETE FROM crank_state WHERE key = ?'),
|
|
272
|
+
// Notes operations
|
|
273
|
+
insertNote: db.prepare(`
|
|
274
|
+
INSERT INTO notes (path, title, content_hash, modified_at, aliases_json, tags_json)
|
|
275
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
276
|
+
`),
|
|
277
|
+
updateNote: db.prepare(`
|
|
278
|
+
UPDATE notes
|
|
279
|
+
SET title = ?, content_hash = ?, modified_at = ?, aliases_json = ?, tags_json = ?
|
|
280
|
+
WHERE path = ?
|
|
281
|
+
`),
|
|
282
|
+
deleteNote: db.prepare('DELETE FROM notes WHERE path = ?'),
|
|
283
|
+
getNoteByPath: db.prepare(`
|
|
284
|
+
SELECT id, path, title, content_hash, modified_at, aliases_json, tags_json
|
|
285
|
+
FROM notes WHERE path = ?
|
|
286
|
+
`),
|
|
287
|
+
getAllNotes: db.prepare(`
|
|
288
|
+
SELECT id, path, title, content_hash, modified_at, aliases_json, tags_json
|
|
289
|
+
FROM notes ORDER BY path
|
|
290
|
+
`),
|
|
291
|
+
clearNotes: db.prepare('DELETE FROM notes'),
|
|
292
|
+
// Metadata operations
|
|
293
|
+
getMetadataValue: db.prepare('SELECT value FROM metadata WHERE key = ?'),
|
|
294
|
+
setMetadataValue: db.prepare(`
|
|
295
|
+
INSERT INTO metadata (key, value, updated_at)
|
|
296
|
+
VALUES (?, ?, datetime('now'))
|
|
297
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
298
|
+
value = excluded.value,
|
|
299
|
+
updated_at = datetime('now')
|
|
300
|
+
`),
|
|
301
|
+
// Transactions
|
|
302
|
+
bulkInsertEntities: db.transaction((entities, category) => {
|
|
303
|
+
let count = 0;
|
|
304
|
+
for (const entity of entities) {
|
|
305
|
+
stateDb.insertEntity.run(entity.name, entity.name.toLowerCase(), entity.path, category, JSON.stringify(entity.aliases), entity.hubScore ?? 0);
|
|
306
|
+
count++;
|
|
307
|
+
}
|
|
308
|
+
return count;
|
|
309
|
+
}),
|
|
310
|
+
bulkInsertLinks: db.transaction((links) => {
|
|
311
|
+
let count = 0;
|
|
312
|
+
for (const link of links) {
|
|
313
|
+
stateDb.insertLink.run(link.sourcePath, link.target, link.targetPath, link.lineNumber);
|
|
314
|
+
count++;
|
|
315
|
+
}
|
|
316
|
+
return count;
|
|
317
|
+
}),
|
|
318
|
+
replaceAllEntities: db.transaction((index) => {
|
|
319
|
+
// Clear existing entities
|
|
320
|
+
stateDb.clearEntities.run();
|
|
321
|
+
// Insert all entities by category
|
|
322
|
+
const categories = [
|
|
323
|
+
'technologies', 'acronyms', 'people', 'projects',
|
|
324
|
+
'organizations', 'locations', 'concepts', 'other'
|
|
325
|
+
];
|
|
326
|
+
let total = 0;
|
|
327
|
+
for (const category of categories) {
|
|
328
|
+
const entities = index[category];
|
|
329
|
+
if (!entities?.length)
|
|
330
|
+
continue;
|
|
331
|
+
for (const entity of entities) {
|
|
332
|
+
// Handle both string and EntityWithAliases formats
|
|
333
|
+
const entityObj = typeof entity === 'string'
|
|
334
|
+
? { name: entity, path: '', aliases: [], hubScore: 0 }
|
|
335
|
+
: entity;
|
|
336
|
+
stateDb.insertEntity.run(entityObj.name, entityObj.name.toLowerCase(), entityObj.path, category, JSON.stringify(entityObj.aliases), entityObj.hubScore ?? 0);
|
|
337
|
+
total++;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Update metadata
|
|
341
|
+
stateDb.setMetadataValue.run('entities_built_at', new Date().toISOString());
|
|
342
|
+
stateDb.setMetadataValue.run('entity_count', String(total));
|
|
343
|
+
return total;
|
|
344
|
+
}),
|
|
345
|
+
close: () => {
|
|
346
|
+
db.close();
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
return stateDb;
|
|
350
|
+
}
|
|
351
|
+
// =============================================================================
|
|
352
|
+
// Entity Operations
|
|
353
|
+
// =============================================================================
|
|
354
|
+
/**
|
|
355
|
+
* Search entities using FTS5 with porter stemming
|
|
356
|
+
*
|
|
357
|
+
* @param stateDb - State database instance
|
|
358
|
+
* @param query - Search query (supports FTS5 syntax)
|
|
359
|
+
* @param limit - Maximum results to return
|
|
360
|
+
* @returns Array of matching entities with relevance scores
|
|
361
|
+
*/
|
|
362
|
+
export function searchEntities(stateDb, query, limit = 20) {
|
|
363
|
+
const escapedQuery = escapeFts5Query(query);
|
|
364
|
+
// Handle empty query - return empty results
|
|
365
|
+
if (!escapedQuery) {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
const rows = stateDb.searchEntitiesFts.all(escapedQuery, limit);
|
|
369
|
+
return rows.map(row => ({
|
|
370
|
+
id: row.id,
|
|
371
|
+
name: row.name,
|
|
372
|
+
nameLower: row.name_lower,
|
|
373
|
+
path: row.path,
|
|
374
|
+
category: row.category,
|
|
375
|
+
aliases: row.aliases_json ? JSON.parse(row.aliases_json) : [],
|
|
376
|
+
hubScore: row.hub_score,
|
|
377
|
+
rank: row.rank,
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Search entities by prefix for autocomplete
|
|
382
|
+
*
|
|
383
|
+
* @param stateDb - State database instance
|
|
384
|
+
* @param prefix - Prefix to search for
|
|
385
|
+
* @param limit - Maximum results to return
|
|
386
|
+
*/
|
|
387
|
+
export function searchEntitiesPrefix(stateDb, prefix, limit = 20) {
|
|
388
|
+
return searchEntities(stateDb, `${escapeFts5Query(prefix)}*`, limit);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get entity by exact name (case-insensitive)
|
|
392
|
+
*/
|
|
393
|
+
export function getEntityByName(stateDb, name) {
|
|
394
|
+
const row = stateDb.getEntityByName.get(name.toLowerCase());
|
|
395
|
+
if (!row)
|
|
396
|
+
return null;
|
|
397
|
+
return {
|
|
398
|
+
id: row.id,
|
|
399
|
+
name: row.name,
|
|
400
|
+
nameLower: row.name_lower,
|
|
401
|
+
path: row.path,
|
|
402
|
+
category: row.category,
|
|
403
|
+
aliases: row.aliases_json ? JSON.parse(row.aliases_json) : [],
|
|
404
|
+
hubScore: row.hub_score,
|
|
405
|
+
rank: 0,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Get all entities from the database
|
|
410
|
+
*/
|
|
411
|
+
export function getAllEntitiesFromDb(stateDb) {
|
|
412
|
+
const rows = stateDb.getAllEntities.all();
|
|
413
|
+
return rows.map(row => ({
|
|
414
|
+
id: row.id,
|
|
415
|
+
name: row.name,
|
|
416
|
+
nameLower: row.name_lower,
|
|
417
|
+
path: row.path,
|
|
418
|
+
category: row.category,
|
|
419
|
+
aliases: row.aliases_json ? JSON.parse(row.aliases_json) : [],
|
|
420
|
+
hubScore: row.hub_score,
|
|
421
|
+
rank: 0,
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Convert database entities back to EntityIndex format
|
|
426
|
+
*/
|
|
427
|
+
export function getEntityIndexFromDb(stateDb) {
|
|
428
|
+
const entities = getAllEntitiesFromDb(stateDb);
|
|
429
|
+
const index = {
|
|
430
|
+
technologies: [],
|
|
431
|
+
acronyms: [],
|
|
432
|
+
people: [],
|
|
433
|
+
projects: [],
|
|
434
|
+
organizations: [],
|
|
435
|
+
locations: [],
|
|
436
|
+
concepts: [],
|
|
437
|
+
other: [],
|
|
438
|
+
_metadata: {
|
|
439
|
+
total_entities: entities.length,
|
|
440
|
+
generated_at: new Date().toISOString(),
|
|
441
|
+
vault_path: stateDb.vaultPath,
|
|
442
|
+
source: 'vault-core sqlite',
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
for (const entity of entities) {
|
|
446
|
+
const entityObj = {
|
|
447
|
+
name: entity.name,
|
|
448
|
+
path: entity.path,
|
|
449
|
+
aliases: entity.aliases,
|
|
450
|
+
hubScore: entity.hubScore,
|
|
451
|
+
};
|
|
452
|
+
index[entity.category].push(entityObj);
|
|
453
|
+
}
|
|
454
|
+
return index;
|
|
455
|
+
}
|
|
456
|
+
// =============================================================================
|
|
457
|
+
// Link Operations
|
|
458
|
+
// =============================================================================
|
|
459
|
+
/**
|
|
460
|
+
* Get all notes that link to a given path (backlinks)
|
|
461
|
+
*/
|
|
462
|
+
export function getBacklinks(stateDb, targetPath) {
|
|
463
|
+
const rows = stateDb.getBacklinks.all(targetPath);
|
|
464
|
+
return rows.map(row => ({
|
|
465
|
+
id: row.id,
|
|
466
|
+
sourcePath: row.source_path,
|
|
467
|
+
target: row.target,
|
|
468
|
+
targetPath: row.target_path,
|
|
469
|
+
lineNumber: row.line_number,
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get all links from a given note (outlinks)
|
|
474
|
+
*/
|
|
475
|
+
export function getOutlinks(stateDb, sourcePath) {
|
|
476
|
+
const rows = stateDb.getOutlinks.all(sourcePath);
|
|
477
|
+
return rows.map(row => ({
|
|
478
|
+
id: row.id,
|
|
479
|
+
sourcePath: row.source_path,
|
|
480
|
+
target: row.target,
|
|
481
|
+
targetPath: row.target_path,
|
|
482
|
+
lineNumber: row.line_number,
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Replace all links from a source note
|
|
487
|
+
*/
|
|
488
|
+
export function replaceLinksFromSource(stateDb, sourcePath, links) {
|
|
489
|
+
const transaction = stateDb.db.transaction(() => {
|
|
490
|
+
stateDb.deleteLinksFromSource.run(sourcePath);
|
|
491
|
+
for (const link of links) {
|
|
492
|
+
stateDb.insertLink.run(sourcePath, link.target, link.targetPath, link.lineNumber);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
transaction();
|
|
496
|
+
}
|
|
497
|
+
// =============================================================================
|
|
498
|
+
// Recency Operations
|
|
499
|
+
// =============================================================================
|
|
500
|
+
/**
|
|
501
|
+
* Record a mention of an entity
|
|
502
|
+
*/
|
|
503
|
+
export function recordEntityMention(stateDb, entityName, mentionedAt = new Date()) {
|
|
504
|
+
stateDb.upsertRecency.run(entityName.toLowerCase(), mentionedAt.getTime());
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Get recency info for an entity
|
|
508
|
+
*/
|
|
509
|
+
export function getEntityRecency(stateDb, entityName) {
|
|
510
|
+
const row = stateDb.getRecency.get(entityName.toLowerCase());
|
|
511
|
+
if (!row)
|
|
512
|
+
return null;
|
|
513
|
+
return {
|
|
514
|
+
entityNameLower: row.entity_name_lower,
|
|
515
|
+
lastMentionedAt: row.last_mentioned_at,
|
|
516
|
+
mentionCount: row.mention_count,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Get all recency data ordered by most recent
|
|
521
|
+
*/
|
|
522
|
+
export function getAllRecency(stateDb) {
|
|
523
|
+
const rows = stateDb.getAllRecency.all();
|
|
524
|
+
return rows.map(row => ({
|
|
525
|
+
entityNameLower: row.entity_name_lower,
|
|
526
|
+
lastMentionedAt: row.last_mentioned_at,
|
|
527
|
+
mentionCount: row.mention_count,
|
|
528
|
+
}));
|
|
529
|
+
}
|
|
530
|
+
// =============================================================================
|
|
531
|
+
// Crank State Operations
|
|
532
|
+
// =============================================================================
|
|
533
|
+
/**
|
|
534
|
+
* Set a crank state value
|
|
535
|
+
*/
|
|
536
|
+
export function setCrankState(stateDb, key, value) {
|
|
537
|
+
stateDb.setCrankState.run(key, JSON.stringify(value));
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Get a crank state value
|
|
541
|
+
*/
|
|
542
|
+
export function getCrankState(stateDb, key) {
|
|
543
|
+
const row = stateDb.getCrankState.get(key);
|
|
544
|
+
if (!row)
|
|
545
|
+
return null;
|
|
546
|
+
return JSON.parse(row.value);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Delete a crank state key
|
|
550
|
+
*/
|
|
551
|
+
export function deleteCrankState(stateDb, key) {
|
|
552
|
+
stateDb.deleteCrankState.run(key);
|
|
553
|
+
}
|
|
554
|
+
// =============================================================================
|
|
555
|
+
// Metadata Operations
|
|
556
|
+
// =============================================================================
|
|
557
|
+
/**
|
|
558
|
+
* Get database metadata
|
|
559
|
+
*/
|
|
560
|
+
export function getStateDbMetadata(stateDb) {
|
|
561
|
+
const schemaRow = stateDb.db.prepare('SELECT MAX(version) as version FROM schema_version').get();
|
|
562
|
+
const entitiesBuiltRow = stateDb.getMetadataValue.get('entities_built_at');
|
|
563
|
+
const entityCountRow = stateDb.getMetadataValue.get('entity_count');
|
|
564
|
+
const notesBuiltRow = stateDb.getMetadataValue.get('notes_built_at');
|
|
565
|
+
const noteCountRow = stateDb.getMetadataValue.get('note_count');
|
|
566
|
+
return {
|
|
567
|
+
schemaVersion: schemaRow?.version ?? 0,
|
|
568
|
+
entitiesBuiltAt: entitiesBuiltRow?.value ?? null,
|
|
569
|
+
entityCount: entityCountRow ? parseInt(entityCountRow.value, 10) : 0,
|
|
570
|
+
notesBuiltAt: notesBuiltRow?.value ?? null,
|
|
571
|
+
noteCount: noteCountRow ? parseInt(noteCountRow.value, 10) : 0,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Check if entity data is stale (older than threshold)
|
|
576
|
+
*/
|
|
577
|
+
export function isEntityDataStale(stateDb, thresholdMs = 60 * 60 * 1000 // 1 hour default
|
|
578
|
+
) {
|
|
579
|
+
const metadata = getStateDbMetadata(stateDb);
|
|
580
|
+
if (!metadata.entitiesBuiltAt) {
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
const builtAt = new Date(metadata.entitiesBuiltAt).getTime();
|
|
584
|
+
const age = Date.now() - builtAt;
|
|
585
|
+
return age > thresholdMs;
|
|
586
|
+
}
|
|
587
|
+
// =============================================================================
|
|
588
|
+
// Utility Functions
|
|
589
|
+
// =============================================================================
|
|
590
|
+
/**
|
|
591
|
+
* Escape special FTS5 characters in a query
|
|
592
|
+
*/
|
|
593
|
+
export function escapeFts5Query(query) {
|
|
594
|
+
// Handle empty query
|
|
595
|
+
if (!query || !query.trim()) {
|
|
596
|
+
return '';
|
|
597
|
+
}
|
|
598
|
+
// Remove or escape FTS5 special characters
|
|
599
|
+
// Keep * for prefix matching, escape others
|
|
600
|
+
return query
|
|
601
|
+
.replace(/"/g, '""') // Escape quotes
|
|
602
|
+
.replace(/[(){}[\]^~:-]/g, ' ') // Remove special operators including hyphen
|
|
603
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
604
|
+
.trim();
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Check if the state database exists for a vault
|
|
608
|
+
*/
|
|
609
|
+
export function stateDbExists(vaultPath) {
|
|
610
|
+
const dbPath = getStateDbPath(vaultPath);
|
|
611
|
+
return fs.existsSync(dbPath);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Delete the state database (for testing or reset)
|
|
615
|
+
*/
|
|
616
|
+
export function deleteStateDb(vaultPath) {
|
|
617
|
+
const dbPath = getStateDbPath(vaultPath);
|
|
618
|
+
if (fs.existsSync(dbPath)) {
|
|
619
|
+
fs.unlinkSync(dbPath);
|
|
620
|
+
}
|
|
621
|
+
// Also remove WAL and SHM files if they exist
|
|
622
|
+
const walPath = dbPath + '-wal';
|
|
623
|
+
const shmPath = dbPath + '-shm';
|
|
624
|
+
if (fs.existsSync(walPath))
|
|
625
|
+
fs.unlinkSync(walPath);
|
|
626
|
+
if (fs.existsSync(shmPath))
|
|
627
|
+
fs.unlinkSync(shmPath);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Get default legacy file paths for a vault
|
|
631
|
+
*/
|
|
632
|
+
export function getLegacyPaths(vaultPath) {
|
|
633
|
+
const claudeDir = path.join(vaultPath, '.claude');
|
|
634
|
+
return {
|
|
635
|
+
entities: path.join(claudeDir, 'wikilink-entities.json'),
|
|
636
|
+
recency: path.join(claudeDir, 'entity-recency.json'),
|
|
637
|
+
lastCommit: path.join(claudeDir, 'last-crank-commit.json'),
|
|
638
|
+
hints: path.join(claudeDir, 'crank-mutation-hints.json'),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Migrate legacy JSON files to SQLite state database
|
|
643
|
+
*
|
|
644
|
+
* This function reads existing JSON state files and imports them
|
|
645
|
+
* into the consolidated SQLite database. It does NOT delete the
|
|
646
|
+
* original JSON files - that should be done manually after verifying
|
|
647
|
+
* the migration was successful.
|
|
648
|
+
*
|
|
649
|
+
* @param stateDb - Open state database
|
|
650
|
+
* @param legacyPaths - Paths to legacy JSON files
|
|
651
|
+
* @returns Migration result with counts and any errors
|
|
652
|
+
*/
|
|
653
|
+
export async function migrateFromJsonToSqlite(stateDb, legacyPaths) {
|
|
654
|
+
const result = {
|
|
655
|
+
success: true,
|
|
656
|
+
entitiesMigrated: 0,
|
|
657
|
+
recencyMigrated: 0,
|
|
658
|
+
crankStateMigrated: 0,
|
|
659
|
+
errors: [],
|
|
660
|
+
};
|
|
661
|
+
// Migrate entities
|
|
662
|
+
if (legacyPaths.entities && fs.existsSync(legacyPaths.entities)) {
|
|
663
|
+
try {
|
|
664
|
+
const content = fs.readFileSync(legacyPaths.entities, 'utf-8');
|
|
665
|
+
const index = JSON.parse(content);
|
|
666
|
+
result.entitiesMigrated = stateDb.replaceAllEntities(index);
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
result.errors.push(`Failed to migrate entities: ${error}`);
|
|
670
|
+
result.success = false;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// Migrate recency data
|
|
674
|
+
if (legacyPaths.recency && fs.existsSync(legacyPaths.recency)) {
|
|
675
|
+
try {
|
|
676
|
+
const content = fs.readFileSync(legacyPaths.recency, 'utf-8');
|
|
677
|
+
const data = JSON.parse(content);
|
|
678
|
+
for (const [entityName, timestamp] of Object.entries(data.lastMentioned)) {
|
|
679
|
+
recordEntityMention(stateDb, entityName, new Date(timestamp));
|
|
680
|
+
result.recencyMigrated++;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
result.errors.push(`Failed to migrate recency: ${error}`);
|
|
685
|
+
result.success = false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// Migrate last commit tracking
|
|
689
|
+
if (legacyPaths.lastCommit && fs.existsSync(legacyPaths.lastCommit)) {
|
|
690
|
+
try {
|
|
691
|
+
const content = fs.readFileSync(legacyPaths.lastCommit, 'utf-8');
|
|
692
|
+
const data = JSON.parse(content);
|
|
693
|
+
setCrankState(stateDb, 'last_commit', data);
|
|
694
|
+
result.crankStateMigrated++;
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
result.errors.push(`Failed to migrate last commit: ${error}`);
|
|
698
|
+
result.success = false;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// Migrate mutation hints
|
|
702
|
+
if (legacyPaths.hints && fs.existsSync(legacyPaths.hints)) {
|
|
703
|
+
try {
|
|
704
|
+
const content = fs.readFileSync(legacyPaths.hints, 'utf-8');
|
|
705
|
+
const data = JSON.parse(content);
|
|
706
|
+
setCrankState(stateDb, 'mutation_hints', data);
|
|
707
|
+
result.crankStateMigrated++;
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
result.errors.push(`Failed to migrate hints: ${error}`);
|
|
711
|
+
result.success = false;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return result;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Backup legacy JSON files before migration
|
|
718
|
+
*
|
|
719
|
+
* Creates .bak files alongside the originals
|
|
720
|
+
*/
|
|
721
|
+
export function backupLegacyFiles(legacyPaths) {
|
|
722
|
+
const backedUp = [];
|
|
723
|
+
for (const [, filePath] of Object.entries(legacyPaths)) {
|
|
724
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
725
|
+
const backupPath = filePath + '.bak';
|
|
726
|
+
fs.copyFileSync(filePath, backupPath);
|
|
727
|
+
backedUp.push(filePath);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return backedUp;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Delete legacy JSON files after successful migration
|
|
734
|
+
*
|
|
735
|
+
* Only deletes files that have corresponding .bak backups
|
|
736
|
+
*/
|
|
737
|
+
export function deleteLegacyFiles(legacyPaths) {
|
|
738
|
+
const deleted = [];
|
|
739
|
+
for (const [, filePath] of Object.entries(legacyPaths)) {
|
|
740
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
741
|
+
const backupPath = filePath + '.bak';
|
|
742
|
+
// Only delete if backup exists (safety check)
|
|
743
|
+
if (fs.existsSync(backupPath)) {
|
|
744
|
+
fs.unlinkSync(filePath);
|
|
745
|
+
deleted.push(filePath);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return deleted;
|
|
750
|
+
}
|
|
751
|
+
//# sourceMappingURL=sqlite.js.map
|