@soleri/core 2.0.2 → 2.4.0

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