@velvetmonkey/vault-core 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/migrations.d.ts +34 -0
- package/dist/migrations.js +476 -1
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +34 -31
- package/dist/sqlite.d.ts +1 -1
- package/dist/sqlite.js +53 -1
- package/dist/wikilinks.js +4 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,6 @@ export { parseMarkdown } from './parseMarkdown.js';
|
|
|
16
16
|
export { getProtectedZonesFromAst } from './astProtectedZones.js';
|
|
17
17
|
export { OperationLogger, createLoggerFromConfig, generateSessionId, getSessionId, setSessionId, } from './logging/index.js';
|
|
18
18
|
export type { OperationLogEntry, SessionMetrics, AggregatedMetrics, LoggingConfig, ProductId, } from './logging/index.js';
|
|
19
|
-
export { openStateDb, deleteStateDb, stateDbExists, searchEntities, searchEntitiesPrefix, getEntityByName, getEntitiesByAlias, getAllEntitiesFromDb, getEntityIndexFromDb, recordEntityMention, getEntityRecency, getAllRecency, setWriteState, getWriteState, deleteWriteState, setFlywheelConfig, getFlywheelConfig, getAllFlywheelConfig, saveFlywheelConfigToDb, loadFlywheelConfigFromDb, getStateDbMetadata, recordMergeDismissal, getDismissedMergePairs, saveVaultIndexCache, loadVaultIndexCache, getVaultIndexCacheInfo, rebuildEntitiesFts, loadContentHashes, saveContentHashBatch, renameContentHash, deleteStateDbFiles, backupStateDb, preserveCorruptedDb, BACKUP_ROTATION_COUNT, SALVAGE_TABLES, rotateBackupFiles, safeBackupAsync, checkDbIntegrity, salvageFeedbackTables, attemptSalvage, SCHEMA_VERSION, STATE_DB_FILENAME, FLYWHEEL_DIR, } from './sqlite.js';
|
|
19
|
+
export { openStateDb, deleteStateDb, stateDbExists, searchEntities, searchEntitiesPrefix, getEntityByName, getEntitiesByAlias, getAllEntitiesFromDb, getEntityIndexFromDb, recordEntityMention, getEntityRecency, getAllRecency, setWriteState, getWriteState, deleteWriteState, setFlywheelConfig, getFlywheelConfig, getAllFlywheelConfig, saveFlywheelConfigToDb, loadFlywheelConfigFromDb, getStateDbMetadata, recordMergeDismissal, getDismissedMergePairs, saveVaultIndexCache, loadVaultIndexCache, getVaultIndexCacheInfo, rebuildEntitiesFts, loadContentHashes, saveContentHashBatch, renameContentHash, deleteStateDbFiles, backupStateDb, preserveCorruptedDb, initSchema, migrateV40, BACKUP_ROTATION_COUNT, SALVAGE_TABLES, rotateBackupFiles, safeBackupAsync, checkDbIntegrity, salvageFeedbackTables, attemptSalvage, SCHEMA_VERSION, STATE_DB_FILENAME, FLYWHEEL_DIR, } from './sqlite.js';
|
|
20
20
|
export type { StateDb, EntitySearchResult, RecencyRow, StateDbMetadata, VaultIndexCacheData, VaultIndexCacheInfo, FlywheelConfigRow, } from './sqlite.js';
|
|
21
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,8 @@ saveVaultIndexCache, loadVaultIndexCache, getVaultIndexCacheInfo, rebuildEntitie
|
|
|
35
35
|
loadContentHashes, saveContentHashBatch, renameContentHash,
|
|
36
36
|
// Database file management
|
|
37
37
|
deleteStateDbFiles, backupStateDb, preserveCorruptedDb,
|
|
38
|
+
// Migrations (exported for tests)
|
|
39
|
+
initSchema, migrateV40,
|
|
38
40
|
// Backup & Recovery
|
|
39
41
|
BACKUP_ROTATION_COUNT, SALVAGE_TABLES, rotateBackupFiles, safeBackupAsync, checkDbIntegrity, salvageFeedbackTables, attemptSalvage,
|
|
40
42
|
// Constants
|
package/dist/migrations.d.ts
CHANGED
|
@@ -17,6 +17,40 @@ export declare function getStateDbPath(vaultPath: string): string;
|
|
|
17
17
|
* Initialize schema and run migrations
|
|
18
18
|
*/
|
|
19
19
|
export declare function initSchema(db: Database.Database): void;
|
|
20
|
+
/**
|
|
21
|
+
* Run the v40 migration: add COLLATE NOCASE to path columns across 14 tables,
|
|
22
|
+
* collapsing mixed-case duplicates with table-specific conflict resolution.
|
|
23
|
+
*
|
|
24
|
+
* Safety:
|
|
25
|
+
* - Wrapped in a single db.transaction(). better-sqlite3 supports transactional
|
|
26
|
+
* DDL (CREATE/DROP/ALTER RENAME participate in transactions), so any rebuild
|
|
27
|
+
* failure rolls back the whole batch. Partial-upgrade state is impossible.
|
|
28
|
+
* - Caller (openStateDb) runs a synchronous VACUUM INTO backup before calling
|
|
29
|
+
* initSchema when upgrading from < v40.
|
|
30
|
+
* - No VACUUM or PRAGMA statements inside the transaction (they auto-commit).
|
|
31
|
+
* - Foreign keys disabled for the duration to permit DROP TABLE on referenced
|
|
32
|
+
* tables. Re-enabled at the end.
|
|
33
|
+
*
|
|
34
|
+
* Conflict resolution per table (see p42 v40 plan S3 for full rationale):
|
|
35
|
+
*
|
|
36
|
+
* | Table | Rule |
|
|
37
|
+
* |------------------------|----------------------------------------------------|
|
|
38
|
+
* | entities | column alter (via rebuild) — no rows to merge |
|
|
39
|
+
* | note_embeddings | MAX(updated_at) |
|
|
40
|
+
* | content_hashes | MAX(updated_at) |
|
|
41
|
+
* | tasks | best-effort MAX(id); file scan reconciles |
|
|
42
|
+
* | note_links | MAX(weight_updated_at), keep matching weight |
|
|
43
|
+
* | note_tags | INSERT OR IGNORE — pure dedup, no values to merge |
|
|
44
|
+
* | note_link_history | MIN(first_seen_at), MAX(edits_survived/last_pos) |
|
|
45
|
+
* | note_moves | column alter — preserve all rows |
|
|
46
|
+
* | suggestion_events | MAX(total_score) per (timestamp, note, entity) |
|
|
47
|
+
* | corrections | column alter — preserve all rows |
|
|
48
|
+
* | prospect_ledger | MIN first_seen, MAX last_seen, SUM sightings |
|
|
49
|
+
* | proactive_queue | MAX(score), prefer 'pending' status |
|
|
50
|
+
* | retrieval_cooccurrence | SUM(weight), MIN(timestamp) |
|
|
51
|
+
* | wikilink_feedback | column alter — preserve all rows |
|
|
52
|
+
*/
|
|
53
|
+
export declare function migrateV40(db: Database.Database): boolean;
|
|
20
54
|
export declare function deleteStateDbFiles(dbPath: string): void;
|
|
21
55
|
/** Back up state.db before opening (skip if missing or 0 bytes). */
|
|
22
56
|
export declare function backupStateDb(dbPath: string): void;
|
package/dist/migrations.js
CHANGED
|
@@ -336,8 +336,483 @@ export function initSchema(db) {
|
|
|
336
336
|
}
|
|
337
337
|
db.prepare('INSERT OR REPLACE INTO schema_version (version) VALUES (?)').run(38);
|
|
338
338
|
}
|
|
339
|
-
|
|
339
|
+
// v39: case-insensitive note_path on wikilink_applications unique index.
|
|
340
|
+
// On Windows NTFS / macOS APFS, `Flywheel.md` and `flywheel.md` are the
|
|
341
|
+
// same physical file. Without COLLATE NOCASE on note_path, mixed-case
|
|
342
|
+
// rows were legal and the same application could be recorded twice,
|
|
343
|
+
// doubling counts and breaking dedup in the doctor report (P42 issue 1).
|
|
344
|
+
if (currentVersion < 39) {
|
|
345
|
+
db.exec('DROP INDEX IF EXISTS idx_wl_apps_unique');
|
|
346
|
+
// Collapse any pre-existing duplicates before re-adding the unique index.
|
|
347
|
+
// Keep the lowest-id row per (entity NOCASE, note_path NOCASE) group.
|
|
348
|
+
db.exec(`
|
|
349
|
+
DELETE FROM wikilink_applications
|
|
350
|
+
WHERE id NOT IN (
|
|
351
|
+
SELECT MIN(id)
|
|
352
|
+
FROM wikilink_applications
|
|
353
|
+
GROUP BY LOWER(entity), LOWER(note_path)
|
|
354
|
+
)
|
|
355
|
+
`);
|
|
356
|
+
db.exec(`
|
|
357
|
+
CREATE UNIQUE INDEX idx_wl_apps_unique
|
|
358
|
+
ON wikilink_applications(entity COLLATE NOCASE, note_path COLLATE NOCASE)
|
|
359
|
+
`);
|
|
360
|
+
db.prepare('INSERT OR REPLACE INTO schema_version (version) VALUES (?)').run(39);
|
|
361
|
+
}
|
|
362
|
+
// v40: COLLATE NOCASE rollout across 14 more path columns.
|
|
363
|
+
// Case-insensitive filesystems (Windows NTFS, macOS APFS default) treat
|
|
364
|
+
// "Flywheel.md" and "flywheel.md" as the same file, but without collation
|
|
365
|
+
// both mixed-case variants could land in the state DB. v40 rebuilds the
|
|
366
|
+
// affected tables with COLLATE NOCASE on their path columns and collapses
|
|
367
|
+
// pre-existing dupes per table-specific rules (see migrateV40 below).
|
|
368
|
+
let v40Applied = true;
|
|
369
|
+
if (currentVersion < 40) {
|
|
370
|
+
v40Applied = migrateV40(db);
|
|
371
|
+
if (v40Applied) {
|
|
372
|
+
db.prepare('INSERT OR REPLACE INTO schema_version (version) VALUES (?)').run(40);
|
|
373
|
+
}
|
|
374
|
+
// Dry-run path: schema_version stays at 39. Server boots in degraded state.
|
|
375
|
+
}
|
|
376
|
+
// Only stamp SCHEMA_VERSION at the end if every migration ran. Dry-run
|
|
377
|
+
// skips v40 → leave schema_version at 39 so the next non-dry-run boot
|
|
378
|
+
// re-enters the v40 branch.
|
|
379
|
+
if (v40Applied) {
|
|
380
|
+
db.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(SCHEMA_VERSION);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// =============================================================================
|
|
385
|
+
// v40 Migration: COLLATE NOCASE rollout
|
|
386
|
+
// =============================================================================
|
|
387
|
+
/**
|
|
388
|
+
* Run the v40 migration: add COLLATE NOCASE to path columns across 14 tables,
|
|
389
|
+
* collapsing mixed-case duplicates with table-specific conflict resolution.
|
|
390
|
+
*
|
|
391
|
+
* Safety:
|
|
392
|
+
* - Wrapped in a single db.transaction(). better-sqlite3 supports transactional
|
|
393
|
+
* DDL (CREATE/DROP/ALTER RENAME participate in transactions), so any rebuild
|
|
394
|
+
* failure rolls back the whole batch. Partial-upgrade state is impossible.
|
|
395
|
+
* - Caller (openStateDb) runs a synchronous VACUUM INTO backup before calling
|
|
396
|
+
* initSchema when upgrading from < v40.
|
|
397
|
+
* - No VACUUM or PRAGMA statements inside the transaction (they auto-commit).
|
|
398
|
+
* - Foreign keys disabled for the duration to permit DROP TABLE on referenced
|
|
399
|
+
* tables. Re-enabled at the end.
|
|
400
|
+
*
|
|
401
|
+
* Conflict resolution per table (see p42 v40 plan S3 for full rationale):
|
|
402
|
+
*
|
|
403
|
+
* | Table | Rule |
|
|
404
|
+
* |------------------------|----------------------------------------------------|
|
|
405
|
+
* | entities | column alter (via rebuild) — no rows to merge |
|
|
406
|
+
* | note_embeddings | MAX(updated_at) |
|
|
407
|
+
* | content_hashes | MAX(updated_at) |
|
|
408
|
+
* | tasks | best-effort MAX(id); file scan reconciles |
|
|
409
|
+
* | note_links | MAX(weight_updated_at), keep matching weight |
|
|
410
|
+
* | note_tags | INSERT OR IGNORE — pure dedup, no values to merge |
|
|
411
|
+
* | note_link_history | MIN(first_seen_at), MAX(edits_survived/last_pos) |
|
|
412
|
+
* | note_moves | column alter — preserve all rows |
|
|
413
|
+
* | suggestion_events | MAX(total_score) per (timestamp, note, entity) |
|
|
414
|
+
* | corrections | column alter — preserve all rows |
|
|
415
|
+
* | prospect_ledger | MIN first_seen, MAX last_seen, SUM sightings |
|
|
416
|
+
* | proactive_queue | MAX(score), prefer 'pending' status |
|
|
417
|
+
* | retrieval_cooccurrence | SUM(weight), MIN(timestamp) |
|
|
418
|
+
* | wikilink_feedback | column alter — preserve all rows |
|
|
419
|
+
*/
|
|
420
|
+
export function migrateV40(db) {
|
|
421
|
+
// Log pre-migration collision counts so users see what's about to collapse.
|
|
422
|
+
// Counts are best-effort: tables that don't exist yet (fresh DB) are skipped.
|
|
423
|
+
const collisionProbes = [
|
|
424
|
+
{ table: 'entities', pathCol: 'path' },
|
|
425
|
+
{ table: 'note_embeddings', pathCol: 'path' },
|
|
426
|
+
{ table: 'content_hashes', pathCol: 'path' },
|
|
427
|
+
{ table: 'tasks', pathCol: 'path', extraCols: 'line' },
|
|
428
|
+
{ table: 'note_links', pathCol: 'note_path', extraCols: 'target' },
|
|
429
|
+
{ table: 'note_tags', pathCol: 'note_path', extraCols: 'tag' },
|
|
430
|
+
{ table: 'note_link_history', pathCol: 'note_path', extraCols: 'target' },
|
|
431
|
+
{ table: 'suggestion_events', pathCol: 'note_path', extraCols: 'timestamp, entity' },
|
|
432
|
+
{ table: 'prospect_ledger', pathCol: 'note_path', extraCols: 'term, seen_day' },
|
|
433
|
+
{ table: 'proactive_queue', pathCol: 'note_path', extraCols: 'entity' },
|
|
434
|
+
{ table: 'retrieval_cooccurrence', pathCol: 'note_a', extraCols: 'note_b, session_id' },
|
|
435
|
+
];
|
|
436
|
+
const collisions = [];
|
|
437
|
+
for (const probe of collisionProbes) {
|
|
438
|
+
try {
|
|
439
|
+
const groupCols = probe.extraCols
|
|
440
|
+
? `LOWER(${probe.pathCol}), ${probe.extraCols}`
|
|
441
|
+
: `LOWER(${probe.pathCol})`;
|
|
442
|
+
const row = db.prepare(`SELECT COUNT(*) AS cnt FROM (
|
|
443
|
+
SELECT 1 FROM ${probe.table}
|
|
444
|
+
GROUP BY ${groupCols}
|
|
445
|
+
HAVING COUNT(*) > 1
|
|
446
|
+
)`).get();
|
|
447
|
+
if (row && row.cnt > 0) {
|
|
448
|
+
collisions.push({ table: probe.table, count: row.cnt });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// Table doesn't exist yet — skip silently.
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (collisions.length > 0) {
|
|
456
|
+
const summary = collisions.map(c => `${c.table}=${c.count}`).join(', ');
|
|
457
|
+
console.error(`[vault-core] v40 migration: collapsing mixed-case duplicates — ${summary}`);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
console.error('[vault-core] v40 migration: no mixed-case duplicates detected');
|
|
461
|
+
}
|
|
462
|
+
if (process.env.FLYWHEEL_MIGRATION_DRY_RUN === '1') {
|
|
463
|
+
console.error('[vault-core] FLYWHEEL_MIGRATION_DRY_RUN=1 — skipping v40 apply, DB stays at v39');
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
// Disable foreign keys for the rebuild. SQLite requires this off when
|
|
467
|
+
// renaming tables that may be referenced by others. Re-enabled after.
|
|
468
|
+
db.pragma('foreign_keys = OFF');
|
|
469
|
+
const runV40 = db.transaction(() => {
|
|
470
|
+
// --- entities: simple rebuild (all existing rows preserved) ---
|
|
471
|
+
db.exec(`
|
|
472
|
+
CREATE TABLE entities_v40_new (
|
|
473
|
+
id INTEGER PRIMARY KEY,
|
|
474
|
+
name TEXT NOT NULL,
|
|
475
|
+
name_lower TEXT NOT NULL,
|
|
476
|
+
path TEXT NOT NULL COLLATE NOCASE,
|
|
477
|
+
category TEXT NOT NULL,
|
|
478
|
+
aliases_json TEXT,
|
|
479
|
+
hub_score INTEGER DEFAULT 0,
|
|
480
|
+
description TEXT
|
|
481
|
+
);
|
|
482
|
+
INSERT INTO entities_v40_new SELECT id, name, name_lower, path, category, aliases_json, hub_score, description FROM entities;
|
|
483
|
+
DROP TABLE entities;
|
|
484
|
+
ALTER TABLE entities_v40_new RENAME TO entities;
|
|
485
|
+
`);
|
|
486
|
+
// --- note_embeddings: dedup by LOWER(path), keep MAX(updated_at).
|
|
487
|
+
// Window function ranks rows per case-folded path by updated_at desc,
|
|
488
|
+
// rowid asc as tie-break. Pick rank 1. ---
|
|
489
|
+
db.exec(`
|
|
490
|
+
CREATE TABLE note_embeddings_v40_new (
|
|
491
|
+
path TEXT PRIMARY KEY COLLATE NOCASE,
|
|
492
|
+
embedding BLOB NOT NULL,
|
|
493
|
+
content_hash TEXT NOT NULL,
|
|
494
|
+
model TEXT NOT NULL,
|
|
495
|
+
updated_at INTEGER NOT NULL
|
|
496
|
+
);
|
|
497
|
+
INSERT INTO note_embeddings_v40_new
|
|
498
|
+
SELECT path, embedding, content_hash, model, updated_at
|
|
499
|
+
FROM (
|
|
500
|
+
SELECT path, embedding, content_hash, model, updated_at,
|
|
501
|
+
ROW_NUMBER() OVER (PARTITION BY LOWER(path) ORDER BY updated_at DESC, rowid ASC) AS rn
|
|
502
|
+
FROM note_embeddings
|
|
503
|
+
) WHERE rn = 1;
|
|
504
|
+
DROP TABLE note_embeddings;
|
|
505
|
+
ALTER TABLE note_embeddings_v40_new RENAME TO note_embeddings;
|
|
506
|
+
`);
|
|
507
|
+
// --- content_hashes: dedup by LOWER(path), keep MAX(updated_at).
|
|
508
|
+
// ROW_NUMBER() picks one row deterministically per case-folded path. ---
|
|
509
|
+
db.exec(`
|
|
510
|
+
CREATE TABLE content_hashes_v40_new (
|
|
511
|
+
path TEXT PRIMARY KEY COLLATE NOCASE,
|
|
512
|
+
hash TEXT NOT NULL,
|
|
513
|
+
updated_at INTEGER NOT NULL
|
|
514
|
+
);
|
|
515
|
+
INSERT INTO content_hashes_v40_new
|
|
516
|
+
SELECT path, hash, updated_at
|
|
517
|
+
FROM (
|
|
518
|
+
SELECT path, hash, updated_at,
|
|
519
|
+
ROW_NUMBER() OVER (PARTITION BY LOWER(path) ORDER BY updated_at DESC, rowid ASC) AS rn
|
|
520
|
+
FROM content_hashes
|
|
521
|
+
) WHERE rn = 1;
|
|
522
|
+
DROP TABLE content_hashes;
|
|
523
|
+
ALTER TABLE content_hashes_v40_new RENAME TO content_hashes;
|
|
524
|
+
`);
|
|
525
|
+
// --- tasks: best-effort dedup by (LOWER(path), line), keep MAX(id).
|
|
526
|
+
// Post-boot file scan repopulates with the canonical filesystem case. ---
|
|
527
|
+
db.exec(`
|
|
528
|
+
CREATE TABLE tasks_v40_new (
|
|
529
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
530
|
+
path TEXT NOT NULL COLLATE NOCASE,
|
|
531
|
+
line INTEGER NOT NULL,
|
|
532
|
+
text TEXT NOT NULL,
|
|
533
|
+
status TEXT NOT NULL,
|
|
534
|
+
raw TEXT NOT NULL,
|
|
535
|
+
context TEXT,
|
|
536
|
+
tags_json TEXT,
|
|
537
|
+
due_date TEXT,
|
|
538
|
+
UNIQUE(path, line)
|
|
539
|
+
);
|
|
540
|
+
INSERT INTO tasks_v40_new
|
|
541
|
+
SELECT id, path, line, text, status, raw, context, tags_json, due_date
|
|
542
|
+
FROM (
|
|
543
|
+
SELECT id, path, line, text, status, raw, context, tags_json, due_date,
|
|
544
|
+
ROW_NUMBER() OVER (PARTITION BY LOWER(path), line ORDER BY id DESC) AS rn
|
|
545
|
+
FROM tasks
|
|
546
|
+
) WHERE rn = 1;
|
|
547
|
+
DROP TABLE tasks;
|
|
548
|
+
ALTER TABLE tasks_v40_new RENAME TO tasks;
|
|
549
|
+
`);
|
|
550
|
+
// --- note_links: dedup by (LOWER(note_path), target), keep row with the
|
|
551
|
+
// latest weight_updated_at. Treat NULL as 0 for ordering so non-NULL
|
|
552
|
+
// wins. rowid tiebreak keeps the pick deterministic. ---
|
|
553
|
+
db.exec(`
|
|
554
|
+
CREATE TABLE note_links_v40_new (
|
|
555
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
556
|
+
target TEXT NOT NULL,
|
|
557
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
558
|
+
weight_updated_at INTEGER,
|
|
559
|
+
PRIMARY KEY (note_path, target)
|
|
560
|
+
);
|
|
561
|
+
INSERT INTO note_links_v40_new
|
|
562
|
+
SELECT note_path, target, weight, weight_updated_at
|
|
563
|
+
FROM (
|
|
564
|
+
SELECT note_path, target, weight, weight_updated_at,
|
|
565
|
+
ROW_NUMBER() OVER (
|
|
566
|
+
PARTITION BY LOWER(note_path), target
|
|
567
|
+
ORDER BY COALESCE(weight_updated_at, 0) DESC, rowid ASC
|
|
568
|
+
) AS rn
|
|
569
|
+
FROM note_links
|
|
570
|
+
) WHERE rn = 1;
|
|
571
|
+
DROP TABLE note_links;
|
|
572
|
+
ALTER TABLE note_links_v40_new RENAME TO note_links;
|
|
573
|
+
`);
|
|
574
|
+
// --- note_tags: pure dedup via INSERT OR IGNORE. No value columns to merge. ---
|
|
575
|
+
db.exec(`
|
|
576
|
+
CREATE TABLE note_tags_v40_new (
|
|
577
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
578
|
+
tag TEXT NOT NULL,
|
|
579
|
+
PRIMARY KEY (note_path, tag)
|
|
580
|
+
);
|
|
581
|
+
INSERT OR IGNORE INTO note_tags_v40_new SELECT note_path, tag FROM note_tags;
|
|
582
|
+
DROP TABLE note_tags;
|
|
583
|
+
ALTER TABLE note_tags_v40_new RENAME TO note_tags;
|
|
584
|
+
`);
|
|
585
|
+
// --- note_link_history: MIN(first_seen_at), MAX(edits_survived, last_positive_at) ---
|
|
586
|
+
db.exec(`
|
|
587
|
+
CREATE TABLE note_link_history_v40_new (
|
|
588
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
589
|
+
target TEXT NOT NULL,
|
|
590
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
591
|
+
edits_survived INTEGER NOT NULL DEFAULT 0,
|
|
592
|
+
last_positive_at TEXT,
|
|
593
|
+
PRIMARY KEY (note_path, target)
|
|
594
|
+
);
|
|
595
|
+
INSERT INTO note_link_history_v40_new
|
|
596
|
+
SELECT MIN(note_path), target, MIN(first_seen_at), MAX(edits_survived), MAX(last_positive_at)
|
|
597
|
+
FROM note_link_history
|
|
598
|
+
GROUP BY LOWER(note_path), target;
|
|
599
|
+
DROP TABLE note_link_history;
|
|
600
|
+
ALTER TABLE note_link_history_v40_new RENAME TO note_link_history;
|
|
601
|
+
`);
|
|
602
|
+
// --- note_moves: column alter via rebuild. All rows preserved (no dedup;
|
|
603
|
+
// history is append-only, old/new paths are legitimately case-variant). ---
|
|
604
|
+
db.exec(`
|
|
605
|
+
CREATE TABLE note_moves_v40_new (
|
|
606
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
607
|
+
old_path TEXT NOT NULL COLLATE NOCASE,
|
|
608
|
+
new_path TEXT NOT NULL COLLATE NOCASE,
|
|
609
|
+
moved_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
610
|
+
old_folder TEXT,
|
|
611
|
+
new_folder TEXT
|
|
612
|
+
);
|
|
613
|
+
INSERT INTO note_moves_v40_new SELECT id, old_path, new_path, moved_at, old_folder, new_folder FROM note_moves;
|
|
614
|
+
DROP TABLE note_moves;
|
|
615
|
+
ALTER TABLE note_moves_v40_new RENAME TO note_moves;
|
|
616
|
+
`);
|
|
617
|
+
// --- suggestion_events: dedup by (timestamp, LOWER(note_path), entity),
|
|
618
|
+
// keep row with MAX(total_score), id DESC tiebreak. ---
|
|
619
|
+
db.exec(`
|
|
620
|
+
CREATE TABLE suggestion_events_v40_new (
|
|
621
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
622
|
+
timestamp INTEGER NOT NULL,
|
|
623
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
624
|
+
entity TEXT NOT NULL,
|
|
625
|
+
total_score REAL NOT NULL,
|
|
626
|
+
breakdown_json TEXT NOT NULL,
|
|
627
|
+
threshold REAL NOT NULL,
|
|
628
|
+
passed INTEGER NOT NULL,
|
|
629
|
+
strictness TEXT NOT NULL,
|
|
630
|
+
applied INTEGER DEFAULT 0,
|
|
631
|
+
pipeline_event_id INTEGER,
|
|
632
|
+
UNIQUE(timestamp, note_path, entity)
|
|
633
|
+
);
|
|
634
|
+
INSERT INTO suggestion_events_v40_new
|
|
635
|
+
SELECT id, timestamp, note_path, entity, total_score, breakdown_json,
|
|
636
|
+
threshold, passed, strictness, applied, pipeline_event_id
|
|
637
|
+
FROM (
|
|
638
|
+
SELECT id, timestamp, note_path, entity, total_score, breakdown_json,
|
|
639
|
+
threshold, passed, strictness, applied, pipeline_event_id,
|
|
640
|
+
ROW_NUMBER() OVER (
|
|
641
|
+
PARTITION BY timestamp, LOWER(note_path), entity
|
|
642
|
+
ORDER BY total_score DESC, id DESC
|
|
643
|
+
) AS rn
|
|
644
|
+
FROM suggestion_events
|
|
645
|
+
) WHERE rn = 1;
|
|
646
|
+
DROP TABLE suggestion_events;
|
|
647
|
+
ALTER TABLE suggestion_events_v40_new RENAME TO suggestion_events;
|
|
648
|
+
`);
|
|
649
|
+
// --- corrections: column alter via rebuild. All rows preserved. ---
|
|
650
|
+
db.exec(`
|
|
651
|
+
CREATE TABLE corrections_v40_new (
|
|
652
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
653
|
+
entity TEXT,
|
|
654
|
+
note_path TEXT COLLATE NOCASE,
|
|
655
|
+
correction_type TEXT NOT NULL,
|
|
656
|
+
description TEXT NOT NULL,
|
|
657
|
+
source TEXT NOT NULL DEFAULT 'user',
|
|
658
|
+
status TEXT DEFAULT 'pending',
|
|
659
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
660
|
+
resolved_at TEXT
|
|
661
|
+
);
|
|
662
|
+
INSERT INTO corrections_v40_new
|
|
663
|
+
SELECT id, entity, note_path, correction_type, description, source, status, created_at, resolved_at
|
|
664
|
+
FROM corrections;
|
|
665
|
+
DROP TABLE corrections;
|
|
666
|
+
ALTER TABLE corrections_v40_new RENAME TO corrections;
|
|
667
|
+
`);
|
|
668
|
+
// --- prospect_ledger: aggregate sums (sighting_count, score, backlink_count,
|
|
669
|
+
// first/last_seen_at) plus non-aggregate cols (display_name, source,
|
|
670
|
+
// pattern, confidence) taken from the row with the latest last_seen_at.
|
|
671
|
+
// CTE: agg computes the sums; ranked picks the winning row per group;
|
|
672
|
+
// INSERT joins the two. ---
|
|
673
|
+
db.exec(`
|
|
674
|
+
CREATE TABLE prospect_ledger_v40_new (
|
|
675
|
+
term TEXT NOT NULL,
|
|
676
|
+
display_name TEXT NOT NULL,
|
|
677
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
678
|
+
seen_day TEXT NOT NULL,
|
|
679
|
+
source TEXT NOT NULL,
|
|
680
|
+
pattern TEXT,
|
|
681
|
+
confidence TEXT NOT NULL DEFAULT 'low',
|
|
682
|
+
backlink_count INTEGER DEFAULT 0,
|
|
683
|
+
score REAL DEFAULT 0,
|
|
684
|
+
first_seen_at INTEGER NOT NULL,
|
|
685
|
+
last_seen_at INTEGER NOT NULL,
|
|
686
|
+
sighting_count INTEGER NOT NULL DEFAULT 1,
|
|
687
|
+
PRIMARY KEY (term, note_path, seen_day)
|
|
688
|
+
);
|
|
689
|
+
INSERT INTO prospect_ledger_v40_new
|
|
690
|
+
WITH agg AS (
|
|
691
|
+
SELECT
|
|
692
|
+
term AS tm,
|
|
693
|
+
LOWER(note_path) AS lnp,
|
|
694
|
+
seen_day AS sd,
|
|
695
|
+
MIN(first_seen_at) AS first_seen,
|
|
696
|
+
MAX(last_seen_at) AS last_seen,
|
|
697
|
+
SUM(sighting_count) AS total_sightings,
|
|
698
|
+
MAX(score) AS best_score,
|
|
699
|
+
MAX(backlink_count) AS total_backlinks
|
|
700
|
+
FROM prospect_ledger
|
|
701
|
+
GROUP BY term, LOWER(note_path), seen_day
|
|
702
|
+
),
|
|
703
|
+
ranked AS (
|
|
704
|
+
SELECT term, display_name, note_path, seen_day, source, pattern, confidence,
|
|
705
|
+
ROW_NUMBER() OVER (
|
|
706
|
+
PARTITION BY term, LOWER(note_path), seen_day
|
|
707
|
+
ORDER BY last_seen_at DESC, rowid ASC
|
|
708
|
+
) AS rn
|
|
709
|
+
FROM prospect_ledger
|
|
710
|
+
)
|
|
711
|
+
SELECT
|
|
712
|
+
r.term,
|
|
713
|
+
r.display_name,
|
|
714
|
+
r.note_path,
|
|
715
|
+
r.seen_day,
|
|
716
|
+
r.source,
|
|
717
|
+
r.pattern,
|
|
718
|
+
r.confidence,
|
|
719
|
+
COALESCE(a.total_backlinks, 0) AS backlink_count,
|
|
720
|
+
COALESCE(a.best_score, 0) AS score,
|
|
721
|
+
a.first_seen AS first_seen_at,
|
|
722
|
+
a.last_seen AS last_seen_at,
|
|
723
|
+
a.total_sightings AS sighting_count
|
|
724
|
+
FROM ranked r
|
|
725
|
+
INNER JOIN agg a
|
|
726
|
+
ON r.term = a.tm AND LOWER(r.note_path) = a.lnp AND r.seen_day = a.sd
|
|
727
|
+
WHERE r.rn = 1;
|
|
728
|
+
DROP TABLE prospect_ledger;
|
|
729
|
+
ALTER TABLE prospect_ledger_v40_new RENAME TO prospect_ledger;
|
|
730
|
+
`);
|
|
731
|
+
// --- proactive_queue: dedup by (LOWER(note_path), entity). Keep row with
|
|
732
|
+
// MAX(score); on score tie, prefer status='pending' over 'applied' (so
|
|
733
|
+
// unfinished work survives); then latest queued_at; then highest id. ---
|
|
734
|
+
db.exec(`
|
|
735
|
+
CREATE TABLE proactive_queue_v40_new (
|
|
736
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
737
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
738
|
+
entity TEXT NOT NULL,
|
|
739
|
+
score REAL NOT NULL,
|
|
740
|
+
confidence TEXT NOT NULL,
|
|
741
|
+
queued_at INTEGER NOT NULL,
|
|
742
|
+
expires_at INTEGER NOT NULL,
|
|
743
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
744
|
+
applied_at INTEGER,
|
|
745
|
+
UNIQUE(note_path, entity)
|
|
746
|
+
);
|
|
747
|
+
INSERT INTO proactive_queue_v40_new
|
|
748
|
+
SELECT id, note_path, entity, score, confidence,
|
|
749
|
+
queued_at, expires_at, status, applied_at
|
|
750
|
+
FROM (
|
|
751
|
+
SELECT id, note_path, entity, score, confidence,
|
|
752
|
+
queued_at, expires_at, status, applied_at,
|
|
753
|
+
ROW_NUMBER() OVER (
|
|
754
|
+
PARTITION BY LOWER(note_path), entity
|
|
755
|
+
ORDER BY score DESC,
|
|
756
|
+
CASE WHEN status = 'pending' THEN 0 ELSE 1 END ASC,
|
|
757
|
+
queued_at DESC,
|
|
758
|
+
id DESC
|
|
759
|
+
) AS rn
|
|
760
|
+
FROM proactive_queue
|
|
761
|
+
) WHERE rn = 1;
|
|
762
|
+
DROP TABLE proactive_queue;
|
|
763
|
+
ALTER TABLE proactive_queue_v40_new RENAME TO proactive_queue;
|
|
764
|
+
`);
|
|
765
|
+
// --- retrieval_cooccurrence: SUM(weight), MIN(timestamp) per
|
|
766
|
+
// (LOWER(note_a), LOWER(note_b), session_id) ---
|
|
767
|
+
db.exec(`
|
|
768
|
+
CREATE TABLE retrieval_cooccurrence_v40_new (
|
|
769
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
770
|
+
note_a TEXT NOT NULL COLLATE NOCASE,
|
|
771
|
+
note_b TEXT NOT NULL COLLATE NOCASE,
|
|
772
|
+
session_id TEXT NOT NULL,
|
|
773
|
+
timestamp INTEGER NOT NULL,
|
|
774
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
775
|
+
UNIQUE(note_a, note_b, session_id)
|
|
776
|
+
);
|
|
777
|
+
INSERT INTO retrieval_cooccurrence_v40_new (note_a, note_b, session_id, timestamp, weight)
|
|
778
|
+
SELECT MIN(note_a), MIN(note_b), session_id, MIN(timestamp), SUM(weight)
|
|
779
|
+
FROM retrieval_cooccurrence
|
|
780
|
+
GROUP BY LOWER(note_a), LOWER(note_b), session_id;
|
|
781
|
+
DROP TABLE retrieval_cooccurrence;
|
|
782
|
+
ALTER TABLE retrieval_cooccurrence_v40_new RENAME TO retrieval_cooccurrence;
|
|
783
|
+
`);
|
|
784
|
+
// --- wikilink_feedback: column alter via rebuild. All rows preserved. ---
|
|
785
|
+
db.exec(`
|
|
786
|
+
CREATE TABLE wikilink_feedback_v40_new (
|
|
787
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
788
|
+
entity TEXT NOT NULL,
|
|
789
|
+
context TEXT NOT NULL,
|
|
790
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
791
|
+
correct INTEGER NOT NULL,
|
|
792
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
793
|
+
matched_term TEXT,
|
|
794
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
795
|
+
);
|
|
796
|
+
INSERT INTO wikilink_feedback_v40_new
|
|
797
|
+
SELECT id, entity, context, note_path, correct, confidence, matched_term, created_at
|
|
798
|
+
FROM wikilink_feedback;
|
|
799
|
+
DROP TABLE wikilink_feedback;
|
|
800
|
+
ALTER TABLE wikilink_feedback_v40_new RENAME TO wikilink_feedback;
|
|
801
|
+
`);
|
|
802
|
+
// Recreate all indexes stripped by DROP TABLE. Re-executing SCHEMA_SQL
|
|
803
|
+
// is safe inside the transaction: CREATE TABLE IF NOT EXISTS is a no-op
|
|
804
|
+
// for the renamed tables, and CREATE INDEX IF NOT EXISTS repopulates
|
|
805
|
+
// the missing indexes. Triggers on entities_fts also get re-created.
|
|
806
|
+
db.exec(SCHEMA_SQL);
|
|
807
|
+
});
|
|
808
|
+
try {
|
|
809
|
+
runV40();
|
|
810
|
+
}
|
|
811
|
+
finally {
|
|
812
|
+
// Always re-enable foreign keys, even if the transaction threw and rolled back.
|
|
813
|
+
db.pragma('foreign_keys = ON');
|
|
340
814
|
}
|
|
815
|
+
return true;
|
|
341
816
|
}
|
|
342
817
|
// =============================================================================
|
|
343
818
|
// Database File Management
|
package/dist/schema.d.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* for the flywheel state database.
|
|
6
6
|
*/
|
|
7
7
|
/** Current schema version - bump when schema changes */
|
|
8
|
-
export declare const SCHEMA_VERSION =
|
|
8
|
+
export declare const SCHEMA_VERSION = 40;
|
|
9
9
|
/** State database filename */
|
|
10
10
|
export declare const STATE_DB_FILENAME = "state.db";
|
|
11
11
|
/** Directory for flywheel state */
|
|
12
12
|
export declare const FLYWHEEL_DIR = ".flywheel";
|
|
13
|
-
export declare const SCHEMA_SQL = "\n-- Schema version tracking\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Metadata key-value store\nCREATE TABLE IF NOT EXISTS metadata (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Entity index (replaces wikilink-entities.json)\nCREATE TABLE IF NOT EXISTS entities (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n name_lower TEXT NOT NULL,\n path TEXT NOT NULL,\n category TEXT NOT NULL,\n aliases_json TEXT,\n hub_score INTEGER DEFAULT 0,\n description TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_entities_name_lower ON entities(name_lower);\nCREATE INDEX IF NOT EXISTS idx_entities_category ON entities(category);\n\n-- FTS5 for entity search with porter stemmer (contentless \u2014 triggers handle sync)\nCREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(\n name, aliases, category,\n content='',\n tokenize='porter unicode61'\n);\n\n-- Auto-sync triggers for entities_fts\nCREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN\n INSERT INTO entities_fts(rowid, name, aliases, category)\n VALUES (\n new.id,\n new.name,\n COALESCE((SELECT group_concat(value, ' ') FROM json_each(new.aliases_json)), ''),\n new.category\n );\nEND;\n\nCREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN\n INSERT INTO entities_fts(entities_fts, rowid, name, aliases, category)\n VALUES (\n 'delete',\n old.id,\n old.name,\n COALESCE((SELECT group_concat(value, ' ') FROM json_each(old.aliases_json)), ''),\n old.category\n );\nEND;\n\nCREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN\n INSERT INTO entities_fts(entities_fts, rowid, name, aliases, category)\n VALUES (\n 'delete',\n old.id,\n old.name,\n COALESCE((SELECT group_concat(value, ' ') FROM json_each(old.aliases_json)), ''),\n old.category\n );\n INSERT INTO entities_fts(rowid, name, aliases, category)\n VALUES (\n new.id,\n new.name,\n COALESCE((SELECT group_concat(value, ' ') FROM json_each(new.aliases_json)), ''),\n new.category\n );\nEND;\n\n-- Recency tracking (replaces entity-recency.json)\nCREATE TABLE IF NOT EXISTS recency (\n entity_name_lower TEXT PRIMARY KEY,\n last_mentioned_at INTEGER NOT NULL,\n mention_count INTEGER DEFAULT 1\n);\n\n-- Write state (replaces last-commit.json and other write state)\nCREATE TABLE IF NOT EXISTS write_state (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Content search FTS5 (migrated from vault-search.db)\n-- v11: Added frontmatter column for weighted search (path, title, frontmatter, content)\nCREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(\n path, title, frontmatter, content,\n tokenize='porter'\n);\n\n-- FTS5 build metadata (consolidated from vault-search.db)\nCREATE TABLE IF NOT EXISTS fts_metadata (\n key TEXT PRIMARY KEY,\n value TEXT\n);\n\n-- Vault index cache (for fast startup)\n-- Stores serialized VaultIndex to avoid full rebuild on startup\nCREATE TABLE IF NOT EXISTS vault_index_cache (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n data BLOB NOT NULL,\n built_at INTEGER NOT NULL,\n note_count INTEGER NOT NULL,\n version INTEGER DEFAULT 1\n);\n\n-- Flywheel configuration (replaces .flywheel.json)\nCREATE TABLE IF NOT EXISTS flywheel_config (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Vault metrics (v4: growth tracking)\nCREATE TABLE IF NOT EXISTS vault_metrics (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n metric TEXT NOT NULL,\n value REAL NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_vault_metrics_ts ON vault_metrics(timestamp);\nCREATE INDEX IF NOT EXISTS idx_vault_metrics_m ON vault_metrics(metric, timestamp);\n\n-- Wikilink feedback (v4: quality tracking)\nCREATE TABLE IF NOT EXISTS wikilink_feedback (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n entity TEXT NOT NULL,\n context TEXT NOT NULL,\n note_path TEXT NOT NULL,\n correct INTEGER NOT NULL,\n confidence REAL NOT NULL DEFAULT 1.0,\n matched_term TEXT,\n created_at TEXT DEFAULT (datetime('now'))\n);\nCREATE INDEX IF NOT EXISTS idx_wl_feedback_entity ON wikilink_feedback(entity);\nCREATE INDEX IF NOT EXISTS idx_wl_feedback_note_path ON wikilink_feedback(note_path);\n\n-- Wikilink suppressions (v4: auto-suppress false positives)\nCREATE TABLE IF NOT EXISTS wikilink_suppressions (\n entity TEXT PRIMARY KEY,\n false_positive_rate REAL NOT NULL,\n updated_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Wikilink applications tracking (v5: implicit feedback, v38: source provenance)\nCREATE TABLE IF NOT EXISTS wikilink_applications (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n entity TEXT NOT NULL,\n note_path TEXT NOT NULL,\n matched_term TEXT,\n applied_at TEXT DEFAULT (datetime('now')),\n status TEXT DEFAULT 'applied',\n source TEXT NOT NULL DEFAULT 'tool'\n);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_wl_apps_unique ON wikilink_applications(entity COLLATE NOCASE, note_path);\n\n-- Index events tracking (v6: index activity history)\nCREATE TABLE IF NOT EXISTS index_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n trigger TEXT NOT NULL,\n duration_ms INTEGER NOT NULL,\n success INTEGER NOT NULL DEFAULT 1,\n note_count INTEGER,\n files_changed INTEGER,\n changed_paths TEXT,\n error TEXT,\n steps TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_index_events_ts ON index_events(timestamp);\n\n-- Tool invocation tracking (v7: usage analytics)\nCREATE TABLE IF NOT EXISTS tool_invocations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n tool_name TEXT NOT NULL,\n session_id TEXT,\n note_paths TEXT,\n duration_ms INTEGER,\n success INTEGER NOT NULL DEFAULT 1,\n response_tokens INTEGER,\n baseline_tokens INTEGER,\n query_context TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_tool_inv_ts ON tool_invocations(timestamp);\nCREATE INDEX IF NOT EXISTS idx_tool_inv_tool ON tool_invocations(tool_name, timestamp);\nCREATE INDEX IF NOT EXISTS idx_tool_inv_session ON tool_invocations(session_id, timestamp);\n\n-- Graph topology snapshots (v8: structural evolution)\nCREATE TABLE IF NOT EXISTS graph_snapshots (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n metric TEXT NOT NULL,\n value REAL NOT NULL,\n details TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_graph_snap_ts ON graph_snapshots(timestamp);\nCREATE INDEX IF NOT EXISTS idx_graph_snap_m ON graph_snapshots(metric, timestamp);\n\n-- Note embeddings for semantic search (v9)\nCREATE TABLE IF NOT EXISTS note_embeddings (\n path TEXT PRIMARY KEY,\n embedding BLOB NOT NULL,\n content_hash TEXT NOT NULL,\n model TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Entity embeddings for semantic entity search (v10)\nCREATE TABLE IF NOT EXISTS entity_embeddings (\n entity_name TEXT PRIMARY KEY,\n embedding BLOB NOT NULL,\n source_hash TEXT NOT NULL,\n model TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Task cache for fast task queries (v12)\nCREATE TABLE IF NOT EXISTS tasks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n path TEXT NOT NULL,\n line INTEGER NOT NULL,\n text TEXT NOT NULL,\n status TEXT NOT NULL,\n raw TEXT NOT NULL,\n context TEXT,\n tags_json TEXT,\n due_date TEXT,\n UNIQUE(path, line)\n);\nCREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);\nCREATE INDEX IF NOT EXISTS idx_tasks_path ON tasks(path);\nCREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due_date);\n\n-- Merge dismissals (v13: persistent merge pair suppression)\nCREATE TABLE IF NOT EXISTS merge_dismissals (\n pair_key TEXT PRIMARY KEY,\n source_path TEXT NOT NULL,\n target_path TEXT NOT NULL,\n source_name TEXT NOT NULL,\n target_name TEXT NOT NULL,\n reason TEXT NOT NULL,\n dismissed_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Suggestion events audit log (v15: pipeline observability)\nCREATE TABLE IF NOT EXISTS suggestion_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n note_path TEXT NOT NULL,\n entity TEXT NOT NULL,\n total_score REAL NOT NULL,\n breakdown_json TEXT NOT NULL,\n threshold REAL NOT NULL,\n passed INTEGER NOT NULL,\n strictness TEXT NOT NULL,\n applied INTEGER DEFAULT 0,\n pipeline_event_id INTEGER,\n UNIQUE(timestamp, note_path, entity)\n);\nCREATE INDEX IF NOT EXISTS idx_suggestion_entity ON suggestion_events(entity);\nCREATE INDEX IF NOT EXISTS idx_suggestion_note ON suggestion_events(note_path);\n\n-- Forward-link persistence for diff-based feedback (v16), edge weights (v22)\nCREATE TABLE IF NOT EXISTS note_links (\n note_path TEXT NOT NULL,\n target TEXT NOT NULL,\n weight REAL NOT NULL DEFAULT 1.0,\n weight_updated_at INTEGER,\n PRIMARY KEY (note_path, target)\n);\n\n-- Entity field change audit log (v17, rowid PK since v32)\nCREATE TABLE IF NOT EXISTS entity_changes (\n entity TEXT NOT NULL,\n field TEXT NOT NULL,\n old_value TEXT,\n new_value TEXT,\n changed_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Note tag persistence for diff-based feedback (v18)\nCREATE TABLE IF NOT EXISTS note_tags (\n note_path TEXT NOT NULL,\n tag TEXT NOT NULL,\n PRIMARY KEY (note_path, tag)\n);\n\n-- Wikilink survival tracking for positive feedback signals (v19)\nCREATE TABLE IF NOT EXISTS note_link_history (\n note_path TEXT NOT NULL,\n target TEXT NOT NULL,\n first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),\n edits_survived INTEGER NOT NULL DEFAULT 0,\n last_positive_at TEXT,\n PRIMARY KEY (note_path, target)\n);\n\n-- Note move history (v20): records when files are moved/renamed to a different folder\nCREATE TABLE IF NOT EXISTS note_moves (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n old_path TEXT NOT NULL,\n new_path TEXT NOT NULL,\n moved_at TEXT NOT NULL DEFAULT (datetime('now')),\n old_folder TEXT,\n new_folder TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_note_moves_old_path ON note_moves(old_path);\nCREATE INDEX IF NOT EXISTS idx_note_moves_new_path ON note_moves(new_path);\nCREATE INDEX IF NOT EXISTS idx_note_moves_moved_at ON note_moves(moved_at);\n\n-- Corrections (v24): persistent correction records from user/engine feedback\nCREATE TABLE IF NOT EXISTS corrections (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n entity TEXT,\n note_path TEXT,\n correction_type TEXT NOT NULL,\n description TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'user',\n status TEXT DEFAULT 'pending',\n created_at TEXT DEFAULT (datetime('now')),\n resolved_at TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_corrections_status ON corrections(status);\nCREATE INDEX IF NOT EXISTS idx_corrections_entity ON corrections(entity);\n\n-- Memories (v26): lightweight key-value working memory for agents\nCREATE TABLE IF NOT EXISTS memories (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n key TEXT NOT NULL,\n value TEXT NOT NULL,\n memory_type TEXT NOT NULL,\n entity TEXT,\n entities_json TEXT,\n source_agent_id TEXT,\n source_session_id TEXT,\n confidence REAL NOT NULL DEFAULT 1.0,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n accessed_at INTEGER NOT NULL,\n ttl_days INTEGER,\n superseded_by INTEGER REFERENCES memories(id),\n visibility TEXT NOT NULL DEFAULT 'shared'\n);\nCREATE INDEX IF NOT EXISTS idx_memories_key ON memories(key);\nCREATE INDEX IF NOT EXISTS idx_memories_entity ON memories(entity);\nCREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(\n key, value,\n content=memories, content_rowid=id,\n tokenize='porter unicode61'\n);\n\n-- Auto-sync triggers for memories_fts\nCREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN\n INSERT INTO memories_fts(rowid, key, value)\n VALUES (new.id, new.key, new.value);\nEND;\n\nCREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN\n INSERT INTO memories_fts(memories_fts, rowid, key, value)\n VALUES ('delete', old.id, old.key, old.value);\nEND;\n\nCREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN\n INSERT INTO memories_fts(memories_fts, rowid, key, value)\n VALUES ('delete', old.id, old.key, old.value);\n INSERT INTO memories_fts(rowid, key, value)\n VALUES (new.id, new.key, new.value);\nEND;\n\n-- Co-occurrence cache (v27): persist co-occurrence index to avoid full vault scan on restart\nCREATE TABLE IF NOT EXISTS cooccurrence_cache (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n data TEXT NOT NULL,\n built_at INTEGER NOT NULL,\n entity_count INTEGER NOT NULL,\n association_count INTEGER NOT NULL\n);\n\n-- Content hashes (v28): persist watcher content hashes across restarts\nCREATE TABLE IF NOT EXISTS content_hashes (\n path TEXT PRIMARY KEY,\n hash TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Session summaries (v26): agent session tracking\nCREATE TABLE IF NOT EXISTS session_summaries (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL UNIQUE,\n summary TEXT NOT NULL,\n topics_json TEXT,\n notes_modified_json TEXT,\n agent_id TEXT,\n started_at INTEGER,\n ended_at INTEGER NOT NULL,\n tool_count INTEGER\n);\n\n-- Retrieval co-occurrence (v30): notes retrieved together build implicit edges\nCREATE TABLE IF NOT EXISTS retrieval_cooccurrence (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n note_a TEXT NOT NULL,\n note_b TEXT NOT NULL,\n session_id TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n weight REAL NOT NULL DEFAULT 1.0,\n UNIQUE(note_a, note_b, session_id)\n);\nCREATE INDEX IF NOT EXISTS idx_retcooc_notes ON retrieval_cooccurrence(note_a, note_b);\nCREATE INDEX IF NOT EXISTS idx_retcooc_ts ON retrieval_cooccurrence(timestamp);\n\n-- Deferred proactive linking queue (v31)\nCREATE TABLE IF NOT EXISTS proactive_queue (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n note_path TEXT NOT NULL,\n entity TEXT NOT NULL,\n score REAL NOT NULL,\n confidence TEXT NOT NULL,\n queued_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n applied_at INTEGER,\n UNIQUE(note_path, entity)\n);\nCREATE INDEX IF NOT EXISTS idx_pq_status ON proactive_queue(status, expires_at);\n\n-- Performance benchmarks (v33: longitudinal tracking)\nCREATE TABLE IF NOT EXISTS performance_benchmarks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n version TEXT NOT NULL,\n benchmark TEXT NOT NULL,\n mean_ms REAL NOT NULL,\n p50_ms REAL,\n p95_ms REAL,\n iterations INTEGER NOT NULL DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_perf_bench_ts ON performance_benchmarks(timestamp);\nCREATE INDEX IF NOT EXISTS idx_perf_bench_name ON performance_benchmarks(benchmark, timestamp);\n\n-- Tool selection feedback (v36: tool selection quality tracking)\nCREATE TABLE IF NOT EXISTS tool_selection_feedback (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n tool_invocation_id INTEGER,\n tool_name TEXT NOT NULL,\n query_context TEXT,\n expected_tool TEXT,\n expected_category TEXT,\n correct INTEGER,\n source TEXT NOT NULL DEFAULT 'explicit',\n rule_id TEXT,\n rule_version INTEGER,\n session_id TEXT,\n created_at TEXT DEFAULT (datetime('now'))\n);\nCREATE INDEX IF NOT EXISTS idx_tsf_tool ON tool_selection_feedback(tool_name);\nCREATE INDEX IF NOT EXISTS idx_tsf_ts ON tool_selection_feedback(timestamp);\n\n-- Prospect ledger (v37): day-grain sightings for pre-entity pattern accumulation\nCREATE TABLE IF NOT EXISTS prospect_ledger (\n term TEXT NOT NULL,\n display_name TEXT NOT NULL,\n note_path TEXT NOT NULL,\n seen_day TEXT NOT NULL,\n source TEXT NOT NULL,\n pattern TEXT,\n confidence TEXT NOT NULL DEFAULT 'low',\n backlink_count INTEGER DEFAULT 0,\n score REAL DEFAULT 0,\n first_seen_at INTEGER NOT NULL,\n last_seen_at INTEGER NOT NULL,\n sighting_count INTEGER NOT NULL DEFAULT 1,\n PRIMARY KEY (term, note_path, seen_day)\n);\nCREATE INDEX IF NOT EXISTS idx_prospect_term ON prospect_ledger(term);\nCREATE INDEX IF NOT EXISTS idx_prospect_last_seen ON prospect_ledger(last_seen_at);\n\n-- Prospect summary (v37): materialized aggregate for fast scoring\nCREATE TABLE IF NOT EXISTS prospect_summary (\n term TEXT PRIMARY KEY,\n display_name TEXT NOT NULL,\n note_count INTEGER NOT NULL DEFAULT 0,\n day_count INTEGER NOT NULL DEFAULT 0,\n total_sightings INTEGER NOT NULL DEFAULT 0,\n backlink_max INTEGER NOT NULL DEFAULT 0,\n cooccurring_entities TEXT,\n best_source TEXT NOT NULL DEFAULT 'implicit',\n best_confidence TEXT NOT NULL DEFAULT 'low',\n best_score REAL NOT NULL DEFAULT 0,\n first_seen_at INTEGER NOT NULL,\n last_seen_at INTEGER NOT NULL,\n promotion_score REAL NOT NULL DEFAULT 0,\n promoted_at INTEGER,\n updated_at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_prospect_summary_score ON prospect_summary(promotion_score DESC);\n\n";
|
|
13
|
+
export declare const SCHEMA_SQL = "\n-- Schema version tracking\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Metadata key-value store\nCREATE TABLE IF NOT EXISTS metadata (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Entity index (replaces wikilink-entities.json)\nCREATE TABLE IF NOT EXISTS entities (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n name_lower TEXT NOT NULL,\n path TEXT NOT NULL COLLATE NOCASE,\n category TEXT NOT NULL,\n aliases_json TEXT,\n hub_score INTEGER DEFAULT 0,\n description TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_entities_name_lower ON entities(name_lower);\nCREATE INDEX IF NOT EXISTS idx_entities_category ON entities(category);\n\n-- FTS5 for entity search with porter stemmer (contentless \u2014 triggers handle sync)\nCREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(\n name, aliases, category,\n content='',\n tokenize='porter unicode61'\n);\n\n-- Auto-sync triggers for entities_fts\nCREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN\n INSERT INTO entities_fts(rowid, name, aliases, category)\n VALUES (\n new.id,\n new.name,\n COALESCE((SELECT group_concat(value, ' ') FROM json_each(new.aliases_json)), ''),\n new.category\n );\nEND;\n\nCREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN\n INSERT INTO entities_fts(entities_fts, rowid, name, aliases, category)\n VALUES (\n 'delete',\n old.id,\n old.name,\n COALESCE((SELECT group_concat(value, ' ') FROM json_each(old.aliases_json)), ''),\n old.category\n );\nEND;\n\nCREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN\n INSERT INTO entities_fts(entities_fts, rowid, name, aliases, category)\n VALUES (\n 'delete',\n old.id,\n old.name,\n COALESCE((SELECT group_concat(value, ' ') FROM json_each(old.aliases_json)), ''),\n old.category\n );\n INSERT INTO entities_fts(rowid, name, aliases, category)\n VALUES (\n new.id,\n new.name,\n COALESCE((SELECT group_concat(value, ' ') FROM json_each(new.aliases_json)), ''),\n new.category\n );\nEND;\n\n-- Recency tracking (replaces entity-recency.json)\nCREATE TABLE IF NOT EXISTS recency (\n entity_name_lower TEXT PRIMARY KEY,\n last_mentioned_at INTEGER NOT NULL,\n mention_count INTEGER DEFAULT 1\n);\n\n-- Write state (replaces last-commit.json and other write state)\nCREATE TABLE IF NOT EXISTS write_state (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Content search FTS5 (migrated from vault-search.db)\n-- v11: Added frontmatter column for weighted search (path, title, frontmatter, content)\nCREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(\n path, title, frontmatter, content,\n tokenize='porter'\n);\n\n-- FTS5 build metadata (consolidated from vault-search.db)\nCREATE TABLE IF NOT EXISTS fts_metadata (\n key TEXT PRIMARY KEY,\n value TEXT\n);\n\n-- Vault index cache (for fast startup)\n-- Stores serialized VaultIndex to avoid full rebuild on startup\nCREATE TABLE IF NOT EXISTS vault_index_cache (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n data BLOB NOT NULL,\n built_at INTEGER NOT NULL,\n note_count INTEGER NOT NULL,\n version INTEGER DEFAULT 1\n);\n\n-- Flywheel configuration (replaces .flywheel.json)\nCREATE TABLE IF NOT EXISTS flywheel_config (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Vault metrics (v4: growth tracking)\nCREATE TABLE IF NOT EXISTS vault_metrics (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n metric TEXT NOT NULL,\n value REAL NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_vault_metrics_ts ON vault_metrics(timestamp);\nCREATE INDEX IF NOT EXISTS idx_vault_metrics_m ON vault_metrics(metric, timestamp);\n\n-- Wikilink feedback (v4: quality tracking, v40: NOCASE note_path)\nCREATE TABLE IF NOT EXISTS wikilink_feedback (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n entity TEXT NOT NULL,\n context TEXT NOT NULL,\n note_path TEXT NOT NULL COLLATE NOCASE,\n correct INTEGER NOT NULL,\n confidence REAL NOT NULL DEFAULT 1.0,\n matched_term TEXT,\n created_at TEXT DEFAULT (datetime('now'))\n);\nCREATE INDEX IF NOT EXISTS idx_wl_feedback_entity ON wikilink_feedback(entity);\nCREATE INDEX IF NOT EXISTS idx_wl_feedback_note_path ON wikilink_feedback(note_path);\n\n-- Wikilink suppressions (v4: auto-suppress false positives)\nCREATE TABLE IF NOT EXISTS wikilink_suppressions (\n entity TEXT PRIMARY KEY,\n false_positive_rate REAL NOT NULL,\n updated_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Wikilink applications tracking (v5: implicit feedback, v38: source provenance)\nCREATE TABLE IF NOT EXISTS wikilink_applications (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n entity TEXT NOT NULL,\n note_path TEXT NOT NULL,\n matched_term TEXT,\n applied_at TEXT DEFAULT (datetime('now')),\n status TEXT DEFAULT 'applied',\n source TEXT NOT NULL DEFAULT 'tool'\n);\n-- v39: note_path uses COLLATE NOCASE so mixed-case paths on case-insensitive\n-- filesystems (Windows NTFS, macOS APFS default) collapse to one row. Prevents\n-- doubled wikilink-application counts when scanner and watcher disagree on casing.\nCREATE UNIQUE INDEX IF NOT EXISTS idx_wl_apps_unique ON wikilink_applications(entity COLLATE NOCASE, note_path COLLATE NOCASE);\n\n-- Index events tracking (v6: index activity history)\nCREATE TABLE IF NOT EXISTS index_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n trigger TEXT NOT NULL,\n duration_ms INTEGER NOT NULL,\n success INTEGER NOT NULL DEFAULT 1,\n note_count INTEGER,\n files_changed INTEGER,\n changed_paths TEXT,\n error TEXT,\n steps TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_index_events_ts ON index_events(timestamp);\n\n-- Tool invocation tracking (v7: usage analytics)\nCREATE TABLE IF NOT EXISTS tool_invocations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n tool_name TEXT NOT NULL,\n session_id TEXT,\n note_paths TEXT,\n duration_ms INTEGER,\n success INTEGER NOT NULL DEFAULT 1,\n response_tokens INTEGER,\n baseline_tokens INTEGER,\n query_context TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_tool_inv_ts ON tool_invocations(timestamp);\nCREATE INDEX IF NOT EXISTS idx_tool_inv_tool ON tool_invocations(tool_name, timestamp);\nCREATE INDEX IF NOT EXISTS idx_tool_inv_session ON tool_invocations(session_id, timestamp);\n\n-- Graph topology snapshots (v8: structural evolution)\nCREATE TABLE IF NOT EXISTS graph_snapshots (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n metric TEXT NOT NULL,\n value REAL NOT NULL,\n details TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_graph_snap_ts ON graph_snapshots(timestamp);\nCREATE INDEX IF NOT EXISTS idx_graph_snap_m ON graph_snapshots(metric, timestamp);\n\n-- Note embeddings for semantic search (v9, v40: NOCASE path)\nCREATE TABLE IF NOT EXISTS note_embeddings (\n path TEXT PRIMARY KEY COLLATE NOCASE,\n embedding BLOB NOT NULL,\n content_hash TEXT NOT NULL,\n model TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Entity embeddings for semantic entity search (v10)\nCREATE TABLE IF NOT EXISTS entity_embeddings (\n entity_name TEXT PRIMARY KEY,\n embedding BLOB NOT NULL,\n source_hash TEXT NOT NULL,\n model TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Task cache for fast task queries (v12, v40: NOCASE path)\nCREATE TABLE IF NOT EXISTS tasks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n path TEXT NOT NULL COLLATE NOCASE,\n line INTEGER NOT NULL,\n text TEXT NOT NULL,\n status TEXT NOT NULL,\n raw TEXT NOT NULL,\n context TEXT,\n tags_json TEXT,\n due_date TEXT,\n UNIQUE(path, line)\n);\nCREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);\nCREATE INDEX IF NOT EXISTS idx_tasks_path ON tasks(path);\nCREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due_date);\n\n-- Merge dismissals (v13: persistent merge pair suppression)\nCREATE TABLE IF NOT EXISTS merge_dismissals (\n pair_key TEXT PRIMARY KEY,\n source_path TEXT NOT NULL,\n target_path TEXT NOT NULL,\n source_name TEXT NOT NULL,\n target_name TEXT NOT NULL,\n reason TEXT NOT NULL,\n dismissed_at TEXT DEFAULT (datetime('now'))\n);\n\n-- Suggestion events audit log (v15: pipeline observability, v40: NOCASE note_path)\nCREATE TABLE IF NOT EXISTS suggestion_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n note_path TEXT NOT NULL COLLATE NOCASE,\n entity TEXT NOT NULL,\n total_score REAL NOT NULL,\n breakdown_json TEXT NOT NULL,\n threshold REAL NOT NULL,\n passed INTEGER NOT NULL,\n strictness TEXT NOT NULL,\n applied INTEGER DEFAULT 0,\n pipeline_event_id INTEGER,\n UNIQUE(timestamp, note_path, entity)\n);\nCREATE INDEX IF NOT EXISTS idx_suggestion_entity ON suggestion_events(entity);\nCREATE INDEX IF NOT EXISTS idx_suggestion_note ON suggestion_events(note_path);\n\n-- Forward-link persistence for diff-based feedback (v16), edge weights (v22), v40: NOCASE note_path\nCREATE TABLE IF NOT EXISTS note_links (\n note_path TEXT NOT NULL COLLATE NOCASE,\n target TEXT NOT NULL,\n weight REAL NOT NULL DEFAULT 1.0,\n weight_updated_at INTEGER,\n PRIMARY KEY (note_path, target)\n);\n\n-- Entity field change audit log (v17, rowid PK since v32)\nCREATE TABLE IF NOT EXISTS entity_changes (\n entity TEXT NOT NULL,\n field TEXT NOT NULL,\n old_value TEXT,\n new_value TEXT,\n changed_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Note tag persistence for diff-based feedback (v18, v40: NOCASE note_path)\nCREATE TABLE IF NOT EXISTS note_tags (\n note_path TEXT NOT NULL COLLATE NOCASE,\n tag TEXT NOT NULL,\n PRIMARY KEY (note_path, tag)\n);\n\n-- Wikilink survival tracking for positive feedback signals (v19, v40: NOCASE note_path)\nCREATE TABLE IF NOT EXISTS note_link_history (\n note_path TEXT NOT NULL COLLATE NOCASE,\n target TEXT NOT NULL,\n first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),\n edits_survived INTEGER NOT NULL DEFAULT 0,\n last_positive_at TEXT,\n PRIMARY KEY (note_path, target)\n);\n\n-- Note move history (v20): records when files are moved/renamed to a different folder, v40: NOCASE paths\nCREATE TABLE IF NOT EXISTS note_moves (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n old_path TEXT NOT NULL COLLATE NOCASE,\n new_path TEXT NOT NULL COLLATE NOCASE,\n moved_at TEXT NOT NULL DEFAULT (datetime('now')),\n old_folder TEXT,\n new_folder TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_note_moves_old_path ON note_moves(old_path);\nCREATE INDEX IF NOT EXISTS idx_note_moves_new_path ON note_moves(new_path);\nCREATE INDEX IF NOT EXISTS idx_note_moves_moved_at ON note_moves(moved_at);\n\n-- Corrections (v24): persistent correction records from user/engine feedback, v40: NOCASE note_path\nCREATE TABLE IF NOT EXISTS corrections (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n entity TEXT,\n note_path TEXT COLLATE NOCASE,\n correction_type TEXT NOT NULL,\n description TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'user',\n status TEXT DEFAULT 'pending',\n created_at TEXT DEFAULT (datetime('now')),\n resolved_at TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_corrections_status ON corrections(status);\nCREATE INDEX IF NOT EXISTS idx_corrections_entity ON corrections(entity);\n\n-- Memories (v26): lightweight key-value working memory for agents\nCREATE TABLE IF NOT EXISTS memories (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n key TEXT NOT NULL,\n value TEXT NOT NULL,\n memory_type TEXT NOT NULL,\n entity TEXT,\n entities_json TEXT,\n source_agent_id TEXT,\n source_session_id TEXT,\n confidence REAL NOT NULL DEFAULT 1.0,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n accessed_at INTEGER NOT NULL,\n ttl_days INTEGER,\n superseded_by INTEGER REFERENCES memories(id),\n visibility TEXT NOT NULL DEFAULT 'shared'\n);\nCREATE INDEX IF NOT EXISTS idx_memories_key ON memories(key);\nCREATE INDEX IF NOT EXISTS idx_memories_entity ON memories(entity);\nCREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(\n key, value,\n content=memories, content_rowid=id,\n tokenize='porter unicode61'\n);\n\n-- Auto-sync triggers for memories_fts\nCREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN\n INSERT INTO memories_fts(rowid, key, value)\n VALUES (new.id, new.key, new.value);\nEND;\n\nCREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN\n INSERT INTO memories_fts(memories_fts, rowid, key, value)\n VALUES ('delete', old.id, old.key, old.value);\nEND;\n\nCREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN\n INSERT INTO memories_fts(memories_fts, rowid, key, value)\n VALUES ('delete', old.id, old.key, old.value);\n INSERT INTO memories_fts(rowid, key, value)\n VALUES (new.id, new.key, new.value);\nEND;\n\n-- Co-occurrence cache (v27): persist co-occurrence index to avoid full vault scan on restart\nCREATE TABLE IF NOT EXISTS cooccurrence_cache (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n data TEXT NOT NULL,\n built_at INTEGER NOT NULL,\n entity_count INTEGER NOT NULL,\n association_count INTEGER NOT NULL\n);\n\n-- Content hashes (v28): persist watcher content hashes across restarts, v40: NOCASE path\nCREATE TABLE IF NOT EXISTS content_hashes (\n path TEXT PRIMARY KEY COLLATE NOCASE,\n hash TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Session summaries (v26): agent session tracking\nCREATE TABLE IF NOT EXISTS session_summaries (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL UNIQUE,\n summary TEXT NOT NULL,\n topics_json TEXT,\n notes_modified_json TEXT,\n agent_id TEXT,\n started_at INTEGER,\n ended_at INTEGER NOT NULL,\n tool_count INTEGER\n);\n\n-- Retrieval co-occurrence (v30): notes retrieved together build implicit edges, v40: NOCASE notes\nCREATE TABLE IF NOT EXISTS retrieval_cooccurrence (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n note_a TEXT NOT NULL COLLATE NOCASE,\n note_b TEXT NOT NULL COLLATE NOCASE,\n session_id TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n weight REAL NOT NULL DEFAULT 1.0,\n UNIQUE(note_a, note_b, session_id)\n);\nCREATE INDEX IF NOT EXISTS idx_retcooc_notes ON retrieval_cooccurrence(note_a, note_b);\nCREATE INDEX IF NOT EXISTS idx_retcooc_ts ON retrieval_cooccurrence(timestamp);\n\n-- Deferred proactive linking queue (v31, v40: NOCASE note_path)\nCREATE TABLE IF NOT EXISTS proactive_queue (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n note_path TEXT NOT NULL COLLATE NOCASE,\n entity TEXT NOT NULL,\n score REAL NOT NULL,\n confidence TEXT NOT NULL,\n queued_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n applied_at INTEGER,\n UNIQUE(note_path, entity)\n);\nCREATE INDEX IF NOT EXISTS idx_pq_status ON proactive_queue(status, expires_at);\n\n-- Performance benchmarks (v33: longitudinal tracking)\nCREATE TABLE IF NOT EXISTS performance_benchmarks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n version TEXT NOT NULL,\n benchmark TEXT NOT NULL,\n mean_ms REAL NOT NULL,\n p50_ms REAL,\n p95_ms REAL,\n iterations INTEGER NOT NULL DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_perf_bench_ts ON performance_benchmarks(timestamp);\nCREATE INDEX IF NOT EXISTS idx_perf_bench_name ON performance_benchmarks(benchmark, timestamp);\n\n-- Tool selection feedback (v36: tool selection quality tracking)\nCREATE TABLE IF NOT EXISTS tool_selection_feedback (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL,\n tool_invocation_id INTEGER,\n tool_name TEXT NOT NULL,\n query_context TEXT,\n expected_tool TEXT,\n expected_category TEXT,\n correct INTEGER,\n source TEXT NOT NULL DEFAULT 'explicit',\n rule_id TEXT,\n rule_version INTEGER,\n session_id TEXT,\n created_at TEXT DEFAULT (datetime('now'))\n);\nCREATE INDEX IF NOT EXISTS idx_tsf_tool ON tool_selection_feedback(tool_name);\nCREATE INDEX IF NOT EXISTS idx_tsf_ts ON tool_selection_feedback(timestamp);\n\n-- Prospect ledger (v37): day-grain sightings for pre-entity pattern accumulation, v40: NOCASE note_path\nCREATE TABLE IF NOT EXISTS prospect_ledger (\n term TEXT NOT NULL,\n display_name TEXT NOT NULL,\n note_path TEXT NOT NULL COLLATE NOCASE,\n seen_day TEXT NOT NULL,\n source TEXT NOT NULL,\n pattern TEXT,\n confidence TEXT NOT NULL DEFAULT 'low',\n backlink_count INTEGER DEFAULT 0,\n score REAL DEFAULT 0,\n first_seen_at INTEGER NOT NULL,\n last_seen_at INTEGER NOT NULL,\n sighting_count INTEGER NOT NULL DEFAULT 1,\n PRIMARY KEY (term, note_path, seen_day)\n);\nCREATE INDEX IF NOT EXISTS idx_prospect_term ON prospect_ledger(term);\nCREATE INDEX IF NOT EXISTS idx_prospect_last_seen ON prospect_ledger(last_seen_at);\n\n-- Prospect summary (v37): materialized aggregate for fast scoring\nCREATE TABLE IF NOT EXISTS prospect_summary (\n term TEXT PRIMARY KEY,\n display_name TEXT NOT NULL,\n note_count INTEGER NOT NULL DEFAULT 0,\n day_count INTEGER NOT NULL DEFAULT 0,\n total_sightings INTEGER NOT NULL DEFAULT 0,\n backlink_max INTEGER NOT NULL DEFAULT 0,\n cooccurring_entities TEXT,\n best_source TEXT NOT NULL DEFAULT 'implicit',\n best_confidence TEXT NOT NULL DEFAULT 'low',\n best_score REAL NOT NULL DEFAULT 0,\n first_seen_at INTEGER NOT NULL,\n last_seen_at INTEGER NOT NULL,\n promotion_score REAL NOT NULL DEFAULT 0,\n promoted_at INTEGER,\n updated_at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_prospect_summary_score ON prospect_summary(promotion_score DESC);\n\n";
|
|
14
14
|
//# sourceMappingURL=schema.d.ts.map
|
package/dist/schema.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Constants
|
|
9
9
|
// =============================================================================
|
|
10
10
|
/** Current schema version - bump when schema changes */
|
|
11
|
-
export const SCHEMA_VERSION =
|
|
11
|
+
export const SCHEMA_VERSION = 40;
|
|
12
12
|
/** State database filename */
|
|
13
13
|
export const STATE_DB_FILENAME = 'state.db';
|
|
14
14
|
/** Directory for flywheel state */
|
|
@@ -35,7 +35,7 @@ CREATE TABLE IF NOT EXISTS entities (
|
|
|
35
35
|
id INTEGER PRIMARY KEY,
|
|
36
36
|
name TEXT NOT NULL,
|
|
37
37
|
name_lower TEXT NOT NULL,
|
|
38
|
-
path TEXT NOT NULL,
|
|
38
|
+
path TEXT NOT NULL COLLATE NOCASE,
|
|
39
39
|
category TEXT NOT NULL,
|
|
40
40
|
aliases_json TEXT,
|
|
41
41
|
hub_score INTEGER DEFAULT 0,
|
|
@@ -145,12 +145,12 @@ CREATE TABLE IF NOT EXISTS vault_metrics (
|
|
|
145
145
|
CREATE INDEX IF NOT EXISTS idx_vault_metrics_ts ON vault_metrics(timestamp);
|
|
146
146
|
CREATE INDEX IF NOT EXISTS idx_vault_metrics_m ON vault_metrics(metric, timestamp);
|
|
147
147
|
|
|
148
|
-
-- Wikilink feedback (v4: quality tracking)
|
|
148
|
+
-- Wikilink feedback (v4: quality tracking, v40: NOCASE note_path)
|
|
149
149
|
CREATE TABLE IF NOT EXISTS wikilink_feedback (
|
|
150
150
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
151
151
|
entity TEXT NOT NULL,
|
|
152
152
|
context TEXT NOT NULL,
|
|
153
|
-
note_path TEXT NOT NULL,
|
|
153
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
154
154
|
correct INTEGER NOT NULL,
|
|
155
155
|
confidence REAL NOT NULL DEFAULT 1.0,
|
|
156
156
|
matched_term TEXT,
|
|
@@ -176,7 +176,10 @@ CREATE TABLE IF NOT EXISTS wikilink_applications (
|
|
|
176
176
|
status TEXT DEFAULT 'applied',
|
|
177
177
|
source TEXT NOT NULL DEFAULT 'tool'
|
|
178
178
|
);
|
|
179
|
-
|
|
179
|
+
-- v39: note_path uses COLLATE NOCASE so mixed-case paths on case-insensitive
|
|
180
|
+
-- filesystems (Windows NTFS, macOS APFS default) collapse to one row. Prevents
|
|
181
|
+
-- doubled wikilink-application counts when scanner and watcher disagree on casing.
|
|
182
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_wl_apps_unique ON wikilink_applications(entity COLLATE NOCASE, note_path COLLATE NOCASE);
|
|
180
183
|
|
|
181
184
|
-- Index events tracking (v6: index activity history)
|
|
182
185
|
CREATE TABLE IF NOT EXISTS index_events (
|
|
@@ -221,9 +224,9 @@ CREATE TABLE IF NOT EXISTS graph_snapshots (
|
|
|
221
224
|
CREATE INDEX IF NOT EXISTS idx_graph_snap_ts ON graph_snapshots(timestamp);
|
|
222
225
|
CREATE INDEX IF NOT EXISTS idx_graph_snap_m ON graph_snapshots(metric, timestamp);
|
|
223
226
|
|
|
224
|
-
-- Note embeddings for semantic search (v9)
|
|
227
|
+
-- Note embeddings for semantic search (v9, v40: NOCASE path)
|
|
225
228
|
CREATE TABLE IF NOT EXISTS note_embeddings (
|
|
226
|
-
path TEXT PRIMARY KEY,
|
|
229
|
+
path TEXT PRIMARY KEY COLLATE NOCASE,
|
|
227
230
|
embedding BLOB NOT NULL,
|
|
228
231
|
content_hash TEXT NOT NULL,
|
|
229
232
|
model TEXT NOT NULL,
|
|
@@ -239,10 +242,10 @@ CREATE TABLE IF NOT EXISTS entity_embeddings (
|
|
|
239
242
|
updated_at INTEGER NOT NULL
|
|
240
243
|
);
|
|
241
244
|
|
|
242
|
-
-- Task cache for fast task queries (v12)
|
|
245
|
+
-- Task cache for fast task queries (v12, v40: NOCASE path)
|
|
243
246
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
244
247
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
245
|
-
path TEXT NOT NULL,
|
|
248
|
+
path TEXT NOT NULL COLLATE NOCASE,
|
|
246
249
|
line INTEGER NOT NULL,
|
|
247
250
|
text TEXT NOT NULL,
|
|
248
251
|
status TEXT NOT NULL,
|
|
@@ -267,11 +270,11 @@ CREATE TABLE IF NOT EXISTS merge_dismissals (
|
|
|
267
270
|
dismissed_at TEXT DEFAULT (datetime('now'))
|
|
268
271
|
);
|
|
269
272
|
|
|
270
|
-
-- Suggestion events audit log (v15: pipeline observability)
|
|
273
|
+
-- Suggestion events audit log (v15: pipeline observability, v40: NOCASE note_path)
|
|
271
274
|
CREATE TABLE IF NOT EXISTS suggestion_events (
|
|
272
275
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
273
276
|
timestamp INTEGER NOT NULL,
|
|
274
|
-
note_path TEXT NOT NULL,
|
|
277
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
275
278
|
entity TEXT NOT NULL,
|
|
276
279
|
total_score REAL NOT NULL,
|
|
277
280
|
breakdown_json TEXT NOT NULL,
|
|
@@ -285,9 +288,9 @@ CREATE TABLE IF NOT EXISTS suggestion_events (
|
|
|
285
288
|
CREATE INDEX IF NOT EXISTS idx_suggestion_entity ON suggestion_events(entity);
|
|
286
289
|
CREATE INDEX IF NOT EXISTS idx_suggestion_note ON suggestion_events(note_path);
|
|
287
290
|
|
|
288
|
-
-- Forward-link persistence for diff-based feedback (v16), edge weights (v22)
|
|
291
|
+
-- Forward-link persistence for diff-based feedback (v16), edge weights (v22), v40: NOCASE note_path
|
|
289
292
|
CREATE TABLE IF NOT EXISTS note_links (
|
|
290
|
-
note_path TEXT NOT NULL,
|
|
293
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
291
294
|
target TEXT NOT NULL,
|
|
292
295
|
weight REAL NOT NULL DEFAULT 1.0,
|
|
293
296
|
weight_updated_at INTEGER,
|
|
@@ -303,16 +306,16 @@ CREATE TABLE IF NOT EXISTS entity_changes (
|
|
|
303
306
|
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
304
307
|
);
|
|
305
308
|
|
|
306
|
-
-- Note tag persistence for diff-based feedback (v18)
|
|
309
|
+
-- Note tag persistence for diff-based feedback (v18, v40: NOCASE note_path)
|
|
307
310
|
CREATE TABLE IF NOT EXISTS note_tags (
|
|
308
|
-
note_path TEXT NOT NULL,
|
|
311
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
309
312
|
tag TEXT NOT NULL,
|
|
310
313
|
PRIMARY KEY (note_path, tag)
|
|
311
314
|
);
|
|
312
315
|
|
|
313
|
-
-- Wikilink survival tracking for positive feedback signals (v19)
|
|
316
|
+
-- Wikilink survival tracking for positive feedback signals (v19, v40: NOCASE note_path)
|
|
314
317
|
CREATE TABLE IF NOT EXISTS note_link_history (
|
|
315
|
-
note_path TEXT NOT NULL,
|
|
318
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
316
319
|
target TEXT NOT NULL,
|
|
317
320
|
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
318
321
|
edits_survived INTEGER NOT NULL DEFAULT 0,
|
|
@@ -320,11 +323,11 @@ CREATE TABLE IF NOT EXISTS note_link_history (
|
|
|
320
323
|
PRIMARY KEY (note_path, target)
|
|
321
324
|
);
|
|
322
325
|
|
|
323
|
-
-- Note move history (v20): records when files are moved/renamed to a different folder
|
|
326
|
+
-- Note move history (v20): records when files are moved/renamed to a different folder, v40: NOCASE paths
|
|
324
327
|
CREATE TABLE IF NOT EXISTS note_moves (
|
|
325
328
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
326
|
-
old_path TEXT NOT NULL,
|
|
327
|
-
new_path TEXT NOT NULL,
|
|
329
|
+
old_path TEXT NOT NULL COLLATE NOCASE,
|
|
330
|
+
new_path TEXT NOT NULL COLLATE NOCASE,
|
|
328
331
|
moved_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
329
332
|
old_folder TEXT,
|
|
330
333
|
new_folder TEXT
|
|
@@ -333,11 +336,11 @@ CREATE INDEX IF NOT EXISTS idx_note_moves_old_path ON note_moves(old_path);
|
|
|
333
336
|
CREATE INDEX IF NOT EXISTS idx_note_moves_new_path ON note_moves(new_path);
|
|
334
337
|
CREATE INDEX IF NOT EXISTS idx_note_moves_moved_at ON note_moves(moved_at);
|
|
335
338
|
|
|
336
|
-
-- Corrections (v24): persistent correction records from user/engine feedback
|
|
339
|
+
-- Corrections (v24): persistent correction records from user/engine feedback, v40: NOCASE note_path
|
|
337
340
|
CREATE TABLE IF NOT EXISTS corrections (
|
|
338
341
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
339
342
|
entity TEXT,
|
|
340
|
-
note_path TEXT,
|
|
343
|
+
note_path TEXT COLLATE NOCASE,
|
|
341
344
|
correction_type TEXT NOT NULL,
|
|
342
345
|
description TEXT NOT NULL,
|
|
343
346
|
source TEXT NOT NULL DEFAULT 'user',
|
|
@@ -403,9 +406,9 @@ CREATE TABLE IF NOT EXISTS cooccurrence_cache (
|
|
|
403
406
|
association_count INTEGER NOT NULL
|
|
404
407
|
);
|
|
405
408
|
|
|
406
|
-
-- Content hashes (v28): persist watcher content hashes across restarts
|
|
409
|
+
-- Content hashes (v28): persist watcher content hashes across restarts, v40: NOCASE path
|
|
407
410
|
CREATE TABLE IF NOT EXISTS content_hashes (
|
|
408
|
-
path TEXT PRIMARY KEY,
|
|
411
|
+
path TEXT PRIMARY KEY COLLATE NOCASE,
|
|
409
412
|
hash TEXT NOT NULL,
|
|
410
413
|
updated_at INTEGER NOT NULL
|
|
411
414
|
);
|
|
@@ -423,11 +426,11 @@ CREATE TABLE IF NOT EXISTS session_summaries (
|
|
|
423
426
|
tool_count INTEGER
|
|
424
427
|
);
|
|
425
428
|
|
|
426
|
-
-- Retrieval co-occurrence (v30): notes retrieved together build implicit edges
|
|
429
|
+
-- Retrieval co-occurrence (v30): notes retrieved together build implicit edges, v40: NOCASE notes
|
|
427
430
|
CREATE TABLE IF NOT EXISTS retrieval_cooccurrence (
|
|
428
431
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
429
|
-
note_a TEXT NOT NULL,
|
|
430
|
-
note_b TEXT NOT NULL,
|
|
432
|
+
note_a TEXT NOT NULL COLLATE NOCASE,
|
|
433
|
+
note_b TEXT NOT NULL COLLATE NOCASE,
|
|
431
434
|
session_id TEXT NOT NULL,
|
|
432
435
|
timestamp INTEGER NOT NULL,
|
|
433
436
|
weight REAL NOT NULL DEFAULT 1.0,
|
|
@@ -436,10 +439,10 @@ CREATE TABLE IF NOT EXISTS retrieval_cooccurrence (
|
|
|
436
439
|
CREATE INDEX IF NOT EXISTS idx_retcooc_notes ON retrieval_cooccurrence(note_a, note_b);
|
|
437
440
|
CREATE INDEX IF NOT EXISTS idx_retcooc_ts ON retrieval_cooccurrence(timestamp);
|
|
438
441
|
|
|
439
|
-
-- Deferred proactive linking queue (v31)
|
|
442
|
+
-- Deferred proactive linking queue (v31, v40: NOCASE note_path)
|
|
440
443
|
CREATE TABLE IF NOT EXISTS proactive_queue (
|
|
441
444
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
442
|
-
note_path TEXT NOT NULL,
|
|
445
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
443
446
|
entity TEXT NOT NULL,
|
|
444
447
|
score REAL NOT NULL,
|
|
445
448
|
confidence TEXT NOT NULL,
|
|
@@ -484,11 +487,11 @@ CREATE TABLE IF NOT EXISTS tool_selection_feedback (
|
|
|
484
487
|
CREATE INDEX IF NOT EXISTS idx_tsf_tool ON tool_selection_feedback(tool_name);
|
|
485
488
|
CREATE INDEX IF NOT EXISTS idx_tsf_ts ON tool_selection_feedback(timestamp);
|
|
486
489
|
|
|
487
|
-
-- Prospect ledger (v37): day-grain sightings for pre-entity pattern accumulation
|
|
490
|
+
-- Prospect ledger (v37): day-grain sightings for pre-entity pattern accumulation, v40: NOCASE note_path
|
|
488
491
|
CREATE TABLE IF NOT EXISTS prospect_ledger (
|
|
489
492
|
term TEXT NOT NULL,
|
|
490
493
|
display_name TEXT NOT NULL,
|
|
491
|
-
note_path TEXT NOT NULL,
|
|
494
|
+
note_path TEXT NOT NULL COLLATE NOCASE,
|
|
492
495
|
seen_day TEXT NOT NULL,
|
|
493
496
|
source TEXT NOT NULL,
|
|
494
497
|
pattern TEXT,
|
package/dist/sqlite.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import Database from 'better-sqlite3';
|
|
|
13
13
|
import type { Statement, Transaction } from 'better-sqlite3';
|
|
14
14
|
import type { EntityCategory, EntityWithAliases, EntityIndex } from './types.js';
|
|
15
15
|
export { SCHEMA_VERSION, STATE_DB_FILENAME, FLYWHEEL_DIR, SCHEMA_SQL } from './schema.js';
|
|
16
|
-
export { getStateDbPath, initSchema, deleteStateDbFiles, backupStateDb, preserveCorruptedDb } from './migrations.js';
|
|
16
|
+
export { getStateDbPath, initSchema, migrateV40, deleteStateDbFiles, backupStateDb, preserveCorruptedDb } from './migrations.js';
|
|
17
17
|
export { BACKUP_ROTATION_COUNT, SALVAGE_TABLES, rotateBackupFiles, safeBackupAsync, checkDbIntegrity, salvageFeedbackTables, attemptSalvage, } from './migrations.js';
|
|
18
18
|
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';
|
|
19
19
|
export type { FlywheelConfigRow, VaultIndexCacheData, VaultIndexCacheInfo } from './queries.js';
|
package/dist/sqlite.js
CHANGED
|
@@ -14,7 +14,7 @@ import * as fs from 'fs';
|
|
|
14
14
|
// Re-export constants from schema
|
|
15
15
|
export { SCHEMA_VERSION, STATE_DB_FILENAME, FLYWHEEL_DIR, SCHEMA_SQL } from './schema.js';
|
|
16
16
|
// Re-export migrations
|
|
17
|
-
export { getStateDbPath, initSchema, deleteStateDbFiles, backupStateDb, preserveCorruptedDb } from './migrations.js';
|
|
17
|
+
export { getStateDbPath, initSchema, migrateV40, deleteStateDbFiles, backupStateDb, preserveCorruptedDb } from './migrations.js';
|
|
18
18
|
// Re-export backup & recovery
|
|
19
19
|
export { BACKUP_ROTATION_COUNT, SALVAGE_TABLES, rotateBackupFiles, safeBackupAsync, checkDbIntegrity, salvageFeedbackTables, attemptSalvage, } from './migrations.js';
|
|
20
20
|
// Re-export all query functions
|
|
@@ -22,6 +22,49 @@ export { searchEntities, searchEntitiesPrefix, getEntityByName, getAllEntitiesFr
|
|
|
22
22
|
// Import for use in openStateDb
|
|
23
23
|
import { getStateDbPath, initSchema, deleteStateDbFiles, preserveCorruptedDb, attemptSalvage } from './migrations.js';
|
|
24
24
|
// =============================================================================
|
|
25
|
+
// Pre-v40 backup hook
|
|
26
|
+
// =============================================================================
|
|
27
|
+
/**
|
|
28
|
+
* Snapshot the state DB to `<dbPath>.pre-v40.backup` before the v40 migration
|
|
29
|
+
* runs. Synchronous (uses VACUUM INTO) so it slots into openStateDb's existing
|
|
30
|
+
* sync flow without cascading async to dozens of callers.
|
|
31
|
+
*
|
|
32
|
+
* Skipped if:
|
|
33
|
+
* - schema_version table doesn't exist (fresh DB, nothing to back up)
|
|
34
|
+
* - currentVersion >= 40 (already migrated, hook is a no-op)
|
|
35
|
+
* - quick_check fails (don't overwrite a good backup with a corrupt DB)
|
|
36
|
+
*
|
|
37
|
+
* Failures are logged but never throw — the migration may still succeed.
|
|
38
|
+
*/
|
|
39
|
+
function runPreV40BackupHook(db, dbPath) {
|
|
40
|
+
try {
|
|
41
|
+
const schemaVersionTable = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'`).get();
|
|
42
|
+
if (!schemaVersionTable)
|
|
43
|
+
return;
|
|
44
|
+
const row = db.prepare('SELECT MAX(version) AS v FROM schema_version').get();
|
|
45
|
+
const currentVersion = row?.v ?? 0;
|
|
46
|
+
if (currentVersion >= 40)
|
|
47
|
+
return;
|
|
48
|
+
const quickCheck = db.pragma('quick_check');
|
|
49
|
+
const firstValue = quickCheck.length > 0 ? Object.values(quickCheck[0])[0] : 'no result';
|
|
50
|
+
if (firstValue !== 'ok') {
|
|
51
|
+
console.error(`[vault-core] Pre-v40 backup skipped: quick_check returned "${firstValue}" — DB may be corrupt`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const backupPath = `${dbPath}.pre-v40.backup`;
|
|
55
|
+
// VACUUM INTO refuses to overwrite an existing file. Clear any prior snapshot first.
|
|
56
|
+
if (fs.existsSync(backupPath)) {
|
|
57
|
+
fs.unlinkSync(backupPath);
|
|
58
|
+
}
|
|
59
|
+
const escaped = backupPath.replace(/'/g, "''");
|
|
60
|
+
db.exec(`VACUUM INTO '${escaped}'`);
|
|
61
|
+
console.error(`[vault-core] Pre-v40 backup created at ${backupPath}`);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error(`[vault-core] Pre-v40 backup failed: ${err instanceof Error ? err.message : err}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// =============================================================================
|
|
25
68
|
// Factory
|
|
26
69
|
// =============================================================================
|
|
27
70
|
/**
|
|
@@ -49,6 +92,15 @@ export function openStateDb(vaultPath) {
|
|
|
49
92
|
let db;
|
|
50
93
|
try {
|
|
51
94
|
db = new Database(dbPath);
|
|
95
|
+
// Pre-v40 backup hook: before initSchema runs migrations, snapshot the
|
|
96
|
+
// current state.db so users can recover if the v40 COLLATE NOCASE rebuild
|
|
97
|
+
// picks an unexpected row in a tie. Synchronous VACUUM INTO is WAL-safe.
|
|
98
|
+
// Skipped on fresh DBs (no schema_version table yet) and on already-migrated
|
|
99
|
+
// databases (currentVersion >= 40). Quick_check guards against backing up
|
|
100
|
+
// a corrupt DB on top of a previously-good backup.
|
|
101
|
+
if (!isNewDb) {
|
|
102
|
+
runPreV40BackupHook(db, dbPath);
|
|
103
|
+
}
|
|
52
104
|
initSchema(db);
|
|
53
105
|
// Enable incremental auto_vacuum on existing databases (one-time cost).
|
|
54
106
|
// New DBs get it from initSchema before tables are created, but existing
|
package/dist/wikilinks.js
CHANGED
|
@@ -816,7 +816,10 @@ export function resolveAliasWikilinks(content, entities, options = {}) {
|
|
|
816
816
|
const DEFAULT_IMPLICIT_CONFIG = {
|
|
817
817
|
detectImplicit: false,
|
|
818
818
|
implicitPatterns: ['proper-nouns', 'quoted-terms'],
|
|
819
|
-
excludePatterns: [
|
|
819
|
+
excludePatterns: [
|
|
820
|
+
'^The ', '^A ', '^An ', '^This ', '^That ', '^These ', '^Those ',
|
|
821
|
+
'^v?\\d+(?:\\.\\d+){1,3}(?:[-.][a-zA-Z0-9]+)?$',
|
|
822
|
+
],
|
|
820
823
|
minEntityLength: 3,
|
|
821
824
|
};
|
|
822
825
|
/**
|
package/package.json
CHANGED