@soleri/core 2.5.0 → 2.6.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.
Files changed (81) hide show
  1. package/dist/brain/intelligence.d.ts +1 -0
  2. package/dist/brain/intelligence.d.ts.map +1 -1
  3. package/dist/brain/intelligence.js +164 -148
  4. package/dist/brain/intelligence.js.map +1 -1
  5. package/dist/control/identity-manager.d.ts +3 -1
  6. package/dist/control/identity-manager.d.ts.map +1 -1
  7. package/dist/control/identity-manager.js +49 -51
  8. package/dist/control/identity-manager.js.map +1 -1
  9. package/dist/control/intent-router.d.ts +1 -0
  10. package/dist/control/intent-router.d.ts.map +1 -1
  11. package/dist/control/intent-router.js +32 -32
  12. package/dist/control/intent-router.js.map +1 -1
  13. package/dist/curator/curator.d.ts +1 -0
  14. package/dist/curator/curator.d.ts.map +1 -1
  15. package/dist/curator/curator.js +48 -99
  16. package/dist/curator/curator.js.map +1 -1
  17. package/dist/governance/governance.d.ts +1 -0
  18. package/dist/governance/governance.d.ts.map +1 -1
  19. package/dist/governance/governance.js +51 -68
  20. package/dist/governance/governance.js.map +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/persistence/index.d.ts +2 -1
  26. package/dist/persistence/index.d.ts.map +1 -1
  27. package/dist/persistence/index.js +1 -0
  28. package/dist/persistence/index.js.map +1 -1
  29. package/dist/persistence/postgres-provider.d.ts +46 -0
  30. package/dist/persistence/postgres-provider.d.ts.map +1 -0
  31. package/dist/persistence/postgres-provider.js +115 -0
  32. package/dist/persistence/postgres-provider.js.map +1 -0
  33. package/dist/persistence/sqlite-provider.d.ts +5 -2
  34. package/dist/persistence/sqlite-provider.d.ts.map +1 -1
  35. package/dist/persistence/sqlite-provider.js +39 -1
  36. package/dist/persistence/sqlite-provider.js.map +1 -1
  37. package/dist/persistence/types.d.ts +23 -1
  38. package/dist/persistence/types.d.ts.map +1 -1
  39. package/dist/project/project-registry.d.ts +4 -4
  40. package/dist/project/project-registry.d.ts.map +1 -1
  41. package/dist/project/project-registry.js +25 -50
  42. package/dist/project/project-registry.js.map +1 -1
  43. package/dist/runtime/admin-extra-ops.d.ts +3 -3
  44. package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
  45. package/dist/runtime/admin-extra-ops.js +29 -3
  46. package/dist/runtime/admin-extra-ops.js.map +1 -1
  47. package/dist/runtime/core-ops.d.ts +4 -4
  48. package/dist/runtime/core-ops.js +4 -4
  49. package/dist/runtime/runtime.js +1 -1
  50. package/dist/runtime/runtime.js.map +1 -1
  51. package/dist/runtime/vault-extra-ops.d.ts +3 -2
  52. package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
  53. package/dist/runtime/vault-extra-ops.js +40 -2
  54. package/dist/runtime/vault-extra-ops.js.map +1 -1
  55. package/dist/vault/vault.d.ts +21 -0
  56. package/dist/vault/vault.d.ts.map +1 -1
  57. package/dist/vault/vault.js +99 -0
  58. package/dist/vault/vault.js.map +1 -1
  59. package/package.json +4 -2
  60. package/src/__tests__/admin-extra-ops.test.ts +2 -2
  61. package/src/__tests__/core-ops.test.ts +8 -2
  62. package/src/__tests__/persistence.test.ts +66 -0
  63. package/src/__tests__/postgres-provider.test.ts +58 -0
  64. package/src/__tests__/vault-extra-ops.test.ts +2 -2
  65. package/src/__tests__/vault.test.ts +184 -0
  66. package/src/brain/intelligence.ts +258 -307
  67. package/src/control/identity-manager.ts +77 -75
  68. package/src/control/intent-router.ts +55 -57
  69. package/src/curator/curator.ts +124 -145
  70. package/src/governance/governance.ts +90 -107
  71. package/src/index.ts +2 -0
  72. package/src/persistence/index.ts +2 -0
  73. package/src/persistence/postgres-provider.ts +157 -0
  74. package/src/persistence/sqlite-provider.ts +55 -2
  75. package/src/persistence/types.ts +31 -1
  76. package/src/project/project-registry.ts +69 -74
  77. package/src/runtime/admin-extra-ops.ts +36 -3
  78. package/src/runtime/core-ops.ts +4 -4
  79. package/src/runtime/runtime.ts +1 -1
  80. package/src/runtime/vault-extra-ops.ts +42 -2
  81. package/src/vault/vault.ts +118 -0
@@ -1,6 +1,7 @@
1
1
  import type { Vault } from '../vault/vault.js';
2
2
  import type { IntelligenceEntry } from '../intelligence/types.js';
3
3
  import type { CogneeClient } from '../cognee/client.js';
4
+ import type { PersistenceProvider } from '../persistence/types.js';
4
5
  import {
5
6
  tokenize,
6
7
  calculateTfIdf,
@@ -51,10 +52,12 @@ const DEFAULT_TAG_ALIASES: Array<[string, string]> = [
51
52
  export class Curator {
52
53
  private vault: Vault;
53
54
  private cognee: CogneeClient | undefined;
55
+ private provider: PersistenceProvider;
54
56
 
55
57
  constructor(vault: Vault, cognee?: CogneeClient) {
56
58
  this.vault = vault;
57
59
  this.cognee = cognee;
60
+ this.provider = vault.getProvider();
58
61
  this.initializeTables();
59
62
  this.seedDefaultAliases();
60
63
  }
@@ -62,8 +65,7 @@ export class Curator {
62
65
  // ─── Schema ─────────────────────────────────────────────────────
63
66
 
64
67
  private initializeTables(): void {
65
- const db = this.vault.getDb();
66
- db.exec(`
68
+ this.provider.execSql(`
67
69
  CREATE TABLE IF NOT EXISTS curator_entry_state (
68
70
  entry_id TEXT PRIMARY KEY,
69
71
  status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'stale', 'archived')),
@@ -115,41 +117,39 @@ export class Curator {
115
117
  resolved_at INTEGER,
116
118
  UNIQUE(pattern_id, antipattern_id)
117
119
  );
120
+ CREATE INDEX IF NOT EXISTS idx_curator_state_status ON curator_entry_state(status);
121
+ CREATE INDEX IF NOT EXISTS idx_curator_changelog_entry ON curator_changelog(entry_id);
118
122
  `);
119
123
  }
120
124
 
121
125
  private seedDefaultAliases(): void {
122
- const db = this.vault.getDb();
123
- const insertCanonical = db.prepare(
124
- 'INSERT OR IGNORE INTO curator_tag_canonical (tag) VALUES (?)',
125
- );
126
- const insertAlias = db.prepare(
127
- 'INSERT OR IGNORE INTO curator_tag_alias (alias, canonical) VALUES (?, ?)',
128
- );
129
- const tx = db.transaction(() => {
126
+ this.provider.transaction(() => {
130
127
  const canonicals = new Set(DEFAULT_TAG_ALIASES.map(([, c]) => c));
131
128
  for (const tag of canonicals) {
132
- insertCanonical.run(tag);
129
+ this.provider.run('INSERT OR IGNORE INTO curator_tag_canonical (tag) VALUES (?)', [tag]);
133
130
  }
134
131
  for (const [alias, canonical] of DEFAULT_TAG_ALIASES) {
135
- insertAlias.run(alias, canonical);
132
+ this.provider.run(
133
+ 'INSERT OR IGNORE INTO curator_tag_alias (alias, canonical) VALUES (?, ?)',
134
+ [alias, canonical],
135
+ );
136
136
  }
137
137
  });
138
- tx();
139
138
  }
140
139
 
141
140
  // ─── Status ─────────────────────────────────────────────────────
142
141
 
143
142
  getStatus(): CuratorStatus {
144
- const db = this.vault.getDb();
145
143
  const tableCount = (table: string): number =>
146
- (db.prepare(`SELECT COUNT(*) as count FROM ${table}`).get() as { count: number }).count;
144
+ (
145
+ this.provider.get<{ count: number }>(`SELECT COUNT(*) as count FROM ${table}`) ?? {
146
+ count: 0,
147
+ }
148
+ ).count;
147
149
 
148
- const lastGroomed = db
149
- .prepare(
150
- 'SELECT MAX(last_groomed_at) as ts FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
151
- )
152
- .get() as { ts: number | null };
150
+ const lastGroomed = this.provider.get<{ ts: number | null }>(
151
+ 'SELECT MAX(last_groomed_at) as ts FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
152
+ ) ?? { ts: null };
153
153
 
154
154
  return {
155
155
  initialized: true,
@@ -167,11 +167,11 @@ export class Curator {
167
167
  // ─── Tag Normalization ──────────────────────────────────────────
168
168
 
169
169
  normalizeTag(tag: string): TagNormalizationResult {
170
- const db = this.vault.getDb();
171
170
  const lower = tag.toLowerCase().trim();
172
- const row = db.prepare('SELECT canonical FROM curator_tag_alias WHERE alias = ?').get(lower) as
173
- | { canonical: string }
174
- | undefined;
171
+ const row = this.provider.get<{ canonical: string }>(
172
+ 'SELECT canonical FROM curator_tag_alias WHERE alias = ?',
173
+ [lower],
174
+ );
175
175
  if (row) {
176
176
  return { original: tag, normalized: row.canonical, wasAliased: true };
177
177
  }
@@ -195,11 +195,10 @@ export class Curator {
195
195
 
196
196
  if (changed) {
197
197
  const dedupedTags = [...new Set(normalizedTags)];
198
- const db = this.vault.getDb();
199
- db.prepare('UPDATE entries SET tags = ?, updated_at = unixepoch() WHERE id = ?').run(
198
+ this.provider.run('UPDATE entries SET tags = ?, updated_at = unixepoch() WHERE id = ?', [
200
199
  JSON.stringify(dedupedTags),
201
200
  entryId,
202
- );
201
+ ]);
203
202
  this.logChange(
204
203
  'normalize_tags',
205
204
  entryId,
@@ -213,26 +212,28 @@ export class Curator {
213
212
  }
214
213
 
215
214
  addTagAlias(alias: string, canonical: string): void {
216
- const db = this.vault.getDb();
217
215
  const lower = alias.toLowerCase().trim();
218
216
  const canonicalLower = canonical.toLowerCase().trim();
219
- db.prepare('INSERT OR IGNORE INTO curator_tag_canonical (tag) VALUES (?)').run(canonicalLower);
220
- db.prepare('INSERT OR REPLACE INTO curator_tag_alias (alias, canonical) VALUES (?, ?)').run(
217
+ this.provider.run('INSERT OR IGNORE INTO curator_tag_canonical (tag) VALUES (?)', [
218
+ canonicalLower,
219
+ ]);
220
+ this.provider.run('INSERT OR REPLACE INTO curator_tag_alias (alias, canonical) VALUES (?, ?)', [
221
221
  lower,
222
222
  canonicalLower,
223
- );
223
+ ]);
224
224
  }
225
225
 
226
226
  getCanonicalTags(): CanonicalTag[] {
227
- const db = this.vault.getDb();
228
- const rows = db
229
- .prepare(
230
- `SELECT c.tag, c.description,
231
- (SELECT COUNT(*) FROM curator_tag_alias a WHERE a.canonical = c.tag) as alias_count
232
- FROM curator_tag_canonical c
233
- ORDER BY c.tag`,
234
- )
235
- .all() as Array<{ tag: string; description: string | null; alias_count: number }>;
227
+ const rows = this.provider.all<{
228
+ tag: string;
229
+ description: string | null;
230
+ alias_count: number;
231
+ }>(
232
+ `SELECT c.tag, c.description,
233
+ (SELECT COUNT(*) FROM curator_tag_alias a WHERE a.canonical = c.tag) as alias_count
234
+ FROM curator_tag_canonical c
235
+ ORDER BY c.tag`,
236
+ );
236
237
 
237
238
  return rows.map((row) => ({
238
239
  tag: row.tag,
@@ -243,11 +244,11 @@ export class Curator {
243
244
  }
244
245
 
245
246
  private countTagUsage(tag: string): number {
246
- const db = this.vault.getDb();
247
- const row = db
248
- .prepare('SELECT COUNT(*) as count FROM entries WHERE tags LIKE ?')
249
- .get(`%"${tag}"%`) as { count: number };
250
- return row.count;
247
+ const row = this.provider.get<{ count: number }>(
248
+ 'SELECT COUNT(*) as count FROM entries WHERE tags LIKE ?',
249
+ [`%"${tag}"%`],
250
+ );
251
+ return row?.count ?? 0;
251
252
  }
252
253
 
253
254
  // ─── Duplicate Detection ────────────────────────────────────────
@@ -315,14 +316,8 @@ export class Curator {
315
316
  if (antipatterns.length === 0 || patterns.length === 0) return [];
316
317
 
317
318
  const vocabulary = this.buildVocabulary(entries);
318
- const db = this.vault.getDb();
319
319
  const detected: Contradiction[] = [];
320
320
 
321
- const insertStmt = db.prepare(
322
- `INSERT OR IGNORE INTO curator_contradictions (pattern_id, antipattern_id, similarity)
323
- VALUES (?, ?, ?)`,
324
- );
325
-
326
321
  for (const ap of antipatterns) {
327
322
  // Stage 1: FTS5 candidate retrieval (fall back to all patterns if FTS returns empty)
328
323
  let candidates: IntelligenceEntry[];
@@ -343,14 +338,16 @@ export class Curator {
343
338
  const similarity = cosineSimilarity(apVec, pVec);
344
339
 
345
340
  if (similarity >= effectiveThreshold) {
346
- const result = insertStmt.run(pattern.id, ap.id, similarity);
341
+ const result = this.provider.run(
342
+ 'INSERT OR IGNORE INTO curator_contradictions (pattern_id, antipattern_id, similarity) VALUES (?, ?, ?)',
343
+ [pattern.id, ap.id, similarity],
344
+ );
347
345
  if (result.changes > 0) {
348
- const row = db
349
- .prepare(
350
- 'SELECT * FROM curator_contradictions WHERE pattern_id = ? AND antipattern_id = ?',
351
- )
352
- .get(pattern.id, ap.id) as Record<string, unknown>;
353
- detected.push(this.rowToContradiction(row));
346
+ const row = this.provider.get<Record<string, unknown>>(
347
+ 'SELECT * FROM curator_contradictions WHERE pattern_id = ? AND antipattern_id = ?',
348
+ [pattern.id, ap.id],
349
+ );
350
+ if (row) detected.push(this.rowToContradiction(row));
354
351
  }
355
352
  }
356
353
  }
@@ -360,24 +357,22 @@ export class Curator {
360
357
  }
361
358
 
362
359
  getContradictions(status?: ContradictionStatus): Contradiction[] {
363
- const db = this.vault.getDb();
364
360
  const query = status
365
361
  ? 'SELECT * FROM curator_contradictions WHERE status = ? ORDER BY similarity DESC'
366
362
  : 'SELECT * FROM curator_contradictions ORDER BY similarity DESC';
367
- const rows = (status ? db.prepare(query).all(status) : db.prepare(query).all()) as Array<
368
- Record<string, unknown>
369
- >;
363
+ const rows = this.provider.all<Record<string, unknown>>(query, status ? [status] : undefined);
370
364
  return rows.map((r) => this.rowToContradiction(r));
371
365
  }
372
366
 
373
367
  resolveContradiction(id: number, resolution: 'resolved' | 'dismissed'): Contradiction | null {
374
- const db = this.vault.getDb();
375
- db.prepare(
368
+ this.provider.run(
376
369
  'UPDATE curator_contradictions SET status = ?, resolved_at = unixepoch() WHERE id = ?',
377
- ).run(resolution, id);
378
- const row = db.prepare('SELECT * FROM curator_contradictions WHERE id = ?').get(id) as
379
- | Record<string, unknown>
380
- | undefined;
370
+ [resolution, id],
371
+ );
372
+ const row = this.provider.get<Record<string, unknown>>(
373
+ 'SELECT * FROM curator_contradictions WHERE id = ?',
374
+ [id],
375
+ );
381
376
  return row ? this.rowToContradiction(row) : null;
382
377
  }
383
378
 
@@ -396,14 +391,8 @@ export class Curator {
396
391
  }
397
392
 
398
393
  const vocabulary = this.buildVocabulary(entries);
399
- const db = this.vault.getDb();
400
394
  const detected: Contradiction[] = [];
401
395
 
402
- const insertStmt = db.prepare(
403
- `INSERT OR IGNORE INTO curator_contradictions (pattern_id, antipattern_id, similarity)
404
- VALUES (?, ?, ?)`,
405
- );
406
-
407
396
  const cogneeAvailable = this.cognee?.isAvailable ?? false;
408
397
 
409
398
  for (const ap of antipatterns) {
@@ -440,14 +429,16 @@ export class Curator {
440
429
  }
441
430
 
442
431
  if (finalScore >= effectiveThreshold) {
443
- const result = insertStmt.run(pattern.id, ap.id, finalScore);
432
+ const result = this.provider.run(
433
+ 'INSERT OR IGNORE INTO curator_contradictions (pattern_id, antipattern_id, similarity) VALUES (?, ?, ?)',
434
+ [pattern.id, ap.id, finalScore],
435
+ );
444
436
  if (result.changes > 0) {
445
- const row = db
446
- .prepare(
447
- 'SELECT * FROM curator_contradictions WHERE pattern_id = ? AND antipattern_id = ?',
448
- )
449
- .get(pattern.id, ap.id) as Record<string, unknown>;
450
- detected.push(this.rowToContradiction(row));
437
+ const row = this.provider.get<Record<string, unknown>>(
438
+ 'SELECT * FROM curator_contradictions WHERE pattern_id = ? AND antipattern_id = ?',
439
+ [pattern.id, ap.id],
440
+ );
441
+ if (row) detected.push(this.rowToContradiction(row));
451
442
  }
452
443
  }
453
444
  }
@@ -469,21 +460,22 @@ export class Curator {
469
460
  const tagsNormalized = this.normalizeTags(entryId);
470
461
 
471
462
  // Check staleness based on entry's updated_at timestamp
472
- const db = this.vault.getDb();
473
- const row = db.prepare('SELECT updated_at FROM entries WHERE id = ?').get(entryId) as
474
- | { updated_at: number }
475
- | undefined;
463
+ const row = this.provider.get<{ updated_at: number }>(
464
+ 'SELECT updated_at FROM entries WHERE id = ?',
465
+ [entryId],
466
+ );
476
467
  const now = Math.floor(Date.now() / 1000);
477
468
  const stale = row ? now - row.updated_at > DEFAULT_STALE_DAYS * 86400 : false;
478
469
 
479
470
  const status = stale ? 'stale' : 'active';
480
471
 
481
472
  // Upsert entry state
482
- db.prepare(
473
+ this.provider.run(
483
474
  `INSERT INTO curator_entry_state (entry_id, status, last_groomed_at)
484
475
  VALUES (?, ?, unixepoch())
485
476
  ON CONFLICT(entry_id) DO UPDATE SET status = excluded.status, last_groomed_at = unixepoch()`,
486
- ).run(entryId, status);
477
+ [entryId, status],
478
+ );
487
479
 
488
480
  this.logChange('groom', entryId, null, `status=${status}`, 'Routine grooming');
489
481
 
@@ -532,12 +524,12 @@ export class Curator {
532
524
  const duplicates = this.detectDuplicates(undefined, duplicateThreshold);
533
525
 
534
526
  // Detect stale entries
535
- const db = this.vault.getDb();
536
527
  const now = Math.floor(Date.now() / 1000);
537
528
  const staleThreshold = now - staleDaysThreshold * 86400;
538
- const staleRows = db
539
- .prepare('SELECT id FROM entries WHERE updated_at < ?')
540
- .all(staleThreshold) as Array<{ id: string }>;
529
+ const staleRows = this.provider.all<{ id: string }>(
530
+ 'SELECT id FROM entries WHERE updated_at < ?',
531
+ [staleThreshold],
532
+ );
541
533
  const staleEntries = staleRows.map((r) => r.id);
542
534
 
543
535
  // Detect contradictions
@@ -548,11 +540,12 @@ export class Curator {
548
540
  if (!dryRun) {
549
541
  // Archive stale entries
550
542
  for (const entryId of staleEntries) {
551
- db.prepare(
543
+ this.provider.run(
552
544
  `INSERT INTO curator_entry_state (entry_id, status, last_groomed_at)
553
545
  VALUES (?, 'archived', unixepoch())
554
546
  ON CONFLICT(entry_id) DO UPDATE SET status = 'archived', last_groomed_at = unixepoch()`,
555
- ).run(entryId);
547
+ [entryId],
548
+ );
556
549
  this.logChange(
557
550
  'archive',
558
551
  entryId,
@@ -596,12 +589,10 @@ export class Curator {
596
589
  // ─── Changelog ──────────────────────────────────────────────────
597
590
 
598
591
  getEntryHistory(entryId: string, limit?: number): ChangelogEntry[] {
599
- const db = this.vault.getDb();
600
- const rows = db
601
- .prepare(
602
- 'SELECT * FROM curator_changelog WHERE entry_id = ? ORDER BY created_at DESC, id DESC LIMIT ?',
603
- )
604
- .all(entryId, limit ?? 50) as Array<Record<string, unknown>>;
592
+ const rows = this.provider.all<Record<string, unknown>>(
593
+ 'SELECT * FROM curator_changelog WHERE entry_id = ? ORDER BY created_at DESC, id DESC LIMIT ?',
594
+ [entryId, limit ?? 50],
595
+ );
605
596
  return rows.map((r) => this.rowToChangelog(r));
606
597
  }
607
598
 
@@ -648,13 +639,13 @@ export class Curator {
648
639
  coverageScore = Math.max(0, coverageScore);
649
640
 
650
641
  // Freshness: penalize stale entries
651
- const db = this.vault.getDb();
652
642
  const now = Math.floor(Date.now() / 1000);
653
643
  const staleThreshold = now - DEFAULT_STALE_DAYS * 86400;
654
644
  const staleCount = (
655
- db
656
- .prepare('SELECT COUNT(*) as count FROM entries WHERE updated_at < ?')
657
- .get(staleThreshold) as { count: number }
645
+ this.provider.get<{ count: number }>(
646
+ 'SELECT COUNT(*) as count FROM entries WHERE updated_at < ?',
647
+ [staleThreshold],
648
+ ) ?? { count: 0 }
658
649
  ).count;
659
650
  const staleRatio = staleCount / entries.length;
660
651
  const freshnessScore = 1 - staleRatio;
@@ -698,11 +689,9 @@ export class Curator {
698
689
 
699
690
  // Penalize ungroomed entries
700
691
  const groomedCount = (
701
- db
702
- .prepare(
703
- 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
704
- )
705
- .get() as { count: number }
692
+ this.provider.get<{ count: number }>(
693
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
694
+ ) ?? { count: 0 }
706
695
  ).count;
707
696
  if (groomedCount < entries.length) {
708
697
  const ungroomed = entries.length - groomedCount;
@@ -739,12 +728,10 @@ export class Curator {
739
728
  const entry = this.vault.get(entryId);
740
729
  if (!entry) return { recorded: false, historyId: -1 };
741
730
 
742
- const db = this.vault.getDb();
743
- const result = db
744
- .prepare(
745
- 'INSERT INTO curator_entry_history (entry_id, snapshot, changed_by, change_reason, created_at) VALUES (?, ?, ?, ?, unixepoch())',
746
- )
747
- .run(entryId, JSON.stringify(entry), changedBy ?? 'system', changeReason ?? null);
731
+ const result = this.provider.run(
732
+ 'INSERT INTO curator_entry_history (entry_id, snapshot, changed_by, change_reason, created_at) VALUES (?, ?, ?, ?, unixepoch())',
733
+ [entryId, JSON.stringify(entry), changedBy ?? 'system', changeReason ?? null],
734
+ );
748
735
 
749
736
  return { recorded: true, historyId: Number(result.lastInsertRowid) };
750
737
  }
@@ -757,12 +744,10 @@ export class Curator {
757
744
  changeReason: string | null;
758
745
  createdAt: number;
759
746
  }> {
760
- const db = this.vault.getDb();
761
- const rows = db
762
- .prepare(
763
- 'SELECT * FROM curator_entry_history WHERE entry_id = ? ORDER BY created_at ASC, id ASC',
764
- )
765
- .all(entryId) as Array<Record<string, unknown>>;
747
+ const rows = this.provider.all<Record<string, unknown>>(
748
+ 'SELECT * FROM curator_entry_history WHERE entry_id = ? ORDER BY created_at ASC, id ASC',
749
+ [entryId],
750
+ );
766
751
 
767
752
  return rows.map((row) => ({
768
753
  historyId: row.id as number,
@@ -784,17 +769,14 @@ export class Curator {
784
769
  freshEntries: number;
785
770
  avgDaysSinceGroom: number;
786
771
  } {
787
- const db = this.vault.getDb();
788
772
  const totalEntries = (
789
- db.prepare('SELECT COUNT(*) as count FROM entries').get() as { count: number }
773
+ this.provider.get<{ count: number }>('SELECT COUNT(*) as count FROM entries') ?? { count: 0 }
790
774
  ).count;
791
775
 
792
776
  const groomedEntries = (
793
- db
794
- .prepare(
795
- 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
796
- )
797
- .get() as { count: number }
777
+ this.provider.get<{ count: number }>(
778
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
779
+ ) ?? { count: 0 }
798
780
  ).count;
799
781
 
800
782
  const ungroomedEntries = totalEntries - groomedEntries;
@@ -804,28 +786,25 @@ export class Curator {
804
786
  const freshThreshold = now - 7 * 86400;
805
787
 
806
788
  const staleEntries = (
807
- db
808
- .prepare(
809
- 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at < ?',
810
- )
811
- .get(staleThreshold) as { count: number }
789
+ this.provider.get<{ count: number }>(
790
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at < ?',
791
+ [staleThreshold],
792
+ ) ?? { count: 0 }
812
793
  ).count;
813
794
 
814
795
  const freshEntries = (
815
- db
816
- .prepare(
817
- 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at >= ?',
818
- )
819
- .get(freshThreshold) as { count: number }
796
+ this.provider.get<{ count: number }>(
797
+ 'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at >= ?',
798
+ [freshThreshold],
799
+ ) ?? { count: 0 }
820
800
  ).count;
821
801
 
822
802
  let avgDaysSinceGroom = 0;
823
803
  if (groomedEntries > 0) {
824
- const sumRow = db
825
- .prepare(
826
- 'SELECT SUM(? - last_groomed_at) as total FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
827
- )
828
- .get(now) as { total: number | null };
804
+ const sumRow = this.provider.get<{ total: number | null }>(
805
+ 'SELECT SUM(? - last_groomed_at) as total FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
806
+ [now],
807
+ ) ?? { total: 0 };
829
808
  const totalSeconds = sumRow.total ?? 0;
830
809
  avgDaysSinceGroom = Math.round((totalSeconds / groomedEntries / 86400) * 100) / 100;
831
810
  }
@@ -961,10 +940,10 @@ export class Curator {
961
940
  afterValue: string | null,
962
941
  reason: string,
963
942
  ): void {
964
- const db = this.vault.getDb();
965
- db.prepare(
943
+ this.provider.run(
966
944
  'INSERT INTO curator_changelog (action, entry_id, before_value, after_value, reason) VALUES (?, ?, ?, ?, ?)',
967
- ).run(action, entryId, beforeValue, afterValue, reason);
945
+ [action, entryId, beforeValue, afterValue, reason],
946
+ );
968
947
  }
969
948
 
970
949
  private rowToContradiction(row: Record<string, unknown>): Contradiction {