@soleri/core 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/brain/brain.d.ts +12 -50
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +147 -12
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +51 -0
  6. package/dist/brain/intelligence.d.ts.map +1 -0
  7. package/dist/brain/intelligence.js +666 -0
  8. package/dist/brain/intelligence.js.map +1 -0
  9. package/dist/brain/types.d.ts +165 -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/curator/curator.d.ts.map +1 -1
  22. package/dist/curator/curator.js +7 -5
  23. package/dist/curator/curator.js.map +1 -1
  24. package/dist/index.d.ts +4 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/llm/llm-client.d.ts.map +1 -1
  29. package/dist/llm/llm-client.js +9 -2
  30. package/dist/llm/llm-client.js.map +1 -1
  31. package/dist/runtime/core-ops.d.ts +3 -3
  32. package/dist/runtime/core-ops.d.ts.map +1 -1
  33. package/dist/runtime/core-ops.js +180 -15
  34. package/dist/runtime/core-ops.js.map +1 -1
  35. package/dist/runtime/runtime.d.ts.map +1 -1
  36. package/dist/runtime/runtime.js +4 -0
  37. package/dist/runtime/runtime.js.map +1 -1
  38. package/dist/runtime/types.d.ts +2 -0
  39. package/dist/runtime/types.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/__tests__/brain-intelligence.test.ts +623 -0
  42. package/src/__tests__/brain.test.ts +265 -27
  43. package/src/__tests__/cognee-client.test.ts +524 -0
  44. package/src/__tests__/core-ops.test.ts +77 -49
  45. package/src/__tests__/curator.test.ts +126 -31
  46. package/src/__tests__/domain-ops.test.ts +45 -9
  47. package/src/__tests__/runtime.test.ts +13 -11
  48. package/src/brain/brain.ts +194 -65
  49. package/src/brain/intelligence.ts +1061 -0
  50. package/src/brain/types.ts +176 -0
  51. package/src/cognee/client.ts +352 -0
  52. package/src/cognee/types.ts +62 -0
  53. package/src/curator/curator.ts +52 -15
  54. package/src/index.ts +26 -1
  55. package/src/llm/llm-client.ts +18 -24
  56. package/src/runtime/core-ops.ts +219 -26
  57. package/src/runtime/runtime.ts +5 -0
  58. package/src/runtime/types.ts +2 -0
@@ -8,59 +8,27 @@ import {
8
8
  cosineSimilarity,
9
9
  jaccardSimilarity,
10
10
  } from '../text/similarity.js';
11
-
12
- // ─── Types ───────────────────────────────────────────────────────────
13
-
14
- export interface ScoringWeights {
15
- semantic: number;
16
- severity: number;
17
- recency: number;
18
- tagOverlap: number;
19
- domainMatch: number;
20
- }
21
-
22
- export interface ScoreBreakdown {
23
- semantic: number;
24
- severity: number;
25
- recency: number;
26
- tagOverlap: number;
27
- domainMatch: number;
28
- total: number;
29
- }
30
-
31
- export interface RankedResult {
32
- entry: IntelligenceEntry;
33
- score: number;
34
- breakdown: ScoreBreakdown;
35
- }
36
-
37
- export interface SearchOptions {
38
- domain?: string;
39
- type?: string;
40
- severity?: string;
41
- limit?: number;
42
- tags?: string[];
43
- }
44
-
45
- export interface CaptureResult {
46
- captured: boolean;
47
- id: string;
48
- autoTags: string[];
49
- duplicate?: { id: string; similarity: number };
50
- blocked?: boolean;
51
- }
52
-
53
- export interface BrainStats {
54
- vocabularySize: number;
55
- feedbackCount: number;
56
- weights: ScoringWeights;
57
- }
58
-
59
- export interface QueryContext {
60
- query: string;
61
- domain?: string;
62
- tags?: string[];
63
- }
11
+ import type { CogneeClient } from '../cognee/client.js';
12
+ import type {
13
+ ScoringWeights,
14
+ ScoreBreakdown,
15
+ RankedResult,
16
+ SearchOptions,
17
+ CaptureResult,
18
+ BrainStats,
19
+ QueryContext,
20
+ } from './types.js';
21
+
22
+ // Re-export types for backward compatibility
23
+ export type {
24
+ ScoringWeights,
25
+ ScoreBreakdown,
26
+ RankedResult,
27
+ SearchOptions,
28
+ CaptureResult,
29
+ BrainStats,
30
+ QueryContext,
31
+ } from './types.js';
64
32
 
65
33
  // ─── Severity scoring ──────────────────────────────────────────────
66
34
 
@@ -74,12 +42,22 @@ const SEVERITY_SCORES: Record<string, number> = {
74
42
 
75
43
  const DEFAULT_WEIGHTS: ScoringWeights = {
76
44
  semantic: 0.4,
45
+ vector: 0.0,
77
46
  severity: 0.15,
78
47
  recency: 0.15,
79
48
  tagOverlap: 0.15,
80
49
  domainMatch: 0.15,
81
50
  };
82
51
 
52
+ const COGNEE_WEIGHTS: ScoringWeights = {
53
+ semantic: 0.25,
54
+ vector: 0.35,
55
+ severity: 0.1,
56
+ recency: 0.1,
57
+ tagOverlap: 0.1,
58
+ domainMatch: 0.1,
59
+ };
60
+
83
61
  const WEIGHT_BOUND = 0.15;
84
62
  const FEEDBACK_THRESHOLD = 30;
85
63
  const DUPLICATE_BLOCK_THRESHOLD = 0.8;
@@ -88,16 +66,18 @@ const RECENCY_HALF_LIFE_DAYS = 365;
88
66
 
89
67
  export class Brain {
90
68
  private vault: Vault;
69
+ private cognee: CogneeClient | undefined;
91
70
  private vocabulary: Map<string, number> = new Map();
92
71
  private weights: ScoringWeights = { ...DEFAULT_WEIGHTS };
93
72
 
94
- constructor(vault: Vault) {
73
+ constructor(vault: Vault, cognee?: CogneeClient) {
95
74
  this.vault = vault;
75
+ this.cognee = cognee;
96
76
  this.rebuildVocabulary();
97
77
  this.recomputeWeights();
98
78
  }
99
79
 
100
- intelligentSearch(query: string, options?: SearchOptions): RankedResult[] {
80
+ async intelligentSearch(query: string, options?: SearchOptions): Promise<RankedResult[]> {
101
81
  const limit = options?.limit ?? 10;
102
82
  const rawResults = this.vault.search(query, {
103
83
  domain: options?.domain,
@@ -106,6 +86,97 @@ export class Brain {
106
86
  limit: Math.max(limit * 3, 30),
107
87
  });
108
88
 
89
+ // Cognee vector search (parallel, with timeout fallback)
90
+ let cogneeScoreMap: Map<string, number> = new Map();
91
+ const cogneeAvailable = this.cognee?.isAvailable ?? false;
92
+ if (cogneeAvailable && this.cognee) {
93
+ try {
94
+ const cogneeResults = await this.cognee.search(query, { limit: Math.max(limit * 2, 20) });
95
+
96
+ // Build title → entryIds reverse index from FTS results for text-based matching.
97
+ // Cognee assigns its own UUIDs to chunks and may strip embedded metadata during
98
+ // chunking, so we need multiple strategies to cross-reference results.
99
+ // Multiple entries can share a title, so map to arrays of IDs.
100
+ const titleToIds = new Map<string, string[]>();
101
+ for (const r of rawResults) {
102
+ const key = r.entry.title.toLowerCase().trim();
103
+ const ids = titleToIds.get(key) ?? [];
104
+ ids.push(r.entry.id);
105
+ titleToIds.set(key, ids);
106
+ }
107
+
108
+ const vaultIdPattern = /\[vault-id:([^\]]+)\]/;
109
+ const unmatchedCogneeResults: Array<{ text: string; score: number }> = [];
110
+
111
+ for (const cr of cogneeResults) {
112
+ const text = cr.text ?? '';
113
+
114
+ // Strategy 1: Extract vault ID from [vault-id:XXX] prefix (if Cognee preserved it)
115
+ const vaultIdMatch = text.match(vaultIdPattern);
116
+ if (vaultIdMatch) {
117
+ const vaultId = vaultIdMatch[1];
118
+ cogneeScoreMap.set(vaultId, Math.max(cogneeScoreMap.get(vaultId) ?? 0, cr.score));
119
+ continue;
120
+ }
121
+
122
+ // Strategy 2: Match first line of chunk text against known entry titles.
123
+ // serializeEntry() puts the title on the first line after the [vault-id:] prefix,
124
+ // and Cognee's chunking typically preserves this as the chunk start.
125
+ const firstLine = text.split('\n')[0]?.trim().toLowerCase() ?? '';
126
+ const matchedIds = firstLine ? titleToIds.get(firstLine) : undefined;
127
+ if (matchedIds) {
128
+ for (const id of matchedIds) {
129
+ cogneeScoreMap.set(id, Math.max(cogneeScoreMap.get(id) ?? 0, cr.score));
130
+ }
131
+ continue;
132
+ }
133
+
134
+ // Strategy 3: Check if any known title appears as a substring in the chunk.
135
+ // Handles cases where the title isn't on the first line (mid-document chunks).
136
+ const textLower = text.toLowerCase();
137
+ let found = false;
138
+ for (const [title, ids] of titleToIds) {
139
+ if (title.length >= 8 && textLower.includes(title)) {
140
+ for (const id of ids) {
141
+ cogneeScoreMap.set(id, Math.max(cogneeScoreMap.get(id) ?? 0, cr.score));
142
+ }
143
+ found = true;
144
+ break;
145
+ }
146
+ }
147
+ if (!found && text.length > 0) {
148
+ unmatchedCogneeResults.push({ text, score: cr.score });
149
+ }
150
+ }
151
+
152
+ // Strategy 4: For Cognee-only semantic matches (not in FTS results),
153
+ // use the first line as a vault FTS query to find the source entry.
154
+ // Preserve caller filters (domain/type/severity) to avoid reintroducing
155
+ // entries the original query excluded.
156
+ for (const unmatched of unmatchedCogneeResults) {
157
+ const searchTerm = unmatched.text.split('\n')[0]?.trim();
158
+ if (!searchTerm || searchTerm.length < 3) continue;
159
+ const vaultHits = this.vault.search(searchTerm, {
160
+ domain: options?.domain,
161
+ type: options?.type,
162
+ severity: options?.severity,
163
+ limit: 1,
164
+ });
165
+ if (vaultHits.length > 0) {
166
+ const id = vaultHits[0].entry.id;
167
+ cogneeScoreMap.set(id, Math.max(cogneeScoreMap.get(id) ?? 0, unmatched.score));
168
+ // Also add to FTS results pool if not already present
169
+ if (!rawResults.some((r) => r.entry.id === id)) {
170
+ rawResults.push(vaultHits[0]);
171
+ }
172
+ }
173
+ }
174
+ } catch {
175
+ // Cognee failed — fall back to FTS5 only
176
+ cogneeScoreMap = new Map();
177
+ }
178
+ }
179
+
109
180
  if (rawResults.length === 0) return [];
110
181
 
111
182
  const queryTokens = tokenize(query);
@@ -113,9 +184,22 @@ export class Brain {
113
184
  const queryDomain = options?.domain;
114
185
  const now = Math.floor(Date.now() / 1000);
115
186
 
187
+ // Use cognee-aware weights only if at least one ranked candidate has a vector score
188
+ const hasVectorCandidate = rawResults.some((r) => cogneeScoreMap.has(r.entry.id));
189
+ const activeWeights = hasVectorCandidate ? this.getCogneeWeights() : this.weights;
190
+
116
191
  const ranked = rawResults.map((result) => {
117
192
  const entry = result.entry;
118
- const breakdown = this.scoreEntry(entry, queryTokens, queryTags, queryDomain, now);
193
+ const vectorScore = cogneeScoreMap.get(entry.id) ?? 0;
194
+ const breakdown = this.scoreEntry(
195
+ entry,
196
+ queryTokens,
197
+ queryTags,
198
+ queryDomain,
199
+ now,
200
+ vectorScore,
201
+ activeWeights,
202
+ );
119
203
  return { entry, score: breakdown.total, breakdown };
120
204
  });
121
205
 
@@ -166,6 +250,11 @@ export class Brain {
166
250
  this.vault.add(fullEntry);
167
251
  this.updateVocabularyIncremental(fullEntry);
168
252
 
253
+ // Fire-and-forget Cognee sync
254
+ if (this.cognee?.isAvailable) {
255
+ this.cognee.addEntries([fullEntry]).catch(() => {});
256
+ }
257
+
169
258
  const result: CaptureResult = {
170
259
  captured: true,
171
260
  id: entry.id,
@@ -189,13 +278,39 @@ export class Brain {
189
278
  this.recomputeWeights();
190
279
  }
191
280
 
192
- getRelevantPatterns(context: QueryContext): RankedResult[] {
281
+ async getRelevantPatterns(context: QueryContext): Promise<RankedResult[]> {
193
282
  return this.intelligentSearch(context.query, {
194
283
  domain: context.domain,
195
284
  tags: context.tags,
196
285
  });
197
286
  }
198
287
 
288
+ async syncToCognee(): Promise<{ synced: number; cognified: boolean }> {
289
+ if (!this.cognee?.isAvailable) return { synced: 0, cognified: false };
290
+
291
+ const batchSize = 1000;
292
+ let offset = 0;
293
+ let totalSynced = 0;
294
+
295
+ while (true) {
296
+ const batch = this.vault.list({ limit: batchSize, offset });
297
+ if (batch.length === 0) break;
298
+
299
+ const { added } = await this.cognee.addEntries(batch);
300
+ totalSynced += added;
301
+ offset += batch.length;
302
+
303
+ if (batch.length < batchSize) break;
304
+ }
305
+
306
+ if (totalSynced === 0) return { synced: 0, cognified: false };
307
+
308
+ let cognified = false;
309
+ const cognifyResult = await this.cognee.cognify();
310
+ cognified = cognifyResult.status === 'ok';
311
+ return { synced: totalSynced, cognified };
312
+ }
313
+
199
314
  rebuildVocabulary(): void {
200
315
  const entries = this.vault.list({ limit: 100000 });
201
316
  const docCount = entries.length;
@@ -249,7 +364,11 @@ export class Brain {
249
364
  queryTags: string[],
250
365
  queryDomain: string | undefined,
251
366
  now: number,
367
+ vectorScore: number = 0,
368
+ activeWeights?: ScoringWeights,
252
369
  ): ScoreBreakdown {
370
+ const w = activeWeights ?? this.weights;
371
+
253
372
  let semantic = 0;
254
373
  if (this.vocabulary.size > 0 && queryTokens.length > 0) {
255
374
  const entryText = [
@@ -274,14 +393,17 @@ export class Brain {
274
393
 
275
394
  const domainMatch = queryDomain && entry.domain === queryDomain ? 1.0 : 0;
276
395
 
277
- const total =
278
- this.weights.semantic * semantic +
279
- this.weights.severity * severity +
280
- this.weights.recency * recency +
281
- this.weights.tagOverlap * tagOverlap +
282
- this.weights.domainMatch * domainMatch;
396
+ const vector = vectorScore;
283
397
 
284
- return { semantic, severity, recency, tagOverlap, domainMatch, total };
398
+ const total =
399
+ w.semantic * semantic +
400
+ w.vector * vector +
401
+ w.severity * severity +
402
+ w.recency * recency +
403
+ w.tagOverlap * tagOverlap +
404
+ w.domainMatch * domainMatch;
405
+
406
+ return { semantic, vector, severity, recency, tagOverlap, domainMatch, total };
285
407
  }
286
408
 
287
409
  private generateTags(title: string, description: string, context?: string): string[] {
@@ -375,6 +497,10 @@ export class Brain {
375
497
  tx();
376
498
  }
377
499
 
500
+ private getCogneeWeights(): ScoringWeights {
501
+ return { ...COGNEE_WEIGHTS };
502
+ }
503
+
378
504
  private recomputeWeights(): void {
379
505
  const db = this.vault.getDb();
380
506
  const feedbackCount = (
@@ -401,7 +527,10 @@ export class Brain {
401
527
  DEFAULT_WEIGHTS.semantic + WEIGHT_BOUND,
402
528
  );
403
529
 
404
- const remaining = 1.0 - newWeights.semantic;
530
+ // vector stays 0 in base weights (only active during hybrid search)
531
+ newWeights.vector = 0;
532
+
533
+ const remaining = 1.0 - newWeights.semantic - newWeights.vector;
405
534
  const otherSum =
406
535
  DEFAULT_WEIGHTS.severity +
407
536
  DEFAULT_WEIGHTS.recency +