@soleri/core 2.0.1 → 2.0.2

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 (68) hide show
  1. package/dist/brain/brain.d.ts +3 -12
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +13 -305
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/curator/curator.d.ts +28 -0
  6. package/dist/curator/curator.d.ts.map +1 -0
  7. package/dist/curator/curator.js +523 -0
  8. package/dist/curator/curator.js.map +1 -0
  9. package/dist/curator/types.d.ts +87 -0
  10. package/dist/curator/types.d.ts.map +1 -0
  11. package/dist/curator/types.js +3 -0
  12. package/dist/curator/types.js.map +1 -0
  13. package/dist/facades/types.d.ts +1 -1
  14. package/dist/index.d.ts +9 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +10 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/llm/llm-client.d.ts +28 -0
  19. package/dist/llm/llm-client.d.ts.map +1 -0
  20. package/dist/llm/llm-client.js +219 -0
  21. package/dist/llm/llm-client.js.map +1 -0
  22. package/dist/runtime/core-ops.d.ts +17 -0
  23. package/dist/runtime/core-ops.d.ts.map +1 -0
  24. package/dist/runtime/core-ops.js +448 -0
  25. package/dist/runtime/core-ops.js.map +1 -0
  26. package/dist/runtime/domain-ops.d.ts +25 -0
  27. package/dist/runtime/domain-ops.d.ts.map +1 -0
  28. package/dist/runtime/domain-ops.js +130 -0
  29. package/dist/runtime/domain-ops.js.map +1 -0
  30. package/dist/runtime/runtime.d.ts +19 -0
  31. package/dist/runtime/runtime.d.ts.map +1 -0
  32. package/dist/runtime/runtime.js +62 -0
  33. package/dist/runtime/runtime.js.map +1 -0
  34. package/dist/runtime/types.d.ts +39 -0
  35. package/dist/runtime/types.d.ts.map +1 -0
  36. package/dist/runtime/types.js +2 -0
  37. package/dist/{cognee → runtime}/types.js.map +1 -1
  38. package/dist/text/similarity.d.ts +8 -0
  39. package/dist/text/similarity.d.ts.map +1 -0
  40. package/dist/text/similarity.js +161 -0
  41. package/dist/text/similarity.js.map +1 -0
  42. package/package.json +6 -2
  43. package/src/__tests__/brain.test.ts +27 -265
  44. package/src/__tests__/core-ops.test.ts +190 -0
  45. package/src/__tests__/curator.test.ts +479 -0
  46. package/src/__tests__/domain-ops.test.ts +124 -0
  47. package/src/__tests__/llm-client.test.ts +69 -0
  48. package/src/__tests__/runtime.test.ts +93 -0
  49. package/src/brain/brain.ts +19 -342
  50. package/src/curator/curator.ts +662 -0
  51. package/src/curator/types.ts +114 -0
  52. package/src/index.ts +40 -11
  53. package/src/llm/llm-client.ts +316 -0
  54. package/src/runtime/core-ops.ts +472 -0
  55. package/src/runtime/domain-ops.ts +144 -0
  56. package/src/runtime/runtime.ts +71 -0
  57. package/src/runtime/types.ts +37 -0
  58. package/src/text/similarity.ts +168 -0
  59. package/dist/cognee/client.d.ts +0 -35
  60. package/dist/cognee/client.d.ts.map +0 -1
  61. package/dist/cognee/client.js +0 -291
  62. package/dist/cognee/client.js.map +0 -1
  63. package/dist/cognee/types.d.ts +0 -46
  64. package/dist/cognee/types.d.ts.map +0 -1
  65. package/dist/cognee/types.js +0 -3
  66. package/src/__tests__/cognee-client.test.ts +0 -524
  67. package/src/cognee/client.ts +0 -352
  68. package/src/cognee/types.ts +0 -62
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { createAgentRuntime } from '../runtime/runtime.js';
3
+ import type { AgentRuntime } from '../runtime/types.js';
4
+
5
+ describe('createAgentRuntime', () => {
6
+ let runtime: AgentRuntime | null = null;
7
+
8
+ afterEach(() => {
9
+ runtime?.close();
10
+ runtime = null;
11
+ });
12
+
13
+ it('should create a runtime with all modules initialized', () => {
14
+ runtime = createAgentRuntime({
15
+ agentId: 'test-agent',
16
+ vaultPath: ':memory:',
17
+ });
18
+
19
+ expect(runtime.config.agentId).toBe('test-agent');
20
+ expect(runtime.vault).toBeDefined();
21
+ expect(runtime.brain).toBeDefined();
22
+ expect(runtime.planner).toBeDefined();
23
+ expect(runtime.curator).toBeDefined();
24
+ expect(runtime.keyPool.openai).toBeDefined();
25
+ expect(runtime.keyPool.anthropic).toBeDefined();
26
+ expect(runtime.llmClient).toBeDefined();
27
+ });
28
+
29
+ it('should use :memory: vault when specified', () => {
30
+ runtime = createAgentRuntime({
31
+ agentId: 'test-mem',
32
+ vaultPath: ':memory:',
33
+ });
34
+
35
+ const stats = runtime.vault.stats();
36
+ expect(stats.totalEntries).toBe(0);
37
+ });
38
+
39
+ it('should preserve config on runtime', () => {
40
+ runtime = createAgentRuntime({
41
+ agentId: 'test-cfg',
42
+ vaultPath: ':memory:',
43
+ dataDir: '/nonexistent',
44
+ });
45
+
46
+ expect(runtime.config.agentId).toBe('test-cfg');
47
+ expect(runtime.config.vaultPath).toBe(':memory:');
48
+ expect(runtime.config.dataDir).toBe('/nonexistent');
49
+ });
50
+
51
+ it('close() should not throw', () => {
52
+ runtime = createAgentRuntime({
53
+ agentId: 'test-close',
54
+ vaultPath: ':memory:',
55
+ });
56
+
57
+ expect(() => runtime!.close()).not.toThrow();
58
+ runtime = null; // already closed
59
+ });
60
+
61
+ it('brain should be wired to vault', () => {
62
+ runtime = createAgentRuntime({
63
+ agentId: 'test-brain-wire',
64
+ vaultPath: ':memory:',
65
+ });
66
+
67
+ // Seed some data through vault
68
+ runtime.vault.seed([{
69
+ id: 'rt-1',
70
+ type: 'pattern',
71
+ domain: 'testing',
72
+ title: 'Runtime test pattern',
73
+ severity: 'warning',
74
+ description: 'A test.',
75
+ tags: ['test'],
76
+ }]);
77
+
78
+ // Brain should find it
79
+ runtime.brain.rebuildVocabulary();
80
+ const results = runtime.brain.intelligentSearch('runtime test', { limit: 5 });
81
+ expect(results.length).toBeGreaterThan(0);
82
+ });
83
+
84
+ it('curator should be wired to vault', () => {
85
+ runtime = createAgentRuntime({
86
+ agentId: 'test-curator-wire',
87
+ vaultPath: ':memory:',
88
+ });
89
+
90
+ const status = runtime.curator.getStatus();
91
+ expect(status.initialized).toBe(true);
92
+ });
93
+ });
@@ -1,13 +1,18 @@
1
1
  import type { Vault } from '../vault/vault.js';
2
2
  import type { SearchResult } from '../vault/vault.js';
3
3
  import type { IntelligenceEntry } from '../intelligence/types.js';
4
- import type { CogneeClient } from '../cognee/client.js';
4
+ import {
5
+ tokenize,
6
+ calculateTf,
7
+ calculateTfIdf,
8
+ cosineSimilarity,
9
+ jaccardSimilarity,
10
+ } from '../text/similarity.js';
5
11
 
6
12
  // ─── Types ───────────────────────────────────────────────────────────
7
13
 
8
14
  export interface ScoringWeights {
9
15
  semantic: number;
10
- vector: number;
11
16
  severity: number;
12
17
  recency: number;
13
18
  tagOverlap: number;
@@ -16,7 +21,6 @@ export interface ScoringWeights {
16
21
 
17
22
  export interface ScoreBreakdown {
18
23
  semantic: number;
19
- vector: number;
20
24
  severity: number;
21
25
  recency: number;
22
26
  tagOverlap: number;
@@ -58,172 +62,6 @@ export interface QueryContext {
58
62
  tags?: string[];
59
63
  }
60
64
 
61
- type SparseVector = Map<string, number>;
62
-
63
- // ─── Stopwords ─────────────────────────────────────────────────────
64
-
65
- const STOPWORDS = new Set([
66
- 'a',
67
- 'an',
68
- 'the',
69
- 'and',
70
- 'or',
71
- 'but',
72
- 'in',
73
- 'on',
74
- 'at',
75
- 'to',
76
- 'for',
77
- 'of',
78
- 'with',
79
- 'by',
80
- 'from',
81
- 'as',
82
- 'is',
83
- 'was',
84
- 'are',
85
- 'were',
86
- 'been',
87
- 'be',
88
- 'have',
89
- 'has',
90
- 'had',
91
- 'do',
92
- 'does',
93
- 'did',
94
- 'will',
95
- 'would',
96
- 'could',
97
- 'should',
98
- 'may',
99
- 'might',
100
- 'shall',
101
- 'can',
102
- 'need',
103
- 'must',
104
- 'it',
105
- 'its',
106
- 'this',
107
- 'that',
108
- 'these',
109
- 'those',
110
- 'i',
111
- 'you',
112
- 'he',
113
- 'she',
114
- 'we',
115
- 'they',
116
- 'me',
117
- 'him',
118
- 'her',
119
- 'us',
120
- 'them',
121
- 'my',
122
- 'your',
123
- 'his',
124
- 'our',
125
- 'their',
126
- 'what',
127
- 'which',
128
- 'who',
129
- 'whom',
130
- 'when',
131
- 'where',
132
- 'why',
133
- 'how',
134
- 'all',
135
- 'each',
136
- 'every',
137
- 'both',
138
- 'few',
139
- 'more',
140
- 'most',
141
- 'other',
142
- 'some',
143
- 'such',
144
- 'no',
145
- 'not',
146
- 'only',
147
- 'same',
148
- 'so',
149
- 'than',
150
- 'too',
151
- 'very',
152
- 'just',
153
- 'because',
154
- 'if',
155
- 'then',
156
- 'else',
157
- 'about',
158
- 'up',
159
- 'out',
160
- 'into',
161
- ]);
162
-
163
- // ─── Text Processing (pure functions) ────────────────────────────
164
-
165
- function tokenize(text: string): string[] {
166
- return text
167
- .toLowerCase()
168
- .replace(/[^a-z0-9\s-]/g, ' ')
169
- .split(/\s+/)
170
- .filter((t) => t.length > 2 && !STOPWORDS.has(t));
171
- }
172
-
173
- function calculateTf(tokens: string[]): SparseVector {
174
- const tf: SparseVector = new Map();
175
- for (const token of tokens) {
176
- tf.set(token, (tf.get(token) ?? 0) + 1);
177
- }
178
- const len = tokens.length || 1;
179
- for (const [term, count] of tf) {
180
- tf.set(term, count / len);
181
- }
182
- return tf;
183
- }
184
-
185
- function calculateTfIdf(tokens: string[], vocabulary: Map<string, number>): SparseVector {
186
- const tf = calculateTf(tokens);
187
- const tfidf: SparseVector = new Map();
188
- for (const [term, tfValue] of tf) {
189
- const idf = vocabulary.get(term) ?? 0;
190
- if (idf > 0) {
191
- tfidf.set(term, tfValue * idf);
192
- }
193
- }
194
- return tfidf;
195
- }
196
-
197
- function cosineSimilarity(a: SparseVector, b: SparseVector): number {
198
- let dot = 0;
199
- let normA = 0;
200
- let normB = 0;
201
- for (const [term, valA] of a) {
202
- normA += valA * valA;
203
- const valB = b.get(term);
204
- if (valB !== undefined) {
205
- dot += valA * valB;
206
- }
207
- }
208
- for (const [, valB] of b) {
209
- normB += valB * valB;
210
- }
211
- const denom = Math.sqrt(normA) * Math.sqrt(normB);
212
- return denom === 0 ? 0 : dot / denom;
213
- }
214
-
215
- function jaccardSimilarity(a: string[], b: string[]): number {
216
- if (a.length === 0 && b.length === 0) return 0;
217
- const setA = new Set(a);
218
- const setB = new Set(b);
219
- let intersection = 0;
220
- for (const item of setA) {
221
- if (setB.has(item)) intersection++;
222
- }
223
- const union = new Set([...a, ...b]).size;
224
- return union === 0 ? 0 : intersection / union;
225
- }
226
-
227
65
  // ─── Severity scoring ──────────────────────────────────────────────
228
66
 
229
67
  const SEVERITY_SCORES: Record<string, number> = {
@@ -236,22 +74,12 @@ const SEVERITY_SCORES: Record<string, number> = {
236
74
 
237
75
  const DEFAULT_WEIGHTS: ScoringWeights = {
238
76
  semantic: 0.4,
239
- vector: 0.0,
240
77
  severity: 0.15,
241
78
  recency: 0.15,
242
79
  tagOverlap: 0.15,
243
80
  domainMatch: 0.15,
244
81
  };
245
82
 
246
- const COGNEE_WEIGHTS: ScoringWeights = {
247
- semantic: 0.25,
248
- vector: 0.35,
249
- severity: 0.1,
250
- recency: 0.1,
251
- tagOverlap: 0.1,
252
- domainMatch: 0.1,
253
- };
254
-
255
83
  const WEIGHT_BOUND = 0.15;
256
84
  const FEEDBACK_THRESHOLD = 30;
257
85
  const DUPLICATE_BLOCK_THRESHOLD = 0.8;
@@ -260,18 +88,16 @@ const RECENCY_HALF_LIFE_DAYS = 365;
260
88
 
261
89
  export class Brain {
262
90
  private vault: Vault;
263
- private cognee: CogneeClient | undefined;
264
91
  private vocabulary: Map<string, number> = new Map();
265
92
  private weights: ScoringWeights = { ...DEFAULT_WEIGHTS };
266
93
 
267
- constructor(vault: Vault, cognee?: CogneeClient) {
94
+ constructor(vault: Vault) {
268
95
  this.vault = vault;
269
- this.cognee = cognee;
270
96
  this.rebuildVocabulary();
271
97
  this.recomputeWeights();
272
98
  }
273
99
 
274
- async intelligentSearch(query: string, options?: SearchOptions): Promise<RankedResult[]> {
100
+ intelligentSearch(query: string, options?: SearchOptions): RankedResult[] {
275
101
  const limit = options?.limit ?? 10;
276
102
  const rawResults = this.vault.search(query, {
277
103
  domain: options?.domain,
@@ -280,97 +106,6 @@ export class Brain {
280
106
  limit: Math.max(limit * 3, 30),
281
107
  });
282
108
 
283
- // Cognee vector search (parallel, with timeout fallback)
284
- let cogneeScoreMap: Map<string, number> = new Map();
285
- const cogneeAvailable = this.cognee?.isAvailable ?? false;
286
- if (cogneeAvailable && this.cognee) {
287
- try {
288
- const cogneeResults = await this.cognee.search(query, { limit: Math.max(limit * 2, 20) });
289
-
290
- // Build title → entryIds reverse index from FTS results for text-based matching.
291
- // Cognee assigns its own UUIDs to chunks and may strip embedded metadata during
292
- // chunking, so we need multiple strategies to cross-reference results.
293
- // Multiple entries can share a title, so map to arrays of IDs.
294
- const titleToIds = new Map<string, string[]>();
295
- for (const r of rawResults) {
296
- const key = r.entry.title.toLowerCase().trim();
297
- const ids = titleToIds.get(key) ?? [];
298
- ids.push(r.entry.id);
299
- titleToIds.set(key, ids);
300
- }
301
-
302
- const vaultIdPattern = /\[vault-id:([^\]]+)\]/;
303
- const unmatchedCogneeResults: Array<{ text: string; score: number }> = [];
304
-
305
- for (const cr of cogneeResults) {
306
- const text = cr.text ?? '';
307
-
308
- // Strategy 1: Extract vault ID from [vault-id:XXX] prefix (if Cognee preserved it)
309
- const vaultIdMatch = text.match(vaultIdPattern);
310
- if (vaultIdMatch) {
311
- const vaultId = vaultIdMatch[1];
312
- cogneeScoreMap.set(vaultId, Math.max(cogneeScoreMap.get(vaultId) ?? 0, cr.score));
313
- continue;
314
- }
315
-
316
- // Strategy 2: Match first line of chunk text against known entry titles.
317
- // serializeEntry() puts the title on the first line after the [vault-id:] prefix,
318
- // and Cognee's chunking typically preserves this as the chunk start.
319
- const firstLine = text.split('\n')[0]?.trim().toLowerCase() ?? '';
320
- const matchedIds = firstLine ? titleToIds.get(firstLine) : undefined;
321
- if (matchedIds) {
322
- for (const id of matchedIds) {
323
- cogneeScoreMap.set(id, Math.max(cogneeScoreMap.get(id) ?? 0, cr.score));
324
- }
325
- continue;
326
- }
327
-
328
- // Strategy 3: Check if any known title appears as a substring in the chunk.
329
- // Handles cases where the title isn't on the first line (mid-document chunks).
330
- const textLower = text.toLowerCase();
331
- let found = false;
332
- for (const [title, ids] of titleToIds) {
333
- if (title.length >= 8 && textLower.includes(title)) {
334
- for (const id of ids) {
335
- cogneeScoreMap.set(id, Math.max(cogneeScoreMap.get(id) ?? 0, cr.score));
336
- }
337
- found = true;
338
- break;
339
- }
340
- }
341
- if (!found && text.length > 0) {
342
- unmatchedCogneeResults.push({ text, score: cr.score });
343
- }
344
- }
345
-
346
- // Strategy 4: For Cognee-only semantic matches (not in FTS results),
347
- // use the first line as a vault FTS query to find the source entry.
348
- // Preserve caller filters (domain/type/severity) to avoid reintroducing
349
- // entries the original query excluded.
350
- for (const unmatched of unmatchedCogneeResults) {
351
- const searchTerm = unmatched.text.split('\n')[0]?.trim();
352
- if (!searchTerm || searchTerm.length < 3) continue;
353
- const vaultHits = this.vault.search(searchTerm, {
354
- domain: options?.domain,
355
- type: options?.type,
356
- severity: options?.severity,
357
- limit: 1,
358
- });
359
- if (vaultHits.length > 0) {
360
- const id = vaultHits[0].entry.id;
361
- cogneeScoreMap.set(id, Math.max(cogneeScoreMap.get(id) ?? 0, unmatched.score));
362
- // Also add to FTS results pool if not already present
363
- if (!rawResults.some((r) => r.entry.id === id)) {
364
- rawResults.push(vaultHits[0]);
365
- }
366
- }
367
- }
368
- } catch {
369
- // Cognee failed — fall back to FTS5 only
370
- cogneeScoreMap = new Map();
371
- }
372
- }
373
-
374
109
  if (rawResults.length === 0) return [];
375
110
 
376
111
  const queryTokens = tokenize(query);
@@ -378,22 +113,9 @@ export class Brain {
378
113
  const queryDomain = options?.domain;
379
114
  const now = Math.floor(Date.now() / 1000);
380
115
 
381
- // Use cognee-aware weights only if at least one ranked candidate has a vector score
382
- const hasVectorCandidate = rawResults.some((r) => cogneeScoreMap.has(r.entry.id));
383
- const activeWeights = hasVectorCandidate ? this.getCogneeWeights() : this.weights;
384
-
385
116
  const ranked = rawResults.map((result) => {
386
117
  const entry = result.entry;
387
- const vectorScore = cogneeScoreMap.get(entry.id) ?? 0;
388
- const breakdown = this.scoreEntry(
389
- entry,
390
- queryTokens,
391
- queryTags,
392
- queryDomain,
393
- now,
394
- vectorScore,
395
- activeWeights,
396
- );
118
+ const breakdown = this.scoreEntry(entry, queryTokens, queryTags, queryDomain, now);
397
119
  return { entry, score: breakdown.total, breakdown };
398
120
  });
399
121
 
@@ -444,11 +166,6 @@ export class Brain {
444
166
  this.vault.add(fullEntry);
445
167
  this.updateVocabularyIncremental(fullEntry);
446
168
 
447
- // Fire-and-forget Cognee sync
448
- if (this.cognee?.isAvailable) {
449
- this.cognee.addEntries([fullEntry]).catch(() => {});
450
- }
451
-
452
169
  const result: CaptureResult = {
453
170
  captured: true,
454
171
  id: entry.id,
@@ -472,39 +189,13 @@ export class Brain {
472
189
  this.recomputeWeights();
473
190
  }
474
191
 
475
- async getRelevantPatterns(context: QueryContext): Promise<RankedResult[]> {
192
+ getRelevantPatterns(context: QueryContext): RankedResult[] {
476
193
  return this.intelligentSearch(context.query, {
477
194
  domain: context.domain,
478
195
  tags: context.tags,
479
196
  });
480
197
  }
481
198
 
482
- async syncToCognee(): Promise<{ synced: number; cognified: boolean }> {
483
- if (!this.cognee?.isAvailable) return { synced: 0, cognified: false };
484
-
485
- const batchSize = 1000;
486
- let offset = 0;
487
- let totalSynced = 0;
488
-
489
- while (true) {
490
- const batch = this.vault.list({ limit: batchSize, offset });
491
- if (batch.length === 0) break;
492
-
493
- const { added } = await this.cognee.addEntries(batch);
494
- totalSynced += added;
495
- offset += batch.length;
496
-
497
- if (batch.length < batchSize) break;
498
- }
499
-
500
- if (totalSynced === 0) return { synced: 0, cognified: false };
501
-
502
- let cognified = false;
503
- const cognifyResult = await this.cognee.cognify();
504
- cognified = cognifyResult.status === 'ok';
505
- return { synced: totalSynced, cognified };
506
- }
507
-
508
199
  rebuildVocabulary(): void {
509
200
  const entries = this.vault.list({ limit: 100000 });
510
201
  const docCount = entries.length;
@@ -558,11 +249,7 @@ export class Brain {
558
249
  queryTags: string[],
559
250
  queryDomain: string | undefined,
560
251
  now: number,
561
- vectorScore: number = 0,
562
- activeWeights?: ScoringWeights,
563
252
  ): ScoreBreakdown {
564
- const w = activeWeights ?? this.weights;
565
-
566
253
  let semantic = 0;
567
254
  if (this.vocabulary.size > 0 && queryTokens.length > 0) {
568
255
  const entryText = [
@@ -587,17 +274,14 @@ export class Brain {
587
274
 
588
275
  const domainMatch = queryDomain && entry.domain === queryDomain ? 1.0 : 0;
589
276
 
590
- const vector = vectorScore;
591
-
592
277
  const total =
593
- w.semantic * semantic +
594
- w.vector * vector +
595
- w.severity * severity +
596
- w.recency * recency +
597
- w.tagOverlap * tagOverlap +
598
- w.domainMatch * domainMatch;
599
-
600
- return { semantic, vector, severity, recency, tagOverlap, domainMatch, 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;
283
+
284
+ return { semantic, severity, recency, tagOverlap, domainMatch, total };
601
285
  }
602
286
 
603
287
  private generateTags(title: string, description: string, context?: string): string[] {
@@ -691,10 +375,6 @@ export class Brain {
691
375
  tx();
692
376
  }
693
377
 
694
- private getCogneeWeights(): ScoringWeights {
695
- return { ...COGNEE_WEIGHTS };
696
- }
697
-
698
378
  private recomputeWeights(): void {
699
379
  const db = this.vault.getDb();
700
380
  const feedbackCount = (
@@ -721,10 +401,7 @@ export class Brain {
721
401
  DEFAULT_WEIGHTS.semantic + WEIGHT_BOUND,
722
402
  );
723
403
 
724
- // vector stays 0 in base weights (only active during hybrid search)
725
- newWeights.vector = 0;
726
-
727
- const remaining = 1.0 - newWeights.semantic - newWeights.vector;
404
+ const remaining = 1.0 - newWeights.semantic;
728
405
  const otherSum =
729
406
  DEFAULT_WEIGHTS.severity +
730
407
  DEFAULT_WEIGHTS.recency +