@soleri/core 2.4.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 (328) hide show
  1. package/dist/brain/brain.d.ts +7 -0
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +56 -9
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +1 -0
  6. package/dist/brain/intelligence.d.ts.map +1 -1
  7. package/dist/brain/intelligence.js +164 -148
  8. package/dist/brain/intelligence.js.map +1 -1
  9. package/dist/brain/types.d.ts +2 -2
  10. package/dist/brain/types.d.ts.map +1 -1
  11. package/dist/cognee/client.d.ts +3 -0
  12. package/dist/cognee/client.d.ts.map +1 -1
  13. package/dist/cognee/client.js +17 -0
  14. package/dist/cognee/client.js.map +1 -1
  15. package/dist/cognee/sync-manager.d.ts +94 -0
  16. package/dist/cognee/sync-manager.d.ts.map +1 -0
  17. package/dist/cognee/sync-manager.js +293 -0
  18. package/dist/cognee/sync-manager.js.map +1 -0
  19. package/dist/control/identity-manager.d.ts +3 -1
  20. package/dist/control/identity-manager.d.ts.map +1 -1
  21. package/dist/control/identity-manager.js +49 -51
  22. package/dist/control/identity-manager.js.map +1 -1
  23. package/dist/control/intent-router.d.ts +1 -0
  24. package/dist/control/intent-router.d.ts.map +1 -1
  25. package/dist/control/intent-router.js +32 -32
  26. package/dist/control/intent-router.js.map +1 -1
  27. package/dist/curator/curator.d.ts +9 -1
  28. package/dist/curator/curator.d.ts.map +1 -1
  29. package/dist/curator/curator.js +104 -92
  30. package/dist/curator/curator.js.map +1 -1
  31. package/dist/errors/classify.d.ts +13 -0
  32. package/dist/errors/classify.d.ts.map +1 -0
  33. package/dist/errors/classify.js +97 -0
  34. package/dist/errors/classify.js.map +1 -0
  35. package/dist/errors/index.d.ts +6 -0
  36. package/dist/errors/index.d.ts.map +1 -0
  37. package/dist/errors/index.js +4 -0
  38. package/dist/errors/index.js.map +1 -0
  39. package/dist/errors/retry.d.ts +40 -0
  40. package/dist/errors/retry.d.ts.map +1 -0
  41. package/dist/errors/retry.js +97 -0
  42. package/dist/errors/retry.js.map +1 -0
  43. package/dist/errors/types.d.ts +48 -0
  44. package/dist/errors/types.d.ts.map +1 -0
  45. package/dist/errors/types.js +59 -0
  46. package/dist/errors/types.js.map +1 -0
  47. package/dist/governance/governance.d.ts +1 -0
  48. package/dist/governance/governance.d.ts.map +1 -1
  49. package/dist/governance/governance.js +51 -68
  50. package/dist/governance/governance.js.map +1 -1
  51. package/dist/index.d.ts +26 -5
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +22 -3
  54. package/dist/index.js.map +1 -1
  55. package/dist/intake/content-classifier.d.ts +14 -0
  56. package/dist/intake/content-classifier.d.ts.map +1 -0
  57. package/dist/intake/content-classifier.js +125 -0
  58. package/dist/intake/content-classifier.js.map +1 -0
  59. package/dist/intake/dedup-gate.d.ts +17 -0
  60. package/dist/intake/dedup-gate.d.ts.map +1 -0
  61. package/dist/intake/dedup-gate.js +66 -0
  62. package/dist/intake/dedup-gate.js.map +1 -0
  63. package/dist/intake/intake-pipeline.d.ts +63 -0
  64. package/dist/intake/intake-pipeline.d.ts.map +1 -0
  65. package/dist/intake/intake-pipeline.js +373 -0
  66. package/dist/intake/intake-pipeline.js.map +1 -0
  67. package/dist/intake/types.d.ts +65 -0
  68. package/dist/intake/types.d.ts.map +1 -0
  69. package/dist/intake/types.js +3 -0
  70. package/dist/intake/types.js.map +1 -0
  71. package/dist/intelligence/loader.js +1 -1
  72. package/dist/intelligence/loader.js.map +1 -1
  73. package/dist/intelligence/types.d.ts +3 -1
  74. package/dist/intelligence/types.d.ts.map +1 -1
  75. package/dist/loop/loop-manager.d.ts +58 -7
  76. package/dist/loop/loop-manager.d.ts.map +1 -1
  77. package/dist/loop/loop-manager.js +280 -6
  78. package/dist/loop/loop-manager.js.map +1 -1
  79. package/dist/loop/types.d.ts +69 -1
  80. package/dist/loop/types.d.ts.map +1 -1
  81. package/dist/loop/types.js +4 -1
  82. package/dist/loop/types.js.map +1 -1
  83. package/dist/persistence/index.d.ts +4 -0
  84. package/dist/persistence/index.d.ts.map +1 -0
  85. package/dist/persistence/index.js +3 -0
  86. package/dist/persistence/index.js.map +1 -0
  87. package/dist/persistence/postgres-provider.d.ts +46 -0
  88. package/dist/persistence/postgres-provider.d.ts.map +1 -0
  89. package/dist/persistence/postgres-provider.js +115 -0
  90. package/dist/persistence/postgres-provider.js.map +1 -0
  91. package/dist/persistence/sqlite-provider.d.ts +28 -0
  92. package/dist/persistence/sqlite-provider.d.ts.map +1 -0
  93. package/dist/persistence/sqlite-provider.js +97 -0
  94. package/dist/persistence/sqlite-provider.js.map +1 -0
  95. package/dist/persistence/types.d.ts +58 -0
  96. package/dist/persistence/types.d.ts.map +1 -0
  97. package/dist/persistence/types.js +8 -0
  98. package/dist/persistence/types.js.map +1 -0
  99. package/dist/planning/gap-analysis.d.ts +47 -4
  100. package/dist/planning/gap-analysis.d.ts.map +1 -1
  101. package/dist/planning/gap-analysis.js +190 -13
  102. package/dist/planning/gap-analysis.js.map +1 -1
  103. package/dist/planning/gap-types.d.ts +1 -1
  104. package/dist/planning/gap-types.d.ts.map +1 -1
  105. package/dist/planning/gap-types.js.map +1 -1
  106. package/dist/planning/planner.d.ts +277 -9
  107. package/dist/planning/planner.d.ts.map +1 -1
  108. package/dist/planning/planner.js +611 -46
  109. package/dist/planning/planner.js.map +1 -1
  110. package/dist/playbooks/generic/brainstorming.d.ts +9 -0
  111. package/dist/playbooks/generic/brainstorming.d.ts.map +1 -0
  112. package/dist/playbooks/generic/brainstorming.js +105 -0
  113. package/dist/playbooks/generic/brainstorming.js.map +1 -0
  114. package/dist/playbooks/generic/code-review.d.ts +11 -0
  115. package/dist/playbooks/generic/code-review.d.ts.map +1 -0
  116. package/dist/playbooks/generic/code-review.js +176 -0
  117. package/dist/playbooks/generic/code-review.js.map +1 -0
  118. package/dist/playbooks/generic/subagent-execution.d.ts +9 -0
  119. package/dist/playbooks/generic/subagent-execution.d.ts.map +1 -0
  120. package/dist/playbooks/generic/subagent-execution.js +68 -0
  121. package/dist/playbooks/generic/subagent-execution.js.map +1 -0
  122. package/dist/playbooks/generic/systematic-debugging.d.ts +9 -0
  123. package/dist/playbooks/generic/systematic-debugging.d.ts.map +1 -0
  124. package/dist/playbooks/generic/systematic-debugging.js +87 -0
  125. package/dist/playbooks/generic/systematic-debugging.js.map +1 -0
  126. package/dist/playbooks/generic/tdd.d.ts +9 -0
  127. package/dist/playbooks/generic/tdd.d.ts.map +1 -0
  128. package/dist/playbooks/generic/tdd.js +70 -0
  129. package/dist/playbooks/generic/tdd.js.map +1 -0
  130. package/dist/playbooks/generic/verification.d.ts +9 -0
  131. package/dist/playbooks/generic/verification.d.ts.map +1 -0
  132. package/dist/playbooks/generic/verification.js +74 -0
  133. package/dist/playbooks/generic/verification.js.map +1 -0
  134. package/dist/playbooks/index.d.ts +4 -0
  135. package/dist/playbooks/index.d.ts.map +1 -0
  136. package/dist/playbooks/index.js +5 -0
  137. package/dist/playbooks/index.js.map +1 -0
  138. package/dist/playbooks/playbook-registry.d.ts +42 -0
  139. package/dist/playbooks/playbook-registry.d.ts.map +1 -0
  140. package/dist/playbooks/playbook-registry.js +227 -0
  141. package/dist/playbooks/playbook-registry.js.map +1 -0
  142. package/dist/playbooks/playbook-seeder.d.ts +47 -0
  143. package/dist/playbooks/playbook-seeder.d.ts.map +1 -0
  144. package/dist/playbooks/playbook-seeder.js +104 -0
  145. package/dist/playbooks/playbook-seeder.js.map +1 -0
  146. package/dist/playbooks/playbook-types.d.ts +132 -0
  147. package/dist/playbooks/playbook-types.d.ts.map +1 -0
  148. package/dist/playbooks/playbook-types.js +12 -0
  149. package/dist/playbooks/playbook-types.js.map +1 -0
  150. package/dist/project/project-registry.d.ts +4 -4
  151. package/dist/project/project-registry.d.ts.map +1 -1
  152. package/dist/project/project-registry.js +30 -57
  153. package/dist/project/project-registry.js.map +1 -1
  154. package/dist/prompts/index.d.ts +4 -0
  155. package/dist/prompts/index.d.ts.map +1 -0
  156. package/dist/prompts/index.js +3 -0
  157. package/dist/prompts/index.js.map +1 -0
  158. package/dist/prompts/parser.d.ts +17 -0
  159. package/dist/prompts/parser.d.ts.map +1 -0
  160. package/dist/prompts/parser.js +47 -0
  161. package/dist/prompts/parser.js.map +1 -0
  162. package/dist/prompts/template-manager.d.ts +25 -0
  163. package/dist/prompts/template-manager.d.ts.map +1 -0
  164. package/dist/prompts/template-manager.js +71 -0
  165. package/dist/prompts/template-manager.js.map +1 -0
  166. package/dist/prompts/types.d.ts +26 -0
  167. package/dist/prompts/types.d.ts.map +1 -0
  168. package/dist/prompts/types.js +5 -0
  169. package/dist/prompts/types.js.map +1 -0
  170. package/dist/runtime/admin-extra-ops.d.ts +5 -3
  171. package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
  172. package/dist/runtime/admin-extra-ops.js +348 -11
  173. package/dist/runtime/admin-extra-ops.js.map +1 -1
  174. package/dist/runtime/admin-ops.d.ts.map +1 -1
  175. package/dist/runtime/admin-ops.js +10 -3
  176. package/dist/runtime/admin-ops.js.map +1 -1
  177. package/dist/runtime/capture-ops.d.ts.map +1 -1
  178. package/dist/runtime/capture-ops.js +20 -2
  179. package/dist/runtime/capture-ops.js.map +1 -1
  180. package/dist/runtime/cognee-sync-ops.d.ts +12 -0
  181. package/dist/runtime/cognee-sync-ops.d.ts.map +1 -0
  182. package/dist/runtime/cognee-sync-ops.js +55 -0
  183. package/dist/runtime/cognee-sync-ops.js.map +1 -0
  184. package/dist/runtime/core-ops.d.ts +8 -6
  185. package/dist/runtime/core-ops.d.ts.map +1 -1
  186. package/dist/runtime/core-ops.js +226 -9
  187. package/dist/runtime/core-ops.js.map +1 -1
  188. package/dist/runtime/curator-extra-ops.d.ts +2 -2
  189. package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
  190. package/dist/runtime/curator-extra-ops.js +15 -3
  191. package/dist/runtime/curator-extra-ops.js.map +1 -1
  192. package/dist/runtime/domain-ops.js +2 -2
  193. package/dist/runtime/domain-ops.js.map +1 -1
  194. package/dist/runtime/grading-ops.d.ts.map +1 -1
  195. package/dist/runtime/grading-ops.js.map +1 -1
  196. package/dist/runtime/intake-ops.d.ts +14 -0
  197. package/dist/runtime/intake-ops.d.ts.map +1 -0
  198. package/dist/runtime/intake-ops.js +110 -0
  199. package/dist/runtime/intake-ops.js.map +1 -0
  200. package/dist/runtime/loop-ops.d.ts +5 -4
  201. package/dist/runtime/loop-ops.d.ts.map +1 -1
  202. package/dist/runtime/loop-ops.js +84 -12
  203. package/dist/runtime/loop-ops.js.map +1 -1
  204. package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -1
  205. package/dist/runtime/memory-cross-project-ops.js.map +1 -1
  206. package/dist/runtime/memory-extra-ops.js +5 -5
  207. package/dist/runtime/memory-extra-ops.js.map +1 -1
  208. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  209. package/dist/runtime/orchestrate-ops.js +8 -2
  210. package/dist/runtime/orchestrate-ops.js.map +1 -1
  211. package/dist/runtime/planning-extra-ops.d.ts +13 -5
  212. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  213. package/dist/runtime/planning-extra-ops.js +381 -18
  214. package/dist/runtime/planning-extra-ops.js.map +1 -1
  215. package/dist/runtime/playbook-ops.d.ts +14 -0
  216. package/dist/runtime/playbook-ops.d.ts.map +1 -0
  217. package/dist/runtime/playbook-ops.js +141 -0
  218. package/dist/runtime/playbook-ops.js.map +1 -0
  219. package/dist/runtime/project-ops.d.ts.map +1 -1
  220. package/dist/runtime/project-ops.js +7 -2
  221. package/dist/runtime/project-ops.js.map +1 -1
  222. package/dist/runtime/runtime.d.ts.map +1 -1
  223. package/dist/runtime/runtime.js +28 -9
  224. package/dist/runtime/runtime.js.map +1 -1
  225. package/dist/runtime/types.d.ts +8 -0
  226. package/dist/runtime/types.d.ts.map +1 -1
  227. package/dist/runtime/vault-extra-ops.d.ts +4 -2
  228. package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
  229. package/dist/runtime/vault-extra-ops.js +383 -4
  230. package/dist/runtime/vault-extra-ops.js.map +1 -1
  231. package/dist/vault/playbook.d.ts +34 -0
  232. package/dist/vault/playbook.d.ts.map +1 -0
  233. package/dist/vault/playbook.js +60 -0
  234. package/dist/vault/playbook.js.map +1 -0
  235. package/dist/vault/vault.d.ts +52 -32
  236. package/dist/vault/vault.d.ts.map +1 -1
  237. package/dist/vault/vault.js +300 -181
  238. package/dist/vault/vault.js.map +1 -1
  239. package/package.json +9 -3
  240. package/src/__tests__/admin-extra-ops.test.ts +62 -15
  241. package/src/__tests__/admin-ops.test.ts +2 -2
  242. package/src/__tests__/brain.test.ts +3 -3
  243. package/src/__tests__/cognee-integration.test.ts +80 -0
  244. package/src/__tests__/cognee-sync-manager.test.ts +103 -0
  245. package/src/__tests__/core-ops.test.ts +36 -4
  246. package/src/__tests__/curator-extra-ops.test.ts +24 -2
  247. package/src/__tests__/errors.test.ts +388 -0
  248. package/src/__tests__/grading-ops.test.ts +28 -7
  249. package/src/__tests__/intake-pipeline.test.ts +162 -0
  250. package/src/__tests__/loop-ops.test.ts +74 -3
  251. package/src/__tests__/memory-cross-project-ops.test.ts +3 -1
  252. package/src/__tests__/orchestrate-ops.test.ts +8 -3
  253. package/src/__tests__/persistence.test.ts +291 -0
  254. package/src/__tests__/planner.test.ts +99 -21
  255. package/src/__tests__/planning-extra-ops.test.ts +168 -10
  256. package/src/__tests__/playbook-registry.test.ts +326 -0
  257. package/src/__tests__/playbook-seeder.test.ts +163 -0
  258. package/src/__tests__/playbook.test.ts +389 -0
  259. package/src/__tests__/postgres-provider.test.ts +58 -0
  260. package/src/__tests__/project-ops.test.ts +18 -4
  261. package/src/__tests__/template-manager.test.ts +222 -0
  262. package/src/__tests__/vault-extra-ops.test.ts +82 -7
  263. package/src/__tests__/vault.test.ts +184 -0
  264. package/src/brain/brain.ts +71 -9
  265. package/src/brain/intelligence.ts +258 -307
  266. package/src/brain/types.ts +2 -2
  267. package/src/cognee/client.ts +18 -0
  268. package/src/cognee/sync-manager.ts +389 -0
  269. package/src/control/identity-manager.ts +77 -75
  270. package/src/control/intent-router.ts +55 -57
  271. package/src/curator/curator.ts +199 -139
  272. package/src/errors/classify.ts +102 -0
  273. package/src/errors/index.ts +5 -0
  274. package/src/errors/retry.ts +132 -0
  275. package/src/errors/types.ts +81 -0
  276. package/src/governance/governance.ts +90 -107
  277. package/src/index.ts +116 -3
  278. package/src/intake/content-classifier.ts +146 -0
  279. package/src/intake/dedup-gate.ts +92 -0
  280. package/src/intake/intake-pipeline.ts +503 -0
  281. package/src/intake/types.ts +69 -0
  282. package/src/intelligence/loader.ts +1 -1
  283. package/src/intelligence/types.ts +3 -1
  284. package/src/loop/loop-manager.ts +325 -7
  285. package/src/loop/types.ts +72 -1
  286. package/src/persistence/index.ts +9 -0
  287. package/src/persistence/postgres-provider.ts +157 -0
  288. package/src/persistence/sqlite-provider.ts +115 -0
  289. package/src/persistence/types.ts +74 -0
  290. package/src/planning/gap-analysis.ts +286 -17
  291. package/src/planning/gap-types.ts +4 -1
  292. package/src/planning/planner.ts +828 -55
  293. package/src/playbooks/generic/brainstorming.ts +110 -0
  294. package/src/playbooks/generic/code-review.ts +181 -0
  295. package/src/playbooks/generic/subagent-execution.ts +74 -0
  296. package/src/playbooks/generic/systematic-debugging.ts +92 -0
  297. package/src/playbooks/generic/tdd.ts +75 -0
  298. package/src/playbooks/generic/verification.ts +79 -0
  299. package/src/playbooks/index.ts +27 -0
  300. package/src/playbooks/playbook-registry.ts +284 -0
  301. package/src/playbooks/playbook-seeder.ts +119 -0
  302. package/src/playbooks/playbook-types.ts +162 -0
  303. package/src/project/project-registry.ts +81 -74
  304. package/src/prompts/index.ts +3 -0
  305. package/src/prompts/parser.ts +59 -0
  306. package/src/prompts/template-manager.ts +77 -0
  307. package/src/prompts/types.ts +28 -0
  308. package/src/runtime/admin-extra-ops.ts +391 -13
  309. package/src/runtime/admin-ops.ts +17 -6
  310. package/src/runtime/capture-ops.ts +25 -6
  311. package/src/runtime/cognee-sync-ops.ts +63 -0
  312. package/src/runtime/core-ops.ts +258 -8
  313. package/src/runtime/curator-extra-ops.ts +17 -3
  314. package/src/runtime/domain-ops.ts +2 -2
  315. package/src/runtime/grading-ops.ts +11 -2
  316. package/src/runtime/intake-ops.ts +126 -0
  317. package/src/runtime/loop-ops.ts +96 -13
  318. package/src/runtime/memory-cross-project-ops.ts +1 -2
  319. package/src/runtime/memory-extra-ops.ts +5 -5
  320. package/src/runtime/orchestrate-ops.ts +8 -2
  321. package/src/runtime/planning-extra-ops.ts +414 -23
  322. package/src/runtime/playbook-ops.ts +169 -0
  323. package/src/runtime/project-ops.ts +9 -3
  324. package/src/runtime/runtime.ts +36 -10
  325. package/src/runtime/types.ts +8 -0
  326. package/src/runtime/vault-extra-ops.ts +425 -4
  327. package/src/vault/playbook.ts +87 -0
  328. package/src/vault/vault.ts +419 -235
@@ -1,6 +1,5 @@
1
- import Database from 'better-sqlite3';
2
- import { mkdirSync } from 'node:fs';
3
- import { dirname } from 'node:path';
1
+ import type { PersistenceProvider } from '../persistence/types.js';
2
+ import { SQLitePersistenceProvider } from '../persistence/sqlite-provider.js';
4
3
  import type { IntelligenceEntry } from '../intelligence/types.js';
5
4
 
6
5
  export interface SearchResult {
@@ -39,21 +38,44 @@ export interface MemoryStats {
39
38
  }
40
39
 
41
40
  export class Vault {
42
- private db: Database.Database;
41
+ private provider: PersistenceProvider;
42
+ private sqliteProvider: SQLitePersistenceProvider | null;
43
+ private syncManager: import('../cognee/sync-manager.js').CogneeSyncManager | null = null;
43
44
 
44
- constructor(dbPath: string = ':memory:') {
45
- if (dbPath !== ':memory:') mkdirSync(dirname(dbPath), { recursive: true });
46
- this.db = new Database(dbPath);
47
- this.db.pragma('journal_mode = WAL');
48
- this.db.pragma('foreign_keys = ON');
45
+ /**
46
+ * Create a Vault with a PersistenceProvider or a SQLite path (backward compat).
47
+ */
48
+ constructor(providerOrPath: PersistenceProvider | string = ':memory:') {
49
+ if (typeof providerOrPath === 'string') {
50
+ const sqlite = new SQLitePersistenceProvider(providerOrPath);
51
+ this.provider = sqlite;
52
+ this.sqliteProvider = sqlite;
53
+ // SQLite-specific pragmas
54
+ this.provider.run('PRAGMA journal_mode = WAL');
55
+ this.provider.run('PRAGMA foreign_keys = ON');
56
+ this.provider.run('PRAGMA synchronous = NORMAL');
57
+ } else {
58
+ this.provider = providerOrPath;
59
+ this.sqliteProvider =
60
+ providerOrPath instanceof SQLitePersistenceProvider ? providerOrPath : null;
61
+ }
49
62
  this.initialize();
50
63
  }
51
64
 
65
+ setSyncManager(mgr: import('../cognee/sync-manager.js').CogneeSyncManager): void {
66
+ this.syncManager = mgr;
67
+ }
68
+
69
+ /** Backward-compatible factory. */
70
+ static createWithSQLite(dbPath: string = ':memory:'): Vault {
71
+ return new Vault(dbPath);
72
+ }
73
+
52
74
  private initialize(): void {
53
- this.db.exec(`
75
+ this.provider.execSql(`
54
76
  CREATE TABLE IF NOT EXISTS entries (
55
77
  id TEXT PRIMARY KEY,
56
- type TEXT NOT NULL CHECK(type IN ('pattern', 'anti-pattern', 'rule')),
78
+ type TEXT NOT NULL CHECK(type IN ('pattern', 'anti-pattern', 'rule', 'playbook')),
57
79
  domain TEXT NOT NULL,
58
80
  title TEXT NOT NULL,
59
81
  severity TEXT NOT NULL CHECK(severity IN ('critical', 'warning', 'suggestion')),
@@ -78,6 +100,23 @@ export class Vault {
78
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);
79
101
  INSERT INTO entries_fts(rowid,id,title,description,context,tags) VALUES(new.rowid,new.id,new.title,new.description,new.context,new.tags);
80
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
+ );
81
120
  CREATE TABLE IF NOT EXISTS projects (
82
121
  path TEXT PRIMARY KEY,
83
122
  name TEXT NOT NULL,
@@ -130,27 +169,36 @@ export class Vault {
130
169
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
131
170
  );
132
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);
133
177
  `);
134
178
  this.migrateBrainSchema();
179
+ this.migrateTemporalSchema();
180
+ }
181
+
182
+ private migrateTemporalSchema(): void {
183
+ try {
184
+ this.provider.run('ALTER TABLE entries ADD COLUMN valid_from INTEGER');
185
+ } catch {
186
+ // Column already exists
187
+ }
188
+ try {
189
+ this.provider.run('ALTER TABLE entries ADD COLUMN valid_until INTEGER');
190
+ } catch {
191
+ // Column already exists
192
+ }
135
193
  }
136
194
 
137
- /**
138
- * Migrate brain_feedback table from old schema (accepted/dismissed only)
139
- * to new schema with source, confidence, duration, context, reason columns.
140
- * Also adds extracted_at to brain_sessions if it exists.
141
- */
142
195
  private migrateBrainSchema(): void {
143
- // Check if brain_feedback needs migration (old schema lacks 'source' column)
144
- const columns = this.db.prepare('PRAGMA table_info(brain_feedback)').all() as Array<{
145
- name: string;
146
- }>;
196
+ const columns = this.provider.all<{ name: string }>('PRAGMA table_info(brain_feedback)');
147
197
  const hasSource = columns.some((c) => c.name === 'source');
148
198
 
149
199
  if (!hasSource && columns.length > 0) {
150
- // Old table exists without new columns — rebuild with expanded schema
151
- this.db.transaction(() => {
152
- this.db
153
- .prepare(`
200
+ this.provider.transaction(() => {
201
+ this.provider.run(`
154
202
  CREATE TABLE brain_feedback_new (
155
203
  id INTEGER PRIMARY KEY AUTOINCREMENT,
156
204
  query TEXT NOT NULL,
@@ -163,29 +211,23 @@ export class Vault {
163
211
  reason TEXT,
164
212
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
165
213
  )
166
- `)
167
- .run();
168
- this.db
169
- .prepare(`
214
+ `);
215
+ this.provider.run(`
170
216
  INSERT INTO brain_feedback_new (id, query, entry_id, action, created_at)
171
217
  SELECT id, query, entry_id, action, created_at FROM brain_feedback
172
- `)
173
- .run();
174
- this.db.prepare('DROP TABLE brain_feedback').run();
175
- this.db.prepare('ALTER TABLE brain_feedback_new RENAME TO brain_feedback').run();
176
- this.db
177
- .prepare('CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query)')
178
- .run();
179
- })();
218
+ `);
219
+ this.provider.run('DROP TABLE brain_feedback');
220
+ this.provider.run('ALTER TABLE brain_feedback_new RENAME TO brain_feedback');
221
+ this.provider.run(
222
+ 'CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query)',
223
+ );
224
+ });
180
225
  }
181
226
 
182
- // Add extracted_at to brain_sessions if it exists but lacks the column
183
227
  try {
184
- const sessionCols = this.db.prepare('PRAGMA table_info(brain_sessions)').all() as Array<{
185
- name: string;
186
- }>;
228
+ const sessionCols = this.provider.all<{ name: string }>('PRAGMA table_info(brain_sessions)');
187
229
  if (sessionCols.length > 0 && !sessionCols.some((c) => c.name === 'extracted_at')) {
188
- this.db.prepare('ALTER TABLE brain_sessions ADD COLUMN extracted_at TEXT').run();
230
+ this.provider.run('ALTER TABLE brain_sessions ADD COLUMN extracted_at TEXT');
189
231
  }
190
232
  } catch {
191
233
  // brain_sessions table doesn't exist yet — BrainIntelligence will create it
@@ -193,17 +235,17 @@ export class Vault {
193
235
  }
194
236
 
195
237
  seed(entries: IntelligenceEntry[]): number {
196
- const upsert = this.db.prepare(`
197
- INSERT INTO entries (id,type,domain,title,severity,description,context,example,counter_example,why,tags,applies_to)
198
- VALUES (@id,@type,@domain,@title,@severity,@description,@context,@example,@counterExample,@why,@tags,@appliesTo)
238
+ const sql = `
239
+ INSERT INTO entries (id,type,domain,title,severity,description,context,example,counter_example,why,tags,applies_to,valid_from,valid_until)
240
+ VALUES (@id,@type,@domain,@title,@severity,@description,@context,@example,@counterExample,@why,@tags,@appliesTo,@validFrom,@validUntil)
199
241
  ON CONFLICT(id) DO UPDATE SET type=excluded.type,domain=excluded.domain,title=excluded.title,severity=excluded.severity,
200
242
  description=excluded.description,context=excluded.context,example=excluded.example,counter_example=excluded.counter_example,
201
- why=excluded.why,tags=excluded.tags,applies_to=excluded.applies_to,updated_at=unixepoch()
202
- `);
203
- const tx = this.db.transaction((items: IntelligenceEntry[]) => {
243
+ why=excluded.why,tags=excluded.tags,applies_to=excluded.applies_to,valid_from=excluded.valid_from,valid_until=excluded.valid_until,updated_at=unixepoch()
244
+ `;
245
+ return this.provider.transaction(() => {
204
246
  let count = 0;
205
- for (const entry of items) {
206
- upsert.run({
247
+ for (const entry of entries) {
248
+ this.provider.run(sql, {
207
249
  id: entry.id,
208
250
  type: entry.type,
209
251
  domain: entry.domain,
@@ -216,21 +258,31 @@ export class Vault {
216
258
  why: entry.why ?? null,
217
259
  tags: JSON.stringify(entry.tags),
218
260
  appliesTo: JSON.stringify(entry.appliesTo ?? []),
261
+ validFrom: entry.validFrom ?? null,
262
+ validUntil: entry.validUntil ?? null,
219
263
  });
220
264
  count++;
265
+ if (this.syncManager) {
266
+ this.syncManager.enqueue('ingest', entry.id, entry);
267
+ }
221
268
  }
222
269
  return count;
223
270
  });
224
- return tx(entries);
225
271
  }
226
272
 
227
273
  search(
228
274
  query: string,
229
- options?: { domain?: string; type?: string; severity?: string; limit?: number },
275
+ options?: {
276
+ domain?: string;
277
+ type?: string;
278
+ severity?: string;
279
+ limit?: number;
280
+ includeExpired?: boolean;
281
+ },
230
282
  ): SearchResult[] {
231
283
  const limit = options?.limit ?? 10;
232
284
  const filters: string[] = [];
233
- const fp: Record<string, string> = {};
285
+ const fp: Record<string, unknown> = {};
234
286
  if (options?.domain) {
235
287
  filters.push('e.domain = @domain');
236
288
  fp.domain = options.domain;
@@ -243,13 +295,18 @@ export class Vault {
243
295
  filters.push('e.severity = @severity');
244
296
  fp.severity = options.severity;
245
297
  }
298
+ if (!options?.includeExpired) {
299
+ const now = Math.floor(Date.now() / 1000);
300
+ filters.push('(e.valid_until IS NULL OR e.valid_until > @now)');
301
+ filters.push('(e.valid_from IS NULL OR e.valid_from <= @now)');
302
+ fp.now = now;
303
+ }
246
304
  const wc = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';
247
305
  try {
248
- const rows = this.db
249
- .prepare(
250
- `SELECT e.*, -rank as score FROM entries_fts fts JOIN entries e ON e.rowid = fts.rowid WHERE entries_fts MATCH @query ${wc} ORDER BY score DESC LIMIT @limit`,
251
- )
252
- .all({ query, limit, ...fp }) as Array<Record<string, unknown>>;
306
+ const rows = this.provider.all<Record<string, unknown>>(
307
+ `SELECT e.*, -rank as score FROM entries_fts fts JOIN entries e ON e.rowid = fts.rowid WHERE entries_fts MATCH @query ${wc} ORDER BY score DESC LIMIT @limit`,
308
+ { query, limit, ...fp },
309
+ );
253
310
  return rows.map(rowToSearchResult);
254
311
  } catch {
255
312
  return [];
@@ -257,9 +314,9 @@ export class Vault {
257
314
  }
258
315
 
259
316
  get(id: string): IntelligenceEntry | null {
260
- const row = this.db.prepare('SELECT * FROM entries WHERE id = ?').get(id) as
261
- | Record<string, unknown>
262
- | undefined;
317
+ const row = this.provider.get<Record<string, unknown>>('SELECT * FROM entries WHERE id = ?', [
318
+ id,
319
+ ]);
263
320
  return row ? rowToEntry(row) : null;
264
321
  }
265
322
 
@@ -270,6 +327,7 @@ export class Vault {
270
327
  tags?: string[];
271
328
  limit?: number;
272
329
  offset?: number;
330
+ includeExpired?: boolean;
273
331
  }): IntelligenceEntry[] {
274
332
  const filters: string[] = [];
275
333
  const params: Record<string, unknown> = {};
@@ -292,26 +350,29 @@ export class Vault {
292
350
  });
293
351
  filters.push(`(${c.join(' OR ')})`);
294
352
  }
353
+ if (!options?.includeExpired) {
354
+ const now = Math.floor(Date.now() / 1000);
355
+ filters.push('(valid_until IS NULL OR valid_until > @now)');
356
+ filters.push('(valid_from IS NULL OR valid_from <= @now)');
357
+ params.now = now;
358
+ }
295
359
  const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
296
- const rows = this.db
297
- .prepare(
298
- `SELECT * FROM entries ${wc} ORDER BY severity, domain, title LIMIT @limit OFFSET @offset`,
299
- )
300
- .all({ ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 }) as Array<
301
- Record<string, unknown>
302
- >;
360
+ const rows = this.provider.all<Record<string, unknown>>(
361
+ `SELECT * FROM entries ${wc} ORDER BY severity, domain, title LIMIT @limit OFFSET @offset`,
362
+ { ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 },
363
+ );
303
364
  return rows.map(rowToEntry);
304
365
  }
305
366
 
306
367
  stats(): VaultStats {
307
- const total = (
308
- this.db.prepare('SELECT COUNT(*) as count FROM entries').get() as { count: number }
309
- ).count;
368
+ const total = this.provider.get<{ count: number }>(
369
+ 'SELECT COUNT(*) as count FROM entries',
370
+ )!.count;
310
371
  return {
311
372
  totalEntries: total,
312
- byType: gc(this.db, 'type'),
313
- byDomain: gc(this.db, 'domain'),
314
- bySeverity: gc(this.db, 'severity'),
373
+ byType: gc(this.provider, 'type'),
374
+ byDomain: gc(this.provider, 'domain'),
375
+ bySeverity: gc(this.provider, 'severity'),
315
376
  };
316
377
  }
317
378
 
@@ -319,13 +380,13 @@ export class Vault {
319
380
  this.seed([entry]);
320
381
  }
321
382
  remove(id: string): boolean {
322
- return this.db.prepare('DELETE FROM entries WHERE id = ?').run(id).changes > 0;
383
+ const deleted = this.provider.run('DELETE FROM entries WHERE id = ?', [id]).changes > 0;
384
+ if (deleted && this.syncManager) {
385
+ this.syncManager.enqueue('delete', id);
386
+ }
387
+ return deleted;
323
388
  }
324
389
 
325
- /**
326
- * Partial update of an existing entry's mutable fields.
327
- * Returns the updated entry or null if not found.
328
- */
329
390
  update(
330
391
  id: string,
331
392
  fields: Partial<
@@ -342,6 +403,8 @@ export class Vault {
342
403
  | 'severity'
343
404
  | 'type'
344
405
  | 'domain'
406
+ | 'validFrom'
407
+ | 'validUntil'
345
408
  >
346
409
  >,
347
410
  ): IntelligenceEntry | null {
@@ -352,27 +415,58 @@ export class Vault {
352
415
  return this.get(id);
353
416
  }
354
417
 
355
- /**
356
- * Remove multiple entries by IDs in a single transaction.
357
- * Returns the number of entries actually removed.
358
- */
418
+ setTemporal(id: string, validFrom?: number, validUntil?: number): boolean {
419
+ const sets: string[] = [];
420
+ const params: Record<string, unknown> = { id };
421
+ if (validFrom !== undefined) {
422
+ sets.push('valid_from = @validFrom');
423
+ params.validFrom = validFrom;
424
+ }
425
+ if (validUntil !== undefined) {
426
+ sets.push('valid_until = @validUntil');
427
+ params.validUntil = validUntil;
428
+ }
429
+ if (sets.length === 0) return false;
430
+ sets.push('updated_at = unixepoch()');
431
+ return (
432
+ this.provider.run(`UPDATE entries SET ${sets.join(', ')} WHERE id = @id`, params).changes > 0
433
+ );
434
+ }
435
+
436
+ findExpiring(withinDays: number): IntelligenceEntry[] {
437
+ const now = Math.floor(Date.now() / 1000);
438
+ const cutoff = now + withinDays * 86400;
439
+ const rows = this.provider.all<Record<string, unknown>>(
440
+ 'SELECT * FROM entries WHERE valid_until IS NOT NULL AND valid_until > @now AND valid_until <= @cutoff ORDER BY valid_until ASC',
441
+ { now, cutoff },
442
+ );
443
+ return rows.map(rowToEntry);
444
+ }
445
+
446
+ findExpired(limit: number = 50): IntelligenceEntry[] {
447
+ const now = Math.floor(Date.now() / 1000);
448
+ const rows = this.provider.all<Record<string, unknown>>(
449
+ 'SELECT * FROM entries WHERE valid_until IS NOT NULL AND valid_until <= @now ORDER BY valid_until DESC LIMIT @limit',
450
+ { now, limit },
451
+ );
452
+ return rows.map(rowToEntry);
453
+ }
454
+
359
455
  bulkRemove(ids: string[]): number {
360
- const stmt = this.db.prepare('DELETE FROM entries WHERE id = ?');
361
- const tx = this.db.transaction((idList: string[]) => {
456
+ return this.provider.transaction(() => {
362
457
  let count = 0;
363
- for (const id of idList) {
364
- count += stmt.run(id).changes;
458
+ for (const id of ids) {
459
+ count += this.provider.run('DELETE FROM entries WHERE id = ?', [id]).changes;
460
+ if (this.syncManager) {
461
+ this.syncManager.enqueue('delete', id);
462
+ }
365
463
  }
366
464
  return count;
367
465
  });
368
- return tx(ids);
369
466
  }
370
467
 
371
- /**
372
- * List all unique tags with their occurrence counts.
373
- */
374
468
  getTags(): Array<{ tag: string; count: number }> {
375
- const rows = this.db.prepare('SELECT tags FROM entries').all() as Array<{ tags: string }>;
469
+ const rows = this.provider.all<{ tags: string }>('SELECT tags FROM entries');
376
470
  const counts = new Map<string, number>();
377
471
  for (const row of rows) {
378
472
  const tags: string[] = JSON.parse(row.tags || '[]');
@@ -385,49 +479,37 @@ export class Vault {
385
479
  .sort((a, b) => b.count - a.count);
386
480
  }
387
481
 
388
- /**
389
- * List all domains with their entry counts.
390
- */
391
482
  getDomains(): Array<{ domain: string; count: number }> {
392
- const rows = this.db
393
- .prepare('SELECT domain, COUNT(*) as count FROM entries GROUP BY domain ORDER BY count DESC')
394
- .all() as Array<{ domain: string; count: number }>;
395
- return rows;
483
+ return this.provider.all<{ domain: string; count: number }>(
484
+ 'SELECT domain, COUNT(*) as count FROM entries GROUP BY domain ORDER BY count DESC',
485
+ );
396
486
  }
397
487
 
398
- /**
399
- * Get recently added or updated entries, ordered by updated_at DESC.
400
- */
401
488
  getRecent(limit: number = 20): IntelligenceEntry[] {
402
- const rows = this.db
403
- .prepare('SELECT * FROM entries ORDER BY updated_at DESC LIMIT ?')
404
- .all(limit) as Array<Record<string, unknown>>;
489
+ const rows = this.provider.all<Record<string, unknown>>(
490
+ 'SELECT * FROM entries ORDER BY updated_at DESC LIMIT ?',
491
+ [limit],
492
+ );
405
493
  return rows.map(rowToEntry);
406
494
  }
407
495
 
408
- /**
409
- * Export the entire vault as a JSON-serializable bundle.
410
- */
411
496
  exportAll(): { entries: IntelligenceEntry[]; exportedAt: number; count: number } {
412
- const rows = this.db
413
- .prepare('SELECT * FROM entries ORDER BY domain, title')
414
- .all() as Array<Record<string, unknown>>;
497
+ const rows = this.provider.all<Record<string, unknown>>(
498
+ 'SELECT * FROM entries ORDER BY domain, title',
499
+ );
415
500
  const entries = rows.map(rowToEntry);
416
501
  return { entries, exportedAt: Math.floor(Date.now() / 1000), count: entries.length };
417
502
  }
418
503
 
419
- /**
420
- * Get entry age distribution — how old entries are, bucketed.
421
- */
422
504
  getAgeReport(): {
423
505
  total: number;
424
506
  buckets: Array<{ label: string; count: number; minDays: number; maxDays: number }>;
425
507
  oldestTimestamp: number | null;
426
508
  newestTimestamp: number | null;
427
509
  } {
428
- const rows = this.db
429
- .prepare('SELECT created_at, updated_at FROM entries')
430
- .all() as Array<{ created_at: number; updated_at: number }>;
510
+ const rows = this.provider.all<{ created_at: number; updated_at: number }>(
511
+ 'SELECT created_at, updated_at FROM entries',
512
+ );
431
513
  const now = Math.floor(Date.now() / 1000);
432
514
  const bucketDefs = [
433
515
  { label: 'today', minDays: 0, maxDays: 1 },
@@ -463,21 +545,21 @@ export class Vault {
463
545
  const projectName = name ?? path.replace(/\/$/, '').split('/').pop() ?? path;
464
546
  const existing = this.getProject(path);
465
547
  if (existing) {
466
- this.db
467
- .prepare(
468
- 'UPDATE projects SET last_seen_at = unixepoch(), session_count = session_count + 1 WHERE path = ?',
469
- )
470
- .run(path);
548
+ this.provider.run(
549
+ 'UPDATE projects SET last_seen_at = unixepoch(), session_count = session_count + 1 WHERE path = ?',
550
+ [path],
551
+ );
471
552
  return this.getProject(path)!;
472
553
  }
473
- this.db.prepare('INSERT INTO projects (path, name) VALUES (?, ?)').run(path, projectName);
554
+ this.provider.run('INSERT INTO projects (path, name) VALUES (?, ?)', [path, projectName]);
474
555
  return this.getProject(path)!;
475
556
  }
476
557
 
477
558
  getProject(path: string): ProjectInfo | null {
478
- const row = this.db.prepare('SELECT * FROM projects WHERE path = ?').get(path) as
479
- | Record<string, unknown>
480
- | undefined;
559
+ const row = this.provider.get<Record<string, unknown>>(
560
+ 'SELECT * FROM projects WHERE path = ?',
561
+ [path],
562
+ );
481
563
  if (!row) return null;
482
564
  return {
483
565
  path: row.path as string,
@@ -489,9 +571,9 @@ export class Vault {
489
571
  }
490
572
 
491
573
  listProjects(): ProjectInfo[] {
492
- const rows = this.db
493
- .prepare('SELECT * FROM projects ORDER BY last_seen_at DESC')
494
- .all() as Array<Record<string, unknown>>;
574
+ const rows = this.provider.all<Record<string, unknown>>(
575
+ 'SELECT * FROM projects ORDER BY last_seen_at DESC',
576
+ );
495
577
  return rows.map((row) => ({
496
578
  path: row.path as string,
497
579
  name: row.name as string,
@@ -503,11 +585,9 @@ export class Vault {
503
585
 
504
586
  captureMemory(memory: Omit<Memory, 'id' | 'createdAt' | 'archivedAt'>): Memory {
505
587
  const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
506
- this.db
507
- .prepare(
508
- `INSERT INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used) VALUES (@id, @projectPath, @type, @context, @summary, @topics, @filesModified, @toolsUsed)`,
509
- )
510
- .run({
588
+ this.provider.run(
589
+ `INSERT INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used) VALUES (@id, @projectPath, @type, @context, @summary, @topics, @filesModified, @toolsUsed)`,
590
+ {
511
591
  id,
512
592
  projectPath: memory.projectPath,
513
593
  type: memory.type,
@@ -516,7 +596,8 @@ export class Vault {
516
596
  topics: JSON.stringify(memory.topics),
517
597
  filesModified: JSON.stringify(memory.filesModified),
518
598
  toolsUsed: JSON.stringify(memory.toolsUsed),
519
- });
599
+ },
600
+ );
520
601
  return this.getMemory(id)!;
521
602
  }
522
603
 
@@ -537,11 +618,10 @@ export class Vault {
537
618
  }
538
619
  const wc = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';
539
620
  try {
540
- const rows = this.db
541
- .prepare(
542
- `SELECT m.* FROM memories_fts fts JOIN memories m ON m.rowid = fts.rowid WHERE memories_fts MATCH @query ${wc} ORDER BY rank LIMIT @limit`,
543
- )
544
- .all({ query, limit, ...fp }) as Array<Record<string, unknown>>;
621
+ const rows = this.provider.all<Record<string, unknown>>(
622
+ `SELECT m.* FROM memories_fts fts JOIN memories m ON m.rowid = fts.rowid WHERE memories_fts MATCH @query ${wc} ORDER BY rank LIMIT @limit`,
623
+ { query, limit, ...fp },
624
+ );
545
625
  return rows.map(rowToMemory);
546
626
  } catch {
547
627
  return [];
@@ -565,30 +645,23 @@ export class Vault {
565
645
  params.projectPath = options.projectPath;
566
646
  }
567
647
  const wc = `WHERE ${filters.join(' AND ')}`;
568
- const rows = this.db
569
- .prepare(`SELECT * FROM memories ${wc} ORDER BY created_at DESC LIMIT @limit OFFSET @offset`)
570
- .all({ ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 }) as Array<
571
- Record<string, unknown>
572
- >;
648
+ const rows = this.provider.all<Record<string, unknown>>(
649
+ `SELECT * FROM memories ${wc} ORDER BY created_at DESC LIMIT @limit OFFSET @offset`,
650
+ { ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 },
651
+ );
573
652
  return rows.map(rowToMemory);
574
653
  }
575
654
 
576
655
  memoryStats(): MemoryStats {
577
- const total = (
578
- this.db.prepare('SELECT COUNT(*) as count FROM memories WHERE archived_at IS NULL').get() as {
579
- count: number;
580
- }
581
- ).count;
582
- const byTypeRows = this.db
583
- .prepare(
584
- 'SELECT type as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY type',
585
- )
586
- .all() as Array<{ key: string; count: number }>;
587
- const byProjectRows = this.db
588
- .prepare(
589
- 'SELECT project_path as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path',
590
- )
591
- .all() as Array<{ key: string; count: number }>;
656
+ const total = this.provider.get<{ count: number }>(
657
+ 'SELECT COUNT(*) as count FROM memories WHERE archived_at IS NULL',
658
+ )!.count;
659
+ const byTypeRows = this.provider.all<{ key: string; count: number }>(
660
+ 'SELECT type as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY type',
661
+ );
662
+ const byProjectRows = this.provider.all<{ key: string; count: number }>(
663
+ 'SELECT project_path as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path',
664
+ );
592
665
  return {
593
666
  total,
594
667
  byType: Object.fromEntries(byTypeRows.map((r) => [r.key, r.count])),
@@ -597,15 +670,14 @@ export class Vault {
597
670
  }
598
671
 
599
672
  getMemory(id: string): Memory | null {
600
- const row = this.db.prepare('SELECT * FROM memories WHERE id = ?').get(id) as
601
- | Record<string, unknown>
602
- | undefined;
673
+ const row = this.provider.get<Record<string, unknown>>('SELECT * FROM memories WHERE id = ?', [
674
+ id,
675
+ ]);
603
676
  return row ? rowToMemory(row) : null;
604
677
  }
605
678
 
606
-
607
679
  deleteMemory(id: string): boolean {
608
- return this.db.prepare('DELETE FROM memories WHERE id = ?').run(id).changes > 0;
680
+ return this.provider.run('DELETE FROM memories WHERE id = ?', [id]).changes > 0;
609
681
  }
610
682
 
611
683
  memoryStatsDetailed(options?: {
@@ -629,35 +701,30 @@ export class Vault {
629
701
  }
630
702
  const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
631
703
 
632
- const total = (
633
- this.db
634
- .prepare(`SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`)
635
- .get(params) as { count: number }
636
- ).count;
637
-
638
- const archivedCount = (
639
- this.db
640
- .prepare(`SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NOT NULL`)
641
- .get(params) as { count: number }
642
- ).count;
643
-
644
- const byTypeRows = this.db
645
- .prepare(
646
- `SELECT type as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY type`,
647
- )
648
- .all(params) as Array<{ key: string; count: number }>;
649
-
650
- const byProjectRows = this.db
651
- .prepare(
652
- `SELECT project_path as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY project_path`,
653
- )
654
- .all(params) as Array<{ key: string; count: number }>;
655
-
656
- const dateRange = this.db
657
- .prepare(
658
- `SELECT MIN(created_at) as oldest, MAX(created_at) as newest FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`,
659
- )
660
- .get(params) as { oldest: number | null; newest: number | null };
704
+ const total = this.provider.get<{ count: number }>(
705
+ `SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`,
706
+ params,
707
+ )!.count;
708
+
709
+ const archivedCount = this.provider.get<{ count: number }>(
710
+ `SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NOT NULL`,
711
+ params,
712
+ )!.count;
713
+
714
+ const byTypeRows = this.provider.all<{ key: string; count: number }>(
715
+ `SELECT type as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY type`,
716
+ params,
717
+ );
718
+
719
+ const byProjectRows = this.provider.all<{ key: string; count: number }>(
720
+ `SELECT project_path as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY project_path`,
721
+ params,
722
+ );
723
+
724
+ const dateRange = this.provider.get<{ oldest: number | null; newest: number | null }>(
725
+ `SELECT MIN(created_at) as oldest, MAX(created_at) as newest FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`,
726
+ params,
727
+ )!;
661
728
 
662
729
  return {
663
730
  total,
@@ -688,22 +755,23 @@ export class Vault {
688
755
  params.type = options.type;
689
756
  }
690
757
  const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
691
- const rows = this.db
692
- .prepare(`SELECT * FROM memories ${wc} ORDER BY created_at ASC`)
693
- .all(params) as Array<Record<string, unknown>>;
758
+ const rows = this.provider.all<Record<string, unknown>>(
759
+ `SELECT * FROM memories ${wc} ORDER BY created_at ASC`,
760
+ Object.keys(params).length > 0 ? params : undefined,
761
+ );
694
762
  return rows.map(rowToMemory);
695
763
  }
696
764
 
697
765
  importMemories(memories: Memory[]): { imported: number; skipped: number } {
698
- const upsert = this.db.prepare(`
766
+ const sql = `
699
767
  INSERT OR IGNORE INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used, created_at, archived_at)
700
768
  VALUES (@id, @projectPath, @type, @context, @summary, @topics, @filesModified, @toolsUsed, @createdAt, @archivedAt)
701
- `);
769
+ `;
702
770
  let imported = 0;
703
771
  let skipped = 0;
704
- const tx = this.db.transaction((items: Memory[]) => {
705
- for (const m of items) {
706
- const result = upsert.run({
772
+ this.provider.transaction(() => {
773
+ for (const m of memories) {
774
+ const result = this.provider.run(sql, {
707
775
  id: m.id,
708
776
  projectPath: m.projectPath,
709
777
  type: m.type,
@@ -719,22 +787,20 @@ export class Vault {
719
787
  else skipped++;
720
788
  }
721
789
  });
722
- tx(memories);
723
790
  return { imported, skipped };
724
791
  }
725
792
 
726
793
  pruneMemories(olderThanDays: number): { pruned: number } {
727
794
  const cutoff = Math.floor(Date.now() / 1000) - olderThanDays * 86400;
728
- const result = this.db
729
- .prepare('DELETE FROM memories WHERE created_at < ? AND archived_at IS NULL')
730
- .run(cutoff);
795
+ const result = this.provider.run(
796
+ 'DELETE FROM memories WHERE created_at < ? AND archived_at IS NULL',
797
+ [cutoff],
798
+ );
731
799
  return { pruned: result.changes };
732
800
  }
733
801
 
734
802
  deduplicateMemories(): { removed: number; groups: Array<{ kept: string; removed: string[] }> } {
735
- // Find duplicates by matching summary + project_path + type
736
- const dupeRows = this.db
737
- .prepare(`
803
+ const dupeRows = this.provider.all<{ id1: string; id2: string }>(`
738
804
  SELECT m1.id as id1, m2.id as id2
739
805
  FROM memories m1
740
806
  JOIN memories m2 ON m1.summary = m2.summary
@@ -743,10 +809,8 @@ export class Vault {
743
809
  AND m1.id < m2.id
744
810
  AND m1.archived_at IS NULL
745
811
  AND m2.archived_at IS NULL
746
- `)
747
- .all() as Array<{ id1: string; id2: string }>;
812
+ `);
748
813
 
749
- // Group: keep the earliest (id1), remove all later duplicates
750
814
  const groupMap = new Map<string, Set<string>>();
751
815
  for (const row of dupeRows) {
752
816
  if (!groupMap.has(row.id1)) groupMap.set(row.id1, new Set());
@@ -764,20 +828,20 @@ export class Vault {
764
828
  }
765
829
 
766
830
  if (toRemove.size > 0) {
767
- const del = this.db.prepare('DELETE FROM memories WHERE id = ?');
768
- const tx = this.db.transaction((ids: string[]) => {
769
- for (const id of ids) del.run(id);
831
+ this.provider.transaction(() => {
832
+ for (const id of toRemove) {
833
+ this.provider.run('DELETE FROM memories WHERE id = ?', [id]);
834
+ }
770
835
  });
771
- tx([...toRemove]);
772
836
  }
773
837
 
774
838
  return { removed: toRemove.size, groups };
775
839
  }
776
840
 
777
841
  memoryTopics(): Array<{ topic: string; count: number }> {
778
- const rows = this.db
779
- .prepare('SELECT topics FROM memories WHERE archived_at IS NULL')
780
- .all() as Array<{ topics: string }>;
842
+ const rows = this.provider.all<{ topics: string }>(
843
+ 'SELECT topics FROM memories WHERE archived_at IS NULL',
844
+ );
781
845
 
782
846
  const topicCounts = new Map<string, number>();
783
847
  for (const row of rows) {
@@ -793,18 +857,15 @@ export class Vault {
793
857
  }
794
858
 
795
859
  memoriesByProject(): Array<{ project: string; count: number; memories: Memory[] }> {
796
- const rows = this.db
797
- .prepare(
798
- 'SELECT project_path as project, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path ORDER BY count DESC',
799
- )
800
- .all() as Array<{ project: string; count: number }>;
860
+ const rows = this.provider.all<{ project: string; count: number }>(
861
+ 'SELECT project_path as project, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path ORDER BY count DESC',
862
+ );
801
863
 
802
864
  return rows.map((row) => {
803
- const memories = this.db
804
- .prepare(
805
- 'SELECT * FROM memories WHERE project_path = ? AND archived_at IS NULL ORDER BY created_at DESC',
806
- )
807
- .all(row.project) as Array<Record<string, unknown>>;
865
+ const memories = this.provider.all<Record<string, unknown>>(
866
+ 'SELECT * FROM memories WHERE project_path = ? AND archived_at IS NULL ORDER BY created_at DESC',
867
+ [row.project],
868
+ );
808
869
  return {
809
870
  project: row.project,
810
871
  count: row.count,
@@ -813,19 +874,140 @@ export class Vault {
813
874
  });
814
875
  }
815
876
 
816
- getDb(): Database.Database {
817
- return this.db;
877
+ /**
878
+ * Rebuild the FTS5 index for the entries table.
879
+ * Useful after bulk operations or if the index gets out of sync.
880
+ */
881
+ rebuildFtsIndex(): void {
882
+ try {
883
+ this.provider.run("INSERT INTO entries_fts(entries_fts) VALUES('rebuild')");
884
+ } catch {
885
+ // Graceful degradation — FTS rebuild failed (e.g. table doesn't exist yet)
886
+ }
887
+ }
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
+
981
+ /**
982
+ * Get the underlying persistence provider.
983
+ */
984
+ getProvider(): PersistenceProvider {
985
+ return this.provider;
986
+ }
987
+
988
+ /**
989
+ * Get the raw better-sqlite3 Database (backward compat).
990
+ * Throws if the provider is not SQLite.
991
+ */
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
+ }
996
+ if (this.sqliteProvider) {
997
+ return this.sqliteProvider.getDatabase();
998
+ }
999
+ throw new Error('getDb() is only available with SQLite provider');
818
1000
  }
819
1001
 
820
1002
  close(): void {
821
- this.db.close();
1003
+ this.provider.close();
822
1004
  }
823
1005
  }
824
1006
 
825
- function gc(db: Database.Database, col: string): Record<string, number> {
826
- const rows = db
827
- .prepare(`SELECT ${col} as key, COUNT(*) as count FROM entries GROUP BY ${col}`)
828
- .all() as Array<{ key: string; count: number }>;
1007
+ function gc(provider: PersistenceProvider, col: string): Record<string, number> {
1008
+ const rows = provider.all<{ key: string; count: number }>(
1009
+ `SELECT ${col} as key, COUNT(*) as count FROM entries GROUP BY ${col}`,
1010
+ );
829
1011
  return Object.fromEntries(rows.map((r) => [r.key, r.count]));
830
1012
  }
831
1013
 
@@ -843,6 +1025,8 @@ function rowToEntry(row: Record<string, unknown>): IntelligenceEntry {
843
1025
  why: (row.why as string) ?? undefined,
844
1026
  tags: JSON.parse((row.tags as string) || '[]'),
845
1027
  appliesTo: JSON.parse((row.applies_to as string) || '[]'),
1028
+ validFrom: (row.valid_from as number) ?? undefined,
1029
+ validUntil: (row.valid_until as number) ?? undefined,
846
1030
  };
847
1031
  }
848
1032