@velvetmonkey/vault-core 2.0.140 → 2.0.142

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/entities.js CHANGED
@@ -249,13 +249,53 @@ function mapFrontmatterType(type) {
249
249
  * 7. Projects - multi-word (fallback)
250
250
  * 8. Other - single word default
251
251
  */
252
- function categorizeEntity(name, techKeywords, frontmatterType) {
252
+ // Folder names that imply entity categories
253
+ const FOLDER_CATEGORY_MAP = {
254
+ 'people': 'people',
255
+ 'person': 'people',
256
+ 'contacts': 'people',
257
+ 'team': 'people',
258
+ 'members': 'people',
259
+ 'projects': 'projects',
260
+ 'project': 'projects',
261
+ 'locations': 'locations',
262
+ 'places': 'locations',
263
+ 'companies': 'organizations',
264
+ 'organizations': 'organizations',
265
+ 'orgs': 'organizations',
266
+ 'concepts': 'concepts',
267
+ 'topics': 'concepts',
268
+ 'tools': 'technologies',
269
+ 'software': 'technologies',
270
+ 'media': 'media',
271
+ 'books': 'media',
272
+ 'films': 'media',
273
+ 'movies': 'media',
274
+ 'music': 'media',
275
+ 'vehicles': 'vehicles',
276
+ 'equipment': 'technologies',
277
+ 'food': 'food',
278
+ 'recipes': 'food',
279
+ 'health': 'health',
280
+ 'finance': 'finance',
281
+ 'hobbies': 'hobbies',
282
+ };
283
+ function categorizeEntity(name, techKeywords, frontmatterType, notePath) {
253
284
  // 0. Frontmatter type takes priority
254
285
  if (frontmatterType) {
255
286
  const mapped = mapFrontmatterType(frontmatterType);
256
287
  if (mapped)
257
288
  return mapped;
258
289
  }
290
+ // 0.5. Folder-based inference (e.g., people/Andrew.md → person)
291
+ if (notePath) {
292
+ const segments = notePath.toLowerCase().split('/');
293
+ for (const segment of segments) {
294
+ const mapped = FOLDER_CATEGORY_MAP[segment];
295
+ if (mapped)
296
+ return mapped;
297
+ }
298
+ }
259
299
  const nameLower = name.toLowerCase();
260
300
  const words = name.split(/\s+/);
261
301
  // 1. Technology check (keyword match)
@@ -438,7 +478,7 @@ export async function scanVaultEntities(vaultPath, options = {}) {
438
478
  },
439
479
  };
440
480
  for (const entity of uniqueEntities) {
441
- const category = categorizeEntity(entity.name, techKeywords, entity.frontmatterType);
481
+ const category = categorizeEntity(entity.name, techKeywords, entity.frontmatterType, entity.relativePath);
442
482
  // Store as EntityWithAliases object
443
483
  const entityObj = {
444
484
  name: entity.name,
@@ -2,9 +2,13 @@
2
2
  * SQLite Schema Migrations
3
3
  *
4
4
  * Database path resolution, schema initialization, migration logic,
5
- * and database file management utilities.
5
+ * database file management, backup rotation, integrity checks, and
6
+ * feedback salvage utilities.
6
7
  */
7
8
  import Database from 'better-sqlite3';
9
+ export declare const BACKUP_ROTATION_COUNT = 3;
10
+ /** High-value tables whose data should survive a corruption recovery. */
11
+ export declare const SALVAGE_TABLES: readonly ["wikilink_feedback", "wikilink_applications", "suggestion_events", "wikilink_suppressions", "note_links", "note_link_history", "memories", "session_summaries", "corrections"];
8
12
  /**
9
13
  * Get the database path for a vault
10
14
  */
@@ -18,4 +22,35 @@ export declare function deleteStateDbFiles(dbPath: string): void;
18
22
  export declare function backupStateDb(dbPath: string): void;
19
23
  /** Preserve a corrupted database for inspection before deleting. */
20
24
  export declare function preserveCorruptedDb(dbPath: string): void;
25
+ /**
26
+ * Rotate existing backup files: .backup → .backup.1 → .backup.2 → .backup.3
27
+ * Drops the oldest if rotation count exceeded. Does NOT create a new backup.
28
+ */
29
+ export declare function rotateBackupFiles(dbPath: string): void;
30
+ /**
31
+ * Create a WAL-safe backup using SQLite's backup API.
32
+ * Rotates existing backups first, then writes a new .backup file.
33
+ */
34
+ export declare function safeBackupAsync(db: Database.Database, dbPath: string): Promise<boolean>;
35
+ /**
36
+ * Run PRAGMA quick_check on the database.
37
+ * Returns { ok: true } or { ok: false, detail: string }.
38
+ */
39
+ export declare function checkDbIntegrity(db: Database.Database): {
40
+ ok: boolean;
41
+ detail?: string;
42
+ };
43
+ /**
44
+ * Attempt to copy high-value feedback tables from a source DB into the target.
45
+ * Opens source read-only; copies rows with INSERT OR IGNORE.
46
+ * Handles missing tables and column mismatches gracefully.
47
+ */
48
+ export declare function salvageFeedbackTables(targetDb: Database.Database, sourceDbPath: string): Record<string, number>;
49
+ /**
50
+ * After corruption forces a fresh DB, attempt to recover feedback data
51
+ * from all available backup files (newest first) and the corrupt file.
52
+ * Merges across all sources — INSERT OR IGNORE deduplicates, so each
53
+ * successive source only adds rows the previous ones didn't cover.
54
+ */
55
+ export declare function attemptSalvage(targetDb: Database.Database, dbPath: string): void;
21
56
  //# sourceMappingURL=migrations.d.ts.map
@@ -2,12 +2,30 @@
2
2
  * SQLite Schema Migrations
3
3
  *
4
4
  * Database path resolution, schema initialization, migration logic,
5
- * and database file management utilities.
5
+ * database file management, backup rotation, integrity checks, and
6
+ * feedback salvage utilities.
6
7
  */
8
+ import Database from 'better-sqlite3';
7
9
  import * as fs from 'fs';
8
10
  import * as path from 'path';
9
11
  import { SCHEMA_VERSION, SCHEMA_SQL, STATE_DB_FILENAME, FLYWHEEL_DIR } from './schema.js';
10
12
  // =============================================================================
13
+ // Backup & Recovery Constants
14
+ // =============================================================================
15
+ export const BACKUP_ROTATION_COUNT = 3;
16
+ /** High-value tables whose data should survive a corruption recovery. */
17
+ export const SALVAGE_TABLES = [
18
+ 'wikilink_feedback',
19
+ 'wikilink_applications',
20
+ 'suggestion_events',
21
+ 'wikilink_suppressions',
22
+ 'note_links',
23
+ 'note_link_history',
24
+ 'memories',
25
+ 'session_summaries',
26
+ 'corrections',
27
+ ];
28
+ // =============================================================================
11
29
  // Database Path Resolution
12
30
  // =============================================================================
13
31
  /**
@@ -317,4 +335,166 @@ export function preserveCorruptedDb(dbPath) {
317
335
  // Best effort — don't block recovery
318
336
  }
319
337
  }
338
+ // =============================================================================
339
+ // Backup Rotation & Safe Backup
340
+ // =============================================================================
341
+ /**
342
+ * Rotate existing backup files: .backup → .backup.1 → .backup.2 → .backup.3
343
+ * Drops the oldest if rotation count exceeded. Does NOT create a new backup.
344
+ */
345
+ export function rotateBackupFiles(dbPath) {
346
+ try {
347
+ // Shift numbered backups down (3→drop, 2→3, 1→2)
348
+ for (let i = BACKUP_ROTATION_COUNT; i >= 1; i--) {
349
+ const src = i === 1
350
+ ? `${dbPath}.backup`
351
+ : `${dbPath}.backup.${i - 1}`;
352
+ const dst = `${dbPath}.backup.${i}`;
353
+ if (fs.existsSync(src)) {
354
+ if (i === BACKUP_ROTATION_COUNT && fs.existsSync(dst)) {
355
+ fs.unlinkSync(dst);
356
+ }
357
+ fs.renameSync(src, dst);
358
+ }
359
+ }
360
+ }
361
+ catch (err) {
362
+ console.error(`[vault-core] Failed to rotate backups: ${err instanceof Error ? err.message : err}`);
363
+ }
364
+ }
365
+ /**
366
+ * Create a WAL-safe backup using SQLite's backup API.
367
+ * Rotates existing backups first, then writes a new .backup file.
368
+ */
369
+ export async function safeBackupAsync(db, dbPath) {
370
+ try {
371
+ rotateBackupFiles(dbPath);
372
+ const backupPath = `${dbPath}.backup`;
373
+ await db.backup(backupPath);
374
+ console.error(`[vault-core] Safe backup created: ${path.basename(backupPath)}`);
375
+ return true;
376
+ }
377
+ catch (err) {
378
+ console.error(`[vault-core] Safe backup failed: ${err instanceof Error ? err.message : err}`);
379
+ return false;
380
+ }
381
+ }
382
+ // =============================================================================
383
+ // Integrity Checks
384
+ // =============================================================================
385
+ /**
386
+ * Run PRAGMA quick_check on the database.
387
+ * Returns { ok: true } or { ok: false, detail: string }.
388
+ */
389
+ export function checkDbIntegrity(db) {
390
+ try {
391
+ const result = db.pragma('quick_check');
392
+ const firstValue = result.length > 0 ? Object.values(result[0])[0] : 'no result';
393
+ if (result.length === 1 && firstValue === 'ok') {
394
+ return { ok: true };
395
+ }
396
+ return { ok: false, detail: firstValue ?? 'unknown' };
397
+ }
398
+ catch (err) {
399
+ return { ok: false, detail: err instanceof Error ? err.message : String(err) };
400
+ }
401
+ }
402
+ // =============================================================================
403
+ // Feedback Salvage
404
+ // =============================================================================
405
+ /**
406
+ * Attempt to copy high-value feedback tables from a source DB into the target.
407
+ * Opens source read-only; copies rows with INSERT OR IGNORE.
408
+ * Handles missing tables and column mismatches gracefully.
409
+ */
410
+ export function salvageFeedbackTables(targetDb, sourceDbPath) {
411
+ const results = {};
412
+ if (!fs.existsSync(sourceDbPath))
413
+ return results;
414
+ let sourceDb = null;
415
+ try {
416
+ sourceDb = new Database(sourceDbPath, { readonly: true, fileMustExist: true });
417
+ for (const table of SALVAGE_TABLES) {
418
+ try {
419
+ // Check table exists in both source and target
420
+ const srcExists = sourceDb.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`).get(table);
421
+ if (!srcExists)
422
+ continue;
423
+ const tgtExists = targetDb.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`).get(table);
424
+ if (!tgtExists)
425
+ continue;
426
+ // Find columns common to both (handles schema version mismatches)
427
+ const targetCols = targetDb.pragma(`table_info('${table}')`)
428
+ .map(c => c.name);
429
+ const sourceCols = sourceDb.pragma(`table_info('${table}')`)
430
+ .map(c => c.name);
431
+ const commonCols = targetCols.filter(c => sourceCols.includes(c));
432
+ if (commonCols.length === 0)
433
+ continue;
434
+ const colList = commonCols.join(', ');
435
+ const placeholders = commonCols.map(() => '?').join(', ');
436
+ const rows = sourceDb.prepare(`SELECT ${colList} FROM ${table}`).all();
437
+ if (rows.length === 0)
438
+ continue;
439
+ const insert = targetDb.prepare(`INSERT OR IGNORE INTO ${table} (${colList}) VALUES (${placeholders})`);
440
+ const insertMany = targetDb.transaction((data) => {
441
+ let count = 0;
442
+ for (const row of data) {
443
+ insert.run(...commonCols.map(c => row[c]));
444
+ count++;
445
+ }
446
+ return count;
447
+ });
448
+ const count = insertMany(rows);
449
+ if (count > 0)
450
+ results[table] = count;
451
+ }
452
+ catch (tableErr) {
453
+ console.error(`[vault-core] Salvage ${table}: ${tableErr instanceof Error ? tableErr.message : tableErr}`);
454
+ }
455
+ }
456
+ }
457
+ catch (err) {
458
+ console.error(`[vault-core] Cannot open ${path.basename(sourceDbPath)} for salvage: ${err instanceof Error ? err.message : err}`);
459
+ }
460
+ finally {
461
+ try {
462
+ sourceDb?.close();
463
+ }
464
+ catch { /* ignore */ }
465
+ }
466
+ return results;
467
+ }
468
+ /**
469
+ * After corruption forces a fresh DB, attempt to recover feedback data
470
+ * from all available backup files (newest first) and the corrupt file.
471
+ * Merges across all sources — INSERT OR IGNORE deduplicates, so each
472
+ * successive source only adds rows the previous ones didn't cover.
473
+ */
474
+ export function attemptSalvage(targetDb, dbPath) {
475
+ const sources = [
476
+ `${dbPath}.backup`,
477
+ ...Array.from({ length: BACKUP_ROTATION_COUNT }, (_, i) => `${dbPath}.backup.${i + 1}`),
478
+ `${dbPath}.corrupt`,
479
+ ];
480
+ let totalSalvaged = 0;
481
+ for (const source of sources) {
482
+ if (!fs.existsSync(source))
483
+ continue;
484
+ console.error(`[vault-core] Attempting feedback salvage from ${path.basename(source)}...`);
485
+ const results = salvageFeedbackTables(targetDb, source);
486
+ const sourceRows = Object.values(results).reduce((a, b) => a + b, 0);
487
+ if (sourceRows > 0) {
488
+ const detail = Object.entries(results).map(([t, n]) => `${t}: ${n}`).join(', ');
489
+ console.error(`[vault-core] Salvaged ${sourceRows} rows from ${path.basename(source)}: ${detail}`);
490
+ totalSalvaged += sourceRows;
491
+ }
492
+ }
493
+ if (totalSalvaged > 0) {
494
+ console.error(`[vault-core] Total salvaged: ${totalSalvaged} rows across all sources`);
495
+ }
496
+ else {
497
+ console.error('[vault-core] No salvageable backup found — starting fresh');
498
+ }
499
+ }
320
500
  //# sourceMappingURL=migrations.js.map
package/dist/sqlite.d.ts CHANGED
@@ -14,6 +14,7 @@ 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
16
  export { getStateDbPath, initSchema, deleteStateDbFiles, backupStateDb, preserveCorruptedDb } from './migrations.js';
17
+ export { BACKUP_ROTATION_COUNT, SALVAGE_TABLES, rotateBackupFiles, safeBackupAsync, checkDbIntegrity, salvageFeedbackTables, attemptSalvage, } from './migrations.js';
17
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';
18
19
  export type { FlywheelConfigRow, VaultIndexCacheData, VaultIndexCacheInfo } from './queries.js';
19
20
  /** Search result from FTS5 entity search */
package/dist/sqlite.js CHANGED
@@ -15,10 +15,12 @@ import * as fs from 'fs';
15
15
  export { SCHEMA_VERSION, STATE_DB_FILENAME, FLYWHEEL_DIR, SCHEMA_SQL } from './schema.js';
16
16
  // Re-export migrations
17
17
  export { getStateDbPath, initSchema, deleteStateDbFiles, backupStateDb, preserveCorruptedDb } from './migrations.js';
18
+ // Re-export backup & recovery
19
+ export { BACKUP_ROTATION_COUNT, SALVAGE_TABLES, rotateBackupFiles, safeBackupAsync, checkDbIntegrity, salvageFeedbackTables, attemptSalvage, } from './migrations.js';
18
20
  // Re-export all query functions
19
21
  export { searchEntities, searchEntitiesPrefix, getEntityByName, getAllEntitiesFromDb, getEntityIndexFromDb, getEntitiesByAlias, recordEntityMention, getEntityRecency, getAllRecency, setWriteState, getWriteState, deleteWriteState, setFlywheelConfig, getFlywheelConfig, getAllFlywheelConfig, deleteFlywheelConfig, saveFlywheelConfigToDb, loadFlywheelConfigFromDb, recordMergeDismissal, getDismissedMergePairs, getStateDbMetadata, isEntityDataStale, escapeFts5Query, rebuildEntitiesFts, stateDbExists, deleteStateDb, saveVaultIndexCache, loadVaultIndexCache, getVaultIndexCacheInfo, clearVaultIndexCache, isVaultIndexCacheValid, loadContentHashes, saveContentHashBatch, renameContentHash, } from './queries.js';
20
22
  // Import for use in openStateDb
21
- import { getStateDbPath, initSchema, deleteStateDbFiles, backupStateDb, preserveCorruptedDb } from './migrations.js';
23
+ import { getStateDbPath, initSchema, deleteStateDbFiles, preserveCorruptedDb, attemptSalvage } from './migrations.js';
22
24
  // =============================================================================
23
25
  // Factory
24
26
  // =============================================================================
@@ -30,8 +32,9 @@ import { getStateDbPath, initSchema, deleteStateDbFiles, backupStateDb, preserve
30
32
  */
31
33
  export function openStateDb(vaultPath) {
32
34
  const dbPath = getStateDbPath(vaultPath);
33
- // Back up existing database before any mutations
34
- backupStateDb(dbPath);
35
+ // Note: safe backup with rotation is done AFTER open + integrity check
36
+ // by the caller (initializeVault), not here. This avoids overwriting
37
+ // good backups with a corrupt DB before we've verified it.
35
38
  // Guard: Delete corrupted 0-byte database files
36
39
  // This can happen when better-sqlite3 fails to compile (e.g., Node 24)
37
40
  // and creates an empty file instead of a valid SQLite database
@@ -42,10 +45,15 @@ export function openStateDb(vaultPath) {
42
45
  deleteStateDbFiles(dbPath);
43
46
  }
44
47
  }
48
+ const isNewDb = !fs.existsSync(dbPath);
45
49
  let db;
46
50
  try {
47
51
  db = new Database(dbPath);
48
52
  initSchema(db);
53
+ // If we just created a fresh DB but backup files exist, salvage from them
54
+ if (isNewDb) {
55
+ attemptSalvage(db, dbPath);
56
+ }
49
57
  }
50
58
  catch (err) {
51
59
  const msg = err instanceof Error ? err.message : String(err);
@@ -61,6 +69,8 @@ export function openStateDb(vaultPath) {
61
69
  deleteStateDbFiles(dbPath);
62
70
  db = new Database(dbPath);
63
71
  initSchema(db);
72
+ // Try to recover feedback data from backups or the corrupt file
73
+ attemptSalvage(db, dbPath);
64
74
  }
65
75
  else {
66
76
  // Recoverable error (constraint violation, migration issue, etc.) — don't destroy the DB
package/dist/wikilinks.js CHANGED
@@ -1083,6 +1083,9 @@ export function detectImplicitEntities(content, config = {}) {
1083
1083
  // Length check
1084
1084
  if (text.length < minEntityLength)
1085
1085
  return true;
1086
+ // Must contain at least one letter — pure punctuation/symbols are never entities
1087
+ if (!/[a-zA-Z]/.test(text))
1088
+ return true;
1086
1089
  // Common words
1087
1090
  if (IMPLICIT_EXCLUDE_WORDS.has(text.toLowerCase()))
1088
1091
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/vault-core",
3
- "version": "2.0.140",
3
+ "version": "2.0.142",
4
4
  "description": "Shared vault utilities for Flywheel ecosystem (entity scanning, wikilinks, protected zones)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",