@soleri/core 1.0.0 → 2.0.1

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.
@@ -1,7 +1,9 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { Vault } from '../vault/vault.js';
3
3
  import { Brain } from '../brain/brain.js';
4
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';
5
7
 
6
8
  function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntry {
7
9
  return {
@@ -15,6 +17,36 @@ function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntr
15
17
  };
16
18
  }
17
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
+
18
50
  describe('Brain', () => {
19
51
  let vault: Vault;
20
52
  let brain: Brain;
@@ -53,6 +85,12 @@ describe('Brain', () => {
53
85
  const brain2 = new Brain(vault);
54
86
  expect(brain2.getVocabularySize()).toBeGreaterThan(0);
55
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
+ });
56
94
  });
57
95
 
58
96
  // ─── Intelligent Search ──────────────────────────────────────
@@ -89,49 +127,52 @@ describe('Brain', () => {
89
127
  brain = new Brain(vault);
90
128
  });
91
129
 
92
- it('should return ranked results', () => {
93
- const results = brain.intelligentSearch('validation input');
130
+ it('should return ranked results', async () => {
131
+ const results = await brain.intelligentSearch('validation input');
94
132
  expect(results.length).toBeGreaterThan(0);
95
133
  expect(results[0].entry.id).toBe('is-1');
96
134
  });
97
135
 
98
- it('should include score breakdown', () => {
99
- const results = brain.intelligentSearch('validation');
136
+ it('should include score breakdown with vector field', async () => {
137
+ const results = await brain.intelligentSearch('validation');
100
138
  expect(results.length).toBeGreaterThan(0);
101
139
  const breakdown = results[0].breakdown;
102
140
  expect(breakdown).toHaveProperty('semantic');
141
+ expect(breakdown).toHaveProperty('vector');
103
142
  expect(breakdown).toHaveProperty('severity');
104
143
  expect(breakdown).toHaveProperty('recency');
105
144
  expect(breakdown).toHaveProperty('tagOverlap');
106
145
  expect(breakdown).toHaveProperty('domainMatch');
107
146
  expect(breakdown).toHaveProperty('total');
108
147
  expect(breakdown.total).toBe(results[0].score);
148
+ // Without cognee, vector should be 0
149
+ expect(breakdown.vector).toBe(0);
109
150
  });
110
151
 
111
- it('should return empty array for no matches', () => {
112
- const results = brain.intelligentSearch('xyznonexistent');
152
+ it('should return empty array for no matches', async () => {
153
+ const results = await brain.intelligentSearch('xyznonexistent');
113
154
  expect(results).toEqual([]);
114
155
  });
115
156
 
116
- it('should respect limit', () => {
117
- const results = brain.intelligentSearch('pattern', { limit: 1 });
157
+ it('should respect limit', async () => {
158
+ const results = await brain.intelligentSearch('pattern', { limit: 1 });
118
159
  expect(results.length).toBeLessThanOrEqual(1);
119
160
  });
120
161
 
121
- it('should filter by domain', () => {
122
- const results = brain.intelligentSearch('pattern', { domain: 'security' });
162
+ it('should filter by domain', async () => {
163
+ const results = await brain.intelligentSearch('pattern', { domain: 'security' });
123
164
  expect(results.every((r) => r.entry.domain === 'security')).toBe(true);
124
165
  });
125
166
 
126
- it('should boost domain matches when domain is specified', () => {
127
- const withDomain = brain.intelligentSearch('pattern', { domain: 'security' });
167
+ it('should boost domain matches when domain is specified', async () => {
168
+ const withDomain = await brain.intelligentSearch('pattern', { domain: 'security' });
128
169
  if (withDomain.length > 0) {
129
170
  expect(withDomain[0].breakdown.domainMatch).toBe(1.0);
130
171
  }
131
172
  });
132
173
 
133
- it('should boost severity in scoring', () => {
134
- const results = brain.intelligentSearch('pattern');
174
+ it('should boost severity in scoring', async () => {
175
+ const results = await brain.intelligentSearch('pattern');
135
176
  if (results.length >= 2) {
136
177
  const critical = results.find((r) => r.entry.severity === 'critical');
137
178
  const suggestion = results.find((r) => r.entry.severity === 'suggestion');
@@ -141,8 +182,10 @@ describe('Brain', () => {
141
182
  }
142
183
  });
143
184
 
144
- it('should boost tag overlap when tags provided', () => {
145
- const results = brain.intelligentSearch('pattern', { tags: ['validation', 'security'] });
185
+ it('should boost tag overlap when tags provided', async () => {
186
+ const results = await brain.intelligentSearch('pattern', {
187
+ tags: ['validation', 'security'],
188
+ });
146
189
  if (results.length > 0) {
147
190
  const secEntry = results.find((r) => r.entry.id === 'is-1');
148
191
  if (secEntry) {
@@ -151,15 +194,185 @@ describe('Brain', () => {
151
194
  }
152
195
  });
153
196
 
154
- it('should handle search on empty vault gracefully', () => {
197
+ it('should handle search on empty vault gracefully', async () => {
155
198
  const emptyVault = new Vault(':memory:');
156
199
  const emptyBrain = new Brain(emptyVault);
157
- const results = emptyBrain.intelligentSearch('anything');
200
+ const results = await emptyBrain.intelligentSearch('anything');
158
201
  expect(results).toEqual([]);
159
202
  emptyVault.close();
160
203
  });
161
204
  });
162
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 match via [vault-id:] prefix (strategy 1)', async () => {
231
+ const cognee = makeMockCognee({
232
+ searchResults: [
233
+ {
234
+ id: 'cognee-uuid-1',
235
+ score: 0.92,
236
+ text: '[vault-id:hs-1]\nAuthentication flow\nJWT-based authentication for API endpoints.',
237
+ searchType: 'CHUNKS',
238
+ },
239
+ ],
240
+ });
241
+ const hybridBrain = new Brain(vault, cognee);
242
+ const results = await hybridBrain.intelligentSearch('authentication');
243
+ expect(results.length).toBeGreaterThan(0);
244
+ const authResult = results.find((r) => r.entry.id === 'hs-1');
245
+ expect(authResult).toBeDefined();
246
+ expect(authResult!.breakdown.vector).toBe(0.92);
247
+ });
248
+
249
+ it('should match via title first-line (strategy 2)', async () => {
250
+ // Cognee stripped the [vault-id:] prefix during chunking — title is on first line
251
+ const cognee = makeMockCognee({
252
+ searchResults: [
253
+ {
254
+ id: 'cognee-uuid-2',
255
+ score: 0.9,
256
+ text: 'Authentication flow\nJWT-based authentication for API endpoints.',
257
+ searchType: 'CHUNKS',
258
+ },
259
+ ],
260
+ });
261
+ const hybridBrain = new Brain(vault, cognee);
262
+ const results = await hybridBrain.intelligentSearch('authentication');
263
+ const authResult = results.find((r) => r.entry.id === 'hs-1');
264
+ expect(authResult).toBeDefined();
265
+ expect(authResult!.breakdown.vector).toBe(0.9);
266
+ });
267
+
268
+ it('should match via title substring (strategy 3)', async () => {
269
+ // Mid-document chunk where title isn't on the first line
270
+ const cognee = makeMockCognee({
271
+ searchResults: [
272
+ {
273
+ id: 'cognee-uuid-3',
274
+ score: 0.85,
275
+ text: 'Some preamble text\nAuthentication flow\nJWT-based auth...',
276
+ searchType: 'CHUNKS',
277
+ },
278
+ ],
279
+ });
280
+ const hybridBrain = new Brain(vault, cognee);
281
+ const results = await hybridBrain.intelligentSearch('authentication');
282
+ const authResult = results.find((r) => r.entry.id === 'hs-1');
283
+ expect(authResult).toBeDefined();
284
+ expect(authResult!.breakdown.vector).toBe(0.85);
285
+ });
286
+
287
+ it('should merge cognee-only entries via FTS fallback (strategy 4)', async () => {
288
+ // hs-2 may not match FTS5 for "authentication" but Cognee finds it via semantic similarity.
289
+ // The chunk text starts with the entry title so strategy 4's vault.search(firstLine) finds it.
290
+ const cognee = makeMockCognee({
291
+ searchResults: [
292
+ {
293
+ id: 'cognee-uuid-a',
294
+ score: 0.95,
295
+ text: 'Authentication flow\nJWT-based authentication for API endpoints.',
296
+ searchType: 'CHUNKS',
297
+ },
298
+ {
299
+ id: 'cognee-uuid-b',
300
+ score: 0.6,
301
+ text: 'Logging best practices\nStructured logging with correlation IDs.',
302
+ searchType: 'CHUNKS',
303
+ },
304
+ ],
305
+ });
306
+ const hybridBrain = new Brain(vault, cognee);
307
+ const results = await hybridBrain.intelligentSearch('authentication');
308
+ // Both entries should be in results (hs-2 merged from Cognee even if not in FTS5)
309
+ const ids = results.map((r) => r.entry.id);
310
+ expect(ids).toContain('hs-1');
311
+ expect(ids).toContain('hs-2');
312
+ const loggingResult = results.find((r) => r.entry.id === 'hs-2');
313
+ expect(loggingResult).toBeDefined();
314
+ expect(loggingResult!.breakdown.vector).toBe(0.6);
315
+ });
316
+
317
+ it('should fall back to FTS5-only on Cognee search error', async () => {
318
+ const cognee = makeMockCognee({ searchError: true });
319
+ const hybridBrain = new Brain(vault, cognee);
320
+ const results = await hybridBrain.intelligentSearch('authentication');
321
+ // Should still work, just without vector scores
322
+ for (const r of results) {
323
+ expect(r.breakdown.vector).toBe(0);
324
+ }
325
+ });
326
+
327
+ it('should work without Cognee (backward compatible)', async () => {
328
+ const noCogneeBrain = new Brain(vault);
329
+ const results = await noCogneeBrain.intelligentSearch('authentication');
330
+ for (const r of results) {
331
+ expect(r.breakdown.vector).toBe(0);
332
+ }
333
+ });
334
+
335
+ it('should handle unavailable Cognee gracefully', async () => {
336
+ const cognee = makeMockCognee({ available: false });
337
+ const hybridBrain = new Brain(vault, cognee);
338
+ const results = await hybridBrain.intelligentSearch('authentication');
339
+ for (const r of results) {
340
+ expect(r.breakdown.vector).toBe(0);
341
+ }
342
+ // search should not have been called
343
+ expect(cognee.search).not.toHaveBeenCalled();
344
+ });
345
+ });
346
+
347
+ // ─── syncToCognee ──────────────────────────────────────────────
348
+
349
+ describe('syncToCognee', () => {
350
+ it('should return 0 when Cognee not available', async () => {
351
+ const result = await brain.syncToCognee();
352
+ expect(result).toEqual({ synced: 0, cognified: false });
353
+ });
354
+
355
+ it('should sync all entries and cognify', async () => {
356
+ vault.seed([makeEntry({ id: 'sync-1' }), makeEntry({ id: 'sync-2' })]);
357
+ const cognee = makeMockCognee();
358
+ (cognee.addEntries as ReturnType<typeof vi.fn>).mockResolvedValue({ added: 2 });
359
+ const hybridBrain = new Brain(vault, cognee);
360
+ const result = await hybridBrain.syncToCognee();
361
+ expect(result.synced).toBe(2);
362
+ expect(result.cognified).toBe(true);
363
+ expect(cognee.addEntries).toHaveBeenCalledTimes(1);
364
+ expect(cognee.cognify).toHaveBeenCalledTimes(1);
365
+ });
366
+
367
+ it('should skip cognify when no entries added', async () => {
368
+ const cognee = makeMockCognee();
369
+ const hybridBrain = new Brain(vault, cognee);
370
+ const result = await hybridBrain.syncToCognee();
371
+ expect(result.synced).toBe(0);
372
+ expect(result.cognified).toBe(false);
373
+ });
374
+ });
375
+
163
376
  // ─── Enrich and Capture ─────────────────────────────────────
164
377
 
165
378
  describe('enrichAndCapture', () => {
@@ -243,6 +456,21 @@ describe('Brain', () => {
243
456
  const entry = vault.get('cap-5');
244
457
  expect(entry!.tags.length).toBeGreaterThan(0);
245
458
  });
459
+
460
+ it('should fire-and-forget sync to Cognee on capture', () => {
461
+ const cognee = makeMockCognee();
462
+ const hybridBrain = new Brain(vault, cognee);
463
+ hybridBrain.enrichAndCapture({
464
+ id: 'cap-cognee-1',
465
+ type: 'pattern',
466
+ domain: 'testing',
467
+ title: 'Cognee sync test',
468
+ severity: 'warning',
469
+ description: 'Testing fire-and-forget Cognee sync.',
470
+ tags: [],
471
+ });
472
+ expect(cognee.addEntries).toHaveBeenCalledTimes(1);
473
+ });
246
474
  });
247
475
 
248
476
  // ─── Duplicate Detection ────────────────────────────────────
@@ -354,6 +582,7 @@ describe('Brain', () => {
354
582
  const stats = brain.getStats();
355
583
  const sum =
356
584
  stats.weights.semantic +
585
+ stats.weights.vector +
357
586
  stats.weights.severity +
358
587
  stats.weights.recency +
359
588
  stats.weights.tagOverlap +
@@ -371,6 +600,11 @@ describe('Brain', () => {
371
600
  const stats = brain.getStats();
372
601
  expect(stats.weights.semantic).toBeCloseTo(0.4, 1);
373
602
  });
603
+
604
+ it('should keep vector weight at 0 in base weights', () => {
605
+ const stats = brain.getStats();
606
+ expect(stats.weights.vector).toBe(0);
607
+ });
374
608
  });
375
609
 
376
610
  // ─── Vocabulary ─────────────────────────────────────────────
@@ -442,6 +676,7 @@ describe('Brain', () => {
442
676
  expect(stats.vocabularySize).toBe(0);
443
677
  expect(stats.feedbackCount).toBe(0);
444
678
  expect(stats.weights.semantic).toBeCloseTo(0.4, 2);
679
+ expect(stats.weights.vector).toBe(0);
445
680
  });
446
681
 
447
682
  it('should return correct vocabulary size after seeding', () => {
@@ -475,7 +710,7 @@ describe('Brain', () => {
475
710
  // ─── Get Relevant Patterns ──────────────────────────────────
476
711
 
477
712
  describe('getRelevantPatterns', () => {
478
- it('should return ranked results for query context', () => {
713
+ it('should return ranked results for query context', async () => {
479
714
  vault.seed([
480
715
  makeEntry({
481
716
  id: 'rel-1',
@@ -493,12 +728,15 @@ describe('Brain', () => {
493
728
  }),
494
729
  ]);
495
730
  brain = new Brain(vault);
496
- const results = brain.getRelevantPatterns({ query: 'authentication', domain: 'security' });
731
+ const results = await brain.getRelevantPatterns({
732
+ query: 'authentication',
733
+ domain: 'security',
734
+ });
497
735
  expect(results.length).toBeGreaterThan(0);
498
736
  });
499
737
 
500
- it('should return empty for no context matches', () => {
501
- const results = brain.getRelevantPatterns({ query: 'nonexistent' });
738
+ it('should return empty for no context matches', async () => {
739
+ const results = await brain.getRelevantPatterns({ query: 'nonexistent' });
502
740
  expect(results).toEqual([]);
503
741
  });
504
742
  });
@@ -506,13 +744,13 @@ describe('Brain', () => {
506
744
  // ─── Graceful Degradation ───────────────────────────────────
507
745
 
508
746
  describe('graceful degradation', () => {
509
- it('should work without vocabulary (empty vault)', () => {
747
+ it('should work without vocabulary (empty vault)', async () => {
510
748
  expect(brain.getVocabularySize()).toBe(0);
511
- const results = brain.intelligentSearch('anything');
749
+ const results = await brain.intelligentSearch('anything');
512
750
  expect(results).toEqual([]);
513
751
  });
514
752
 
515
- it('should fall back to severity + recency scoring when vocabulary is empty', () => {
753
+ it('should fall back to severity + recency scoring when vocabulary is empty', async () => {
516
754
  vault.seed([
517
755
  makeEntry({
518
756
  id: 'gd-1',
@@ -523,7 +761,7 @@ describe('Brain', () => {
523
761
  }),
524
762
  ]);
525
763
  brain = new Brain(vault);
526
- const results = brain.intelligentSearch('fallback test');
764
+ const results = await brain.intelligentSearch('fallback test');
527
765
  expect(results.length).toBeGreaterThan(0);
528
766
  expect(results[0].score).toBeGreaterThan(0);
529
767
  });