@soleri/core 2.0.2 → 2.1.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/dist/brain/brain.d.ts +12 -50
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +147 -12
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts +51 -0
- package/dist/brain/intelligence.d.ts.map +1 -0
- package/dist/brain/intelligence.js +666 -0
- package/dist/brain/intelligence.js.map +1 -0
- package/dist/brain/types.d.ts +165 -0
- package/dist/brain/types.d.ts.map +1 -0
- package/dist/brain/types.js +2 -0
- package/dist/brain/types.js.map +1 -0
- package/dist/cognee/client.d.ts +35 -0
- package/dist/cognee/client.d.ts.map +1 -0
- package/dist/cognee/client.js +291 -0
- package/dist/cognee/client.js.map +1 -0
- package/dist/cognee/types.d.ts +46 -0
- package/dist/cognee/types.d.ts.map +1 -0
- package/dist/cognee/types.js +3 -0
- package/dist/cognee/types.js.map +1 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +7 -5
- package/dist/curator/curator.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/llm-client.d.ts.map +1 -1
- package/dist/llm/llm-client.js +9 -2
- package/dist/llm/llm-client.js.map +1 -1
- package/dist/runtime/core-ops.d.ts +3 -3
- package/dist/runtime/core-ops.d.ts.map +1 -1
- package/dist/runtime/core-ops.js +180 -15
- package/dist/runtime/core-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +4 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/brain-intelligence.test.ts +623 -0
- package/src/__tests__/brain.test.ts +265 -27
- package/src/__tests__/cognee-client.test.ts +524 -0
- package/src/__tests__/core-ops.test.ts +77 -49
- package/src/__tests__/curator.test.ts +126 -31
- package/src/__tests__/domain-ops.test.ts +45 -9
- package/src/__tests__/runtime.test.ts +13 -11
- package/src/brain/brain.ts +194 -65
- package/src/brain/intelligence.ts +1061 -0
- package/src/brain/types.ts +176 -0
- package/src/cognee/client.ts +352 -0
- package/src/cognee/types.ts +62 -0
- package/src/curator/curator.ts +52 -15
- package/src/index.ts +26 -1
- package/src/llm/llm-client.ts +18 -24
- package/src/runtime/core-ops.ts +219 -26
- package/src/runtime/runtime.ts +5 -0
- package/src/runtime/types.ts +2 -0
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain Intelligence — pattern strength scoring, session knowledge extraction,
|
|
3
|
+
* and cross-domain intelligence pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Follows the Curator pattern: separate class, own SQLite tables,
|
|
6
|
+
* takes Vault + Brain as constructor deps.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import type { Vault } from '../vault/vault.js';
|
|
11
|
+
import type { Brain } from './brain.js';
|
|
12
|
+
import type {
|
|
13
|
+
PatternStrength,
|
|
14
|
+
StrengthsQuery,
|
|
15
|
+
BrainSession,
|
|
16
|
+
SessionLifecycleInput,
|
|
17
|
+
KnowledgeProposal,
|
|
18
|
+
ExtractionResult,
|
|
19
|
+
GlobalPattern,
|
|
20
|
+
DomainProfile,
|
|
21
|
+
BuildIntelligenceResult,
|
|
22
|
+
BrainIntelligenceStats,
|
|
23
|
+
SessionContext,
|
|
24
|
+
BrainExportData,
|
|
25
|
+
BrainImportResult,
|
|
26
|
+
} from './types.js';
|
|
27
|
+
|
|
28
|
+
// ─── Constants ──────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const USAGE_MAX = 10;
|
|
31
|
+
const SPREAD_MAX = 5;
|
|
32
|
+
const RECENCY_DECAY_DAYS = 30;
|
|
33
|
+
const EXTRACTION_TOOL_THRESHOLD = 3;
|
|
34
|
+
const EXTRACTION_FILE_THRESHOLD = 3;
|
|
35
|
+
const EXTRACTION_LONG_SESSION_MINUTES = 30;
|
|
36
|
+
const EXTRACTION_HIGH_FEEDBACK_RATIO = 0.8;
|
|
37
|
+
|
|
38
|
+
// ─── Class ──────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export class BrainIntelligence {
|
|
41
|
+
private vault: Vault;
|
|
42
|
+
private brain: Brain;
|
|
43
|
+
|
|
44
|
+
constructor(vault: Vault, brain: Brain) {
|
|
45
|
+
this.vault = vault;
|
|
46
|
+
this.brain = brain;
|
|
47
|
+
this.initializeTables();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Table Initialization ─────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
private initializeTables(): void {
|
|
53
|
+
const db = this.vault.getDb();
|
|
54
|
+
db.exec(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS brain_strengths (
|
|
56
|
+
pattern TEXT NOT NULL,
|
|
57
|
+
domain TEXT NOT NULL,
|
|
58
|
+
strength REAL NOT NULL DEFAULT 0,
|
|
59
|
+
usage_score REAL NOT NULL DEFAULT 0,
|
|
60
|
+
spread_score REAL NOT NULL DEFAULT 0,
|
|
61
|
+
success_score REAL NOT NULL DEFAULT 0,
|
|
62
|
+
recency_score REAL NOT NULL DEFAULT 0,
|
|
63
|
+
usage_count INTEGER NOT NULL DEFAULT 0,
|
|
64
|
+
unique_contexts INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
success_rate REAL NOT NULL DEFAULT 0,
|
|
66
|
+
last_used TEXT NOT NULL,
|
|
67
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
68
|
+
PRIMARY KEY (pattern, domain)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS brain_sessions (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
74
|
+
ended_at TEXT,
|
|
75
|
+
domain TEXT,
|
|
76
|
+
context TEXT,
|
|
77
|
+
tools_used TEXT NOT NULL DEFAULT '[]',
|
|
78
|
+
files_modified TEXT NOT NULL DEFAULT '[]',
|
|
79
|
+
plan_id TEXT,
|
|
80
|
+
plan_outcome TEXT
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
CREATE TABLE IF NOT EXISTS brain_proposals (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
session_id TEXT NOT NULL,
|
|
86
|
+
rule TEXT NOT NULL,
|
|
87
|
+
type TEXT NOT NULL DEFAULT 'pattern',
|
|
88
|
+
title TEXT NOT NULL,
|
|
89
|
+
description TEXT NOT NULL,
|
|
90
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
91
|
+
promoted INTEGER NOT NULL DEFAULT 0,
|
|
92
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
93
|
+
FOREIGN KEY (session_id) REFERENCES brain_sessions(id)
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS brain_global_registry (
|
|
97
|
+
pattern TEXT PRIMARY KEY,
|
|
98
|
+
domains TEXT NOT NULL DEFAULT '[]',
|
|
99
|
+
total_strength REAL NOT NULL DEFAULT 0,
|
|
100
|
+
avg_strength REAL NOT NULL DEFAULT 0,
|
|
101
|
+
domain_count INTEGER NOT NULL DEFAULT 0,
|
|
102
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE TABLE IF NOT EXISTS brain_domain_profiles (
|
|
106
|
+
domain TEXT PRIMARY KEY,
|
|
107
|
+
top_patterns TEXT NOT NULL DEFAULT '[]',
|
|
108
|
+
session_count INTEGER NOT NULL DEFAULT 0,
|
|
109
|
+
avg_session_duration REAL NOT NULL DEFAULT 0,
|
|
110
|
+
last_activity TEXT NOT NULL DEFAULT (datetime('now')),
|
|
111
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
112
|
+
);
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Session Lifecycle ────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
lifecycle(input: SessionLifecycleInput): BrainSession {
|
|
119
|
+
const db = this.vault.getDb();
|
|
120
|
+
|
|
121
|
+
if (input.action === 'start') {
|
|
122
|
+
const id = input.sessionId ?? randomUUID();
|
|
123
|
+
db.prepare(
|
|
124
|
+
`INSERT INTO brain_sessions (id, domain, context, tools_used, files_modified, plan_id)
|
|
125
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
126
|
+
).run(
|
|
127
|
+
id,
|
|
128
|
+
input.domain ?? null,
|
|
129
|
+
input.context ?? null,
|
|
130
|
+
JSON.stringify(input.toolsUsed ?? []),
|
|
131
|
+
JSON.stringify(input.filesModified ?? []),
|
|
132
|
+
input.planId ?? null,
|
|
133
|
+
);
|
|
134
|
+
return this.getSession(id)!;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// action === 'end'
|
|
138
|
+
const sessionId = input.sessionId;
|
|
139
|
+
if (!sessionId) throw new Error('sessionId required for end action');
|
|
140
|
+
|
|
141
|
+
const updates: string[] = ["ended_at = datetime('now')"];
|
|
142
|
+
const values: unknown[] = [];
|
|
143
|
+
|
|
144
|
+
if (input.toolsUsed) {
|
|
145
|
+
updates.push('tools_used = ?');
|
|
146
|
+
values.push(JSON.stringify(input.toolsUsed));
|
|
147
|
+
}
|
|
148
|
+
if (input.filesModified) {
|
|
149
|
+
updates.push('files_modified = ?');
|
|
150
|
+
values.push(JSON.stringify(input.filesModified));
|
|
151
|
+
}
|
|
152
|
+
if (input.planId) {
|
|
153
|
+
updates.push('plan_id = ?');
|
|
154
|
+
values.push(input.planId);
|
|
155
|
+
}
|
|
156
|
+
if (input.planOutcome) {
|
|
157
|
+
updates.push('plan_outcome = ?');
|
|
158
|
+
values.push(input.planOutcome);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
values.push(sessionId);
|
|
162
|
+
db.prepare(`UPDATE brain_sessions SET ${updates.join(', ')} WHERE id = ?`).run(...values);
|
|
163
|
+
|
|
164
|
+
return this.getSession(sessionId)!;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getSessionContext(limit = 10): SessionContext {
|
|
168
|
+
const db = this.vault.getDb();
|
|
169
|
+
|
|
170
|
+
const rows = db
|
|
171
|
+
.prepare('SELECT * FROM brain_sessions ORDER BY started_at DESC LIMIT ?')
|
|
172
|
+
.all(limit) as Array<{
|
|
173
|
+
id: string;
|
|
174
|
+
started_at: string;
|
|
175
|
+
ended_at: string | null;
|
|
176
|
+
domain: string | null;
|
|
177
|
+
context: string | null;
|
|
178
|
+
tools_used: string;
|
|
179
|
+
files_modified: string;
|
|
180
|
+
plan_id: string | null;
|
|
181
|
+
plan_outcome: string | null;
|
|
182
|
+
}>;
|
|
183
|
+
|
|
184
|
+
const sessions = rows.map((r) => this.rowToSession(r));
|
|
185
|
+
|
|
186
|
+
// Aggregate tool frequency
|
|
187
|
+
const toolCounts = new Map<string, number>();
|
|
188
|
+
const fileCounts = new Map<string, number>();
|
|
189
|
+
for (const s of sessions) {
|
|
190
|
+
for (const t of s.toolsUsed) {
|
|
191
|
+
toolCounts.set(t, (toolCounts.get(t) ?? 0) + 1);
|
|
192
|
+
}
|
|
193
|
+
for (const f of s.filesModified) {
|
|
194
|
+
fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const toolFrequency = [...toolCounts.entries()]
|
|
199
|
+
.map(([tool, count]) => ({ tool, count }))
|
|
200
|
+
.sort((a, b) => b.count - a.count);
|
|
201
|
+
|
|
202
|
+
const fileFrequency = [...fileCounts.entries()]
|
|
203
|
+
.map(([file, count]) => ({ file, count }))
|
|
204
|
+
.sort((a, b) => b.count - a.count);
|
|
205
|
+
|
|
206
|
+
return { recentSessions: sessions, toolFrequency, fileFrequency };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
archiveSessions(olderThanDays = 30): { archived: number } {
|
|
210
|
+
const db = this.vault.getDb();
|
|
211
|
+
const result = db
|
|
212
|
+
.prepare(
|
|
213
|
+
`DELETE FROM brain_sessions
|
|
214
|
+
WHERE ended_at IS NOT NULL
|
|
215
|
+
AND started_at < datetime('now', '-' || ? || ' days')`,
|
|
216
|
+
)
|
|
217
|
+
.run(olderThanDays);
|
|
218
|
+
return { archived: result.changes };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── Strength Scoring ─────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
computeStrengths(): PatternStrength[] {
|
|
224
|
+
const db = this.vault.getDb();
|
|
225
|
+
|
|
226
|
+
// Gather feedback data grouped by entry_id
|
|
227
|
+
const feedbackRows = db
|
|
228
|
+
.prepare(
|
|
229
|
+
`SELECT entry_id,
|
|
230
|
+
COUNT(*) as total,
|
|
231
|
+
SUM(CASE WHEN action = 'accepted' THEN 1 ELSE 0 END) as accepted,
|
|
232
|
+
SUM(CASE WHEN action = 'dismissed' THEN 1 ELSE 0 END) as dismissed,
|
|
233
|
+
MAX(created_at) as last_used
|
|
234
|
+
FROM brain_feedback
|
|
235
|
+
GROUP BY entry_id`,
|
|
236
|
+
)
|
|
237
|
+
.all() as Array<{
|
|
238
|
+
entry_id: string;
|
|
239
|
+
total: number;
|
|
240
|
+
accepted: number;
|
|
241
|
+
dismissed: number;
|
|
242
|
+
last_used: string;
|
|
243
|
+
}>;
|
|
244
|
+
|
|
245
|
+
// Count unique session domains as spread proxy
|
|
246
|
+
const sessionRows = db
|
|
247
|
+
.prepare('SELECT DISTINCT domain FROM brain_sessions WHERE domain IS NOT NULL')
|
|
248
|
+
.all() as Array<{ domain: string }>;
|
|
249
|
+
const uniqueDomains = new Set(sessionRows.map((r) => r.domain));
|
|
250
|
+
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
const strengths: PatternStrength[] = [];
|
|
253
|
+
|
|
254
|
+
for (const row of feedbackRows) {
|
|
255
|
+
// Look up vault entry for domain info
|
|
256
|
+
const entry = this.vault.get(row.entry_id);
|
|
257
|
+
const domain = entry?.domain ?? 'unknown';
|
|
258
|
+
const pattern = entry?.title ?? row.entry_id;
|
|
259
|
+
|
|
260
|
+
// Usage score: min(25, (count / USAGE_MAX) * 25)
|
|
261
|
+
const usageScore = Math.min(25, (row.total / USAGE_MAX) * 25);
|
|
262
|
+
|
|
263
|
+
// Spread score: use unique domains from sessions as proxy
|
|
264
|
+
const uniqueContexts = Math.min(uniqueDomains.size, 5);
|
|
265
|
+
const spreadScore = Math.min(25, (uniqueContexts / SPREAD_MAX) * 25);
|
|
266
|
+
|
|
267
|
+
// Success score: 25 * successRate
|
|
268
|
+
const successRate = row.total > 0 ? row.accepted / row.total : 0;
|
|
269
|
+
const successScore = 25 * successRate;
|
|
270
|
+
|
|
271
|
+
// Recency score: max(0, 25 * (1 - daysSince / RECENCY_DECAY_DAYS))
|
|
272
|
+
const lastUsedMs = new Date(row.last_used).getTime();
|
|
273
|
+
const daysSince = (now - lastUsedMs) / (1000 * 60 * 60 * 24);
|
|
274
|
+
const recencyScore = Math.max(0, 25 * (1 - daysSince / RECENCY_DECAY_DAYS));
|
|
275
|
+
|
|
276
|
+
const strength = usageScore + spreadScore + successScore + recencyScore;
|
|
277
|
+
|
|
278
|
+
const ps: PatternStrength = {
|
|
279
|
+
pattern,
|
|
280
|
+
domain,
|
|
281
|
+
strength,
|
|
282
|
+
usageScore,
|
|
283
|
+
spreadScore,
|
|
284
|
+
successScore,
|
|
285
|
+
recencyScore,
|
|
286
|
+
usageCount: row.total,
|
|
287
|
+
uniqueContexts,
|
|
288
|
+
successRate,
|
|
289
|
+
lastUsed: row.last_used,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
strengths.push(ps);
|
|
293
|
+
|
|
294
|
+
// Persist
|
|
295
|
+
db.prepare(
|
|
296
|
+
`INSERT OR REPLACE INTO brain_strengths
|
|
297
|
+
(pattern, domain, strength, usage_score, spread_score, success_score, recency_score,
|
|
298
|
+
usage_count, unique_contexts, success_rate, last_used, updated_at)
|
|
299
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
300
|
+
).run(
|
|
301
|
+
ps.pattern,
|
|
302
|
+
ps.domain,
|
|
303
|
+
ps.strength,
|
|
304
|
+
ps.usageScore,
|
|
305
|
+
ps.spreadScore,
|
|
306
|
+
ps.successScore,
|
|
307
|
+
ps.recencyScore,
|
|
308
|
+
ps.usageCount,
|
|
309
|
+
ps.uniqueContexts,
|
|
310
|
+
ps.successRate,
|
|
311
|
+
ps.lastUsed,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return strengths;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
getStrengths(query?: StrengthsQuery): PatternStrength[] {
|
|
319
|
+
const db = this.vault.getDb();
|
|
320
|
+
const conditions: string[] = [];
|
|
321
|
+
const values: unknown[] = [];
|
|
322
|
+
|
|
323
|
+
if (query?.domain) {
|
|
324
|
+
conditions.push('domain = ?');
|
|
325
|
+
values.push(query.domain);
|
|
326
|
+
}
|
|
327
|
+
if (query?.minStrength !== undefined && query.minStrength !== null) {
|
|
328
|
+
conditions.push('strength >= ?');
|
|
329
|
+
values.push(query.minStrength);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
|
|
333
|
+
const limit = query?.limit ?? 50;
|
|
334
|
+
values.push(limit);
|
|
335
|
+
|
|
336
|
+
const rows = db
|
|
337
|
+
.prepare(`SELECT * FROM brain_strengths ${where} ORDER BY strength DESC LIMIT ?`)
|
|
338
|
+
.all(...values) as Array<{
|
|
339
|
+
pattern: string;
|
|
340
|
+
domain: string;
|
|
341
|
+
strength: number;
|
|
342
|
+
usage_score: number;
|
|
343
|
+
spread_score: number;
|
|
344
|
+
success_score: number;
|
|
345
|
+
recency_score: number;
|
|
346
|
+
usage_count: number;
|
|
347
|
+
unique_contexts: number;
|
|
348
|
+
success_rate: number;
|
|
349
|
+
last_used: string;
|
|
350
|
+
}>;
|
|
351
|
+
|
|
352
|
+
return rows.map((r) => ({
|
|
353
|
+
pattern: r.pattern,
|
|
354
|
+
domain: r.domain,
|
|
355
|
+
strength: r.strength,
|
|
356
|
+
usageScore: r.usage_score,
|
|
357
|
+
spreadScore: r.spread_score,
|
|
358
|
+
successScore: r.success_score,
|
|
359
|
+
recencyScore: r.recency_score,
|
|
360
|
+
usageCount: r.usage_count,
|
|
361
|
+
uniqueContexts: r.unique_contexts,
|
|
362
|
+
successRate: r.success_rate,
|
|
363
|
+
lastUsed: r.last_used,
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
recommend(context: { domain?: string; task?: string; limit?: number }): PatternStrength[] {
|
|
368
|
+
const limit = context.limit ?? 5;
|
|
369
|
+
const strengths = this.getStrengths({
|
|
370
|
+
domain: context.domain,
|
|
371
|
+
minStrength: 30,
|
|
372
|
+
limit: limit * 2,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// If task context provided, boost patterns with matching terms
|
|
376
|
+
if (context.task) {
|
|
377
|
+
const taskTerms = new Set(context.task.toLowerCase().split(/\W+/).filter(Boolean));
|
|
378
|
+
for (const s of strengths) {
|
|
379
|
+
const patternTerms = s.pattern.toLowerCase().split(/\W+/);
|
|
380
|
+
const overlap = patternTerms.filter((t) => taskTerms.has(t)).length;
|
|
381
|
+
if (overlap > 0) {
|
|
382
|
+
// Temporarily boost strength for ranking (doesn't persist)
|
|
383
|
+
(s as { strength: number }).strength += overlap * 5;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
strengths.sort((a, b) => b.strength - a.strength);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return strengths.slice(0, limit);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ─── Knowledge Extraction ─────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
extractKnowledge(sessionId: string): ExtractionResult {
|
|
395
|
+
const session = this.getSession(sessionId);
|
|
396
|
+
if (!session) throw new Error('Session not found: ' + sessionId);
|
|
397
|
+
|
|
398
|
+
const proposals: KnowledgeProposal[] = [];
|
|
399
|
+
const rulesApplied: string[] = [];
|
|
400
|
+
const db = this.vault.getDb();
|
|
401
|
+
|
|
402
|
+
// Rule 1: Repeated tool usage (3+ same tool)
|
|
403
|
+
const toolCounts = new Map<string, number>();
|
|
404
|
+
for (const t of session.toolsUsed) {
|
|
405
|
+
toolCounts.set(t, (toolCounts.get(t) ?? 0) + 1);
|
|
406
|
+
}
|
|
407
|
+
for (const [tool, count] of toolCounts) {
|
|
408
|
+
if (count >= EXTRACTION_TOOL_THRESHOLD) {
|
|
409
|
+
rulesApplied.push('repeated_tool_usage');
|
|
410
|
+
proposals.push(
|
|
411
|
+
this.createProposal(db, sessionId, 'repeated_tool_usage', 'pattern', {
|
|
412
|
+
title: `Frequent use of ${tool}`,
|
|
413
|
+
description: `Tool ${tool} was used ${count} times in session. Consider automating or abstracting this workflow.`,
|
|
414
|
+
confidence: Math.min(0.9, 0.5 + count * 0.1),
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Rule 2: Multi-file edits (3+ files)
|
|
421
|
+
if (session.filesModified.length >= EXTRACTION_FILE_THRESHOLD) {
|
|
422
|
+
rulesApplied.push('multi_file_edit');
|
|
423
|
+
proposals.push(
|
|
424
|
+
this.createProposal(db, sessionId, 'multi_file_edit', 'pattern', {
|
|
425
|
+
title: `Multi-file change pattern (${session.filesModified.length} files)`,
|
|
426
|
+
description: `Session modified ${session.filesModified.length} files: ${session.filesModified.slice(0, 5).join(', ')}${session.filesModified.length > 5 ? '...' : ''}. This may indicate an architectural pattern.`,
|
|
427
|
+
confidence: Math.min(0.8, 0.4 + session.filesModified.length * 0.05),
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Rule 3: Long session (>30min)
|
|
433
|
+
if (session.endedAt && session.startedAt) {
|
|
434
|
+
const durationMs =
|
|
435
|
+
new Date(session.endedAt).getTime() - new Date(session.startedAt).getTime();
|
|
436
|
+
const durationMin = durationMs / 60000;
|
|
437
|
+
if (durationMin > EXTRACTION_LONG_SESSION_MINUTES) {
|
|
438
|
+
rulesApplied.push('long_session');
|
|
439
|
+
proposals.push(
|
|
440
|
+
this.createProposal(db, sessionId, 'long_session', 'anti-pattern', {
|
|
441
|
+
title: `Long session (${Math.round(durationMin)} minutes)`,
|
|
442
|
+
description: `Session lasted ${Math.round(durationMin)} minutes. Consider breaking complex tasks into smaller steps or improving automation.`,
|
|
443
|
+
confidence: 0.5,
|
|
444
|
+
}),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Rule 4: Plan completed
|
|
450
|
+
if (session.planId && session.planOutcome === 'completed') {
|
|
451
|
+
rulesApplied.push('plan_completed');
|
|
452
|
+
proposals.push(
|
|
453
|
+
this.createProposal(db, sessionId, 'plan_completed', 'workflow', {
|
|
454
|
+
title: `Successful plan: ${session.planId}`,
|
|
455
|
+
description: `Plan ${session.planId} completed successfully. This workflow can be reused for similar tasks.`,
|
|
456
|
+
confidence: 0.8,
|
|
457
|
+
}),
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Rule 5: Plan abandoned
|
|
462
|
+
if (session.planId && session.planOutcome === 'abandoned') {
|
|
463
|
+
rulesApplied.push('plan_abandoned');
|
|
464
|
+
proposals.push(
|
|
465
|
+
this.createProposal(db, sessionId, 'plan_abandoned', 'anti-pattern', {
|
|
466
|
+
title: `Abandoned plan: ${session.planId}`,
|
|
467
|
+
description: `Plan ${session.planId} was abandoned. Review what went wrong to avoid repeating in future sessions.`,
|
|
468
|
+
confidence: 0.7,
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Rule 6: High feedback ratio (>80% accept or dismiss)
|
|
474
|
+
const feedbackRow = db
|
|
475
|
+
.prepare(
|
|
476
|
+
`SELECT COUNT(*) as total,
|
|
477
|
+
SUM(CASE WHEN action = 'accepted' THEN 1 ELSE 0 END) as accepted,
|
|
478
|
+
SUM(CASE WHEN action = 'dismissed' THEN 1 ELSE 0 END) as dismissed
|
|
479
|
+
FROM brain_feedback
|
|
480
|
+
WHERE created_at >= ? AND created_at <= ?`,
|
|
481
|
+
)
|
|
482
|
+
.get(session.startedAt, session.endedAt ?? new Date().toISOString()) as {
|
|
483
|
+
total: number;
|
|
484
|
+
accepted: number;
|
|
485
|
+
dismissed: number;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
if (feedbackRow.total >= 3) {
|
|
489
|
+
const acceptRate = feedbackRow.accepted / feedbackRow.total;
|
|
490
|
+
const dismissRate = feedbackRow.dismissed / feedbackRow.total;
|
|
491
|
+
|
|
492
|
+
if (acceptRate >= EXTRACTION_HIGH_FEEDBACK_RATIO) {
|
|
493
|
+
rulesApplied.push('high_accept_ratio');
|
|
494
|
+
proposals.push(
|
|
495
|
+
this.createProposal(db, sessionId, 'high_accept_ratio', 'pattern', {
|
|
496
|
+
title: `High search acceptance rate (${Math.round(acceptRate * 100)}%)`,
|
|
497
|
+
description: `Search results were accepted ${Math.round(acceptRate * 100)}% of the time. Brain scoring is well-calibrated for this type of work.`,
|
|
498
|
+
confidence: 0.7,
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
} else if (dismissRate >= EXTRACTION_HIGH_FEEDBACK_RATIO) {
|
|
502
|
+
rulesApplied.push('high_dismiss_ratio');
|
|
503
|
+
proposals.push(
|
|
504
|
+
this.createProposal(db, sessionId, 'high_dismiss_ratio', 'anti-pattern', {
|
|
505
|
+
title: `High search dismissal rate (${Math.round(dismissRate * 100)}%)`,
|
|
506
|
+
description: `Search results were dismissed ${Math.round(dismissRate * 100)}% of the time. Brain scoring may need recalibration for this domain.`,
|
|
507
|
+
confidence: 0.7,
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
sessionId,
|
|
515
|
+
proposals,
|
|
516
|
+
rulesApplied: [...new Set(rulesApplied)],
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
getProposals(options?: {
|
|
521
|
+
sessionId?: string;
|
|
522
|
+
promoted?: boolean;
|
|
523
|
+
limit?: number;
|
|
524
|
+
}): KnowledgeProposal[] {
|
|
525
|
+
const db = this.vault.getDb();
|
|
526
|
+
const conditions: string[] = [];
|
|
527
|
+
const values: unknown[] = [];
|
|
528
|
+
|
|
529
|
+
if (options?.sessionId) {
|
|
530
|
+
conditions.push('session_id = ?');
|
|
531
|
+
values.push(options.sessionId);
|
|
532
|
+
}
|
|
533
|
+
if (options?.promoted !== undefined && options.promoted !== null) {
|
|
534
|
+
conditions.push('promoted = ?');
|
|
535
|
+
values.push(options.promoted ? 1 : 0);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
|
|
539
|
+
const limit = options?.limit ?? 50;
|
|
540
|
+
values.push(limit);
|
|
541
|
+
|
|
542
|
+
const rows = db
|
|
543
|
+
.prepare(`SELECT * FROM brain_proposals ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
544
|
+
.all(...values) as Array<{
|
|
545
|
+
id: string;
|
|
546
|
+
session_id: string;
|
|
547
|
+
rule: string;
|
|
548
|
+
type: string;
|
|
549
|
+
title: string;
|
|
550
|
+
description: string;
|
|
551
|
+
confidence: number;
|
|
552
|
+
promoted: number;
|
|
553
|
+
created_at: string;
|
|
554
|
+
}>;
|
|
555
|
+
|
|
556
|
+
return rows.map((r) => this.rowToProposal(r));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
promoteProposals(proposalIds: string[]): { promoted: number; failed: string[] } {
|
|
560
|
+
const db = this.vault.getDb();
|
|
561
|
+
let promoted = 0;
|
|
562
|
+
const failed: string[] = [];
|
|
563
|
+
|
|
564
|
+
for (const id of proposalIds) {
|
|
565
|
+
const row = db.prepare('SELECT * FROM brain_proposals WHERE id = ?').get(id) as
|
|
566
|
+
| {
|
|
567
|
+
id: string;
|
|
568
|
+
session_id: string;
|
|
569
|
+
rule: string;
|
|
570
|
+
type: string;
|
|
571
|
+
title: string;
|
|
572
|
+
description: string;
|
|
573
|
+
confidence: number;
|
|
574
|
+
promoted: number;
|
|
575
|
+
created_at: string;
|
|
576
|
+
}
|
|
577
|
+
| undefined;
|
|
578
|
+
|
|
579
|
+
if (!row) {
|
|
580
|
+
failed.push(id);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (row.promoted) continue; // Already promoted
|
|
585
|
+
|
|
586
|
+
// Add to vault as intelligence entry — map workflow to pattern since vault only accepts pattern/anti-pattern/rule
|
|
587
|
+
const rawType = row.type;
|
|
588
|
+
const vaultType: 'pattern' | 'anti-pattern' | 'rule' =
|
|
589
|
+
rawType === 'anti-pattern' ? 'anti-pattern' : 'pattern';
|
|
590
|
+
this.brain.enrichAndCapture({
|
|
591
|
+
id: `proposal-${id}`,
|
|
592
|
+
type: vaultType,
|
|
593
|
+
domain: 'brain-intelligence',
|
|
594
|
+
title: row.title,
|
|
595
|
+
severity: 'suggestion',
|
|
596
|
+
description: row.description,
|
|
597
|
+
tags: ['auto-extracted', row.rule],
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
db.prepare('UPDATE brain_proposals SET promoted = 1 WHERE id = ?').run(id);
|
|
601
|
+
promoted++;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return { promoted, failed };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ─── Intelligence Pipeline ────────────────────────────────────────
|
|
608
|
+
|
|
609
|
+
buildIntelligence(): BuildIntelligenceResult {
|
|
610
|
+
// Step 1: Compute and persist strengths
|
|
611
|
+
const strengths = this.computeStrengths();
|
|
612
|
+
|
|
613
|
+
// Step 2: Build global registry
|
|
614
|
+
const globalPatterns = this.buildGlobalRegistry(strengths);
|
|
615
|
+
|
|
616
|
+
// Step 3: Build domain profiles
|
|
617
|
+
const domainProfiles = this.buildDomainProfiles(strengths);
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
strengthsComputed: strengths.length,
|
|
621
|
+
globalPatterns,
|
|
622
|
+
domainProfiles,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
getGlobalPatterns(limit = 20): GlobalPattern[] {
|
|
627
|
+
const db = this.vault.getDb();
|
|
628
|
+
const rows = db
|
|
629
|
+
.prepare('SELECT * FROM brain_global_registry ORDER BY total_strength DESC LIMIT ?')
|
|
630
|
+
.all(limit) as Array<{
|
|
631
|
+
pattern: string;
|
|
632
|
+
domains: string;
|
|
633
|
+
total_strength: number;
|
|
634
|
+
avg_strength: number;
|
|
635
|
+
domain_count: number;
|
|
636
|
+
}>;
|
|
637
|
+
|
|
638
|
+
return rows.map((r) => ({
|
|
639
|
+
pattern: r.pattern,
|
|
640
|
+
domains: JSON.parse(r.domains) as string[],
|
|
641
|
+
totalStrength: r.total_strength,
|
|
642
|
+
avgStrength: r.avg_strength,
|
|
643
|
+
domainCount: r.domain_count,
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
getDomainProfile(domain: string): DomainProfile | null {
|
|
648
|
+
const db = this.vault.getDb();
|
|
649
|
+
const row = db.prepare('SELECT * FROM brain_domain_profiles WHERE domain = ?').get(domain) as
|
|
650
|
+
| {
|
|
651
|
+
domain: string;
|
|
652
|
+
top_patterns: string;
|
|
653
|
+
session_count: number;
|
|
654
|
+
avg_session_duration: number;
|
|
655
|
+
last_activity: string;
|
|
656
|
+
}
|
|
657
|
+
| undefined;
|
|
658
|
+
|
|
659
|
+
if (!row) return null;
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
domain: row.domain,
|
|
663
|
+
topPatterns: JSON.parse(row.top_patterns) as Array<{ pattern: string; strength: number }>,
|
|
664
|
+
sessionCount: row.session_count,
|
|
665
|
+
avgSessionDuration: row.avg_session_duration,
|
|
666
|
+
lastActivity: row.last_activity,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ─── Data Management ──────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
getStats(): BrainIntelligenceStats {
|
|
673
|
+
const db = this.vault.getDb();
|
|
674
|
+
|
|
675
|
+
const strengths = (
|
|
676
|
+
db.prepare('SELECT COUNT(*) as c FROM brain_strengths').get() as { c: number }
|
|
677
|
+
).c;
|
|
678
|
+
const sessions = (db.prepare('SELECT COUNT(*) as c FROM brain_sessions').get() as { c: number })
|
|
679
|
+
.c;
|
|
680
|
+
const activeSessions = (
|
|
681
|
+
db.prepare('SELECT COUNT(*) as c FROM brain_sessions WHERE ended_at IS NULL').get() as {
|
|
682
|
+
c: number;
|
|
683
|
+
}
|
|
684
|
+
).c;
|
|
685
|
+
const proposals = (
|
|
686
|
+
db.prepare('SELECT COUNT(*) as c FROM brain_proposals').get() as { c: number }
|
|
687
|
+
).c;
|
|
688
|
+
const promotedProposals = (
|
|
689
|
+
db.prepare('SELECT COUNT(*) as c FROM brain_proposals WHERE promoted = 1').get() as {
|
|
690
|
+
c: number;
|
|
691
|
+
}
|
|
692
|
+
).c;
|
|
693
|
+
const globalPatterns = (
|
|
694
|
+
db.prepare('SELECT COUNT(*) as c FROM brain_global_registry').get() as { c: number }
|
|
695
|
+
).c;
|
|
696
|
+
const domainProfiles = (
|
|
697
|
+
db.prepare('SELECT COUNT(*) as c FROM brain_domain_profiles').get() as { c: number }
|
|
698
|
+
).c;
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
strengths,
|
|
702
|
+
sessions,
|
|
703
|
+
activeSessions,
|
|
704
|
+
proposals,
|
|
705
|
+
promotedProposals,
|
|
706
|
+
globalPatterns,
|
|
707
|
+
domainProfiles,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
exportData(): BrainExportData {
|
|
712
|
+
const db = this.vault.getDb();
|
|
713
|
+
|
|
714
|
+
const strengths = this.getStrengths({ limit: 10000 });
|
|
715
|
+
|
|
716
|
+
const sessionRows = db
|
|
717
|
+
.prepare('SELECT * FROM brain_sessions ORDER BY started_at DESC')
|
|
718
|
+
.all() as Array<{
|
|
719
|
+
id: string;
|
|
720
|
+
started_at: string;
|
|
721
|
+
ended_at: string | null;
|
|
722
|
+
domain: string | null;
|
|
723
|
+
context: string | null;
|
|
724
|
+
tools_used: string;
|
|
725
|
+
files_modified: string;
|
|
726
|
+
plan_id: string | null;
|
|
727
|
+
plan_outcome: string | null;
|
|
728
|
+
}>;
|
|
729
|
+
const sessions = sessionRows.map((r) => this.rowToSession(r));
|
|
730
|
+
|
|
731
|
+
const proposals = this.getProposals({ limit: 10000 });
|
|
732
|
+
const globalPatterns = this.getGlobalPatterns(10000);
|
|
733
|
+
|
|
734
|
+
const profileRows = db.prepare('SELECT * FROM brain_domain_profiles').all() as Array<{
|
|
735
|
+
domain: string;
|
|
736
|
+
top_patterns: string;
|
|
737
|
+
session_count: number;
|
|
738
|
+
avg_session_duration: number;
|
|
739
|
+
last_activity: string;
|
|
740
|
+
}>;
|
|
741
|
+
const domainProfiles = profileRows.map((r) => ({
|
|
742
|
+
domain: r.domain,
|
|
743
|
+
topPatterns: JSON.parse(r.top_patterns) as Array<{ pattern: string; strength: number }>,
|
|
744
|
+
sessionCount: r.session_count,
|
|
745
|
+
avgSessionDuration: r.avg_session_duration,
|
|
746
|
+
lastActivity: r.last_activity,
|
|
747
|
+
}));
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
strengths,
|
|
751
|
+
sessions,
|
|
752
|
+
proposals,
|
|
753
|
+
globalPatterns,
|
|
754
|
+
domainProfiles,
|
|
755
|
+
exportedAt: new Date().toISOString(),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
importData(data: BrainExportData): BrainImportResult {
|
|
760
|
+
const db = this.vault.getDb();
|
|
761
|
+
const result: BrainImportResult = {
|
|
762
|
+
imported: { strengths: 0, sessions: 0, proposals: 0, globalPatterns: 0, domainProfiles: 0 },
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
const tx = db.transaction(() => {
|
|
766
|
+
// Import strengths
|
|
767
|
+
const insertStrength = db.prepare(
|
|
768
|
+
`INSERT OR REPLACE INTO brain_strengths
|
|
769
|
+
(pattern, domain, strength, usage_score, spread_score, success_score, recency_score,
|
|
770
|
+
usage_count, unique_contexts, success_rate, last_used, updated_at)
|
|
771
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
772
|
+
);
|
|
773
|
+
for (const s of data.strengths) {
|
|
774
|
+
insertStrength.run(
|
|
775
|
+
s.pattern,
|
|
776
|
+
s.domain,
|
|
777
|
+
s.strength,
|
|
778
|
+
s.usageScore,
|
|
779
|
+
s.spreadScore,
|
|
780
|
+
s.successScore,
|
|
781
|
+
s.recencyScore,
|
|
782
|
+
s.usageCount,
|
|
783
|
+
s.uniqueContexts,
|
|
784
|
+
s.successRate,
|
|
785
|
+
s.lastUsed,
|
|
786
|
+
);
|
|
787
|
+
result.imported.strengths++;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Import sessions
|
|
791
|
+
const insertSession = db.prepare(
|
|
792
|
+
`INSERT OR IGNORE INTO brain_sessions
|
|
793
|
+
(id, started_at, ended_at, domain, context, tools_used, files_modified, plan_id, plan_outcome)
|
|
794
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
795
|
+
);
|
|
796
|
+
for (const s of data.sessions) {
|
|
797
|
+
const changes = insertSession.run(
|
|
798
|
+
s.id,
|
|
799
|
+
s.startedAt,
|
|
800
|
+
s.endedAt,
|
|
801
|
+
s.domain,
|
|
802
|
+
s.context,
|
|
803
|
+
JSON.stringify(s.toolsUsed),
|
|
804
|
+
JSON.stringify(s.filesModified),
|
|
805
|
+
s.planId,
|
|
806
|
+
s.planOutcome,
|
|
807
|
+
);
|
|
808
|
+
if (changes.changes > 0) result.imported.sessions++;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Import proposals
|
|
812
|
+
const insertProposal = db.prepare(
|
|
813
|
+
`INSERT OR IGNORE INTO brain_proposals
|
|
814
|
+
(id, session_id, rule, type, title, description, confidence, promoted, created_at)
|
|
815
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
816
|
+
);
|
|
817
|
+
for (const p of data.proposals) {
|
|
818
|
+
const changes = insertProposal.run(
|
|
819
|
+
p.id,
|
|
820
|
+
p.sessionId,
|
|
821
|
+
p.rule,
|
|
822
|
+
p.type,
|
|
823
|
+
p.title,
|
|
824
|
+
p.description,
|
|
825
|
+
p.confidence,
|
|
826
|
+
p.promoted ? 1 : 0,
|
|
827
|
+
p.createdAt,
|
|
828
|
+
);
|
|
829
|
+
if (changes.changes > 0) result.imported.proposals++;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Import global patterns
|
|
833
|
+
const insertGlobal = db.prepare(
|
|
834
|
+
`INSERT OR REPLACE INTO brain_global_registry
|
|
835
|
+
(pattern, domains, total_strength, avg_strength, domain_count, updated_at)
|
|
836
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
|
837
|
+
);
|
|
838
|
+
for (const g of data.globalPatterns) {
|
|
839
|
+
insertGlobal.run(
|
|
840
|
+
g.pattern,
|
|
841
|
+
JSON.stringify(g.domains),
|
|
842
|
+
g.totalStrength,
|
|
843
|
+
g.avgStrength,
|
|
844
|
+
g.domainCount,
|
|
845
|
+
);
|
|
846
|
+
result.imported.globalPatterns++;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Import domain profiles
|
|
850
|
+
const insertProfile = db.prepare(
|
|
851
|
+
`INSERT OR REPLACE INTO brain_domain_profiles
|
|
852
|
+
(domain, top_patterns, session_count, avg_session_duration, last_activity, updated_at)
|
|
853
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
|
854
|
+
);
|
|
855
|
+
for (const d of data.domainProfiles) {
|
|
856
|
+
insertProfile.run(
|
|
857
|
+
d.domain,
|
|
858
|
+
JSON.stringify(d.topPatterns),
|
|
859
|
+
d.sessionCount,
|
|
860
|
+
d.avgSessionDuration,
|
|
861
|
+
d.lastActivity,
|
|
862
|
+
);
|
|
863
|
+
result.imported.domainProfiles++;
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
tx();
|
|
868
|
+
return result;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ─── Private Helpers ──────────────────────────────────────────────
|
|
872
|
+
|
|
873
|
+
private getSession(id: string): BrainSession | null {
|
|
874
|
+
const db = this.vault.getDb();
|
|
875
|
+
const row = db.prepare('SELECT * FROM brain_sessions WHERE id = ?').get(id) as
|
|
876
|
+
| {
|
|
877
|
+
id: string;
|
|
878
|
+
started_at: string;
|
|
879
|
+
ended_at: string | null;
|
|
880
|
+
domain: string | null;
|
|
881
|
+
context: string | null;
|
|
882
|
+
tools_used: string;
|
|
883
|
+
files_modified: string;
|
|
884
|
+
plan_id: string | null;
|
|
885
|
+
plan_outcome: string | null;
|
|
886
|
+
}
|
|
887
|
+
| undefined;
|
|
888
|
+
|
|
889
|
+
if (!row) return null;
|
|
890
|
+
return this.rowToSession(row);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
private rowToSession(row: {
|
|
894
|
+
id: string;
|
|
895
|
+
started_at: string;
|
|
896
|
+
ended_at: string | null;
|
|
897
|
+
domain: string | null;
|
|
898
|
+
context: string | null;
|
|
899
|
+
tools_used: string;
|
|
900
|
+
files_modified: string;
|
|
901
|
+
plan_id: string | null;
|
|
902
|
+
plan_outcome: string | null;
|
|
903
|
+
}): BrainSession {
|
|
904
|
+
return {
|
|
905
|
+
id: row.id,
|
|
906
|
+
startedAt: row.started_at,
|
|
907
|
+
endedAt: row.ended_at,
|
|
908
|
+
domain: row.domain,
|
|
909
|
+
context: row.context,
|
|
910
|
+
toolsUsed: JSON.parse(row.tools_used) as string[],
|
|
911
|
+
filesModified: JSON.parse(row.files_modified) as string[],
|
|
912
|
+
planId: row.plan_id,
|
|
913
|
+
planOutcome: row.plan_outcome,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private rowToProposal(row: {
|
|
918
|
+
id: string;
|
|
919
|
+
session_id: string;
|
|
920
|
+
rule: string;
|
|
921
|
+
type: string;
|
|
922
|
+
title: string;
|
|
923
|
+
description: string;
|
|
924
|
+
confidence: number;
|
|
925
|
+
promoted: number;
|
|
926
|
+
created_at: string;
|
|
927
|
+
}): KnowledgeProposal {
|
|
928
|
+
return {
|
|
929
|
+
id: row.id,
|
|
930
|
+
sessionId: row.session_id,
|
|
931
|
+
rule: row.rule,
|
|
932
|
+
type: row.type as 'pattern' | 'anti-pattern' | 'workflow',
|
|
933
|
+
title: row.title,
|
|
934
|
+
description: row.description,
|
|
935
|
+
confidence: row.confidence,
|
|
936
|
+
promoted: row.promoted === 1,
|
|
937
|
+
createdAt: row.created_at,
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
private createProposal(
|
|
942
|
+
db: ReturnType<Vault['getDb']>,
|
|
943
|
+
sessionId: string,
|
|
944
|
+
rule: string,
|
|
945
|
+
type: 'pattern' | 'anti-pattern' | 'workflow',
|
|
946
|
+
data: { title: string; description: string; confidence: number },
|
|
947
|
+
): KnowledgeProposal {
|
|
948
|
+
const id = randomUUID();
|
|
949
|
+
db.prepare(
|
|
950
|
+
`INSERT INTO brain_proposals (id, session_id, rule, type, title, description, confidence)
|
|
951
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
952
|
+
).run(id, sessionId, rule, type, data.title, data.description, data.confidence);
|
|
953
|
+
|
|
954
|
+
return {
|
|
955
|
+
id,
|
|
956
|
+
sessionId,
|
|
957
|
+
rule,
|
|
958
|
+
type,
|
|
959
|
+
title: data.title,
|
|
960
|
+
description: data.description,
|
|
961
|
+
confidence: data.confidence,
|
|
962
|
+
promoted: false,
|
|
963
|
+
createdAt: new Date().toISOString(),
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
private buildGlobalRegistry(strengths: PatternStrength[]): number {
|
|
968
|
+
const db = this.vault.getDb();
|
|
969
|
+
|
|
970
|
+
// Group strengths by pattern
|
|
971
|
+
const patternMap = new Map<string, PatternStrength[]>();
|
|
972
|
+
for (const s of strengths) {
|
|
973
|
+
const list = patternMap.get(s.pattern) ?? [];
|
|
974
|
+
list.push(s);
|
|
975
|
+
patternMap.set(s.pattern, list);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
db.prepare('DELETE FROM brain_global_registry').run();
|
|
979
|
+
|
|
980
|
+
const insert = db.prepare(
|
|
981
|
+
`INSERT INTO brain_global_registry
|
|
982
|
+
(pattern, domains, total_strength, avg_strength, domain_count, updated_at)
|
|
983
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
let count = 0;
|
|
987
|
+
for (const [pattern, entries] of patternMap) {
|
|
988
|
+
const domains = [...new Set(entries.map((e) => e.domain))];
|
|
989
|
+
const totalStrength = entries.reduce((sum, e) => sum + e.strength, 0);
|
|
990
|
+
const avgStrength = totalStrength / entries.length;
|
|
991
|
+
|
|
992
|
+
insert.run(pattern, JSON.stringify(domains), totalStrength, avgStrength, domains.length);
|
|
993
|
+
count++;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return count;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private buildDomainProfiles(strengths: PatternStrength[]): number {
|
|
1000
|
+
const db = this.vault.getDb();
|
|
1001
|
+
|
|
1002
|
+
// Group strengths by domain
|
|
1003
|
+
const domainMap = new Map<string, PatternStrength[]>();
|
|
1004
|
+
for (const s of strengths) {
|
|
1005
|
+
const list = domainMap.get(s.domain) ?? [];
|
|
1006
|
+
list.push(s);
|
|
1007
|
+
domainMap.set(s.domain, list);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
db.prepare('DELETE FROM brain_domain_profiles').run();
|
|
1011
|
+
|
|
1012
|
+
const insert = db.prepare(
|
|
1013
|
+
`INSERT INTO brain_domain_profiles
|
|
1014
|
+
(domain, top_patterns, session_count, avg_session_duration, last_activity, updated_at)
|
|
1015
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
let count = 0;
|
|
1019
|
+
for (const [domain, entries] of domainMap) {
|
|
1020
|
+
entries.sort((a, b) => b.strength - a.strength);
|
|
1021
|
+
const topPatterns = entries.slice(0, 10).map((e) => ({
|
|
1022
|
+
pattern: e.pattern,
|
|
1023
|
+
strength: e.strength,
|
|
1024
|
+
}));
|
|
1025
|
+
|
|
1026
|
+
// Count sessions for this domain
|
|
1027
|
+
const sessionCount = (
|
|
1028
|
+
db.prepare('SELECT COUNT(*) as c FROM brain_sessions WHERE domain = ?').get(domain) as {
|
|
1029
|
+
c: number;
|
|
1030
|
+
}
|
|
1031
|
+
).c;
|
|
1032
|
+
|
|
1033
|
+
// Average session duration (in minutes)
|
|
1034
|
+
const durationRow = db
|
|
1035
|
+
.prepare(
|
|
1036
|
+
`SELECT AVG(
|
|
1037
|
+
(julianday(ended_at) - julianday(started_at)) * 1440
|
|
1038
|
+
) as avg_min
|
|
1039
|
+
FROM brain_sessions
|
|
1040
|
+
WHERE domain = ? AND ended_at IS NOT NULL`,
|
|
1041
|
+
)
|
|
1042
|
+
.get(domain) as { avg_min: number | null };
|
|
1043
|
+
|
|
1044
|
+
const lastActivity = entries.reduce(
|
|
1045
|
+
(latest, e) => (e.lastUsed > latest ? e.lastUsed : latest),
|
|
1046
|
+
'',
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
insert.run(
|
|
1050
|
+
domain,
|
|
1051
|
+
JSON.stringify(topPatterns),
|
|
1052
|
+
sessionCount,
|
|
1053
|
+
durationRow.avg_min ?? 0,
|
|
1054
|
+
lastActivity || new Date().toISOString(),
|
|
1055
|
+
);
|
|
1056
|
+
count++;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return count;
|
|
1060
|
+
}
|
|
1061
|
+
}
|