@soleri/core 0.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brain/brain.d.ts +85 -0
- package/dist/brain/brain.d.ts.map +1 -0
- package/dist/brain/brain.js +506 -0
- package/dist/brain/brain.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 +289 -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/facades/facade-factory.d.ts +5 -0
- package/dist/facades/facade-factory.d.ts.map +1 -0
- package/dist/facades/facade-factory.js +49 -0
- package/dist/facades/facade-factory.js.map +1 -0
- package/dist/facades/types.d.ts +42 -0
- package/dist/facades/types.d.ts.map +1 -0
- package/dist/facades/types.js +6 -0
- package/dist/facades/types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/intelligence/loader.d.ts +3 -0
- package/dist/intelligence/loader.d.ts.map +1 -0
- package/dist/intelligence/loader.js +41 -0
- package/dist/intelligence/loader.js.map +1 -0
- package/dist/intelligence/types.d.ts +20 -0
- package/dist/intelligence/types.d.ts.map +1 -0
- package/dist/intelligence/types.js +2 -0
- package/dist/intelligence/types.js.map +1 -0
- package/dist/llm/key-pool.d.ts +38 -0
- package/dist/llm/key-pool.d.ts.map +1 -0
- package/dist/llm/key-pool.js +154 -0
- package/dist/llm/key-pool.js.map +1 -0
- package/dist/llm/types.d.ts +80 -0
- package/dist/llm/types.d.ts.map +1 -0
- package/dist/llm/types.js +37 -0
- package/dist/llm/types.js.map +1 -0
- package/dist/llm/utils.d.ts +26 -0
- package/dist/llm/utils.d.ts.map +1 -0
- package/dist/llm/utils.js +197 -0
- package/dist/llm/utils.js.map +1 -0
- package/dist/planning/planner.d.ts +48 -0
- package/dist/planning/planner.d.ts.map +1 -0
- package/dist/planning/planner.js +109 -0
- package/dist/planning/planner.js.map +1 -0
- package/dist/vault/vault.d.ts +80 -0
- package/dist/vault/vault.d.ts.map +1 -0
- package/dist/vault/vault.js +353 -0
- package/dist/vault/vault.js.map +1 -0
- package/package.json +56 -4
- package/src/__tests__/brain.test.ts +740 -0
- package/src/__tests__/cognee-client.test.ts +524 -0
- package/src/__tests__/llm.test.ts +556 -0
- package/src/__tests__/loader.test.ts +176 -0
- package/src/__tests__/planner.test.ts +261 -0
- package/src/__tests__/vault.test.ts +494 -0
- package/src/brain/brain.ts +678 -0
- package/src/cognee/client.ts +350 -0
- package/src/cognee/types.ts +62 -0
- package/src/facades/facade-factory.ts +64 -0
- package/src/facades/types.ts +42 -0
- package/src/index.ts +75 -0
- package/src/intelligence/loader.ts +42 -0
- package/src/intelligence/types.ts +20 -0
- package/src/llm/key-pool.ts +190 -0
- package/src/llm/types.ts +116 -0
- package/src/llm/utils.ts +248 -0
- package/src/planning/planner.ts +151 -0
- package/src/vault/vault.ts +455 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { Vault } from '../vault/vault.js';
|
|
3
|
+
import { Brain } from '../brain/brain.js';
|
|
4
|
+
import type { IntelligenceEntry } from '../intelligence/types.js';
|
|
5
|
+
import type { CogneeClient } from '../cognee/client.js';
|
|
6
|
+
import type { CogneeSearchResult, CogneeStatus } from '../cognee/types.js';
|
|
7
|
+
|
|
8
|
+
function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntry {
|
|
9
|
+
return {
|
|
10
|
+
id: overrides.id ?? 'test-1',
|
|
11
|
+
type: overrides.type ?? 'pattern',
|
|
12
|
+
domain: overrides.domain ?? 'testing',
|
|
13
|
+
title: overrides.title ?? 'Test Pattern',
|
|
14
|
+
severity: overrides.severity ?? 'warning',
|
|
15
|
+
description: overrides.description ?? 'A test pattern for unit tests.',
|
|
16
|
+
tags: overrides.tags ?? ['testing', 'assertions'],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeMockCognee(
|
|
21
|
+
overrides: {
|
|
22
|
+
available?: boolean;
|
|
23
|
+
searchResults?: CogneeSearchResult[];
|
|
24
|
+
searchError?: boolean;
|
|
25
|
+
} = {},
|
|
26
|
+
): CogneeClient {
|
|
27
|
+
const available = overrides.available ?? true;
|
|
28
|
+
return {
|
|
29
|
+
get isAvailable() {
|
|
30
|
+
return available;
|
|
31
|
+
},
|
|
32
|
+
search: overrides.searchError
|
|
33
|
+
? vi.fn().mockRejectedValue(new Error('timeout'))
|
|
34
|
+
: vi.fn().mockResolvedValue(overrides.searchResults ?? []),
|
|
35
|
+
addEntries: vi.fn().mockResolvedValue({ added: 0 }),
|
|
36
|
+
cognify: vi.fn().mockResolvedValue({ status: 'ok' }),
|
|
37
|
+
healthCheck: vi
|
|
38
|
+
.fn()
|
|
39
|
+
.mockResolvedValue({ available, url: 'http://localhost:8000', latencyMs: 1 } as CogneeStatus),
|
|
40
|
+
getConfig: vi.fn().mockReturnValue({
|
|
41
|
+
baseUrl: 'http://localhost:8000',
|
|
42
|
+
dataset: 'vault',
|
|
43
|
+
timeoutMs: 5000,
|
|
44
|
+
healthCacheTtlMs: 60000,
|
|
45
|
+
}),
|
|
46
|
+
getStatus: vi.fn().mockReturnValue(null),
|
|
47
|
+
} as unknown as CogneeClient;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('Brain', () => {
|
|
51
|
+
let vault: Vault;
|
|
52
|
+
let brain: Brain;
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
vault = new Vault(':memory:');
|
|
56
|
+
brain = new Brain(vault);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
vault.close();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ─── Constructor ──────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe('constructor', () => {
|
|
66
|
+
it('should create brain with empty vocabulary on empty vault', () => {
|
|
67
|
+
expect(brain.getVocabularySize()).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should build vocabulary from existing entries', () => {
|
|
71
|
+
vault.seed([
|
|
72
|
+
makeEntry({
|
|
73
|
+
id: 'v1',
|
|
74
|
+
title: 'Input validation pattern',
|
|
75
|
+
description: 'Always validate user input at boundaries.',
|
|
76
|
+
tags: ['validation', 'security'],
|
|
77
|
+
}),
|
|
78
|
+
makeEntry({
|
|
79
|
+
id: 'v2',
|
|
80
|
+
title: 'Caching strategy',
|
|
81
|
+
description: 'Use cache-aside for read-heavy workloads.',
|
|
82
|
+
tags: ['caching', 'performance'],
|
|
83
|
+
}),
|
|
84
|
+
]);
|
|
85
|
+
const brain2 = new Brain(vault);
|
|
86
|
+
expect(brain2.getVocabularySize()).toBeGreaterThan(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should accept optional CogneeClient', () => {
|
|
90
|
+
const cognee = makeMockCognee();
|
|
91
|
+
const brain2 = new Brain(vault, cognee);
|
|
92
|
+
expect(brain2.getVocabularySize()).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── Intelligent Search ──────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe('intelligentSearch', () => {
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
vault.seed([
|
|
101
|
+
makeEntry({
|
|
102
|
+
id: 'is-1',
|
|
103
|
+
title: 'Input validation pattern',
|
|
104
|
+
description:
|
|
105
|
+
'Always validate user input at system boundaries to prevent injection attacks.',
|
|
106
|
+
domain: 'security',
|
|
107
|
+
severity: 'critical',
|
|
108
|
+
tags: ['validation', 'security', 'input'],
|
|
109
|
+
}),
|
|
110
|
+
makeEntry({
|
|
111
|
+
id: 'is-2',
|
|
112
|
+
title: 'Caching strategy for APIs',
|
|
113
|
+
description: 'Use cache-aside pattern for read-heavy API workloads.',
|
|
114
|
+
domain: 'performance',
|
|
115
|
+
severity: 'warning',
|
|
116
|
+
tags: ['caching', 'api', 'performance'],
|
|
117
|
+
}),
|
|
118
|
+
makeEntry({
|
|
119
|
+
id: 'is-3',
|
|
120
|
+
title: 'Error handling best practices',
|
|
121
|
+
description: 'Use typed errors with context for better debugging experience.',
|
|
122
|
+
domain: 'clean-code',
|
|
123
|
+
severity: 'suggestion',
|
|
124
|
+
tags: ['errors', 'debugging'],
|
|
125
|
+
}),
|
|
126
|
+
]);
|
|
127
|
+
brain = new Brain(vault);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return ranked results', async () => {
|
|
131
|
+
const results = await brain.intelligentSearch('validation input');
|
|
132
|
+
expect(results.length).toBeGreaterThan(0);
|
|
133
|
+
expect(results[0].entry.id).toBe('is-1');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should include score breakdown with vector field', async () => {
|
|
137
|
+
const results = await brain.intelligentSearch('validation');
|
|
138
|
+
expect(results.length).toBeGreaterThan(0);
|
|
139
|
+
const breakdown = results[0].breakdown;
|
|
140
|
+
expect(breakdown).toHaveProperty('semantic');
|
|
141
|
+
expect(breakdown).toHaveProperty('vector');
|
|
142
|
+
expect(breakdown).toHaveProperty('severity');
|
|
143
|
+
expect(breakdown).toHaveProperty('recency');
|
|
144
|
+
expect(breakdown).toHaveProperty('tagOverlap');
|
|
145
|
+
expect(breakdown).toHaveProperty('domainMatch');
|
|
146
|
+
expect(breakdown).toHaveProperty('total');
|
|
147
|
+
expect(breakdown.total).toBe(results[0].score);
|
|
148
|
+
// Without cognee, vector should be 0
|
|
149
|
+
expect(breakdown.vector).toBe(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should return empty array for no matches', async () => {
|
|
153
|
+
const results = await brain.intelligentSearch('xyznonexistent');
|
|
154
|
+
expect(results).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should respect limit', async () => {
|
|
158
|
+
const results = await brain.intelligentSearch('pattern', { limit: 1 });
|
|
159
|
+
expect(results.length).toBeLessThanOrEqual(1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should filter by domain', async () => {
|
|
163
|
+
const results = await brain.intelligentSearch('pattern', { domain: 'security' });
|
|
164
|
+
expect(results.every((r) => r.entry.domain === 'security')).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should boost domain matches when domain is specified', async () => {
|
|
168
|
+
const withDomain = await brain.intelligentSearch('pattern', { domain: 'security' });
|
|
169
|
+
if (withDomain.length > 0) {
|
|
170
|
+
expect(withDomain[0].breakdown.domainMatch).toBe(1.0);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should boost severity in scoring', async () => {
|
|
175
|
+
const results = await brain.intelligentSearch('pattern');
|
|
176
|
+
if (results.length >= 2) {
|
|
177
|
+
const critical = results.find((r) => r.entry.severity === 'critical');
|
|
178
|
+
const suggestion = results.find((r) => r.entry.severity === 'suggestion');
|
|
179
|
+
if (critical && suggestion) {
|
|
180
|
+
expect(critical.breakdown.severity).toBeGreaterThan(suggestion.breakdown.severity);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should boost tag overlap when tags provided', async () => {
|
|
186
|
+
const results = await brain.intelligentSearch('pattern', {
|
|
187
|
+
tags: ['validation', 'security'],
|
|
188
|
+
});
|
|
189
|
+
if (results.length > 0) {
|
|
190
|
+
const secEntry = results.find((r) => r.entry.id === 'is-1');
|
|
191
|
+
if (secEntry) {
|
|
192
|
+
expect(secEntry.breakdown.tagOverlap).toBeGreaterThan(0);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should handle search on empty vault gracefully', async () => {
|
|
198
|
+
const emptyVault = new Vault(':memory:');
|
|
199
|
+
const emptyBrain = new Brain(emptyVault);
|
|
200
|
+
const results = await emptyBrain.intelligentSearch('anything');
|
|
201
|
+
expect(results).toEqual([]);
|
|
202
|
+
emptyVault.close();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ─── Hybrid Search (with Cognee) ──────────────────────────────
|
|
207
|
+
|
|
208
|
+
describe('hybrid search with Cognee', () => {
|
|
209
|
+
beforeEach(() => {
|
|
210
|
+
vault.seed([
|
|
211
|
+
makeEntry({
|
|
212
|
+
id: 'hs-1',
|
|
213
|
+
title: 'Authentication flow',
|
|
214
|
+
description: 'JWT-based authentication for API endpoints.',
|
|
215
|
+
domain: 'security',
|
|
216
|
+
severity: 'critical',
|
|
217
|
+
tags: ['auth', 'jwt'],
|
|
218
|
+
}),
|
|
219
|
+
makeEntry({
|
|
220
|
+
id: 'hs-2',
|
|
221
|
+
title: 'Logging best practices',
|
|
222
|
+
description: 'Structured logging with correlation IDs for debugging.',
|
|
223
|
+
domain: 'observability',
|
|
224
|
+
severity: 'warning',
|
|
225
|
+
tags: ['logging', 'debugging'],
|
|
226
|
+
}),
|
|
227
|
+
]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should include vector scores from Cognee in breakdown', async () => {
|
|
231
|
+
const cognee = makeMockCognee({
|
|
232
|
+
searchResults: [{ id: 'hs-1', score: 0.92, text: 'Auth flow', searchType: 'INSIGHTS' }],
|
|
233
|
+
});
|
|
234
|
+
const hybridBrain = new Brain(vault, cognee);
|
|
235
|
+
const results = await hybridBrain.intelligentSearch('authentication');
|
|
236
|
+
expect(results.length).toBeGreaterThan(0);
|
|
237
|
+
const authResult = results.find((r) => r.entry.id === 'hs-1');
|
|
238
|
+
expect(authResult).toBeDefined();
|
|
239
|
+
expect(authResult!.breakdown.vector).toBe(0.92);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should use cognee-aware weights when vector results are present', async () => {
|
|
243
|
+
const cognee = makeMockCognee({
|
|
244
|
+
searchResults: [{ id: 'hs-1', score: 0.9, text: 'Auth', searchType: 'INSIGHTS' }],
|
|
245
|
+
});
|
|
246
|
+
const hybridBrain = new Brain(vault, cognee);
|
|
247
|
+
const results = await hybridBrain.intelligentSearch('authentication');
|
|
248
|
+
// With cognee weights, vector contributes significantly
|
|
249
|
+
const authResult = results.find((r) => r.entry.id === 'hs-1');
|
|
250
|
+
expect(authResult).toBeDefined();
|
|
251
|
+
// vector=0.9 * weight=0.35 = 0.315 contribution
|
|
252
|
+
expect(authResult!.breakdown.vector).toBe(0.9);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should merge cognee-only entries into results', async () => {
|
|
256
|
+
// hs-2 may not match FTS5 for "authentication" but Cognee finds it via semantic similarity
|
|
257
|
+
const cognee = makeMockCognee({
|
|
258
|
+
searchResults: [
|
|
259
|
+
{ id: 'hs-1', score: 0.95, text: 'Auth', searchType: 'INSIGHTS' },
|
|
260
|
+
{ id: 'hs-2', score: 0.6, text: 'Logging', searchType: 'INSIGHTS' },
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
const hybridBrain = new Brain(vault, cognee);
|
|
264
|
+
const results = await hybridBrain.intelligentSearch('authentication');
|
|
265
|
+
// Both entries should be in results (hs-2 merged from Cognee even if not in FTS5)
|
|
266
|
+
const ids = results.map((r) => r.entry.id);
|
|
267
|
+
expect(ids).toContain('hs-1');
|
|
268
|
+
expect(ids).toContain('hs-2');
|
|
269
|
+
const loggingResult = results.find((r) => r.entry.id === 'hs-2');
|
|
270
|
+
expect(loggingResult).toBeDefined();
|
|
271
|
+
expect(loggingResult!.breakdown.vector).toBe(0.6);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should fall back to FTS5-only on Cognee search error', async () => {
|
|
275
|
+
const cognee = makeMockCognee({ searchError: true });
|
|
276
|
+
const hybridBrain = new Brain(vault, cognee);
|
|
277
|
+
const results = await hybridBrain.intelligentSearch('authentication');
|
|
278
|
+
// Should still work, just without vector scores
|
|
279
|
+
for (const r of results) {
|
|
280
|
+
expect(r.breakdown.vector).toBe(0);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should work without Cognee (backward compatible)', async () => {
|
|
285
|
+
const noCogneeBrain = new Brain(vault);
|
|
286
|
+
const results = await noCogneeBrain.intelligentSearch('authentication');
|
|
287
|
+
for (const r of results) {
|
|
288
|
+
expect(r.breakdown.vector).toBe(0);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should handle unavailable Cognee gracefully', async () => {
|
|
293
|
+
const cognee = makeMockCognee({ available: false });
|
|
294
|
+
const hybridBrain = new Brain(vault, cognee);
|
|
295
|
+
const results = await hybridBrain.intelligentSearch('authentication');
|
|
296
|
+
for (const r of results) {
|
|
297
|
+
expect(r.breakdown.vector).toBe(0);
|
|
298
|
+
}
|
|
299
|
+
// search should not have been called
|
|
300
|
+
expect(cognee.search).not.toHaveBeenCalled();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ─── syncToCognee ──────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
describe('syncToCognee', () => {
|
|
307
|
+
it('should return 0 when Cognee not available', async () => {
|
|
308
|
+
const result = await brain.syncToCognee();
|
|
309
|
+
expect(result).toEqual({ synced: 0, cognified: false });
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should sync all entries and cognify', async () => {
|
|
313
|
+
vault.seed([makeEntry({ id: 'sync-1' }), makeEntry({ id: 'sync-2' })]);
|
|
314
|
+
const cognee = makeMockCognee();
|
|
315
|
+
(cognee.addEntries as ReturnType<typeof vi.fn>).mockResolvedValue({ added: 2 });
|
|
316
|
+
const hybridBrain = new Brain(vault, cognee);
|
|
317
|
+
const result = await hybridBrain.syncToCognee();
|
|
318
|
+
expect(result.synced).toBe(2);
|
|
319
|
+
expect(result.cognified).toBe(true);
|
|
320
|
+
expect(cognee.addEntries).toHaveBeenCalledTimes(1);
|
|
321
|
+
expect(cognee.cognify).toHaveBeenCalledTimes(1);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should skip cognify when no entries added', async () => {
|
|
325
|
+
const cognee = makeMockCognee();
|
|
326
|
+
const hybridBrain = new Brain(vault, cognee);
|
|
327
|
+
const result = await hybridBrain.syncToCognee();
|
|
328
|
+
expect(result.synced).toBe(0);
|
|
329
|
+
expect(result.cognified).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// ─── Enrich and Capture ─────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
describe('enrichAndCapture', () => {
|
|
336
|
+
it('should capture entry and return auto-tags', () => {
|
|
337
|
+
const result = brain.enrichAndCapture({
|
|
338
|
+
id: 'cap-1',
|
|
339
|
+
type: 'pattern',
|
|
340
|
+
domain: 'security',
|
|
341
|
+
title: 'SQL injection prevention',
|
|
342
|
+
severity: 'critical',
|
|
343
|
+
description:
|
|
344
|
+
'Always use parameterized queries to prevent SQL injection attacks on database.',
|
|
345
|
+
tags: [],
|
|
346
|
+
});
|
|
347
|
+
expect(result.captured).toBe(true);
|
|
348
|
+
expect(result.id).toBe('cap-1');
|
|
349
|
+
expect(result.autoTags.length).toBeGreaterThan(0);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should merge auto-tags with user-provided tags', () => {
|
|
353
|
+
const result = brain.enrichAndCapture({
|
|
354
|
+
id: 'cap-2',
|
|
355
|
+
type: 'pattern',
|
|
356
|
+
domain: 'security',
|
|
357
|
+
title: 'XSS prevention methods',
|
|
358
|
+
severity: 'critical',
|
|
359
|
+
description:
|
|
360
|
+
'Sanitize all user input before rendering in the browser to prevent cross-site scripting.',
|
|
361
|
+
tags: ['user-tag'],
|
|
362
|
+
});
|
|
363
|
+
expect(result.captured).toBe(true);
|
|
364
|
+
const entry = vault.get('cap-2');
|
|
365
|
+
expect(entry).not.toBeNull();
|
|
366
|
+
expect(entry!.tags).toContain('user-tag');
|
|
367
|
+
expect(entry!.tags.length).toBeGreaterThan(1);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should store entry in vault', () => {
|
|
371
|
+
brain.enrichAndCapture({
|
|
372
|
+
id: 'cap-3',
|
|
373
|
+
type: 'rule',
|
|
374
|
+
domain: 'testing',
|
|
375
|
+
title: 'Always test edge cases',
|
|
376
|
+
severity: 'warning',
|
|
377
|
+
description: 'Write tests for boundary values, null inputs, and error conditions.',
|
|
378
|
+
tags: ['testing'],
|
|
379
|
+
});
|
|
380
|
+
const entry = vault.get('cap-3');
|
|
381
|
+
expect(entry).not.toBeNull();
|
|
382
|
+
expect(entry!.title).toBe('Always test edge cases');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should update vocabulary incrementally after capture', () => {
|
|
386
|
+
const sizeBefore = brain.getVocabularySize();
|
|
387
|
+
brain.enrichAndCapture({
|
|
388
|
+
id: 'cap-4',
|
|
389
|
+
type: 'pattern',
|
|
390
|
+
domain: 'performance',
|
|
391
|
+
title: 'Connection pooling optimization',
|
|
392
|
+
severity: 'warning',
|
|
393
|
+
description:
|
|
394
|
+
'Use connection pooling for database connections to reduce overhead and improve throughput.',
|
|
395
|
+
tags: ['database', 'performance'],
|
|
396
|
+
});
|
|
397
|
+
expect(brain.getVocabularySize()).toBeGreaterThan(sizeBefore);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should capture entry without tags and auto-generate them', () => {
|
|
401
|
+
const result = brain.enrichAndCapture({
|
|
402
|
+
id: 'cap-5',
|
|
403
|
+
type: 'anti-pattern',
|
|
404
|
+
domain: 'clean-code',
|
|
405
|
+
title: 'Deeply nested conditionals',
|
|
406
|
+
severity: 'warning',
|
|
407
|
+
description:
|
|
408
|
+
'Avoid deeply nested if-else blocks. Use early returns and guard clauses instead.',
|
|
409
|
+
tags: [],
|
|
410
|
+
});
|
|
411
|
+
expect(result.captured).toBe(true);
|
|
412
|
+
expect(result.autoTags.length).toBeGreaterThan(0);
|
|
413
|
+
const entry = vault.get('cap-5');
|
|
414
|
+
expect(entry!.tags.length).toBeGreaterThan(0);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should fire-and-forget sync to Cognee on capture', () => {
|
|
418
|
+
const cognee = makeMockCognee();
|
|
419
|
+
const hybridBrain = new Brain(vault, cognee);
|
|
420
|
+
hybridBrain.enrichAndCapture({
|
|
421
|
+
id: 'cap-cognee-1',
|
|
422
|
+
type: 'pattern',
|
|
423
|
+
domain: 'testing',
|
|
424
|
+
title: 'Cognee sync test',
|
|
425
|
+
severity: 'warning',
|
|
426
|
+
description: 'Testing fire-and-forget Cognee sync.',
|
|
427
|
+
tags: [],
|
|
428
|
+
});
|
|
429
|
+
expect(cognee.addEntries).toHaveBeenCalledTimes(1);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// ─── Duplicate Detection ────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
describe('duplicate detection', () => {
|
|
436
|
+
beforeEach(() => {
|
|
437
|
+
vault.seed([
|
|
438
|
+
makeEntry({
|
|
439
|
+
id: 'dup-existing',
|
|
440
|
+
domain: 'security',
|
|
441
|
+
title: 'Input validation pattern for user forms',
|
|
442
|
+
description:
|
|
443
|
+
'Always validate user input at system boundaries to prevent injection attacks.',
|
|
444
|
+
tags: ['validation', 'security'],
|
|
445
|
+
}),
|
|
446
|
+
]);
|
|
447
|
+
brain = new Brain(vault);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should warn on similar entry', () => {
|
|
451
|
+
const result = brain.enrichAndCapture({
|
|
452
|
+
id: 'dup-new-1',
|
|
453
|
+
type: 'pattern',
|
|
454
|
+
domain: 'security',
|
|
455
|
+
title: 'Input validation pattern for user forms and APIs',
|
|
456
|
+
severity: 'warning',
|
|
457
|
+
description: 'Validate all user input at boundaries to block injection vectors.',
|
|
458
|
+
tags: ['validation'],
|
|
459
|
+
});
|
|
460
|
+
expect(result.captured).toBe(true);
|
|
461
|
+
if (result.duplicate) {
|
|
462
|
+
expect(result.duplicate.id).toBe('dup-existing');
|
|
463
|
+
expect(result.duplicate.similarity).toBeGreaterThanOrEqual(0.6);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should allow dissimilar entries without duplicate warning', () => {
|
|
468
|
+
const result = brain.enrichAndCapture({
|
|
469
|
+
id: 'dup-different',
|
|
470
|
+
type: 'pattern',
|
|
471
|
+
domain: 'security',
|
|
472
|
+
title: 'Rate limiting configuration',
|
|
473
|
+
severity: 'warning',
|
|
474
|
+
description: 'Configure rate limits on API endpoints to prevent abuse.',
|
|
475
|
+
tags: ['rate-limiting'],
|
|
476
|
+
});
|
|
477
|
+
expect(result.captured).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// ─── Record Feedback ────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
describe('recordFeedback', () => {
|
|
484
|
+
it('should record feedback in database', () => {
|
|
485
|
+
brain.recordFeedback('test query', 'entry-1', 'accepted');
|
|
486
|
+
const stats = brain.getStats();
|
|
487
|
+
expect(stats.feedbackCount).toBe(1);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should record multiple feedback entries', () => {
|
|
491
|
+
brain.recordFeedback('query-1', 'entry-1', 'accepted');
|
|
492
|
+
brain.recordFeedback('query-2', 'entry-2', 'dismissed');
|
|
493
|
+
brain.recordFeedback('query-3', 'entry-3', 'accepted');
|
|
494
|
+
const stats = brain.getStats();
|
|
495
|
+
expect(stats.feedbackCount).toBe(3);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should keep default weights below feedback threshold', () => {
|
|
499
|
+
for (let i = 0; i < 10; i++) {
|
|
500
|
+
brain.recordFeedback('q' + i, 'e' + i, 'accepted');
|
|
501
|
+
}
|
|
502
|
+
const stats = brain.getStats();
|
|
503
|
+
expect(stats.weights.semantic).toBeCloseTo(0.4, 2);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// ─── Adaptive Weights ───────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
describe('adaptive weights', () => {
|
|
510
|
+
it('should adjust weights after reaching feedback threshold', () => {
|
|
511
|
+
for (let i = 0; i < 35; i++) {
|
|
512
|
+
brain.recordFeedback('query-' + i, 'entry-' + i, 'accepted');
|
|
513
|
+
}
|
|
514
|
+
const stats = brain.getStats();
|
|
515
|
+
expect(stats.weights.semantic).toBeGreaterThan(0.4);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should decrease semantic weight with high dismiss rate', () => {
|
|
519
|
+
for (let i = 0; i < 35; i++) {
|
|
520
|
+
brain.recordFeedback('query-' + i, 'entry-' + i, 'dismissed');
|
|
521
|
+
}
|
|
522
|
+
const stats = brain.getStats();
|
|
523
|
+
expect(stats.weights.semantic).toBeLessThan(0.4);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should keep weights bounded within +/-0.15 of defaults', () => {
|
|
527
|
+
for (let i = 0; i < 50; i++) {
|
|
528
|
+
brain.recordFeedback('query-' + i, 'entry-' + i, 'accepted');
|
|
529
|
+
}
|
|
530
|
+
const stats = brain.getStats();
|
|
531
|
+
expect(stats.weights.semantic).toBeLessThanOrEqual(0.55);
|
|
532
|
+
expect(stats.weights.semantic).toBeGreaterThanOrEqual(0.25);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('should normalize weights to sum to 1.0', () => {
|
|
536
|
+
for (let i = 0; i < 35; i++) {
|
|
537
|
+
brain.recordFeedback('query-' + i, 'entry-' + i, 'accepted');
|
|
538
|
+
}
|
|
539
|
+
const stats = brain.getStats();
|
|
540
|
+
const sum =
|
|
541
|
+
stats.weights.semantic +
|
|
542
|
+
stats.weights.vector +
|
|
543
|
+
stats.weights.severity +
|
|
544
|
+
stats.weights.recency +
|
|
545
|
+
stats.weights.tagOverlap +
|
|
546
|
+
stats.weights.domainMatch;
|
|
547
|
+
expect(sum).toBeCloseTo(1.0, 5);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should keep default weights with balanced feedback', () => {
|
|
551
|
+
for (let i = 0; i < 20; i++) {
|
|
552
|
+
brain.recordFeedback('qa-' + i, 'ea-' + i, 'accepted');
|
|
553
|
+
}
|
|
554
|
+
for (let i = 0; i < 20; i++) {
|
|
555
|
+
brain.recordFeedback('qd-' + i, 'ed-' + i, 'dismissed');
|
|
556
|
+
}
|
|
557
|
+
const stats = brain.getStats();
|
|
558
|
+
expect(stats.weights.semantic).toBeCloseTo(0.4, 1);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('should keep vector weight at 0 in base weights', () => {
|
|
562
|
+
const stats = brain.getStats();
|
|
563
|
+
expect(stats.weights.vector).toBe(0);
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// ─── Vocabulary ─────────────────────────────────────────────
|
|
568
|
+
|
|
569
|
+
describe('vocabulary', () => {
|
|
570
|
+
it('should rebuild vocabulary from vault entries', () => {
|
|
571
|
+
vault.seed([
|
|
572
|
+
makeEntry({
|
|
573
|
+
id: 'voc-1',
|
|
574
|
+
title: 'Authentication pattern',
|
|
575
|
+
description: 'JWT tokens for API auth.',
|
|
576
|
+
tags: ['auth', 'jwt'],
|
|
577
|
+
}),
|
|
578
|
+
makeEntry({
|
|
579
|
+
id: 'voc-2',
|
|
580
|
+
title: 'Authorization rules',
|
|
581
|
+
description: 'Role-based access control.',
|
|
582
|
+
tags: ['rbac', 'auth'],
|
|
583
|
+
}),
|
|
584
|
+
]);
|
|
585
|
+
brain.rebuildVocabulary();
|
|
586
|
+
expect(brain.getVocabularySize()).toBeGreaterThan(0);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('should clear vocabulary when vault is empty', () => {
|
|
590
|
+
vault.seed([
|
|
591
|
+
makeEntry({
|
|
592
|
+
id: 'voc-3',
|
|
593
|
+
title: 'Temp entry',
|
|
594
|
+
description: 'Will be removed.',
|
|
595
|
+
tags: ['temp'],
|
|
596
|
+
}),
|
|
597
|
+
]);
|
|
598
|
+
brain.rebuildVocabulary();
|
|
599
|
+
expect(brain.getVocabularySize()).toBeGreaterThan(0);
|
|
600
|
+
vault.remove('voc-3');
|
|
601
|
+
brain.rebuildVocabulary();
|
|
602
|
+
expect(brain.getVocabularySize()).toBe(0);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('should persist vocabulary to database', () => {
|
|
606
|
+
vault.seed([
|
|
607
|
+
makeEntry({
|
|
608
|
+
id: 'voc-4',
|
|
609
|
+
title: 'Persistent vocabulary test',
|
|
610
|
+
description: 'Testing database persistence.',
|
|
611
|
+
tags: ['persistence'],
|
|
612
|
+
}),
|
|
613
|
+
]);
|
|
614
|
+
brain.rebuildVocabulary();
|
|
615
|
+
const db = vault.getDb();
|
|
616
|
+
const count = (
|
|
617
|
+
db.prepare('SELECT COUNT(*) as count FROM brain_vocabulary').get() as { count: number }
|
|
618
|
+
).count;
|
|
619
|
+
expect(count).toBeGreaterThan(0);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('should handle rebuild on empty vault gracefully', () => {
|
|
623
|
+
brain.rebuildVocabulary();
|
|
624
|
+
expect(brain.getVocabularySize()).toBe(0);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// ─── Stats ──────────────────────────────────────────────────
|
|
629
|
+
|
|
630
|
+
describe('getStats', () => {
|
|
631
|
+
it('should return stats with zero counts for new brain', () => {
|
|
632
|
+
const stats = brain.getStats();
|
|
633
|
+
expect(stats.vocabularySize).toBe(0);
|
|
634
|
+
expect(stats.feedbackCount).toBe(0);
|
|
635
|
+
expect(stats.weights.semantic).toBeCloseTo(0.4, 2);
|
|
636
|
+
expect(stats.weights.vector).toBe(0);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('should return correct vocabulary size after seeding', () => {
|
|
640
|
+
vault.seed([
|
|
641
|
+
makeEntry({
|
|
642
|
+
id: 'st-1',
|
|
643
|
+
title: 'Pattern one',
|
|
644
|
+
description: 'Description one.',
|
|
645
|
+
tags: ['a'],
|
|
646
|
+
}),
|
|
647
|
+
makeEntry({
|
|
648
|
+
id: 'st-2',
|
|
649
|
+
title: 'Pattern two',
|
|
650
|
+
description: 'Description two.',
|
|
651
|
+
tags: ['b'],
|
|
652
|
+
}),
|
|
653
|
+
]);
|
|
654
|
+
brain.rebuildVocabulary();
|
|
655
|
+
const stats = brain.getStats();
|
|
656
|
+
expect(stats.vocabularySize).toBeGreaterThan(0);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('should return correct feedback count', () => {
|
|
660
|
+
brain.recordFeedback('q1', 'e1', 'accepted');
|
|
661
|
+
brain.recordFeedback('q2', 'e2', 'dismissed');
|
|
662
|
+
const stats = brain.getStats();
|
|
663
|
+
expect(stats.feedbackCount).toBe(2);
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// ─── Get Relevant Patterns ──────────────────────────────────
|
|
668
|
+
|
|
669
|
+
describe('getRelevantPatterns', () => {
|
|
670
|
+
it('should return ranked results for query context', async () => {
|
|
671
|
+
vault.seed([
|
|
672
|
+
makeEntry({
|
|
673
|
+
id: 'rel-1',
|
|
674
|
+
title: 'Authentication pattern',
|
|
675
|
+
description: 'JWT for API auth.',
|
|
676
|
+
domain: 'security',
|
|
677
|
+
tags: ['auth'],
|
|
678
|
+
}),
|
|
679
|
+
makeEntry({
|
|
680
|
+
id: 'rel-2',
|
|
681
|
+
title: 'Database indexing',
|
|
682
|
+
description: 'Index frequently queried columns.',
|
|
683
|
+
domain: 'performance',
|
|
684
|
+
tags: ['indexing'],
|
|
685
|
+
}),
|
|
686
|
+
]);
|
|
687
|
+
brain = new Brain(vault);
|
|
688
|
+
const results = await brain.getRelevantPatterns({
|
|
689
|
+
query: 'authentication',
|
|
690
|
+
domain: 'security',
|
|
691
|
+
});
|
|
692
|
+
expect(results.length).toBeGreaterThan(0);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('should return empty for no context matches', async () => {
|
|
696
|
+
const results = await brain.getRelevantPatterns({ query: 'nonexistent' });
|
|
697
|
+
expect(results).toEqual([]);
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// ─── Graceful Degradation ───────────────────────────────────
|
|
702
|
+
|
|
703
|
+
describe('graceful degradation', () => {
|
|
704
|
+
it('should work without vocabulary (empty vault)', async () => {
|
|
705
|
+
expect(brain.getVocabularySize()).toBe(0);
|
|
706
|
+
const results = await brain.intelligentSearch('anything');
|
|
707
|
+
expect(results).toEqual([]);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('should fall back to severity + recency scoring when vocabulary is empty', async () => {
|
|
711
|
+
vault.seed([
|
|
712
|
+
makeEntry({
|
|
713
|
+
id: 'gd-1',
|
|
714
|
+
title: 'Fallback test pattern',
|
|
715
|
+
description: 'Testing graceful degradation.',
|
|
716
|
+
severity: 'critical',
|
|
717
|
+
tags: ['fallback'],
|
|
718
|
+
}),
|
|
719
|
+
]);
|
|
720
|
+
brain = new Brain(vault);
|
|
721
|
+
const results = await brain.intelligentSearch('fallback test');
|
|
722
|
+
expect(results.length).toBeGreaterThan(0);
|
|
723
|
+
expect(results[0].score).toBeGreaterThan(0);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('should handle capture on empty vault without errors', () => {
|
|
727
|
+
const result = brain.enrichAndCapture({
|
|
728
|
+
id: 'gd-cap-1',
|
|
729
|
+
type: 'pattern',
|
|
730
|
+
domain: 'testing',
|
|
731
|
+
title: 'First pattern ever',
|
|
732
|
+
severity: 'warning',
|
|
733
|
+
description: 'The very first pattern captured in an empty vault.',
|
|
734
|
+
tags: [],
|
|
735
|
+
});
|
|
736
|
+
expect(result.captured).toBe(true);
|
|
737
|
+
expect(result.autoTags.length).toBeGreaterThan(0);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
});
|