@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,21 +1,40 @@
1
- import Database from 'better-sqlite3';
2
- import { mkdirSync } from 'node:fs';
3
- import { dirname } from 'node:path';
1
+ import { SQLitePersistenceProvider } from '../persistence/sqlite-provider.js';
4
2
  export class Vault {
5
- db;
6
- constructor(dbPath = ':memory:') {
7
- if (dbPath !== ':memory:')
8
- mkdirSync(dirname(dbPath), { recursive: true });
9
- this.db = new Database(dbPath);
10
- this.db.pragma('journal_mode = WAL');
11
- this.db.pragma('foreign_keys = ON');
3
+ provider;
4
+ sqliteProvider;
5
+ syncManager = null;
6
+ /**
7
+ * Create a Vault with a PersistenceProvider or a SQLite path (backward compat).
8
+ */
9
+ constructor(providerOrPath = ':memory:') {
10
+ if (typeof providerOrPath === 'string') {
11
+ const sqlite = new SQLitePersistenceProvider(providerOrPath);
12
+ this.provider = sqlite;
13
+ this.sqliteProvider = sqlite;
14
+ // SQLite-specific pragmas
15
+ this.provider.run('PRAGMA journal_mode = WAL');
16
+ this.provider.run('PRAGMA foreign_keys = ON');
17
+ this.provider.run('PRAGMA synchronous = NORMAL');
18
+ }
19
+ else {
20
+ this.provider = providerOrPath;
21
+ this.sqliteProvider =
22
+ providerOrPath instanceof SQLitePersistenceProvider ? providerOrPath : null;
23
+ }
12
24
  this.initialize();
13
25
  }
26
+ setSyncManager(mgr) {
27
+ this.syncManager = mgr;
28
+ }
29
+ /** Backward-compatible factory. */
30
+ static createWithSQLite(dbPath = ':memory:') {
31
+ return new Vault(dbPath);
32
+ }
14
33
  initialize() {
15
- this.db.exec(`
34
+ this.provider.execSql(`
16
35
  CREATE TABLE IF NOT EXISTS entries (
17
36
  id TEXT PRIMARY KEY,
18
- type TEXT NOT NULL CHECK(type IN ('pattern', 'anti-pattern', 'rule')),
37
+ type TEXT NOT NULL CHECK(type IN ('pattern', 'anti-pattern', 'rule', 'playbook')),
19
38
  domain TEXT NOT NULL,
20
39
  title TEXT NOT NULL,
21
40
  severity TEXT NOT NULL CHECK(severity IN ('critical', 'warning', 'suggestion')),
@@ -40,6 +59,23 @@ export class Vault {
40
59
  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);
41
60
  INSERT INTO entries_fts(rowid,id,title,description,context,tags) VALUES(new.rowid,new.id,new.title,new.description,new.context,new.tags);
42
61
  END;
62
+ CREATE TABLE IF NOT EXISTS entries_archive (
63
+ id TEXT PRIMARY KEY,
64
+ type TEXT NOT NULL,
65
+ domain TEXT NOT NULL,
66
+ title TEXT NOT NULL,
67
+ severity TEXT NOT NULL,
68
+ description TEXT NOT NULL,
69
+ context TEXT, example TEXT, counter_example TEXT, why TEXT,
70
+ tags TEXT NOT NULL DEFAULT '[]',
71
+ applies_to TEXT DEFAULT '[]',
72
+ created_at INTEGER NOT NULL,
73
+ updated_at INTEGER NOT NULL,
74
+ valid_from INTEGER,
75
+ valid_until INTEGER,
76
+ archived_at INTEGER NOT NULL DEFAULT (unixepoch()),
77
+ archive_reason TEXT
78
+ );
43
79
  CREATE TABLE IF NOT EXISTS projects (
44
80
  path TEXT PRIMARY KEY,
45
81
  name TEXT NOT NULL,
@@ -92,23 +128,35 @@ export class Vault {
92
128
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
93
129
  );
94
130
  CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query);
131
+ CREATE INDEX IF NOT EXISTS idx_entries_domain ON entries(domain);
132
+ CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(type);
133
+ CREATE INDEX IF NOT EXISTS idx_entries_severity ON entries(severity);
134
+ CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_path);
135
+ CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
95
136
  `);
96
137
  this.migrateBrainSchema();
138
+ this.migrateTemporalSchema();
139
+ }
140
+ migrateTemporalSchema() {
141
+ try {
142
+ this.provider.run('ALTER TABLE entries ADD COLUMN valid_from INTEGER');
143
+ }
144
+ catch {
145
+ // Column already exists
146
+ }
147
+ try {
148
+ this.provider.run('ALTER TABLE entries ADD COLUMN valid_until INTEGER');
149
+ }
150
+ catch {
151
+ // Column already exists
152
+ }
97
153
  }
98
- /**
99
- * Migrate brain_feedback table from old schema (accepted/dismissed only)
100
- * to new schema with source, confidence, duration, context, reason columns.
101
- * Also adds extracted_at to brain_sessions if it exists.
102
- */
103
154
  migrateBrainSchema() {
104
- // Check if brain_feedback needs migration (old schema lacks 'source' column)
105
- const columns = this.db.prepare('PRAGMA table_info(brain_feedback)').all();
155
+ const columns = this.provider.all('PRAGMA table_info(brain_feedback)');
106
156
  const hasSource = columns.some((c) => c.name === 'source');
107
157
  if (!hasSource && columns.length > 0) {
108
- // Old table exists without new columns — rebuild with expanded schema
109
- this.db.transaction(() => {
110
- this.db
111
- .prepare(`
158
+ this.provider.transaction(() => {
159
+ this.provider.run(`
112
160
  CREATE TABLE brain_feedback_new (
113
161
  id INTEGER PRIMARY KEY AUTOINCREMENT,
114
162
  query TEXT NOT NULL,
@@ -121,26 +169,20 @@ export class Vault {
121
169
  reason TEXT,
122
170
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
123
171
  )
124
- `)
125
- .run();
126
- this.db
127
- .prepare(`
172
+ `);
173
+ this.provider.run(`
128
174
  INSERT INTO brain_feedback_new (id, query, entry_id, action, created_at)
129
175
  SELECT id, query, entry_id, action, created_at FROM brain_feedback
130
- `)
131
- .run();
132
- this.db.prepare('DROP TABLE brain_feedback').run();
133
- this.db.prepare('ALTER TABLE brain_feedback_new RENAME TO brain_feedback').run();
134
- this.db
135
- .prepare('CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query)')
136
- .run();
137
- })();
138
- }
139
- // Add extracted_at to brain_sessions if it exists but lacks the column
176
+ `);
177
+ this.provider.run('DROP TABLE brain_feedback');
178
+ this.provider.run('ALTER TABLE brain_feedback_new RENAME TO brain_feedback');
179
+ this.provider.run('CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query)');
180
+ });
181
+ }
140
182
  try {
141
- const sessionCols = this.db.prepare('PRAGMA table_info(brain_sessions)').all();
183
+ const sessionCols = this.provider.all('PRAGMA table_info(brain_sessions)');
142
184
  if (sessionCols.length > 0 && !sessionCols.some((c) => c.name === 'extracted_at')) {
143
- this.db.prepare('ALTER TABLE brain_sessions ADD COLUMN extracted_at TEXT').run();
185
+ this.provider.run('ALTER TABLE brain_sessions ADD COLUMN extracted_at TEXT');
144
186
  }
145
187
  }
146
188
  catch {
@@ -148,17 +190,17 @@ export class Vault {
148
190
  }
149
191
  }
150
192
  seed(entries) {
151
- const upsert = this.db.prepare(`
152
- INSERT INTO entries (id,type,domain,title,severity,description,context,example,counter_example,why,tags,applies_to)
153
- VALUES (@id,@type,@domain,@title,@severity,@description,@context,@example,@counterExample,@why,@tags,@appliesTo)
193
+ const sql = `
194
+ INSERT INTO entries (id,type,domain,title,severity,description,context,example,counter_example,why,tags,applies_to,valid_from,valid_until)
195
+ VALUES (@id,@type,@domain,@title,@severity,@description,@context,@example,@counterExample,@why,@tags,@appliesTo,@validFrom,@validUntil)
154
196
  ON CONFLICT(id) DO UPDATE SET type=excluded.type,domain=excluded.domain,title=excluded.title,severity=excluded.severity,
155
197
  description=excluded.description,context=excluded.context,example=excluded.example,counter_example=excluded.counter_example,
156
- why=excluded.why,tags=excluded.tags,applies_to=excluded.applies_to,updated_at=unixepoch()
157
- `);
158
- const tx = this.db.transaction((items) => {
198
+ why=excluded.why,tags=excluded.tags,applies_to=excluded.applies_to,valid_from=excluded.valid_from,valid_until=excluded.valid_until,updated_at=unixepoch()
199
+ `;
200
+ return this.provider.transaction(() => {
159
201
  let count = 0;
160
- for (const entry of items) {
161
- upsert.run({
202
+ for (const entry of entries) {
203
+ this.provider.run(sql, {
162
204
  id: entry.id,
163
205
  type: entry.type,
164
206
  domain: entry.domain,
@@ -171,12 +213,16 @@ export class Vault {
171
213
  why: entry.why ?? null,
172
214
  tags: JSON.stringify(entry.tags),
173
215
  appliesTo: JSON.stringify(entry.appliesTo ?? []),
216
+ validFrom: entry.validFrom ?? null,
217
+ validUntil: entry.validUntil ?? null,
174
218
  });
175
219
  count++;
220
+ if (this.syncManager) {
221
+ this.syncManager.enqueue('ingest', entry.id, entry);
222
+ }
176
223
  }
177
224
  return count;
178
225
  });
179
- return tx(entries);
180
226
  }
181
227
  search(query, options) {
182
228
  const limit = options?.limit ?? 10;
@@ -194,11 +240,15 @@ export class Vault {
194
240
  filters.push('e.severity = @severity');
195
241
  fp.severity = options.severity;
196
242
  }
243
+ if (!options?.includeExpired) {
244
+ const now = Math.floor(Date.now() / 1000);
245
+ filters.push('(e.valid_until IS NULL OR e.valid_until > @now)');
246
+ filters.push('(e.valid_from IS NULL OR e.valid_from <= @now)');
247
+ fp.now = now;
248
+ }
197
249
  const wc = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';
198
250
  try {
199
- const rows = this.db
200
- .prepare(`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`)
201
- .all({ query, limit, ...fp });
251
+ const rows = this.provider.all(`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`, { query, limit, ...fp });
202
252
  return rows.map(rowToSearchResult);
203
253
  }
204
254
  catch {
@@ -206,7 +256,9 @@ export class Vault {
206
256
  }
207
257
  }
208
258
  get(id) {
209
- const row = this.db.prepare('SELECT * FROM entries WHERE id = ?').get(id);
259
+ const row = this.provider.get('SELECT * FROM entries WHERE id = ?', [
260
+ id,
261
+ ]);
210
262
  return row ? rowToEntry(row) : null;
211
263
  }
212
264
  list(options) {
@@ -231,31 +283,35 @@ export class Vault {
231
283
  });
232
284
  filters.push(`(${c.join(' OR ')})`);
233
285
  }
286
+ if (!options?.includeExpired) {
287
+ const now = Math.floor(Date.now() / 1000);
288
+ filters.push('(valid_until IS NULL OR valid_until > @now)');
289
+ filters.push('(valid_from IS NULL OR valid_from <= @now)');
290
+ params.now = now;
291
+ }
234
292
  const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
235
- const rows = this.db
236
- .prepare(`SELECT * FROM entries ${wc} ORDER BY severity, domain, title LIMIT @limit OFFSET @offset`)
237
- .all({ ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 });
293
+ const rows = this.provider.all(`SELECT * FROM entries ${wc} ORDER BY severity, domain, title LIMIT @limit OFFSET @offset`, { ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 });
238
294
  return rows.map(rowToEntry);
239
295
  }
240
296
  stats() {
241
- const total = this.db.prepare('SELECT COUNT(*) as count FROM entries').get().count;
297
+ const total = this.provider.get('SELECT COUNT(*) as count FROM entries').count;
242
298
  return {
243
299
  totalEntries: total,
244
- byType: gc(this.db, 'type'),
245
- byDomain: gc(this.db, 'domain'),
246
- bySeverity: gc(this.db, 'severity'),
300
+ byType: gc(this.provider, 'type'),
301
+ byDomain: gc(this.provider, 'domain'),
302
+ bySeverity: gc(this.provider, 'severity'),
247
303
  };
248
304
  }
249
305
  add(entry) {
250
306
  this.seed([entry]);
251
307
  }
252
308
  remove(id) {
253
- return this.db.prepare('DELETE FROM entries WHERE id = ?').run(id).changes > 0;
309
+ const deleted = this.provider.run('DELETE FROM entries WHERE id = ?', [id]).changes > 0;
310
+ if (deleted && this.syncManager) {
311
+ this.syncManager.enqueue('delete', id);
312
+ }
313
+ return deleted;
254
314
  }
255
- /**
256
- * Partial update of an existing entry's mutable fields.
257
- * Returns the updated entry or null if not found.
258
- */
259
315
  update(id, fields) {
260
316
  const existing = this.get(id);
261
317
  if (!existing)
@@ -264,26 +320,47 @@ export class Vault {
264
320
  this.seed([merged]);
265
321
  return this.get(id);
266
322
  }
267
- /**
268
- * Remove multiple entries by IDs in a single transaction.
269
- * Returns the number of entries actually removed.
270
- */
323
+ setTemporal(id, validFrom, validUntil) {
324
+ const sets = [];
325
+ const params = { id };
326
+ if (validFrom !== undefined) {
327
+ sets.push('valid_from = @validFrom');
328
+ params.validFrom = validFrom;
329
+ }
330
+ if (validUntil !== undefined) {
331
+ sets.push('valid_until = @validUntil');
332
+ params.validUntil = validUntil;
333
+ }
334
+ if (sets.length === 0)
335
+ return false;
336
+ sets.push('updated_at = unixepoch()');
337
+ return (this.provider.run(`UPDATE entries SET ${sets.join(', ')} WHERE id = @id`, params).changes > 0);
338
+ }
339
+ findExpiring(withinDays) {
340
+ const now = Math.floor(Date.now() / 1000);
341
+ const cutoff = now + withinDays * 86400;
342
+ const rows = this.provider.all('SELECT * FROM entries WHERE valid_until IS NOT NULL AND valid_until > @now AND valid_until <= @cutoff ORDER BY valid_until ASC', { now, cutoff });
343
+ return rows.map(rowToEntry);
344
+ }
345
+ findExpired(limit = 50) {
346
+ const now = Math.floor(Date.now() / 1000);
347
+ const rows = this.provider.all('SELECT * FROM entries WHERE valid_until IS NOT NULL AND valid_until <= @now ORDER BY valid_until DESC LIMIT @limit', { now, limit });
348
+ return rows.map(rowToEntry);
349
+ }
271
350
  bulkRemove(ids) {
272
- const stmt = this.db.prepare('DELETE FROM entries WHERE id = ?');
273
- const tx = this.db.transaction((idList) => {
351
+ return this.provider.transaction(() => {
274
352
  let count = 0;
275
- for (const id of idList) {
276
- count += stmt.run(id).changes;
353
+ for (const id of ids) {
354
+ count += this.provider.run('DELETE FROM entries WHERE id = ?', [id]).changes;
355
+ if (this.syncManager) {
356
+ this.syncManager.enqueue('delete', id);
357
+ }
277
358
  }
278
359
  return count;
279
360
  });
280
- return tx(ids);
281
361
  }
282
- /**
283
- * List all unique tags with their occurrence counts.
284
- */
285
362
  getTags() {
286
- const rows = this.db.prepare('SELECT tags FROM entries').all();
363
+ const rows = this.provider.all('SELECT tags FROM entries');
287
364
  const counts = new Map();
288
365
  for (const row of rows) {
289
366
  const tags = JSON.parse(row.tags || '[]');
@@ -295,41 +372,20 @@ export class Vault {
295
372
  .map(([tag, count]) => ({ tag, count }))
296
373
  .sort((a, b) => b.count - a.count);
297
374
  }
298
- /**
299
- * List all domains with their entry counts.
300
- */
301
375
  getDomains() {
302
- const rows = this.db
303
- .prepare('SELECT domain, COUNT(*) as count FROM entries GROUP BY domain ORDER BY count DESC')
304
- .all();
305
- return rows;
376
+ return this.provider.all('SELECT domain, COUNT(*) as count FROM entries GROUP BY domain ORDER BY count DESC');
306
377
  }
307
- /**
308
- * Get recently added or updated entries, ordered by updated_at DESC.
309
- */
310
378
  getRecent(limit = 20) {
311
- const rows = this.db
312
- .prepare('SELECT * FROM entries ORDER BY updated_at DESC LIMIT ?')
313
- .all(limit);
379
+ const rows = this.provider.all('SELECT * FROM entries ORDER BY updated_at DESC LIMIT ?', [limit]);
314
380
  return rows.map(rowToEntry);
315
381
  }
316
- /**
317
- * Export the entire vault as a JSON-serializable bundle.
318
- */
319
382
  exportAll() {
320
- const rows = this.db
321
- .prepare('SELECT * FROM entries ORDER BY domain, title')
322
- .all();
383
+ const rows = this.provider.all('SELECT * FROM entries ORDER BY domain, title');
323
384
  const entries = rows.map(rowToEntry);
324
385
  return { entries, exportedAt: Math.floor(Date.now() / 1000), count: entries.length };
325
386
  }
326
- /**
327
- * Get entry age distribution — how old entries are, bucketed.
328
- */
329
387
  getAgeReport() {
330
- const rows = this.db
331
- .prepare('SELECT created_at, updated_at FROM entries')
332
- .all();
388
+ const rows = this.provider.all('SELECT created_at, updated_at FROM entries');
333
389
  const now = Math.floor(Date.now() / 1000);
334
390
  const bucketDefs = [
335
391
  { label: 'today', minDays: 0, maxDays: 1 },
@@ -366,16 +422,14 @@ export class Vault {
366
422
  const projectName = name ?? path.replace(/\/$/, '').split('/').pop() ?? path;
367
423
  const existing = this.getProject(path);
368
424
  if (existing) {
369
- this.db
370
- .prepare('UPDATE projects SET last_seen_at = unixepoch(), session_count = session_count + 1 WHERE path = ?')
371
- .run(path);
425
+ this.provider.run('UPDATE projects SET last_seen_at = unixepoch(), session_count = session_count + 1 WHERE path = ?', [path]);
372
426
  return this.getProject(path);
373
427
  }
374
- this.db.prepare('INSERT INTO projects (path, name) VALUES (?, ?)').run(path, projectName);
428
+ this.provider.run('INSERT INTO projects (path, name) VALUES (?, ?)', [path, projectName]);
375
429
  return this.getProject(path);
376
430
  }
377
431
  getProject(path) {
378
- const row = this.db.prepare('SELECT * FROM projects WHERE path = ?').get(path);
432
+ const row = this.provider.get('SELECT * FROM projects WHERE path = ?', [path]);
379
433
  if (!row)
380
434
  return null;
381
435
  return {
@@ -387,9 +441,7 @@ export class Vault {
387
441
  };
388
442
  }
389
443
  listProjects() {
390
- const rows = this.db
391
- .prepare('SELECT * FROM projects ORDER BY last_seen_at DESC')
392
- .all();
444
+ const rows = this.provider.all('SELECT * FROM projects ORDER BY last_seen_at DESC');
393
445
  return rows.map((row) => ({
394
446
  path: row.path,
395
447
  name: row.name,
@@ -400,9 +452,7 @@ export class Vault {
400
452
  }
401
453
  captureMemory(memory) {
402
454
  const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
403
- this.db
404
- .prepare(`INSERT INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used) VALUES (@id, @projectPath, @type, @context, @summary, @topics, @filesModified, @toolsUsed)`)
405
- .run({
455
+ this.provider.run(`INSERT INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used) VALUES (@id, @projectPath, @type, @context, @summary, @topics, @filesModified, @toolsUsed)`, {
406
456
  id,
407
457
  projectPath: memory.projectPath,
408
458
  type: memory.type,
@@ -428,9 +478,7 @@ export class Vault {
428
478
  }
429
479
  const wc = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';
430
480
  try {
431
- const rows = this.db
432
- .prepare(`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`)
433
- .all({ query, limit, ...fp });
481
+ const rows = this.provider.all(`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`, { query, limit, ...fp });
434
482
  return rows.map(rowToMemory);
435
483
  }
436
484
  catch {
@@ -449,19 +497,13 @@ export class Vault {
449
497
  params.projectPath = options.projectPath;
450
498
  }
451
499
  const wc = `WHERE ${filters.join(' AND ')}`;
452
- const rows = this.db
453
- .prepare(`SELECT * FROM memories ${wc} ORDER BY created_at DESC LIMIT @limit OFFSET @offset`)
454
- .all({ ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 });
500
+ const rows = this.provider.all(`SELECT * FROM memories ${wc} ORDER BY created_at DESC LIMIT @limit OFFSET @offset`, { ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 });
455
501
  return rows.map(rowToMemory);
456
502
  }
457
503
  memoryStats() {
458
- const total = this.db.prepare('SELECT COUNT(*) as count FROM memories WHERE archived_at IS NULL').get().count;
459
- const byTypeRows = this.db
460
- .prepare('SELECT type as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY type')
461
- .all();
462
- const byProjectRows = this.db
463
- .prepare('SELECT project_path as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path')
464
- .all();
504
+ const total = this.provider.get('SELECT COUNT(*) as count FROM memories WHERE archived_at IS NULL').count;
505
+ const byTypeRows = this.provider.all('SELECT type as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY type');
506
+ const byProjectRows = this.provider.all('SELECT project_path as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path');
465
507
  return {
466
508
  total,
467
509
  byType: Object.fromEntries(byTypeRows.map((r) => [r.key, r.count])),
@@ -469,11 +511,13 @@ export class Vault {
469
511
  };
470
512
  }
471
513
  getMemory(id) {
472
- const row = this.db.prepare('SELECT * FROM memories WHERE id = ?').get(id);
514
+ const row = this.provider.get('SELECT * FROM memories WHERE id = ?', [
515
+ id,
516
+ ]);
473
517
  return row ? rowToMemory(row) : null;
474
518
  }
475
519
  deleteMemory(id) {
476
- return this.db.prepare('DELETE FROM memories WHERE id = ?').run(id).changes > 0;
520
+ return this.provider.run('DELETE FROM memories WHERE id = ?', [id]).changes > 0;
477
521
  }
478
522
  memoryStatsDetailed(options) {
479
523
  const filters = [];
@@ -491,21 +535,11 @@ export class Vault {
491
535
  params.toDate = options.toDate;
492
536
  }
493
537
  const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
494
- const total = this.db
495
- .prepare(`SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`)
496
- .get(params).count;
497
- const archivedCount = this.db
498
- .prepare(`SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NOT NULL`)
499
- .get(params).count;
500
- const byTypeRows = this.db
501
- .prepare(`SELECT type as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY type`)
502
- .all(params);
503
- const byProjectRows = this.db
504
- .prepare(`SELECT project_path as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY project_path`)
505
- .all(params);
506
- const dateRange = this.db
507
- .prepare(`SELECT MIN(created_at) as oldest, MAX(created_at) as newest FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`)
508
- .get(params);
538
+ const total = this.provider.get(`SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`, params).count;
539
+ const archivedCount = this.provider.get(`SELECT COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NOT NULL`, params).count;
540
+ const byTypeRows = this.provider.all(`SELECT type as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY type`, params);
541
+ const byProjectRows = this.provider.all(`SELECT project_path as key, COUNT(*) as count FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL GROUP BY project_path`, params);
542
+ const dateRange = this.provider.get(`SELECT MIN(created_at) as oldest, MAX(created_at) as newest FROM memories ${wc}${wc ? ' AND' : ' WHERE'} archived_at IS NULL`, params);
509
543
  return {
510
544
  total,
511
545
  byType: Object.fromEntries(byTypeRows.map((r) => [r.key, r.count])),
@@ -530,21 +564,19 @@ export class Vault {
530
564
  params.type = options.type;
531
565
  }
532
566
  const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
533
- const rows = this.db
534
- .prepare(`SELECT * FROM memories ${wc} ORDER BY created_at ASC`)
535
- .all(params);
567
+ const rows = this.provider.all(`SELECT * FROM memories ${wc} ORDER BY created_at ASC`, Object.keys(params).length > 0 ? params : undefined);
536
568
  return rows.map(rowToMemory);
537
569
  }
538
570
  importMemories(memories) {
539
- const upsert = this.db.prepare(`
571
+ const sql = `
540
572
  INSERT OR IGNORE INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used, created_at, archived_at)
541
573
  VALUES (@id, @projectPath, @type, @context, @summary, @topics, @filesModified, @toolsUsed, @createdAt, @archivedAt)
542
- `);
574
+ `;
543
575
  let imported = 0;
544
576
  let skipped = 0;
545
- const tx = this.db.transaction((items) => {
546
- for (const m of items) {
547
- const result = upsert.run({
577
+ this.provider.transaction(() => {
578
+ for (const m of memories) {
579
+ const result = this.provider.run(sql, {
548
580
  id: m.id,
549
581
  projectPath: m.projectPath,
550
582
  type: m.type,
@@ -562,20 +594,15 @@ export class Vault {
562
594
  skipped++;
563
595
  }
564
596
  });
565
- tx(memories);
566
597
  return { imported, skipped };
567
598
  }
568
599
  pruneMemories(olderThanDays) {
569
600
  const cutoff = Math.floor(Date.now() / 1000) - olderThanDays * 86400;
570
- const result = this.db
571
- .prepare('DELETE FROM memories WHERE created_at < ? AND archived_at IS NULL')
572
- .run(cutoff);
601
+ const result = this.provider.run('DELETE FROM memories WHERE created_at < ? AND archived_at IS NULL', [cutoff]);
573
602
  return { pruned: result.changes };
574
603
  }
575
604
  deduplicateMemories() {
576
- // Find duplicates by matching summary + project_path + type
577
- const dupeRows = this.db
578
- .prepare(`
605
+ const dupeRows = this.provider.all(`
579
606
  SELECT m1.id as id1, m2.id as id2
580
607
  FROM memories m1
581
608
  JOIN memories m2 ON m1.summary = m2.summary
@@ -584,9 +611,7 @@ export class Vault {
584
611
  AND m1.id < m2.id
585
612
  AND m1.archived_at IS NULL
586
613
  AND m2.archived_at IS NULL
587
- `)
588
- .all();
589
- // Group: keep the earliest (id1), remove all later duplicates
614
+ `);
590
615
  const groupMap = new Map();
591
616
  for (const row of dupeRows) {
592
617
  if (!groupMap.has(row.id1))
@@ -604,19 +629,16 @@ export class Vault {
604
629
  }
605
630
  }
606
631
  if (toRemove.size > 0) {
607
- const del = this.db.prepare('DELETE FROM memories WHERE id = ?');
608
- const tx = this.db.transaction((ids) => {
609
- for (const id of ids)
610
- del.run(id);
632
+ this.provider.transaction(() => {
633
+ for (const id of toRemove) {
634
+ this.provider.run('DELETE FROM memories WHERE id = ?', [id]);
635
+ }
611
636
  });
612
- tx([...toRemove]);
613
637
  }
614
638
  return { removed: toRemove.size, groups };
615
639
  }
616
640
  memoryTopics() {
617
- const rows = this.db
618
- .prepare('SELECT topics FROM memories WHERE archived_at IS NULL')
619
- .all();
641
+ const rows = this.provider.all('SELECT topics FROM memories WHERE archived_at IS NULL');
620
642
  const topicCounts = new Map();
621
643
  for (const row of rows) {
622
644
  const topics = JSON.parse(row.topics || '[]');
@@ -629,13 +651,9 @@ export class Vault {
629
651
  .sort((a, b) => b.count - a.count);
630
652
  }
631
653
  memoriesByProject() {
632
- const rows = this.db
633
- .prepare('SELECT project_path as project, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path ORDER BY count DESC')
634
- .all();
654
+ const rows = this.provider.all('SELECT project_path as project, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path ORDER BY count DESC');
635
655
  return rows.map((row) => {
636
- const memories = this.db
637
- .prepare('SELECT * FROM memories WHERE project_path = ? AND archived_at IS NULL ORDER BY created_at DESC')
638
- .all(row.project);
656
+ const memories = this.provider.all('SELECT * FROM memories WHERE project_path = ? AND archived_at IS NULL ORDER BY created_at DESC', [row.project]);
639
657
  return {
640
658
  project: row.project,
641
659
  count: row.count,
@@ -643,17 +661,116 @@ export class Vault {
643
661
  };
644
662
  });
645
663
  }
664
+ /**
665
+ * Rebuild the FTS5 index for the entries table.
666
+ * Useful after bulk operations or if the index gets out of sync.
667
+ */
668
+ rebuildFtsIndex() {
669
+ try {
670
+ this.provider.run("INSERT INTO entries_fts(entries_fts) VALUES('rebuild')");
671
+ }
672
+ catch {
673
+ // Graceful degradation — FTS rebuild failed (e.g. table doesn't exist yet)
674
+ }
675
+ }
676
+ /**
677
+ * Archive entries older than N days. Moves them to entries_archive.
678
+ */
679
+ archive(options) {
680
+ const cutoff = Math.floor(Date.now() / 1000) - options.olderThanDays * 86400;
681
+ const reason = options.reason ?? `Archived: older than ${options.olderThanDays} days`;
682
+ return this.provider.transaction(() => {
683
+ // Find candidates
684
+ const candidates = this.provider.all('SELECT id FROM entries WHERE updated_at < ?', [cutoff]);
685
+ if (candidates.length === 0)
686
+ return { archived: 0 };
687
+ let archived = 0;
688
+ for (const { id } of candidates) {
689
+ // Copy to archive
690
+ this.provider.run(`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)
691
+ SELECT id, type, domain, title, severity, description, context, example, counter_example, why, tags, applies_to, created_at, updated_at, valid_from, valid_until, ?
692
+ FROM entries WHERE id = ?`, [reason, id]);
693
+ // Delete from active
694
+ const result = this.provider.run('DELETE FROM entries WHERE id = ?', [id]);
695
+ archived += result.changes;
696
+ }
697
+ return { archived };
698
+ });
699
+ }
700
+ /**
701
+ * Restore an archived entry back to the active table.
702
+ */
703
+ restore(id) {
704
+ return this.provider.transaction(() => {
705
+ const archived = this.provider.get('SELECT * FROM entries_archive WHERE id = ?', [id]);
706
+ if (!archived)
707
+ return false;
708
+ this.provider.run(`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)
709
+ SELECT id, type, domain, title, severity, description, context, example, counter_example, why, tags, applies_to, created_at, updated_at, valid_from, valid_until
710
+ FROM entries_archive WHERE id = ?`, [id]);
711
+ this.provider.run('DELETE FROM entries_archive WHERE id = ?', [id]);
712
+ return true;
713
+ });
714
+ }
715
+ /**
716
+ * Optimize the database: VACUUM (SQLite only), ANALYZE, and FTS rebuild.
717
+ */
718
+ optimize() {
719
+ let vacuumed = false;
720
+ let analyzed = false;
721
+ let ftsRebuilt = false;
722
+ // VACUUM only for SQLite
723
+ if (this.provider.backend === 'sqlite') {
724
+ try {
725
+ this.provider.execSql('VACUUM');
726
+ vacuumed = true;
727
+ }
728
+ catch {
729
+ // VACUUM may fail inside a transaction
730
+ }
731
+ }
732
+ try {
733
+ this.provider.execSql('ANALYZE');
734
+ analyzed = true;
735
+ }
736
+ catch {
737
+ // Non-critical
738
+ }
739
+ try {
740
+ this.provider.ftsRebuild('entries');
741
+ this.provider.ftsRebuild('memories');
742
+ ftsRebuilt = true;
743
+ }
744
+ catch {
745
+ // Non-critical
746
+ }
747
+ return { vacuumed, analyzed, ftsRebuilt };
748
+ }
749
+ /**
750
+ * Get the underlying persistence provider.
751
+ */
752
+ getProvider() {
753
+ return this.provider;
754
+ }
755
+ /**
756
+ * Get the raw better-sqlite3 Database (backward compat).
757
+ * Throws if the provider is not SQLite.
758
+ */
646
759
  getDb() {
647
- return this.db;
760
+ if (process.env.NODE_ENV !== 'test' && process.env.VITEST !== 'true') {
761
+ console.warn('Vault.getDb() is deprecated. Use vault.getProvider() instead.');
762
+ }
763
+ if (this.sqliteProvider) {
764
+ return this.sqliteProvider.getDatabase();
765
+ }
766
+ throw new Error('getDb() is only available with SQLite provider');
648
767
  }
649
768
  close() {
650
- this.db.close();
769
+ this.provider.close();
651
770
  }
652
771
  }
653
- function gc(db, col) {
654
- const rows = db
655
- .prepare(`SELECT ${col} as key, COUNT(*) as count FROM entries GROUP BY ${col}`)
656
- .all();
772
+ function gc(provider, col) {
773
+ const rows = provider.all(`SELECT ${col} as key, COUNT(*) as count FROM entries GROUP BY ${col}`);
657
774
  return Object.fromEntries(rows.map((r) => [r.key, r.count]));
658
775
  }
659
776
  function rowToEntry(row) {
@@ -670,6 +787,8 @@ function rowToEntry(row) {
670
787
  why: row.why ?? undefined,
671
788
  tags: JSON.parse(row.tags || '[]'),
672
789
  appliesTo: JSON.parse(row.applies_to || '[]'),
790
+ validFrom: row.valid_from ?? undefined,
791
+ validUntil: row.valid_until ?? undefined,
673
792
  };
674
793
  }
675
794
  function rowToSearchResult(row) {