@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
@@ -2,11 +2,11 @@
2
2
  * Project Registry — SQLite-backed registry for tracking projects,
3
3
  * rules, and cross-project links.
4
4
  *
5
- * Uses the Vault's underlying SQLite database via `vault.getDb()`.
5
+ * Uses the Vault's underlying database via PersistenceProvider.
6
6
  * Tables are created lazily on first access.
7
7
  */
8
8
 
9
- import type Database from 'better-sqlite3';
9
+ import type { PersistenceProvider } from '../persistence/types.js';
10
10
  import type { RegisteredProject, ProjectRule, ProjectLink, LinkType } from './types.js';
11
11
 
12
12
  /** Row shape for the projects table. */
@@ -82,15 +82,15 @@ function rowToLink(row: LinkRow): ProjectLink {
82
82
  }
83
83
 
84
84
  export class ProjectRegistry {
85
- private db: Database.Database;
85
+ private provider: PersistenceProvider;
86
86
 
87
- constructor(db: Database.Database) {
88
- this.db = db;
87
+ constructor(provider: PersistenceProvider) {
88
+ this.provider = provider;
89
89
  this.initTables();
90
90
  }
91
91
 
92
92
  private initTables(): void {
93
- this.db.exec(`
93
+ this.provider.execSql(`
94
94
  CREATE TABLE IF NOT EXISTS registered_projects (
95
95
  id TEXT PRIMARY KEY,
96
96
  path TEXT UNIQUE NOT NULL,
@@ -128,19 +128,19 @@ export class ProjectRegistry {
128
128
  const id = pathToId(path);
129
129
  const now = Date.now();
130
130
 
131
- const existing = this.db.prepare('SELECT * FROM registered_projects WHERE id = ?').get(id) as
132
- | ProjectRow
133
- | undefined;
131
+ const existing = this.provider.get<ProjectRow>(
132
+ 'SELECT * FROM registered_projects WHERE id = ?',
133
+ [id],
134
+ );
134
135
 
135
136
  if (existing) {
136
137
  // Update lastAccessedAt, and optionally name/metadata if provided
137
138
  const newName = name ?? existing.name;
138
139
  const newMetadata = metadata ? JSON.stringify(metadata) : existing.metadata;
139
- this.db
140
- .prepare(
141
- 'UPDATE registered_projects SET last_accessed_at = ?, name = ?, metadata = ? WHERE id = ?',
142
- )
143
- .run(now, newName, newMetadata, id);
140
+ this.provider.run(
141
+ 'UPDATE registered_projects SET last_accessed_at = ?, name = ?, metadata = ? WHERE id = ?',
142
+ [now, newName, newMetadata, id],
143
+ );
144
144
  return rowToProject({
145
145
  ...existing,
146
146
  last_accessed_at: now,
@@ -157,11 +157,10 @@ export class ProjectRegistry {
157
157
  last_accessed_at: now,
158
158
  metadata: JSON.stringify(metadata ?? {}),
159
159
  };
160
- this.db
161
- .prepare(
162
- 'INSERT INTO registered_projects (id, path, name, registered_at, last_accessed_at, metadata) VALUES (?, ?, ?, ?, ?, ?)',
163
- )
164
- .run(row.id, row.path, row.name, row.registered_at, row.last_accessed_at, row.metadata);
160
+ this.provider.run(
161
+ 'INSERT INTO registered_projects (id, path, name, registered_at, last_accessed_at, metadata) VALUES (?, ?, ?, ?, ?, ?)',
162
+ [row.id, row.path, row.name, row.registered_at, row.last_accessed_at, row.metadata],
163
+ );
165
164
 
166
165
  return rowToProject(row);
167
166
  }
@@ -170,9 +169,9 @@ export class ProjectRegistry {
170
169
  * Get a project by ID.
171
170
  */
172
171
  get(projectId: string): RegisteredProject | null {
173
- const row = this.db.prepare('SELECT * FROM registered_projects WHERE id = ?').get(projectId) as
174
- | ProjectRow
175
- | undefined;
172
+ const row = this.provider.get<ProjectRow>('SELECT * FROM registered_projects WHERE id = ?', [
173
+ projectId,
174
+ ]);
176
175
  return row ? rowToProject(row) : null;
177
176
  }
178
177
 
@@ -180,9 +179,9 @@ export class ProjectRegistry {
180
179
  * Get a project by its filesystem path.
181
180
  */
182
181
  getByPath(path: string): RegisteredProject | null {
183
- const row = this.db.prepare('SELECT * FROM registered_projects WHERE path = ?').get(path) as
184
- | ProjectRow
185
- | undefined;
182
+ const row = this.provider.get<ProjectRow>('SELECT * FROM registered_projects WHERE path = ?', [
183
+ path,
184
+ ]);
186
185
  return row ? rowToProject(row) : null;
187
186
  }
188
187
 
@@ -190,9 +189,9 @@ export class ProjectRegistry {
190
189
  * List all registered projects.
191
190
  */
192
191
  list(): RegisteredProject[] {
193
- const rows = this.db
194
- .prepare('SELECT * FROM registered_projects ORDER BY last_accessed_at DESC')
195
- .all() as ProjectRow[];
192
+ const rows = this.provider.all<ProjectRow>(
193
+ 'SELECT * FROM registered_projects ORDER BY last_accessed_at DESC',
194
+ );
196
195
  return rows.map(rowToProject);
197
196
  }
198
197
 
@@ -200,25 +199,26 @@ export class ProjectRegistry {
200
199
  * Unregister a project by ID. Also removes associated rules and links.
201
200
  */
202
201
  unregister(projectId: string): boolean {
203
- const result = this.db.transaction(() => {
204
- this.db.prepare('DELETE FROM project_rules WHERE project_id = ?').run(projectId);
205
- this.db
206
- .prepare('DELETE FROM project_links WHERE source_project_id = ? OR target_project_id = ?')
207
- .run(projectId, projectId);
202
+ return this.provider.transaction(() => {
203
+ this.provider.run('DELETE FROM project_rules WHERE project_id = ?', [projectId]);
204
+ this.provider.run(
205
+ 'DELETE FROM project_links WHERE source_project_id = ? OR target_project_id = ?',
206
+ [projectId, projectId],
207
+ );
208
208
  return (
209
- this.db.prepare('DELETE FROM registered_projects WHERE id = ?').run(projectId).changes > 0
209
+ this.provider.run('DELETE FROM registered_projects WHERE id = ?', [projectId]).changes > 0
210
210
  );
211
- })();
212
- return result;
211
+ });
213
212
  }
214
213
 
215
214
  /**
216
215
  * Update the lastAccessedAt timestamp for a project.
217
216
  */
218
217
  touch(projectId: string): void {
219
- this.db
220
- .prepare('UPDATE registered_projects SET last_accessed_at = ? WHERE id = ?')
221
- .run(Date.now(), projectId);
218
+ this.provider.run('UPDATE registered_projects SET last_accessed_at = ? WHERE id = ?', [
219
+ Date.now(),
220
+ projectId,
221
+ ]);
222
222
  }
223
223
 
224
224
  // ─── Rules ──────────────────────────────────────────────────────────
@@ -232,11 +232,10 @@ export class ProjectRegistry {
232
232
  ): ProjectRule {
233
233
  const id = `rule-${projectId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
234
234
  const now = Date.now();
235
- this.db
236
- .prepare(
237
- 'INSERT INTO project_rules (id, project_id, category, text, priority, created_at) VALUES (?, ?, ?, ?, ?, ?)',
238
- )
239
- .run(id, projectId, rule.category, rule.text, rule.priority, now);
235
+ this.provider.run(
236
+ 'INSERT INTO project_rules (id, project_id, category, text, priority, created_at) VALUES (?, ?, ?, ?, ?, ?)',
237
+ [id, projectId, rule.category, rule.text, rule.priority, now],
238
+ );
240
239
 
241
240
  return {
242
241
  id,
@@ -252,11 +251,10 @@ export class ProjectRegistry {
252
251
  * Get all rules for a project.
253
252
  */
254
253
  getRules(projectId: string): ProjectRule[] {
255
- const rows = this.db
256
- .prepare(
257
- 'SELECT * FROM project_rules WHERE project_id = ? ORDER BY priority DESC, created_at ASC',
258
- )
259
- .all(projectId) as RuleRow[];
254
+ const rows = this.provider.all<RuleRow>(
255
+ 'SELECT * FROM project_rules WHERE project_id = ? ORDER BY priority DESC, created_at ASC',
256
+ [projectId],
257
+ );
260
258
  return rows.map(rowToRule);
261
259
  }
262
260
 
@@ -264,7 +262,7 @@ export class ProjectRegistry {
264
262
  * Remove a rule by ID.
265
263
  */
266
264
  removeRule(ruleId: string): boolean {
267
- return this.db.prepare('DELETE FROM project_rules WHERE id = ?').run(ruleId).changes > 0;
265
+ return this.provider.run('DELETE FROM project_rules WHERE id = ?', [ruleId]).changes > 0;
268
266
  }
269
267
 
270
268
  /**
@@ -285,20 +283,18 @@ export class ProjectRegistry {
285
283
  */
286
284
  link(sourceId: string, targetId: string, linkType: LinkType): ProjectLink {
287
285
  const now = Date.now();
288
- const info = this.db
289
- .prepare(
290
- 'INSERT OR IGNORE INTO project_links (source_project_id, target_project_id, link_type, created_at) VALUES (?, ?, ?, ?)',
291
- )
292
- .run(sourceId, targetId, linkType, now);
286
+ const info = this.provider.run(
287
+ 'INSERT OR IGNORE INTO project_links (source_project_id, target_project_id, link_type, created_at) VALUES (?, ?, ?, ?)',
288
+ [sourceId, targetId, linkType, now],
289
+ );
293
290
 
294
291
  // If insert was ignored (duplicate), fetch existing
295
292
  if (info.changes === 0) {
296
- const existing = this.db
297
- .prepare(
298
- 'SELECT * FROM project_links WHERE source_project_id = ? AND target_project_id = ? AND link_type = ?',
299
- )
300
- .get(sourceId, targetId, linkType) as LinkRow;
301
- return rowToLink(existing);
293
+ const existing = this.provider.get<LinkRow>(
294
+ 'SELECT * FROM project_links WHERE source_project_id = ? AND target_project_id = ? AND link_type = ?',
295
+ [sourceId, targetId, linkType],
296
+ );
297
+ return rowToLink(existing!);
302
298
  }
303
299
 
304
300
  return {
@@ -316,26 +312,25 @@ export class ProjectRegistry {
316
312
  */
317
313
  unlink(sourceId: string, targetId: string, linkType?: LinkType): number {
318
314
  if (linkType) {
319
- return this.db
320
- .prepare(
321
- 'DELETE FROM project_links WHERE source_project_id = ? AND target_project_id = ? AND link_type = ?',
322
- )
323
- .run(sourceId, targetId, linkType).changes;
315
+ return this.provider.run(
316
+ 'DELETE FROM project_links WHERE source_project_id = ? AND target_project_id = ? AND link_type = ?',
317
+ [sourceId, targetId, linkType],
318
+ ).changes;
324
319
  }
325
- return this.db
326
- .prepare('DELETE FROM project_links WHERE source_project_id = ? AND target_project_id = ?')
327
- .run(sourceId, targetId).changes;
320
+ return this.provider.run(
321
+ 'DELETE FROM project_links WHERE source_project_id = ? AND target_project_id = ?',
322
+ [sourceId, targetId],
323
+ ).changes;
328
324
  }
329
325
 
330
326
  /**
331
327
  * Get all links for a project (both outgoing and incoming).
332
328
  */
333
329
  getLinks(projectId: string): ProjectLink[] {
334
- const rows = this.db
335
- .prepare(
336
- 'SELECT * FROM project_links WHERE source_project_id = ? OR target_project_id = ? ORDER BY created_at DESC',
337
- )
338
- .all(projectId, projectId) as LinkRow[];
330
+ const rows = this.provider.all<LinkRow>(
331
+ 'SELECT * FROM project_links WHERE source_project_id = ? OR target_project_id = ? ORDER BY created_at DESC',
332
+ [projectId, projectId],
333
+ );
339
334
  return rows.map(rowToLink);
340
335
  }
341
336
 
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Extended admin operations — 23 ops for production readiness.
2
+ * Extended admin operations — 24 ops for production readiness.
3
3
  *
4
4
  * Groups: telemetry (3), permissions (1), vault analytics (1),
5
5
  * search insights (1), module status (1), env (1), gc (1), export config (1),
6
6
  * key pool (4), profiles (5), plugins (2), instruction validation (1),
7
- * hot reload (1).
7
+ * hot reload (1), persistence (1).
8
8
  */
9
9
 
10
10
  import { z } from 'zod';
@@ -33,7 +33,7 @@ interface PluginInfo {
33
33
  }
34
34
 
35
35
  /**
36
- * Create 23 extended admin operations for production observability.
36
+ * Create 24 extended admin operations for production observability.
37
37
  */
38
38
  export function createAdminExtraOps(runtime: AgentRuntime): OpDefinition[] {
39
39
  const { vault, brain, cognee, telemetry } = runtime;
@@ -648,5 +648,38 @@ export function createAdminExtraOps(runtime: AgentRuntime): OpDefinition[] {
648
648
  }
649
649
  },
650
650
  },
651
+
652
+ // ─── Persistence ────────────────────────────────────────────────
653
+ {
654
+ name: 'admin_persistence_info',
655
+ description: 'Get persistence backend info: type, connection status, and table row counts.',
656
+ auth: 'read',
657
+ schema: z.object({}),
658
+ handler: async () => {
659
+ const provider = runtime.vault.getProvider();
660
+ const backend = provider.backend;
661
+
662
+ const tables: Record<string, number> = {};
663
+ const tableNames = [
664
+ 'entries',
665
+ 'entries_archive',
666
+ 'memories',
667
+ 'projects',
668
+ 'brain_vocabulary',
669
+ 'brain_feedback',
670
+ ];
671
+
672
+ for (const table of tableNames) {
673
+ try {
674
+ const row = provider.get<{ count: number }>(`SELECT COUNT(*) as count FROM ${table}`);
675
+ tables[table] = row?.count ?? 0;
676
+ } catch {
677
+ tables[table] = -1; // Table doesn't exist
678
+ }
679
+ }
680
+
681
+ return { backend, tables };
682
+ },
683
+ },
651
684
  ];
652
685
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Generic core operations factory — 158 ops that every agent gets.
2
+ * Generic core operations factory — 207 ops that every agent gets.
3
3
  *
4
4
  * These ops are agent-agnostic (no persona, no activation).
5
5
  * The 5 agent-specific ops (health, identity, activate, inject_claude_md, setup)
@@ -30,14 +30,14 @@ import { createCogneeSyncOps } from './cognee-sync-ops.js';
30
30
  import { createIntakeOps } from './intake-ops.js';
31
31
 
32
32
  /**
33
- * Create the 203 generic core operations for an agent runtime.
33
+ * Create the 207 generic core operations for an agent runtime.
34
34
  *
35
35
  * Groups: search/vault (4), memory (4), export (1), planning (5),
36
36
  * brain (8), brain intelligence (11), cognee (5),
37
37
  * llm (2), curator (8), control (8), governance (5),
38
38
  * playbook (5), prompt templates (2),
39
- * planning-extra (22), memory-extra (8), vault-extra (20),
40
- * admin (8), admin-extra (23), loop (9), orchestrate (5),
39
+ * planning-extra (22), memory-extra (8), vault-extra (23),
40
+ * admin (8), admin-extra (24), loop (9), orchestrate (5),
41
41
  * grading (5), capture (4), curator-extra (5), project (12),
42
42
  * cognee-sync (3), intake (4).
43
43
  */
@@ -94,7 +94,7 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
94
94
  const telemetry = new Telemetry();
95
95
 
96
96
  // Project Registry — multi-project tracking with rules and links
97
- const projectRegistry = new ProjectRegistry(vault.getDb());
97
+ const projectRegistry = new ProjectRegistry(vault.getProvider());
98
98
 
99
99
  // Template Manager — prompt templates with variable substitution
100
100
  const templatesDir = config.templatesDir ?? join(agentHome, 'templates');
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Extra vault operations — 20 ops that extend the 4 base vault ops in core-ops.ts.
2
+ * Extra vault operations — 23 ops that extend the 4 base vault ops in core-ops.ts.
3
3
  *
4
4
  * Groups: single-entry CRUD (3), bulk (2), discovery (3), import/export (3),
5
- * analytics (1), seed canonical (1), knowledge lifecycle (4), temporal (3).
5
+ * analytics (1), seed canonical (1), knowledge lifecycle (4), temporal (3),
6
+ * archival (3).
6
7
  */
7
8
 
8
9
  import { z } from 'zod';
@@ -546,6 +547,45 @@ export function createVaultExtraOps(runtime: AgentRuntime): OpDefinition[] {
546
547
  return { entries, count: entries.length };
547
548
  },
548
549
  },
550
+
551
+ // ─── Archival ───────────────────────────────────────────────────
552
+ {
553
+ name: 'vault_archive',
554
+ description:
555
+ 'Archive entries older than N days to entries_archive table. Keeps active table lean.',
556
+ auth: 'write',
557
+ schema: z.object({
558
+ olderThanDays: z.number().describe('Archive entries not updated in this many days'),
559
+ reason: z.string().optional().describe('Reason for archiving'),
560
+ }),
561
+ handler: async (params) => {
562
+ return vault.archive({
563
+ olderThanDays: params.olderThanDays as number,
564
+ reason: params.reason as string | undefined,
565
+ });
566
+ },
567
+ },
568
+ {
569
+ name: 'vault_restore',
570
+ description: 'Restore an archived entry back to the active entries table.',
571
+ auth: 'write',
572
+ schema: z.object({
573
+ id: z.string().describe('ID of the archived entry to restore'),
574
+ }),
575
+ handler: async (params) => {
576
+ const restored = vault.restore(params.id as string);
577
+ return { restored, id: params.id };
578
+ },
579
+ },
580
+ {
581
+ name: 'vault_optimize',
582
+ description: 'Optimize the vault database: VACUUM (SQLite), ANALYZE, and FTS index rebuild.',
583
+ auth: 'write',
584
+ schema: z.object({}),
585
+ handler: async () => {
586
+ return vault.optimize();
587
+ },
588
+ },
549
589
  ];
550
590
  }
551
591
 
@@ -53,6 +53,7 @@ export class Vault {
53
53
  // SQLite-specific pragmas
54
54
  this.provider.run('PRAGMA journal_mode = WAL');
55
55
  this.provider.run('PRAGMA foreign_keys = ON');
56
+ this.provider.run('PRAGMA synchronous = NORMAL');
56
57
  } else {
57
58
  this.provider = providerOrPath;
58
59
  this.sqliteProvider =
@@ -99,6 +100,23 @@ export class Vault {
99
100
  INSERT INTO entries_fts(entries_fts,rowid,id,title,description,context,tags) VALUES('delete',old.rowid,old.id,old.title,old.description,old.context,old.tags);
100
101
  INSERT INTO entries_fts(rowid,id,title,description,context,tags) VALUES(new.rowid,new.id,new.title,new.description,new.context,new.tags);
101
102
  END;
103
+ CREATE TABLE IF NOT EXISTS entries_archive (
104
+ id TEXT PRIMARY KEY,
105
+ type TEXT NOT NULL,
106
+ domain TEXT NOT NULL,
107
+ title TEXT NOT NULL,
108
+ severity TEXT NOT NULL,
109
+ description TEXT NOT NULL,
110
+ context TEXT, example TEXT, counter_example TEXT, why TEXT,
111
+ tags TEXT NOT NULL DEFAULT '[]',
112
+ applies_to TEXT DEFAULT '[]',
113
+ created_at INTEGER NOT NULL,
114
+ updated_at INTEGER NOT NULL,
115
+ valid_from INTEGER,
116
+ valid_until INTEGER,
117
+ archived_at INTEGER NOT NULL DEFAULT (unixepoch()),
118
+ archive_reason TEXT
119
+ );
102
120
  CREATE TABLE IF NOT EXISTS projects (
103
121
  path TEXT PRIMARY KEY,
104
122
  name TEXT NOT NULL,
@@ -151,6 +169,11 @@ export class Vault {
151
169
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
152
170
  );
153
171
  CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query);
172
+ CREATE INDEX IF NOT EXISTS idx_entries_domain ON entries(domain);
173
+ CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(type);
174
+ CREATE INDEX IF NOT EXISTS idx_entries_severity ON entries(severity);
175
+ CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_path);
176
+ CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
154
177
  `);
155
178
  this.migrateBrainSchema();
156
179
  this.migrateTemporalSchema();
@@ -863,6 +886,98 @@ export class Vault {
863
886
  }
864
887
  }
865
888
 
889
+ /**
890
+ * Archive entries older than N days. Moves them to entries_archive.
891
+ */
892
+ archive(options: { olderThanDays: number; reason?: string }): { archived: number } {
893
+ const cutoff = Math.floor(Date.now() / 1000) - options.olderThanDays * 86400;
894
+ const reason = options.reason ?? `Archived: older than ${options.olderThanDays} days`;
895
+
896
+ return this.provider.transaction(() => {
897
+ // Find candidates
898
+ const candidates = this.provider.all<{ id: string }>(
899
+ 'SELECT id FROM entries WHERE updated_at < ?',
900
+ [cutoff],
901
+ );
902
+
903
+ if (candidates.length === 0) return { archived: 0 };
904
+
905
+ let archived = 0;
906
+ for (const { id } of candidates) {
907
+ // Copy to archive
908
+ this.provider.run(
909
+ `INSERT OR IGNORE INTO entries_archive (id, type, domain, title, severity, description, context, example, counter_example, why, tags, applies_to, created_at, updated_at, valid_from, valid_until, archive_reason)
910
+ SELECT id, type, domain, title, severity, description, context, example, counter_example, why, tags, applies_to, created_at, updated_at, valid_from, valid_until, ?
911
+ FROM entries WHERE id = ?`,
912
+ [reason, id],
913
+ );
914
+ // Delete from active
915
+ const result = this.provider.run('DELETE FROM entries WHERE id = ?', [id]);
916
+ archived += result.changes;
917
+ }
918
+
919
+ return { archived };
920
+ });
921
+ }
922
+
923
+ /**
924
+ * Restore an archived entry back to the active table.
925
+ */
926
+ restore(id: string): boolean {
927
+ return this.provider.transaction(() => {
928
+ const archived = this.provider.get<Record<string, unknown>>(
929
+ 'SELECT * FROM entries_archive WHERE id = ?',
930
+ [id],
931
+ );
932
+ if (!archived) return false;
933
+
934
+ this.provider.run(
935
+ `INSERT OR REPLACE INTO entries (id, type, domain, title, severity, description, context, example, counter_example, why, tags, applies_to, created_at, updated_at, valid_from, valid_until)
936
+ SELECT id, type, domain, title, severity, description, context, example, counter_example, why, tags, applies_to, created_at, updated_at, valid_from, valid_until
937
+ FROM entries_archive WHERE id = ?`,
938
+ [id],
939
+ );
940
+ this.provider.run('DELETE FROM entries_archive WHERE id = ?', [id]);
941
+ return true;
942
+ });
943
+ }
944
+
945
+ /**
946
+ * Optimize the database: VACUUM (SQLite only), ANALYZE, and FTS rebuild.
947
+ */
948
+ optimize(): { vacuumed: boolean; analyzed: boolean; ftsRebuilt: boolean } {
949
+ let vacuumed = false;
950
+ let analyzed = false;
951
+ let ftsRebuilt = false;
952
+
953
+ // VACUUM only for SQLite
954
+ if (this.provider.backend === 'sqlite') {
955
+ try {
956
+ this.provider.execSql('VACUUM');
957
+ vacuumed = true;
958
+ } catch {
959
+ // VACUUM may fail inside a transaction
960
+ }
961
+ }
962
+
963
+ try {
964
+ this.provider.execSql('ANALYZE');
965
+ analyzed = true;
966
+ } catch {
967
+ // Non-critical
968
+ }
969
+
970
+ try {
971
+ this.provider.ftsRebuild('entries');
972
+ this.provider.ftsRebuild('memories');
973
+ ftsRebuilt = true;
974
+ } catch {
975
+ // Non-critical
976
+ }
977
+
978
+ return { vacuumed, analyzed, ftsRebuilt };
979
+ }
980
+
866
981
  /**
867
982
  * Get the underlying persistence provider.
868
983
  */
@@ -875,6 +990,9 @@ export class Vault {
875
990
  * Throws if the provider is not SQLite.
876
991
  */
877
992
  getDb(): import('better-sqlite3').Database {
993
+ if (process.env.NODE_ENV !== 'test' && process.env.VITEST !== 'true') {
994
+ console.warn('Vault.getDb() is deprecated. Use vault.getProvider() instead.');
995
+ }
878
996
  if (this.sqliteProvider) {
879
997
  return this.sqliteProvider.getDatabase();
880
998
  }