@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,828 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdirSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { createAgentRuntime } from '../runtime/runtime.js';
6
+ import type { AgentRuntime } from '../runtime/types.js';
7
+ import type { BrainExportData } from '../brain/types.js';
8
+
9
+ describe('BrainIntelligence', () => {
10
+ let runtime: AgentRuntime;
11
+ let plannerDir: string;
12
+
13
+ beforeEach(() => {
14
+ plannerDir = join(tmpdir(), 'brain-intel-test-' + Date.now());
15
+ mkdirSync(plannerDir, { recursive: true });
16
+ runtime = createAgentRuntime({
17
+ agentId: 'test-brain-intel',
18
+ vaultPath: ':memory:',
19
+ plansPath: join(plannerDir, 'plans.json'),
20
+ });
21
+ });
22
+
23
+ afterEach(() => {
24
+ runtime.close();
25
+ rmSync(plannerDir, { recursive: true, force: true });
26
+ });
27
+
28
+ // ─── Initialization ─────────────────────────────────────────────
29
+
30
+ it('should initialize with empty stats', () => {
31
+ const stats = runtime.brainIntelligence.getStats();
32
+ expect(stats.strengths).toBe(0);
33
+ expect(stats.sessions).toBe(0);
34
+ expect(stats.activeSessions).toBe(0);
35
+ expect(stats.proposals).toBe(0);
36
+ expect(stats.promotedProposals).toBe(0);
37
+ expect(stats.globalPatterns).toBe(0);
38
+ expect(stats.domainProfiles).toBe(0);
39
+ });
40
+
41
+ it('should be accessible from runtime', () => {
42
+ expect(runtime.brainIntelligence).toBeDefined();
43
+ expect(typeof runtime.brainIntelligence.getStats).toBe('function');
44
+ });
45
+
46
+ // ─── Session Lifecycle ──────────────────────────────────────────
47
+
48
+ it('should start a session', () => {
49
+ const session = runtime.brainIntelligence.lifecycle({
50
+ action: 'start',
51
+ domain: 'testing',
52
+ context: 'unit tests',
53
+ });
54
+ expect(session.id).toBeDefined();
55
+ expect(session.domain).toBe('testing');
56
+ expect(session.context).toBe('unit tests');
57
+ expect(session.endedAt).toBeNull();
58
+ });
59
+
60
+ it('should start a session with custom id', () => {
61
+ const session = runtime.brainIntelligence.lifecycle({
62
+ action: 'start',
63
+ sessionId: 'custom-123',
64
+ domain: 'dev',
65
+ });
66
+ expect(session.id).toBe('custom-123');
67
+ });
68
+
69
+ it('should end a session', () => {
70
+ const started = runtime.brainIntelligence.lifecycle({
71
+ action: 'start',
72
+ sessionId: 'end-test',
73
+ domain: 'testing',
74
+ });
75
+
76
+ const ended = runtime.brainIntelligence.lifecycle({
77
+ action: 'end',
78
+ sessionId: started.id,
79
+ toolsUsed: ['search', 'edit'],
80
+ filesModified: ['a.ts', 'b.ts'],
81
+ planOutcome: 'completed',
82
+ });
83
+
84
+ expect(ended.endedAt).not.toBeNull();
85
+ expect(ended.toolsUsed).toEqual(['search', 'edit']);
86
+ expect(ended.filesModified).toEqual(['a.ts', 'b.ts']);
87
+ expect(ended.planOutcome).toBe('completed');
88
+ });
89
+
90
+ it('should throw when ending session without sessionId', () => {
91
+ expect(() => {
92
+ runtime.brainIntelligence.lifecycle({ action: 'end' });
93
+ }).toThrow('sessionId required');
94
+ });
95
+
96
+ it('should track active sessions in stats', () => {
97
+ runtime.brainIntelligence.lifecycle({ action: 'start', sessionId: 'active-1' });
98
+ runtime.brainIntelligence.lifecycle({ action: 'start', sessionId: 'active-2' });
99
+ runtime.brainIntelligence.lifecycle({
100
+ action: 'end',
101
+ sessionId: 'active-1',
102
+ });
103
+
104
+ const stats = runtime.brainIntelligence.getStats();
105
+ expect(stats.sessions).toBe(2);
106
+ expect(stats.activeSessions).toBe(1);
107
+ });
108
+
109
+ // ─── Session Context ────────────────────────────────────────────
110
+
111
+ it('should return session context with frequencies', () => {
112
+ runtime.brainIntelligence.lifecycle({
113
+ action: 'start',
114
+ sessionId: 'ctx-1',
115
+ toolsUsed: ['search', 'edit', 'search'],
116
+ filesModified: ['a.ts'],
117
+ });
118
+ runtime.brainIntelligence.lifecycle({
119
+ action: 'start',
120
+ sessionId: 'ctx-2',
121
+ toolsUsed: ['search', 'write'],
122
+ filesModified: ['a.ts', 'b.ts'],
123
+ });
124
+
125
+ const ctx = runtime.brainIntelligence.getSessionContext();
126
+ expect(ctx.recentSessions.length).toBe(2);
127
+ expect(ctx.toolFrequency.length).toBeGreaterThan(0);
128
+ expect(ctx.fileFrequency.length).toBeGreaterThan(0);
129
+
130
+ const searchFreq = ctx.toolFrequency.find((t) => t.tool === 'search');
131
+ expect(searchFreq).toBeDefined();
132
+ expect(searchFreq!.count).toBe(3);
133
+ });
134
+
135
+ it('should limit session context results', () => {
136
+ for (let i = 0; i < 5; i++) {
137
+ runtime.brainIntelligence.lifecycle({
138
+ action: 'start',
139
+ sessionId: `limit-${i}`,
140
+ });
141
+ }
142
+ const ctx = runtime.brainIntelligence.getSessionContext(2);
143
+ expect(ctx.recentSessions.length).toBe(2);
144
+ });
145
+
146
+ // ─── Archive Sessions ────────────────────────────────────────────
147
+
148
+ it('should archive old sessions', () => {
149
+ const db = runtime.vault.getDb();
150
+ // Insert an old session directly
151
+ db.prepare(
152
+ `INSERT INTO brain_sessions (id, started_at, ended_at, tools_used, files_modified)
153
+ VALUES (?, datetime('now', '-60 days'), datetime('now', '-59 days'), '[]', '[]')`,
154
+ ).run('old-session');
155
+
156
+ runtime.brainIntelligence.lifecycle({ action: 'start', sessionId: 'new-session' });
157
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'new-session' });
158
+
159
+ const result = runtime.brainIntelligence.archiveSessions(30);
160
+ expect(result.archived).toBe(1);
161
+
162
+ const stats = runtime.brainIntelligence.getStats();
163
+ expect(stats.sessions).toBe(1); // only new-session remains
164
+ });
165
+
166
+ it('should not archive active sessions', () => {
167
+ runtime.brainIntelligence.lifecycle({ action: 'start', sessionId: 'active' });
168
+ const result = runtime.brainIntelligence.archiveSessions(0);
169
+ expect(result.archived).toBe(0);
170
+ });
171
+
172
+ // ─── Strength Scoring ──────────────────────────────────────────
173
+
174
+ it('should compute strengths from feedback', () => {
175
+ // Seed vault and record feedback
176
+ runtime.vault.seed([
177
+ {
178
+ id: 's-1',
179
+ type: 'pattern',
180
+ domain: 'testing',
181
+ title: 'Test pattern',
182
+ severity: 'warning',
183
+ description: 'A test pattern.',
184
+ tags: ['test'],
185
+ },
186
+ ]);
187
+
188
+ runtime.brain.recordFeedback('test query', 's-1', 'accepted');
189
+ runtime.brain.recordFeedback('test query 2', 's-1', 'accepted');
190
+ runtime.brain.recordFeedback('test query 3', 's-1', 'dismissed');
191
+
192
+ const strengths = runtime.brainIntelligence.computeStrengths();
193
+ expect(strengths.length).toBe(1);
194
+ expect(strengths[0].pattern).toBe('Test pattern');
195
+ expect(strengths[0].domain).toBe('testing');
196
+ expect(strengths[0].usageCount).toBe(3);
197
+ expect(strengths[0].successRate).toBeCloseTo(2 / 3, 2);
198
+ expect(strengths[0].strength).toBeGreaterThan(0);
199
+ });
200
+
201
+ it('should return empty strengths with no feedback', () => {
202
+ const strengths = runtime.brainIntelligence.computeStrengths();
203
+ expect(strengths).toEqual([]);
204
+ });
205
+
206
+ it('should get strengths with filters', () => {
207
+ runtime.vault.seed([
208
+ {
209
+ id: 'sf-1',
210
+ type: 'pattern',
211
+ domain: 'd1',
212
+ title: 'P1',
213
+ severity: 'warning',
214
+ description: 'D',
215
+ tags: [],
216
+ },
217
+ {
218
+ id: 'sf-2',
219
+ type: 'pattern',
220
+ domain: 'd2',
221
+ title: 'P2',
222
+ severity: 'warning',
223
+ description: 'D',
224
+ tags: [],
225
+ },
226
+ ]);
227
+ runtime.brain.recordFeedback('q', 'sf-1', 'accepted');
228
+ runtime.brain.recordFeedback('q', 'sf-2', 'accepted');
229
+ runtime.brainIntelligence.computeStrengths();
230
+
231
+ const d1 = runtime.brainIntelligence.getStrengths({ domain: 'd1' });
232
+ expect(d1.length).toBe(1);
233
+ expect(d1[0].domain).toBe('d1');
234
+ });
235
+
236
+ // ─── Recommendations ──────────────────────────────────────────
237
+
238
+ it('should recommend patterns', () => {
239
+ runtime.vault.seed([
240
+ {
241
+ id: 'r-1',
242
+ type: 'pattern',
243
+ domain: 'testing',
244
+ title: 'Test pattern',
245
+ severity: 'warning',
246
+ description: 'D',
247
+ tags: [],
248
+ },
249
+ ]);
250
+ // Need enough feedback to reach minStrength threshold of 30
251
+ for (let i = 0; i < 10; i++) {
252
+ runtime.brain.recordFeedback(`q${i}`, 'r-1', 'accepted');
253
+ }
254
+ runtime.brainIntelligence.computeStrengths();
255
+
256
+ const recommendations = runtime.brainIntelligence.recommend({ domain: 'testing' });
257
+ expect(recommendations.length).toBeGreaterThan(0);
258
+ });
259
+
260
+ it('should boost recommendations by task context', () => {
261
+ runtime.vault.seed([
262
+ {
263
+ id: 'rb-1',
264
+ type: 'pattern',
265
+ domain: 'd',
266
+ title: 'Database migration pattern',
267
+ severity: 'warning',
268
+ description: 'D',
269
+ tags: [],
270
+ },
271
+ {
272
+ id: 'rb-2',
273
+ type: 'pattern',
274
+ domain: 'd',
275
+ title: 'API endpoint pattern',
276
+ severity: 'warning',
277
+ description: 'D',
278
+ tags: [],
279
+ },
280
+ ]);
281
+ for (let i = 0; i < 10; i++) {
282
+ runtime.brain.recordFeedback(`q${i}`, 'rb-1', 'accepted');
283
+ runtime.brain.recordFeedback(`q${i}`, 'rb-2', 'accepted');
284
+ }
285
+ runtime.brainIntelligence.computeStrengths();
286
+
287
+ const recs = runtime.brainIntelligence.recommend({ task: 'database migration' });
288
+ if (recs.length >= 2) {
289
+ // Database pattern should be boosted
290
+ expect(recs[0].pattern).toContain('Database');
291
+ }
292
+ });
293
+
294
+ // ─── Knowledge Extraction ─────────────────────────────────────
295
+
296
+ it('should extract knowledge from session with repeated tools', () => {
297
+ runtime.brainIntelligence.lifecycle({
298
+ action: 'start',
299
+ sessionId: 'extract-1',
300
+ toolsUsed: ['search', 'search', 'search', 'edit'],
301
+ });
302
+ runtime.brainIntelligence.lifecycle({
303
+ action: 'end',
304
+ sessionId: 'extract-1',
305
+ });
306
+
307
+ const result = runtime.brainIntelligence.extractKnowledge('extract-1');
308
+ expect(result.sessionId).toBe('extract-1');
309
+ expect(result.rulesApplied).toContain('repeated_tool_usage');
310
+ expect(result.proposals.length).toBeGreaterThan(0);
311
+ expect(result.proposals[0].rule).toBe('repeated_tool_usage');
312
+ });
313
+
314
+ it('should extract multi-file edit pattern', () => {
315
+ runtime.brainIntelligence.lifecycle({
316
+ action: 'start',
317
+ sessionId: 'extract-2',
318
+ filesModified: ['a.ts', 'b.ts', 'c.ts'],
319
+ });
320
+ runtime.brainIntelligence.lifecycle({
321
+ action: 'end',
322
+ sessionId: 'extract-2',
323
+ });
324
+
325
+ const result = runtime.brainIntelligence.extractKnowledge('extract-2');
326
+ expect(result.rulesApplied).toContain('multi_file_edit');
327
+ });
328
+
329
+ it('should extract plan completed workflow', () => {
330
+ runtime.brainIntelligence.lifecycle({
331
+ action: 'start',
332
+ sessionId: 'extract-3',
333
+ planId: 'plan-1',
334
+ });
335
+ runtime.brainIntelligence.lifecycle({
336
+ action: 'end',
337
+ sessionId: 'extract-3',
338
+ planOutcome: 'completed',
339
+ });
340
+
341
+ const result = runtime.brainIntelligence.extractKnowledge('extract-3');
342
+ expect(result.rulesApplied).toContain('plan_completed');
343
+ });
344
+
345
+ it('should extract plan abandoned anti-pattern', () => {
346
+ runtime.brainIntelligence.lifecycle({
347
+ action: 'start',
348
+ sessionId: 'extract-4',
349
+ planId: 'plan-2',
350
+ });
351
+ runtime.brainIntelligence.lifecycle({
352
+ action: 'end',
353
+ sessionId: 'extract-4',
354
+ planOutcome: 'abandoned',
355
+ });
356
+
357
+ const result = runtime.brainIntelligence.extractKnowledge('extract-4');
358
+ expect(result.rulesApplied).toContain('plan_abandoned');
359
+ });
360
+
361
+ it('should throw for non-existent session', () => {
362
+ expect(() => {
363
+ runtime.brainIntelligence.extractKnowledge('non-existent');
364
+ }).toThrow('Session not found');
365
+ });
366
+
367
+ // ─── Proposals ─────────────────────────────────────────────────
368
+
369
+ it('should list proposals', () => {
370
+ runtime.brainIntelligence.lifecycle({
371
+ action: 'start',
372
+ sessionId: 'prop-1',
373
+ toolsUsed: ['a', 'a', 'a'],
374
+ });
375
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'prop-1' });
376
+ runtime.brainIntelligence.extractKnowledge('prop-1');
377
+
378
+ const proposals = runtime.brainIntelligence.getProposals();
379
+ expect(proposals.length).toBeGreaterThan(0);
380
+ expect(proposals[0].promoted).toBe(false);
381
+ });
382
+
383
+ it('should filter proposals by session', () => {
384
+ runtime.brainIntelligence.lifecycle({
385
+ action: 'start',
386
+ sessionId: 'prop-s1',
387
+ toolsUsed: ['x', 'x', 'x'],
388
+ });
389
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'prop-s1' });
390
+ runtime.brainIntelligence.extractKnowledge('prop-s1');
391
+
392
+ runtime.brainIntelligence.lifecycle({
393
+ action: 'start',
394
+ sessionId: 'prop-s2',
395
+ toolsUsed: ['y', 'y', 'y'],
396
+ });
397
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'prop-s2' });
398
+ runtime.brainIntelligence.extractKnowledge('prop-s2');
399
+
400
+ const s1 = runtime.brainIntelligence.getProposals({ sessionId: 'prop-s1' });
401
+ const s2 = runtime.brainIntelligence.getProposals({ sessionId: 'prop-s2' });
402
+ expect(s1.every((p) => p.sessionId === 'prop-s1')).toBe(true);
403
+ expect(s2.every((p) => p.sessionId === 'prop-s2')).toBe(true);
404
+ });
405
+
406
+ it('should promote proposals to vault', () => {
407
+ runtime.brainIntelligence.lifecycle({
408
+ action: 'start',
409
+ sessionId: 'promo-1',
410
+ toolsUsed: ['z', 'z', 'z'],
411
+ });
412
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'promo-1' });
413
+ runtime.brainIntelligence.extractKnowledge('promo-1');
414
+
415
+ const proposals = runtime.brainIntelligence.getProposals({ sessionId: 'promo-1' });
416
+ expect(proposals.length).toBeGreaterThan(0);
417
+
418
+ const result = runtime.brainIntelligence.promoteProposals([proposals[0].id]);
419
+ expect(result.promoted).toBe(1);
420
+ expect(result.failed).toEqual([]);
421
+
422
+ // Check vault has the entry
423
+ const entry = runtime.vault.get(`proposal-${proposals[0].id}`);
424
+ expect(entry).not.toBeNull();
425
+ expect(entry!.domain).toBe('brain-intelligence');
426
+ });
427
+
428
+ it('should handle promoting non-existent proposal', () => {
429
+ const result = runtime.brainIntelligence.promoteProposals(['non-existent']);
430
+ expect(result.promoted).toBe(0);
431
+ expect(result.failed).toContain('non-existent');
432
+ });
433
+
434
+ it('should gate promote through governance when strict preset', () => {
435
+ // Create a brain session and extract proposals
436
+ runtime.brainIntelligence.lifecycle({
437
+ action: 'start',
438
+ sessionId: 'gov-promo-1',
439
+ toolsUsed: ['w', 'w', 'w'],
440
+ });
441
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'gov-promo-1' });
442
+ runtime.brainIntelligence.extractKnowledge('gov-promo-1');
443
+
444
+ const proposals = runtime.brainIntelligence.getProposals({ sessionId: 'gov-promo-1' });
445
+ expect(proposals.length).toBeGreaterThan(0);
446
+
447
+ // Apply strict preset — requireReview: true
448
+ runtime.governance.applyPreset('.', 'strict', 'test');
449
+
450
+ const result = runtime.brainIntelligence.promoteProposals(
451
+ [proposals[0].id],
452
+ runtime.governance,
453
+ '.',
454
+ );
455
+
456
+ // Should be gated, not promoted
457
+ expect(result.promoted).toBe(0);
458
+ expect(result.gated.length).toBe(1);
459
+ expect(result.gated[0].action).toBe('propose');
460
+
461
+ // Entry should NOT be in vault
462
+ expect(runtime.vault.get(`proposal-${proposals[0].id}`)).toBeNull();
463
+
464
+ // But should be in governance proposals
465
+ const pending = runtime.governance.listPendingProposals('.');
466
+ expect(pending.some((p) => p.source === 'brain-promote')).toBe(true);
467
+ });
468
+
469
+ // ─── Build Intelligence ────────────────────────────────────────
470
+
471
+ it('should build intelligence pipeline', () => {
472
+ runtime.vault.seed([
473
+ {
474
+ id: 'bi-1',
475
+ type: 'pattern',
476
+ domain: 'd1',
477
+ title: 'Pattern A',
478
+ severity: 'warning',
479
+ description: 'D',
480
+ tags: [],
481
+ },
482
+ ]);
483
+ runtime.brain.recordFeedback('q', 'bi-1', 'accepted');
484
+
485
+ const result = runtime.brainIntelligence.buildIntelligence();
486
+ expect(result.strengthsComputed).toBe(1);
487
+ expect(result.globalPatterns).toBe(1);
488
+ expect(result.domainProfiles).toBe(1);
489
+ });
490
+
491
+ it('should build with empty data', () => {
492
+ const result = runtime.brainIntelligence.buildIntelligence();
493
+ expect(result.strengthsComputed).toBe(0);
494
+ expect(result.globalPatterns).toBe(0);
495
+ expect(result.domainProfiles).toBe(0);
496
+ });
497
+
498
+ // ─── Global Patterns ──────────────────────────────────────────
499
+
500
+ it('should return global patterns after build', () => {
501
+ runtime.vault.seed([
502
+ {
503
+ id: 'gp-1',
504
+ type: 'pattern',
505
+ domain: 'd1',
506
+ title: 'Cross Pattern',
507
+ severity: 'warning',
508
+ description: 'D',
509
+ tags: [],
510
+ },
511
+ ]);
512
+ runtime.brain.recordFeedback('q', 'gp-1', 'accepted');
513
+ runtime.brainIntelligence.buildIntelligence();
514
+
515
+ const patterns = runtime.brainIntelligence.getGlobalPatterns();
516
+ expect(patterns.length).toBe(1);
517
+ expect(patterns[0].pattern).toBe('Cross Pattern');
518
+ expect(patterns[0].domains).toContain('d1');
519
+ });
520
+
521
+ // ─── Domain Profiles ──────────────────────────────────────────
522
+
523
+ it('should return domain profile after build', () => {
524
+ runtime.vault.seed([
525
+ {
526
+ id: 'dp-1',
527
+ type: 'pattern',
528
+ domain: 'security',
529
+ title: 'Security Pattern',
530
+ severity: 'warning',
531
+ description: 'D',
532
+ tags: [],
533
+ },
534
+ ]);
535
+ runtime.brain.recordFeedback('q', 'dp-1', 'accepted');
536
+ runtime.brainIntelligence.buildIntelligence();
537
+
538
+ const profile = runtime.brainIntelligence.getDomainProfile('security');
539
+ expect(profile).not.toBeNull();
540
+ expect(profile!.domain).toBe('security');
541
+ expect(profile!.topPatterns.length).toBe(1);
542
+ });
543
+
544
+ it('should return null for unknown domain', () => {
545
+ const profile = runtime.brainIntelligence.getDomainProfile('nonexistent');
546
+ expect(profile).toBeNull();
547
+ });
548
+
549
+ // ─── Export / Import ──────────────────────────────────────────
550
+
551
+ it('should export all data', () => {
552
+ runtime.brainIntelligence.lifecycle({
553
+ action: 'start',
554
+ sessionId: 'exp-1',
555
+ domain: 'testing',
556
+ });
557
+ runtime.brainIntelligence.lifecycle({
558
+ action: 'end',
559
+ sessionId: 'exp-1',
560
+ });
561
+
562
+ const data = runtime.brainIntelligence.exportData();
563
+ expect(data.sessions.length).toBe(1);
564
+ expect(data.exportedAt).toBeDefined();
565
+ });
566
+
567
+ it('should import data', () => {
568
+ const data: BrainExportData = {
569
+ strengths: [
570
+ {
571
+ pattern: 'Imported Pattern',
572
+ domain: 'testing',
573
+ strength: 75,
574
+ usageScore: 20,
575
+ spreadScore: 15,
576
+ successScore: 20,
577
+ recencyScore: 20,
578
+ usageCount: 8,
579
+ uniqueContexts: 3,
580
+ successRate: 0.8,
581
+ lastUsed: new Date().toISOString(),
582
+ },
583
+ ],
584
+ sessions: [
585
+ {
586
+ id: 'imp-s1',
587
+ startedAt: new Date().toISOString(),
588
+ endedAt: new Date().toISOString(),
589
+ domain: 'testing',
590
+ context: 'import test',
591
+ toolsUsed: ['search'],
592
+ filesModified: [],
593
+ planId: null,
594
+ planOutcome: null,
595
+ extractedAt: null,
596
+ },
597
+ ],
598
+ proposals: [],
599
+ globalPatterns: [
600
+ {
601
+ pattern: 'Imported Global',
602
+ domains: ['testing'],
603
+ totalStrength: 75,
604
+ avgStrength: 75,
605
+ domainCount: 1,
606
+ },
607
+ ],
608
+ domainProfiles: [
609
+ {
610
+ domain: 'testing',
611
+ topPatterns: [{ pattern: 'Imported', strength: 75 }],
612
+ sessionCount: 1,
613
+ avgSessionDuration: 5,
614
+ lastActivity: new Date().toISOString(),
615
+ },
616
+ ],
617
+ exportedAt: new Date().toISOString(),
618
+ };
619
+
620
+ const result = runtime.brainIntelligence.importData(data);
621
+ expect(result.imported.strengths).toBe(1);
622
+ expect(result.imported.sessions).toBe(1);
623
+ expect(result.imported.globalPatterns).toBe(1);
624
+ expect(result.imported.domainProfiles).toBe(1);
625
+
626
+ // Verify imported data is accessible
627
+ const strengths = runtime.brainIntelligence.getStrengths();
628
+ expect(strengths.length).toBe(1);
629
+ expect(strengths[0].pattern).toBe('Imported Pattern');
630
+ });
631
+
632
+ it('should handle duplicate session import gracefully', () => {
633
+ runtime.brainIntelligence.lifecycle({ action: 'start', sessionId: 'dup-1' });
634
+
635
+ const data: BrainExportData = {
636
+ strengths: [],
637
+ sessions: [
638
+ {
639
+ id: 'dup-1',
640
+ startedAt: new Date().toISOString(),
641
+ endedAt: null,
642
+ domain: null,
643
+ context: null,
644
+ toolsUsed: [],
645
+ filesModified: [],
646
+ planId: null,
647
+ planOutcome: null,
648
+ extractedAt: null,
649
+ },
650
+ ],
651
+ proposals: [],
652
+ globalPatterns: [],
653
+ domainProfiles: [],
654
+ exportedAt: new Date().toISOString(),
655
+ };
656
+
657
+ const result = runtime.brainIntelligence.importData(data);
658
+ expect(result.imported.sessions).toBe(0); // Duplicate ignored
659
+ });
660
+
661
+ // ─── Auto-extraction on session end ─────────────────────────
662
+
663
+ it('should auto-extract when session ends with tools used', () => {
664
+ runtime.brainIntelligence.lifecycle({
665
+ action: 'start',
666
+ sessionId: 'auto-1',
667
+ domain: 'testing',
668
+ toolsUsed: ['search', 'search', 'search'],
669
+ });
670
+ const session = runtime.brainIntelligence.lifecycle({
671
+ action: 'end',
672
+ sessionId: 'auto-1',
673
+ });
674
+ // extractedAt should be set automatically
675
+ expect(session.extractedAt).not.toBeNull();
676
+ });
677
+
678
+ it('should not auto-extract when session has no signal', () => {
679
+ runtime.brainIntelligence.lifecycle({
680
+ action: 'start',
681
+ sessionId: 'auto-2',
682
+ });
683
+ const session = runtime.brainIntelligence.lifecycle({
684
+ action: 'end',
685
+ sessionId: 'auto-2',
686
+ });
687
+ // No tools, no files, no plan — should not extract
688
+ expect(session.extractedAt).toBeNull();
689
+ });
690
+
691
+ it('should auto-extract when session has a plan', () => {
692
+ runtime.brainIntelligence.lifecycle({
693
+ action: 'start',
694
+ sessionId: 'auto-3',
695
+ planId: 'plan-123',
696
+ });
697
+ const session = runtime.brainIntelligence.lifecycle({
698
+ action: 'end',
699
+ sessionId: 'auto-3',
700
+ planOutcome: 'completed',
701
+ });
702
+ expect(session.extractedAt).not.toBeNull();
703
+ });
704
+
705
+ // ─── extractedAt Tracking ──────────────────────────────────
706
+
707
+ it('should set extractedAt when extractKnowledge is called manually', () => {
708
+ // Use a session with no tools/files/plan so auto-extraction doesn't fire
709
+ runtime.brainIntelligence.lifecycle({
710
+ action: 'start',
711
+ sessionId: 'ext-1',
712
+ domain: 'testing',
713
+ });
714
+ runtime.brainIntelligence.lifecycle({
715
+ action: 'end',
716
+ sessionId: 'ext-1',
717
+ });
718
+
719
+ // Before manual extraction, extractedAt should be null (no auto-extract — no signal)
720
+ const ctx = runtime.brainIntelligence.getSessionContext(10);
721
+ const before = ctx.recentSessions.find((s) => s.id === 'ext-1');
722
+ expect(before?.extractedAt).toBeNull();
723
+
724
+ // Manual extract
725
+ runtime.brainIntelligence.extractKnowledge('ext-1');
726
+
727
+ // After extraction, extractedAt should be set
728
+ const ctx2 = runtime.brainIntelligence.getSessionContext(10);
729
+ const after = ctx2.recentSessions.find((s) => s.id === 'ext-1');
730
+ expect(after?.extractedAt).not.toBeNull();
731
+ });
732
+
733
+ // ─── resetExtracted ────────────────────────────────────────
734
+
735
+ it('should reset extractedAt for a specific session', () => {
736
+ runtime.brainIntelligence.lifecycle({
737
+ action: 'start',
738
+ sessionId: 'reset-1',
739
+ toolsUsed: ['a', 'a', 'a'],
740
+ });
741
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'reset-1' });
742
+ runtime.brainIntelligence.extractKnowledge('reset-1');
743
+
744
+ const result = runtime.brainIntelligence.resetExtracted({ sessionId: 'reset-1' });
745
+ expect(result.reset).toBe(1);
746
+
747
+ const ctx = runtime.brainIntelligence.getSessionContext(10);
748
+ const session = ctx.recentSessions.find((s) => s.id === 'reset-1');
749
+ expect(session?.extractedAt).toBeNull();
750
+ });
751
+
752
+ it('should reset all extracted sessions', () => {
753
+ runtime.brainIntelligence.lifecycle({
754
+ action: 'start',
755
+ sessionId: 'reset-a',
756
+ toolsUsed: ['a', 'a', 'a'],
757
+ });
758
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'reset-a' });
759
+ runtime.brainIntelligence.extractKnowledge('reset-a');
760
+
761
+ runtime.brainIntelligence.lifecycle({
762
+ action: 'start',
763
+ sessionId: 'reset-b',
764
+ toolsUsed: ['b', 'b', 'b'],
765
+ });
766
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'reset-b' });
767
+ runtime.brainIntelligence.extractKnowledge('reset-b');
768
+
769
+ const result = runtime.brainIntelligence.resetExtracted({ all: true });
770
+ expect(result.reset).toBe(2);
771
+ });
772
+
773
+ it('should return 0 when nothing to reset', () => {
774
+ const result = runtime.brainIntelligence.resetExtracted({ all: true });
775
+ expect(result.reset).toBe(0);
776
+ });
777
+
778
+ it('should return 0 when no filter is provided', () => {
779
+ const result = runtime.brainIntelligence.resetExtracted();
780
+ expect(result.reset).toBe(0);
781
+ });
782
+
783
+ it('should allow re-extraction after reset', () => {
784
+ runtime.brainIntelligence.lifecycle({
785
+ action: 'start',
786
+ sessionId: 'reext-1',
787
+ toolsUsed: ['search', 'search', 'search'],
788
+ });
789
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'reext-1' });
790
+
791
+ const first = runtime.brainIntelligence.extractKnowledge('reext-1');
792
+ expect(first.proposals.length).toBeGreaterThan(0);
793
+
794
+ runtime.brainIntelligence.resetExtracted({ sessionId: 'reext-1' });
795
+
796
+ const second = runtime.brainIntelligence.extractKnowledge('reext-1');
797
+ expect(second.proposals.length).toBeGreaterThan(0);
798
+ });
799
+
800
+ // ─── computeStrengths with modified feedback ───────────────
801
+
802
+ it('should weight modified feedback at 0.5 in computeStrengths', () => {
803
+ // Seed an entry for feedback to reference
804
+ runtime.vault.seed([
805
+ {
806
+ id: 'str-1',
807
+ type: 'pattern',
808
+ domain: 'testing',
809
+ title: 'Strength test pattern',
810
+ severity: 'warning',
811
+ description: 'Testing strength computation with modified feedback.',
812
+ tags: ['test'],
813
+ },
814
+ ]);
815
+
816
+ // Record feedback: 1 accepted, 1 modified, 1 failed
817
+ runtime.brain.recordFeedback({ query: 'q1', entryId: 'str-1', action: 'accepted' });
818
+ runtime.brain.recordFeedback({ query: 'q2', entryId: 'str-1', action: 'modified' });
819
+ runtime.brain.recordFeedback({ query: 'q3', entryId: 'str-1', action: 'failed' });
820
+
821
+ const strengths = runtime.brainIntelligence.computeStrengths();
822
+ const s = strengths.find((p) => p.pattern === 'Strength test pattern');
823
+ expect(s).toBeDefined();
824
+
825
+ // successRate = (1 + 0.5) / (3 - 1) = 1.5/2 = 0.75
826
+ expect(s!.successRate).toBeCloseTo(0.75, 2);
827
+ });
828
+ });