wogiflow 1.0.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 (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,1110 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Memory Database Module
5
+ *
6
+ * Shared database operations for memory storage.
7
+ * Used by both MCP server and CLI tools.
8
+ *
9
+ * Features:
10
+ * - SQLite database with sql.js
11
+ * - Embeddings via @xenova/transformers
12
+ * - Facts, proposals, and PRD storage
13
+ * - Semantic similarity search
14
+ *
15
+ * Part of v1.8.0 - Consolidated memory storage
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ // ============================================================
22
+ // Configuration
23
+ // ============================================================
24
+
25
+ const PROJECT_ROOT = process.env.WOGI_PROJECT_ROOT || process.cwd();
26
+ const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
27
+ const MEMORY_DIR = path.join(WORKFLOW_DIR, 'memory');
28
+ const DB_PATH = path.join(MEMORY_DIR, 'local.db');
29
+
30
+ // ============================================================
31
+ // Database Singleton
32
+ // ============================================================
33
+
34
+ let SQL = null;
35
+ let db = null;
36
+ let embedder = null;
37
+ let initPromise = null;
38
+
39
+ /**
40
+ * Ensure directory exists
41
+ */
42
+ function ensureDir(dirPath) {
43
+ if (!fs.existsSync(dirPath)) {
44
+ fs.mkdirSync(dirPath, { recursive: true });
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Initialize database (singleton)
50
+ */
51
+ async function initDatabase() {
52
+ if (db) return db;
53
+ if (initPromise) return initPromise;
54
+
55
+ initPromise = (async () => {
56
+ ensureDir(MEMORY_DIR);
57
+
58
+ // Initialize sql.js
59
+ if (!SQL) {
60
+ const initSqlJs = require('sql.js');
61
+ SQL = await initSqlJs();
62
+ }
63
+
64
+ // Load existing database or create new
65
+ if (fs.existsSync(DB_PATH)) {
66
+ const buffer = fs.readFileSync(DB_PATH);
67
+ db = new SQL.Database(buffer);
68
+ } else {
69
+ db = new SQL.Database();
70
+ }
71
+
72
+ // Create tables
73
+ db.run(`
74
+ CREATE TABLE IF NOT EXISTS facts (
75
+ id TEXT PRIMARY KEY,
76
+ fact TEXT NOT NULL,
77
+ category TEXT,
78
+ scope TEXT DEFAULT 'local',
79
+ model TEXT,
80
+ embedding TEXT,
81
+ source_context TEXT,
82
+ created_at TEXT DEFAULT (datetime('now')),
83
+ updated_at TEXT DEFAULT (datetime('now')),
84
+ last_accessed TEXT,
85
+ access_count INTEGER DEFAULT 0,
86
+ recall_count INTEGER DEFAULT 0,
87
+ relevance_score REAL DEFAULT 1.0,
88
+ promoted_to TEXT
89
+ )
90
+ `);
91
+
92
+ // Cold storage for demoted facts
93
+ db.run(`
94
+ CREATE TABLE IF NOT EXISTS facts_cold (
95
+ id TEXT PRIMARY KEY,
96
+ fact TEXT NOT NULL,
97
+ category TEXT,
98
+ scope TEXT DEFAULT 'local',
99
+ model TEXT,
100
+ embedding TEXT,
101
+ source_context TEXT,
102
+ created_at TEXT,
103
+ updated_at TEXT,
104
+ last_accessed TEXT,
105
+ access_count INTEGER DEFAULT 0,
106
+ recall_count INTEGER DEFAULT 0,
107
+ relevance_score REAL,
108
+ promoted_to TEXT,
109
+ archived_at TEXT DEFAULT (datetime('now')),
110
+ archive_reason TEXT
111
+ )
112
+ `);
113
+
114
+ // Memory metrics for tracking entropy over time
115
+ db.run(`
116
+ CREATE TABLE IF NOT EXISTS memory_metrics (
117
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
118
+ timestamp TEXT DEFAULT (datetime('now')),
119
+ total_facts INTEGER,
120
+ cold_facts INTEGER,
121
+ entropy_score REAL,
122
+ avg_relevance REAL,
123
+ never_accessed INTEGER,
124
+ action_taken TEXT
125
+ )
126
+ `);
127
+
128
+ db.run(`
129
+ CREATE TABLE IF NOT EXISTS proposals (
130
+ id TEXT PRIMARY KEY,
131
+ rule TEXT NOT NULL,
132
+ category TEXT,
133
+ rationale TEXT,
134
+ source_context TEXT,
135
+ status TEXT DEFAULT 'pending',
136
+ votes TEXT DEFAULT '[]',
137
+ synced INTEGER DEFAULT 0,
138
+ remote_id TEXT,
139
+ created_at TEXT DEFAULT (datetime('now')),
140
+ decided_at TEXT
141
+ )
142
+ `);
143
+
144
+ db.run(`
145
+ CREATE TABLE IF NOT EXISTS prd_chunks (
146
+ id TEXT PRIMARY KEY,
147
+ prd_id TEXT,
148
+ section TEXT,
149
+ content TEXT,
150
+ chunk_type TEXT,
151
+ embedding TEXT,
152
+ file_name TEXT,
153
+ created_at TEXT DEFAULT (datetime('now'))
154
+ )
155
+ `);
156
+
157
+ db.run(`
158
+ CREATE TABLE IF NOT EXISTS sync_state (
159
+ key TEXT PRIMARY KEY,
160
+ value TEXT,
161
+ updated_at TEXT DEFAULT (datetime('now'))
162
+ )
163
+ `);
164
+
165
+ // Migrate existing databases - add new columns if they don't exist
166
+ const migrations = [
167
+ 'ALTER TABLE facts ADD COLUMN last_accessed TEXT',
168
+ 'ALTER TABLE facts ADD COLUMN access_count INTEGER DEFAULT 0',
169
+ 'ALTER TABLE facts ADD COLUMN recall_count INTEGER DEFAULT 0',
170
+ 'ALTER TABLE facts ADD COLUMN relevance_score REAL DEFAULT 1.0',
171
+ 'ALTER TABLE facts ADD COLUMN promoted_to TEXT'
172
+ ];
173
+ for (const migration of migrations) {
174
+ try { db.run(migration); } catch {}
175
+ }
176
+
177
+ // Create indexes
178
+ try { db.run('CREATE INDEX IF NOT EXISTS idx_facts_category ON facts(category)'); } catch {}
179
+ try { db.run('CREATE INDEX IF NOT EXISTS idx_facts_scope ON facts(scope)'); } catch {}
180
+ try { db.run('CREATE INDEX IF NOT EXISTS idx_facts_model ON facts(model)'); } catch {}
181
+ try { db.run('CREATE INDEX IF NOT EXISTS idx_facts_relevance ON facts(relevance_score)'); } catch {}
182
+ try { db.run('CREATE INDEX IF NOT EXISTS idx_facts_accessed ON facts(last_accessed)'); } catch {}
183
+ try { db.run('CREATE INDEX IF NOT EXISTS idx_facts_cold_archived ON facts_cold(archived_at)'); } catch {}
184
+ try { db.run('CREATE INDEX IF NOT EXISTS idx_proposals_status ON proposals(status)'); } catch {}
185
+ try { db.run('CREATE INDEX IF NOT EXISTS idx_prd_prd_id ON prd_chunks(prd_id)'); } catch {}
186
+
187
+ saveDatabase();
188
+ return db;
189
+ })();
190
+
191
+ return initPromise;
192
+ }
193
+
194
+ /**
195
+ * Save database to disk
196
+ */
197
+ function saveDatabase() {
198
+ if (!db) return;
199
+ const data = db.export();
200
+ const buffer = Buffer.from(data);
201
+ fs.writeFileSync(DB_PATH, buffer);
202
+ }
203
+
204
+ /**
205
+ * Close database
206
+ */
207
+ function closeDatabase() {
208
+ if (db) {
209
+ saveDatabase();
210
+ db.close();
211
+ db = null;
212
+ initPromise = null;
213
+ }
214
+ }
215
+
216
+ // ============================================================
217
+ // Embeddings
218
+ // ============================================================
219
+
220
+ // Track if embeddings are available
221
+ let embeddingsAvailable = null; // null = unknown, true/false after first check
222
+
223
+ /**
224
+ * Get embedder (lazy load)
225
+ * Returns null if @xenova/transformers is not installed
226
+ */
227
+ async function getEmbedder() {
228
+ if (embeddingsAvailable === false) return null;
229
+
230
+ if (!embedder) {
231
+ try {
232
+ const { pipeline } = await import('@xenova/transformers');
233
+ embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
234
+ embeddingsAvailable = true;
235
+ } catch (err) {
236
+ if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') {
237
+ embeddingsAvailable = false;
238
+ if (process.env.DEBUG) {
239
+ console.warn('[DEBUG] @xenova/transformers not installed - semantic search disabled');
240
+ }
241
+ return null;
242
+ }
243
+ throw e; // Re-throw other errors
244
+ }
245
+ }
246
+ return embedder;
247
+ }
248
+
249
+ /**
250
+ * Get embedding for text
251
+ * Returns null if embeddings are not available
252
+ */
253
+ async function getEmbedding(text) {
254
+ const embed = await getEmbedder();
255
+ if (!embed) return null;
256
+ const result = await embed(text, { pooling: 'mean', normalize: true });
257
+ return Array.from(result.data);
258
+ }
259
+
260
+ /**
261
+ * Calculate cosine similarity
262
+ */
263
+ function cosineSimilarity(a, b) {
264
+ if (!a || !b || a.length !== b.length) return 0;
265
+ let dotProduct = 0, normA = 0, normB = 0;
266
+ for (let i = 0; i < a.length; i++) {
267
+ dotProduct += a[i] * b[i];
268
+ normA += a[i] * a[i];
269
+ normB += b[i] * b[i];
270
+ }
271
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
272
+ }
273
+
274
+ // ============================================================
275
+ // Utility Functions
276
+ // ============================================================
277
+
278
+ function generateId(prefix) {
279
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
280
+ }
281
+
282
+ function embeddingToJson(embedding) {
283
+ return JSON.stringify(embedding);
284
+ }
285
+
286
+ function jsonToEmbedding(json) {
287
+ try {
288
+ return JSON.parse(json);
289
+ } catch {
290
+ return [];
291
+ }
292
+ }
293
+
294
+ function queryToRows(result) {
295
+ if (!result.length) return [];
296
+ const columns = result[0].columns;
297
+ return result[0].values.map(row => {
298
+ const obj = {};
299
+ columns.forEach((col, i) => { obj[col] = row[i]; });
300
+ return obj;
301
+ });
302
+ }
303
+
304
+ // ============================================================
305
+ // Facts Operations
306
+ // ============================================================
307
+
308
+ /**
309
+ * Store a fact
310
+ */
311
+ async function storeFact({ fact, category, scope, model, sourceContext }) {
312
+ await initDatabase();
313
+ const id = generateId('fact');
314
+ const embedding = await getEmbedding(fact);
315
+
316
+ db.run(`
317
+ INSERT INTO facts (id, fact, category, scope, model, embedding, source_context)
318
+ VALUES (?, ?, ?, ?, ?, ?, ?)
319
+ `, [id, fact, category || 'general', scope || 'local', model || null, embeddingToJson(embedding), sourceContext || null]);
320
+ saveDatabase();
321
+
322
+ return { id, stored: true };
323
+ }
324
+
325
+ /**
326
+ * Search facts by similarity (with access tracking)
327
+ * Falls back to text search if embeddings are not available
328
+ */
329
+ async function searchFacts({ query, category, model, scope, limit = 10, trackAccess = true }) {
330
+ await initDatabase();
331
+ const queryEmbedding = await getEmbedding(query);
332
+
333
+ let sql = 'SELECT * FROM facts WHERE 1=1';
334
+ const params = [];
335
+
336
+ if (category) {
337
+ sql += ' AND category = ?';
338
+ params.push(category);
339
+ }
340
+ if (model) {
341
+ sql += ' AND model = ?';
342
+ params.push(model);
343
+ }
344
+ if (scope) {
345
+ sql += ' AND scope = ?';
346
+ params.push(scope);
347
+ }
348
+
349
+ const result = db.exec(sql, params);
350
+ const facts = queryToRows(result);
351
+
352
+ // Calculate similarity and rank
353
+ let ranked;
354
+ if (queryEmbedding) {
355
+ // Semantic search with embeddings
356
+ ranked = facts.map(f => {
357
+ const embedding = f.embedding ? jsonToEmbedding(f.embedding) : [];
358
+ const similarity = cosineSimilarity(queryEmbedding, embedding);
359
+ return { ...f, similarity, embedding: undefined };
360
+ }).sort((a, b) => b.similarity - a.similarity).slice(0, limit);
361
+ } else {
362
+ // Fallback: simple text matching when embeddings unavailable
363
+ const queryLower = query.toLowerCase();
364
+ const queryWords = queryLower.split(/\s+/).filter(w => w.length > 2);
365
+ ranked = facts.map(f => {
366
+ const factLower = f.fact.toLowerCase();
367
+ // Score based on word matches
368
+ const matches = queryWords.filter(w => factLower.includes(w)).length;
369
+ const similarity = queryWords.length > 0 ? matches / queryWords.length : 0;
370
+ return { ...f, similarity, embedding: undefined };
371
+ }).sort((a, b) => b.similarity - a.similarity).slice(0, limit);
372
+ }
373
+
374
+ // Track access for returned facts (strategic forgetting support)
375
+ if (trackAccess && ranked.length > 0) {
376
+ for (const fact of ranked) {
377
+ // Boost relevance when recalled (max 1.0)
378
+ const newRelevance = Math.min(1.0, (fact.relevance_score || 0.5) + 0.1);
379
+ db.run(`
380
+ UPDATE facts SET
381
+ last_accessed = datetime('now'),
382
+ access_count = COALESCE(access_count, 0) + 1,
383
+ recall_count = COALESCE(recall_count, 0) + 1,
384
+ relevance_score = ?
385
+ WHERE id = ?
386
+ `, [newRelevance, fact.id]);
387
+ }
388
+ saveDatabase();
389
+ }
390
+
391
+ return ranked.map(({ id, fact, category, scope, model, similarity, created_at, relevance_score, access_count }) => ({
392
+ id, fact, category, scope, model,
393
+ relevance: Math.round(similarity * 100),
394
+ storedRelevance: Math.round((relevance_score || 1.0) * 100),
395
+ accessCount: access_count || 0,
396
+ createdAt: created_at
397
+ }));
398
+ }
399
+
400
+ /**
401
+ * Delete a fact
402
+ */
403
+ async function deleteFact(factId) {
404
+ await initDatabase();
405
+ db.run('DELETE FROM facts WHERE id = ?', [factId]);
406
+ const changes = db.getRowsModified();
407
+ saveDatabase();
408
+ return { deleted: changes > 0 };
409
+ }
410
+
411
+ /**
412
+ * Get all facts (for export/sync)
413
+ */
414
+ async function getAllFacts({ scope } = {}) {
415
+ await initDatabase();
416
+ let sql = 'SELECT id, fact, category, scope, model, source_context, created_at FROM facts';
417
+ const params = [];
418
+ if (scope) {
419
+ sql += ' WHERE scope = ?';
420
+ params.push(scope);
421
+ }
422
+ const result = db.exec(sql, params);
423
+ return queryToRows(result);
424
+ }
425
+
426
+ // ============================================================
427
+ // Proposals Operations
428
+ // ============================================================
429
+
430
+ /**
431
+ * Create a proposal
432
+ */
433
+ async function createProposal({ rule, category, rationale, sourceContext }) {
434
+ await initDatabase();
435
+ const id = generateId('proposal');
436
+
437
+ db.run(`
438
+ INSERT INTO proposals (id, rule, category, rationale, source_context)
439
+ VALUES (?, ?, ?, ?, ?)
440
+ `, [id, rule, category || 'pattern', rationale || '', sourceContext || null]);
441
+ saveDatabase();
442
+
443
+ return { id, status: 'pending' };
444
+ }
445
+
446
+ /**
447
+ * Get proposals by status
448
+ */
449
+ async function getProposals(status = 'pending') {
450
+ await initDatabase();
451
+ const result = db.exec(
452
+ `SELECT * FROM proposals WHERE status = ? ORDER BY created_at DESC`,
453
+ [status]
454
+ );
455
+ const proposals = queryToRows(result);
456
+
457
+ return proposals.map(p => ({
458
+ id: p.id,
459
+ rule: p.rule,
460
+ category: p.category,
461
+ rationale: p.rationale,
462
+ sourceContext: p.source_context,
463
+ status: p.status,
464
+ votes: JSON.parse(p.votes || '[]'),
465
+ synced: !!p.synced,
466
+ remoteId: p.remote_id,
467
+ createdAt: p.created_at
468
+ }));
469
+ }
470
+
471
+ /**
472
+ * Update proposal (for sync)
473
+ */
474
+ async function updateProposal(id, updates) {
475
+ await initDatabase();
476
+ const sets = [];
477
+ const params = [];
478
+
479
+ if (updates.status !== undefined) {
480
+ sets.push('status = ?');
481
+ params.push(updates.status);
482
+ }
483
+ if (updates.synced !== undefined) {
484
+ sets.push('synced = ?');
485
+ params.push(updates.synced ? 1 : 0);
486
+ }
487
+ if (updates.remoteId !== undefined) {
488
+ sets.push('remote_id = ?');
489
+ params.push(updates.remoteId);
490
+ }
491
+ if (updates.votes !== undefined) {
492
+ sets.push('votes = ?');
493
+ params.push(JSON.stringify(updates.votes));
494
+ }
495
+
496
+ if (sets.length === 0) return { updated: false };
497
+
498
+ params.push(id);
499
+ db.run(`UPDATE proposals SET ${sets.join(', ')} WHERE id = ?`, params);
500
+ saveDatabase();
501
+
502
+ return { updated: db.getRowsModified() > 0 };
503
+ }
504
+
505
+ /**
506
+ * Get unsynced proposals
507
+ */
508
+ async function getUnsyncedProposals() {
509
+ await initDatabase();
510
+ const result = db.exec('SELECT * FROM proposals WHERE synced = 0 AND status = ?', ['pending']);
511
+ return queryToRows(result);
512
+ }
513
+
514
+ // ============================================================
515
+ // PRD Operations
516
+ // ============================================================
517
+
518
+ /**
519
+ * Detect chunk type
520
+ */
521
+ function detectChunkType(content) {
522
+ if (/^[-*]\s/m.test(content)) return 'list';
523
+ if (/acceptance criteria|given.*when.*then/i.test(content)) return 'criteria';
524
+ if (/constraint|must not|required|shall not|shall be/i.test(content)) return 'constraint';
525
+ if (/goal|objective|purpose|aim|target/i.test(content)) return 'goal';
526
+ if (/api|endpoint|database|schema|interface|component/i.test(content)) return 'technical';
527
+ return 'description';
528
+ }
529
+
530
+ /**
531
+ * Chunk PRD content
532
+ */
533
+ function chunkPRD(content, options = {}) {
534
+ const { chunkSize = 500 } = options;
535
+ const chunks = [];
536
+ const sections = content.split(/(?=^##\s+)/m);
537
+
538
+ for (const section of sections) {
539
+ if (!section.trim()) continue;
540
+
541
+ const titleMatch = section.match(/^(#{2,3})\s+(.+)/m);
542
+ const sectionTitle = titleMatch ? titleMatch[2].trim() : 'Introduction';
543
+ const sectionContent = titleMatch
544
+ ? section.slice(titleMatch[0].length).trim()
545
+ : section.trim();
546
+
547
+ const paragraphs = sectionContent.split(/\n\n+/);
548
+
549
+ for (const para of paragraphs) {
550
+ const trimmed = para.trim();
551
+ if (!trimmed || trimmed.length < 30) continue;
552
+
553
+ const type = detectChunkType(trimmed);
554
+
555
+ if (trimmed.length > chunkSize) {
556
+ // Split by sentences
557
+ const sentences = trimmed.split(/(?<=[.!?])\s+/);
558
+ let current = '';
559
+
560
+ for (const sentence of sentences) {
561
+ if (current.length + sentence.length > chunkSize && current.length > 30) {
562
+ chunks.push({ section: sectionTitle, content: current.trim(), type });
563
+ current = sentence;
564
+ } else {
565
+ current += (current ? ' ' : '') + sentence;
566
+ }
567
+ }
568
+ if (current.length > 30) {
569
+ chunks.push({ section: sectionTitle, content: current.trim(), type });
570
+ }
571
+ } else {
572
+ chunks.push({ section: sectionTitle, content: trimmed, type });
573
+ }
574
+ }
575
+ }
576
+
577
+ return chunks;
578
+ }
579
+
580
+ /**
581
+ * Store PRD chunks
582
+ */
583
+ async function storePRD({ content, prdId, fileName }) {
584
+ await initDatabase();
585
+ const chunks = chunkPRD(content);
586
+ const storedChunks = [];
587
+
588
+ // Remove old chunks for this PRD
589
+ db.run('DELETE FROM prd_chunks WHERE prd_id = ?', [prdId]);
590
+
591
+ for (const chunk of chunks) {
592
+ const id = generateId('prd');
593
+ const embedding = await getEmbedding(chunk.content);
594
+
595
+ db.run(`
596
+ INSERT INTO prd_chunks (id, prd_id, section, content, chunk_type, embedding, file_name)
597
+ VALUES (?, ?, ?, ?, ?, ?, ?)
598
+ `, [id, prdId, chunk.section, chunk.content, chunk.type, embeddingToJson(embedding), fileName || null]);
599
+
600
+ storedChunks.push({ id, section: chunk.section, type: chunk.type });
601
+ }
602
+
603
+ saveDatabase();
604
+
605
+ return {
606
+ prdId,
607
+ chunkCount: storedChunks.length,
608
+ sections: [...new Set(storedChunks.map(c => c.section))]
609
+ };
610
+ }
611
+
612
+ /**
613
+ * Get PRD context for a task
614
+ * Falls back to text search if embeddings are not available
615
+ */
616
+ async function getPRDContext({ query, maxTokens = 2000, prdId }) {
617
+ await initDatabase();
618
+ const queryEmbedding = await getEmbedding(query);
619
+
620
+ let sql = 'SELECT * FROM prd_chunks';
621
+ const params = [];
622
+ if (prdId) {
623
+ sql += ' WHERE prd_id = ?';
624
+ params.push(prdId);
625
+ }
626
+
627
+ const result = db.exec(sql, params);
628
+ const chunks = queryToRows(result);
629
+
630
+ if (chunks.length === 0) return null;
631
+
632
+ // Calculate similarity and rank
633
+ let ranked;
634
+ if (queryEmbedding) {
635
+ // Semantic search with embeddings
636
+ ranked = chunks.map(c => {
637
+ const embedding = c.embedding ? jsonToEmbedding(c.embedding) : [];
638
+ const similarity = cosineSimilarity(queryEmbedding, embedding);
639
+ return { ...c, similarity };
640
+ });
641
+ } else {
642
+ // Fallback: simple text matching when embeddings unavailable
643
+ const queryLower = query.toLowerCase();
644
+ const queryWords = queryLower.split(/\s+/).filter(w => w.length > 2);
645
+ ranked = chunks.map(c => {
646
+ const contentLower = c.content.toLowerCase();
647
+ const matches = queryWords.filter(w => contentLower.includes(w)).length;
648
+ const similarity = queryWords.length > 0 ? matches / queryWords.length : 0;
649
+ return { ...c, similarity };
650
+ });
651
+ }
652
+
653
+ // Sort by similarity, then by type priority
654
+ const typePriority = { constraint: 0, criteria: 1, goal: 2, technical: 3, description: 4, list: 5 };
655
+ ranked.sort((a, b) => {
656
+ if (Math.abs(a.similarity - b.similarity) > 0.1) return b.similarity - a.similarity;
657
+ return (typePriority[a.chunk_type] || 99) - (typePriority[b.chunk_type] || 99);
658
+ });
659
+
660
+ // Build context within token limit
661
+ let context = '## Relevant PRD Context\n\n';
662
+ let charCount = context.length;
663
+ const maxChars = maxTokens * 4;
664
+ const includedSections = new Set();
665
+
666
+ for (const chunk of ranked) {
667
+ if (chunk.similarity < 0.1 && includedSections.size >= 3) continue;
668
+
669
+ const prefix = !includedSections.has(chunk.section) ? `### ${chunk.section}\n` : '';
670
+ const text = prefix + chunk.content + '\n\n';
671
+
672
+ if (charCount + text.length > maxChars) break;
673
+
674
+ if (prefix) includedSections.add(chunk.section);
675
+ context += text;
676
+ charCount += text.length;
677
+ }
678
+
679
+ return {
680
+ context: context.trim(),
681
+ topRelevance: ranked[0] ? Math.round(ranked[0].similarity * 100) : 0
682
+ };
683
+ }
684
+
685
+ /**
686
+ * List stored PRDs
687
+ */
688
+ async function listPRDs() {
689
+ await initDatabase();
690
+ const result = db.exec(`
691
+ SELECT prd_id, file_name, COUNT(*) as chunk_count, MIN(created_at) as created_at
692
+ FROM prd_chunks
693
+ GROUP BY prd_id
694
+ `);
695
+ return queryToRows(result);
696
+ }
697
+
698
+ /**
699
+ * Delete a PRD
700
+ */
701
+ async function deletePRD(prdId) {
702
+ await initDatabase();
703
+ db.run('DELETE FROM prd_chunks WHERE prd_id = ?', [prdId]);
704
+ const changes = db.getRowsModified();
705
+ saveDatabase();
706
+ return { deleted: changes > 0 };
707
+ }
708
+
709
+ /**
710
+ * Clear all PRDs
711
+ */
712
+ async function clearPRDs() {
713
+ await initDatabase();
714
+ db.run('DELETE FROM prd_chunks');
715
+ saveDatabase();
716
+ return { cleared: true };
717
+ }
718
+
719
+ // ============================================================
720
+ // Sync State
721
+ // ============================================================
722
+
723
+ async function getSyncState(key) {
724
+ await initDatabase();
725
+ const result = db.exec('SELECT value FROM sync_state WHERE key = ?', [key]);
726
+ const rows = queryToRows(result);
727
+ return rows[0]?.value || null;
728
+ }
729
+
730
+ async function setSyncState(key, value) {
731
+ await initDatabase();
732
+ db.run(`
733
+ INSERT OR REPLACE INTO sync_state (key, value, updated_at)
734
+ VALUES (?, ?, datetime('now'))
735
+ `, [key, value]);
736
+ saveDatabase();
737
+ }
738
+
739
+ // ============================================================
740
+ // Statistics
741
+ // ============================================================
742
+
743
+ async function getStats() {
744
+ await initDatabase();
745
+
746
+ function count(sql, params = []) {
747
+ const result = db.exec(sql, params);
748
+ if (!result.length || !result[0].values.length) return 0;
749
+ return result[0].values[0][0];
750
+ }
751
+
752
+ function grouped(sql) {
753
+ const result = db.exec(sql);
754
+ if (!result.length) return {};
755
+ return Object.fromEntries(result[0].values.map(row => [row[0] || 'null', row[1]]));
756
+ }
757
+
758
+ return {
759
+ facts: {
760
+ total: count('SELECT COUNT(*) FROM facts'),
761
+ byCategory: grouped('SELECT category, COUNT(*) FROM facts GROUP BY category'),
762
+ byScope: grouped('SELECT scope, COUNT(*) FROM facts GROUP BY scope')
763
+ },
764
+ proposals: {
765
+ pending: count('SELECT COUNT(*) FROM proposals WHERE status = ?', ['pending']),
766
+ total: count('SELECT COUNT(*) FROM proposals')
767
+ },
768
+ prds: {
769
+ total: count('SELECT COUNT(DISTINCT prd_id) FROM prd_chunks'),
770
+ chunks: count('SELECT COUNT(*) FROM prd_chunks')
771
+ }
772
+ };
773
+ }
774
+
775
+ // ============================================================
776
+ // Strategic Forgetting & Entropy
777
+ // ============================================================
778
+
779
+ /**
780
+ * Get entropy statistics for memory health
781
+ */
782
+ async function getEntropyStats(config = {}) {
783
+ await initDatabase();
784
+ const maxFacts = config.maxLocalFacts || 1000;
785
+
786
+ function count(sql, params = []) {
787
+ const result = db.exec(sql, params);
788
+ if (!result.length || !result[0].values.length) return 0;
789
+ return result[0].values[0][0];
790
+ }
791
+
792
+ function avg(sql) {
793
+ const result = db.exec(sql);
794
+ if (!result.length || !result[0].values.length || result[0].values[0][0] === null) return 0;
795
+ return result[0].values[0][0];
796
+ }
797
+
798
+ const totalFacts = count('SELECT COUNT(*) FROM facts');
799
+ const coldFacts = count('SELECT COUNT(*) FROM facts_cold');
800
+ const neverAccessed = count('SELECT COUNT(*) FROM facts WHERE last_accessed IS NULL');
801
+ const avgRelevance = avg('SELECT AVG(relevance_score) FROM facts');
802
+ const avgAgeDays = avg(`
803
+ SELECT AVG(julianday('now') - julianday(created_at))
804
+ FROM facts
805
+ `);
806
+ const lowRelevanceCount = count('SELECT COUNT(*) FROM facts WHERE relevance_score < 0.3');
807
+
808
+ // Calculate entropy score (0-1, higher = needs cleanup)
809
+ const capacityRatio = Math.min(1, totalFacts / maxFacts);
810
+ const ageRatio = Math.min(1, avgAgeDays / 30);
811
+ const neverAccessedRatio = totalFacts > 0 ? neverAccessed / totalFacts : 0;
812
+ const lowRelevanceRatio = totalFacts > 0 ? lowRelevanceCount / totalFacts : 0;
813
+
814
+ const entropy = (
815
+ capacityRatio * 0.3 +
816
+ ageRatio * 0.2 +
817
+ neverAccessedRatio * 0.25 +
818
+ lowRelevanceRatio * 0.25
819
+ );
820
+
821
+ return {
822
+ totalFacts,
823
+ coldFacts,
824
+ maxFacts,
825
+ neverAccessed,
826
+ avgRelevance: Math.round(avgRelevance * 100) / 100,
827
+ avgAgeDays: Math.round(avgAgeDays * 10) / 10,
828
+ lowRelevanceCount,
829
+ entropy: Math.round(entropy * 1000) / 1000,
830
+ needsCompaction: entropy > 0.7,
831
+ status: entropy < 0.4 ? 'healthy' : entropy < 0.7 ? 'moderate' : 'needs_cleanup'
832
+ };
833
+ }
834
+
835
+ /**
836
+ * Apply relevance decay to facts (run daily or on session end)
837
+ */
838
+ async function applyRelevanceDecay(config = {}) {
839
+ await initDatabase();
840
+ const decayRate = config.decayRate || 0.033; // ~1/30, decay over 30 days
841
+ const neverAccessedPenalty = config.neverAccessedPenalty || 0.1;
842
+
843
+ // Decay facts based on time since last access
844
+ db.run(`
845
+ UPDATE facts SET
846
+ relevance_score = MAX(0.1, relevance_score * (1.0 - ? * (julianday('now') - julianday(COALESCE(last_accessed, created_at)))))
847
+ WHERE last_accessed IS NOT NULL
848
+ `, [decayRate]);
849
+
850
+ // Faster decay for never-accessed facts (older than 7 days)
851
+ db.run(`
852
+ UPDATE facts SET
853
+ relevance_score = MAX(0.1, relevance_score - ?)
854
+ WHERE last_accessed IS NULL
855
+ AND julianday('now') - julianday(created_at) > 7
856
+ `, [neverAccessedPenalty]);
857
+
858
+ const changes = db.getRowsModified();
859
+ saveDatabase();
860
+
861
+ return { decayed: changes };
862
+ }
863
+
864
+ /**
865
+ * Demote low-relevance facts to cold storage
866
+ */
867
+ async function demoteToColdStorage(config = {}) {
868
+ await initDatabase();
869
+ const relevanceThreshold = config.relevanceThreshold || 0.3;
870
+
871
+ // Find facts to demote (low relevance, not promoted anywhere)
872
+ const result = db.exec(`
873
+ SELECT * FROM facts
874
+ WHERE relevance_score < ?
875
+ AND (promoted_to IS NULL OR promoted_to = '')
876
+ `, [relevanceThreshold]);
877
+ const toDemote = queryToRows(result);
878
+
879
+ let demoted = 0;
880
+ for (const fact of toDemote) {
881
+ // Insert into cold storage
882
+ db.run(`
883
+ INSERT INTO facts_cold (id, fact, category, scope, model, embedding, source_context,
884
+ created_at, updated_at, last_accessed, access_count, recall_count,
885
+ relevance_score, promoted_to, archived_at, archive_reason)
886
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), 'low_relevance')
887
+ `, [fact.id, fact.fact, fact.category, fact.scope, fact.model, fact.embedding,
888
+ fact.source_context, fact.created_at, fact.updated_at, fact.last_accessed,
889
+ fact.access_count, fact.recall_count, fact.relevance_score, fact.promoted_to]);
890
+
891
+ // Delete from active facts
892
+ db.run('DELETE FROM facts WHERE id = ?', [fact.id]);
893
+ demoted++;
894
+ }
895
+
896
+ saveDatabase();
897
+ return { demoted };
898
+ }
899
+
900
+ /**
901
+ * Purge old facts from cold storage
902
+ */
903
+ async function purgeColdFacts(config = {}) {
904
+ await initDatabase();
905
+ const retentionDays = config.coldRetentionDays || 90;
906
+
907
+ db.run(`
908
+ DELETE FROM facts_cold
909
+ WHERE julianday('now') - julianday(archived_at) > ?
910
+ `, [retentionDays]);
911
+
912
+ const purged = db.getRowsModified();
913
+ saveDatabase();
914
+
915
+ return { purged };
916
+ }
917
+
918
+ /**
919
+ * Find and merge similar facts (deduplication)
920
+ */
921
+ async function mergeSimilarFacts(config = {}) {
922
+ await initDatabase();
923
+ const similarityThreshold = config.mergeSimilarityThreshold || 0.95;
924
+
925
+ const result = db.exec('SELECT id, fact, embedding, relevance_score FROM facts');
926
+ const facts = queryToRows(result);
927
+
928
+ const merged = [];
929
+ const toDelete = new Set();
930
+
931
+ for (let i = 0; i < facts.length; i++) {
932
+ if (toDelete.has(facts[i].id)) continue;
933
+
934
+ const embeddingA = facts[i].embedding ? jsonToEmbedding(facts[i].embedding) : [];
935
+ if (embeddingA.length === 0) continue;
936
+
937
+ for (let j = i + 1; j < facts.length; j++) {
938
+ if (toDelete.has(facts[j].id)) continue;
939
+
940
+ const embeddingB = facts[j].embedding ? jsonToEmbedding(facts[j].embedding) : [];
941
+ if (embeddingB.length === 0) continue;
942
+
943
+ const similarity = cosineSimilarity(embeddingA, embeddingB);
944
+ if (similarity >= similarityThreshold) {
945
+ // Keep the one with higher relevance, delete the other
946
+ const keepId = facts[i].relevance_score >= facts[j].relevance_score ? facts[i].id : facts[j].id;
947
+ const deleteId = keepId === facts[i].id ? facts[j].id : facts[i].id;
948
+
949
+ toDelete.add(deleteId);
950
+ merged.push({ kept: keepId, deleted: deleteId, similarity });
951
+ }
952
+ }
953
+ }
954
+
955
+ // Delete duplicates
956
+ for (const id of toDelete) {
957
+ db.run('DELETE FROM facts WHERE id = ?', [id]);
958
+ }
959
+
960
+ if (toDelete.size > 0) saveDatabase();
961
+
962
+ return { merged: merged.length, details: merged };
963
+ }
964
+
965
+ /**
966
+ * Record entropy metric for tracking over time
967
+ */
968
+ async function recordMemoryMetric(action = null) {
969
+ await initDatabase();
970
+ const stats = await getEntropyStats();
971
+
972
+ db.run(`
973
+ INSERT INTO memory_metrics (total_facts, cold_facts, entropy_score, avg_relevance, never_accessed, action_taken)
974
+ VALUES (?, ?, ?, ?, ?, ?)
975
+ `, [stats.totalFacts, stats.coldFacts, stats.entropy, stats.avgRelevance, stats.neverAccessed, action]);
976
+
977
+ saveDatabase();
978
+ return stats;
979
+ }
980
+
981
+ /**
982
+ * Get memory metrics history
983
+ */
984
+ async function getMemoryMetrics(limit = 30) {
985
+ await initDatabase();
986
+ const result = db.exec(`
987
+ SELECT * FROM memory_metrics
988
+ ORDER BY timestamp DESC
989
+ LIMIT ?
990
+ `, [limit]);
991
+ return queryToRows(result);
992
+ }
993
+
994
+ /**
995
+ * Mark a fact as promoted (to decisions.md, etc.)
996
+ */
997
+ async function markFactPromoted(factId, destination) {
998
+ await initDatabase();
999
+ db.run(`
1000
+ UPDATE facts SET promoted_to = ?, updated_at = datetime('now')
1001
+ WHERE id = ?
1002
+ `, [destination, factId]);
1003
+ saveDatabase();
1004
+ return { marked: db.getRowsModified() > 0 };
1005
+ }
1006
+
1007
+ /**
1008
+ * Get facts that are candidates for promotion (high relevance, frequently accessed)
1009
+ */
1010
+ async function getPromotionCandidates(config = {}) {
1011
+ await initDatabase();
1012
+ const minRelevance = config.minRelevance || 0.8;
1013
+ const minAccessCount = config.minAccessCount || 3;
1014
+
1015
+ const result = db.exec(`
1016
+ SELECT * FROM facts
1017
+ WHERE relevance_score >= ?
1018
+ AND access_count >= ?
1019
+ AND (promoted_to IS NULL OR promoted_to = '')
1020
+ ORDER BY relevance_score DESC, access_count DESC
1021
+ `, [minRelevance, minAccessCount]);
1022
+
1023
+ return queryToRows(result);
1024
+ }
1025
+
1026
+ /**
1027
+ * Restore a fact from cold storage
1028
+ */
1029
+ async function restoreFromColdStorage(factId) {
1030
+ await initDatabase();
1031
+
1032
+ // Find in cold storage
1033
+ const result = db.exec('SELECT * FROM facts_cold WHERE id = ?', [factId]);
1034
+ const facts = queryToRows(result);
1035
+ if (facts.length === 0) return { restored: false, error: 'Fact not found in cold storage' };
1036
+
1037
+ const fact = facts[0];
1038
+
1039
+ // Insert back into active facts with boosted relevance
1040
+ db.run(`
1041
+ INSERT INTO facts (id, fact, category, scope, model, embedding, source_context,
1042
+ created_at, updated_at, last_accessed, access_count, recall_count, relevance_score, promoted_to)
1043
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'), ?, ?, 0.5, ?)
1044
+ `, [fact.id, fact.fact, fact.category, fact.scope, fact.model, fact.embedding,
1045
+ fact.source_context, fact.created_at, fact.access_count, fact.recall_count, fact.promoted_to]);
1046
+
1047
+ // Remove from cold storage
1048
+ db.run('DELETE FROM facts_cold WHERE id = ?', [factId]);
1049
+
1050
+ saveDatabase();
1051
+ return { restored: true };
1052
+ }
1053
+
1054
+ // ============================================================
1055
+ // Exports
1056
+ // ============================================================
1057
+
1058
+ module.exports = {
1059
+ // Database management
1060
+ initDatabase,
1061
+ saveDatabase,
1062
+ closeDatabase,
1063
+
1064
+ // Embeddings
1065
+ getEmbedding,
1066
+ cosineSimilarity,
1067
+
1068
+ // Facts
1069
+ storeFact,
1070
+ searchFacts,
1071
+ deleteFact,
1072
+ getAllFacts,
1073
+
1074
+ // Proposals
1075
+ createProposal,
1076
+ getProposals,
1077
+ updateProposal,
1078
+ getUnsyncedProposals,
1079
+
1080
+ // PRDs
1081
+ chunkPRD,
1082
+ storePRD,
1083
+ getPRDContext,
1084
+ listPRDs,
1085
+ deletePRD,
1086
+ clearPRDs,
1087
+
1088
+ // Sync
1089
+ getSyncState,
1090
+ setSyncState,
1091
+
1092
+ // Stats
1093
+ getStats,
1094
+
1095
+ // Strategic Forgetting & Entropy
1096
+ getEntropyStats,
1097
+ applyRelevanceDecay,
1098
+ demoteToColdStorage,
1099
+ purgeColdFacts,
1100
+ mergeSimilarFacts,
1101
+ recordMemoryMetric,
1102
+ getMemoryMetrics,
1103
+ markFactPromoted,
1104
+ getPromotionCandidates,
1105
+ restoreFromColdStorage,
1106
+
1107
+ // Paths
1108
+ DB_PATH,
1109
+ MEMORY_DIR
1110
+ };