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.
- package/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- 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
|
+
};
|