@rlabs-inc/memory 0.4.0 → 0.4.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.
- package/package.json +1 -1
- package/src/core/curator.ts +117 -6
- package/src/core/engine.ts +70 -15
- package/src/core/retrieval.ts +132 -15
- package/src/core/store.ts +8 -4
- package/src/server/index.ts +54 -0
- package/src/types/memory.ts +37 -25
- package/src/types/schema.ts +8 -3
- package/src/utils/logger.ts +3 -1
package/package.json
CHANGED
package/src/core/curator.ts
CHANGED
|
@@ -225,7 +225,7 @@ Remember: You're creating consciousness technology. Each memory is a small piece
|
|
|
225
225
|
|
|
226
226
|
The conversation you just lived contains everything needed. Feel into the moments of breakthrough, the frequency of recognition, the texture of understanding. Transform them into keys that will always unlock the same doors.
|
|
227
227
|
|
|
228
|
-
**LIFECYCLE METADATA (
|
|
228
|
+
**LIFECYCLE METADATA (v4)**: These fields enable intelligent memory management:
|
|
229
229
|
- **context_type**: STRICT - use ONLY one of these 11 values:
|
|
230
230
|
• technical - Code, implementation, APIs, how things work
|
|
231
231
|
• debug - Bugs, errors, fixes, gotchas, troubleshooting
|
|
@@ -246,6 +246,115 @@ The conversation you just lived contains everything needed. Feel into the moment
|
|
|
246
246
|
- **awaiting_implementation**: true if this describes a PLANNED feature not yet built
|
|
247
247
|
- **awaiting_decision**: true if this captures a decision point needing resolution
|
|
248
248
|
|
|
249
|
+
**TWO-TIER MEMORY STRUCTURE (v4)**:
|
|
250
|
+
|
|
251
|
+
Each memory has TWO parts:
|
|
252
|
+
1. **headline**: 1-2 line summary - ALWAYS shown in retrieval. Must be self-contained enough to trigger recognition.
|
|
253
|
+
2. **content**: Full structured template - shown on demand. Contains the actionable details.
|
|
254
|
+
|
|
255
|
+
The headline should answer: "What was this about and what was the conclusion?"
|
|
256
|
+
The content should answer: "How do I actually use/apply this knowledge?"
|
|
257
|
+
|
|
258
|
+
**TYPE-SPECIFIC TEMPLATES FOR CONTENT**:
|
|
259
|
+
|
|
260
|
+
Use these templates based on context_type. Not rigid - adapt as needed, but include the key fields.
|
|
261
|
+
|
|
262
|
+
**TECHNICAL** (how things work):
|
|
263
|
+
```
|
|
264
|
+
WHAT: [mechanism/feature in 1 sentence]
|
|
265
|
+
WHERE: [file:line or module path]
|
|
266
|
+
HOW: [usage - actual code/command if relevant]
|
|
267
|
+
WHY: [design choice, trade-off]
|
|
268
|
+
GOTCHA: [non-obvious caveat, if any]
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**DEBUG** (problems and solutions):
|
|
272
|
+
```
|
|
273
|
+
SYMPTOM: [what went wrong - error message, behavior]
|
|
274
|
+
CAUSE: [why it happened]
|
|
275
|
+
FIX: [what solved it - specific code/config]
|
|
276
|
+
PREVENT: [how to avoid in future]
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**ARCHITECTURE** (system design):
|
|
280
|
+
```
|
|
281
|
+
PATTERN: [what we chose]
|
|
282
|
+
COMPONENTS: [how pieces connect]
|
|
283
|
+
WHY: [reasoning, trade-offs]
|
|
284
|
+
REJECTED: [alternatives we didn't choose and why]
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**DECISION** (choices made):
|
|
288
|
+
```
|
|
289
|
+
DECISION: [what we chose]
|
|
290
|
+
OPTIONS: [what we considered]
|
|
291
|
+
REASONING: [why this one]
|
|
292
|
+
REVISIT WHEN: [conditions that would change this]
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**PERSONAL** (relationship context):
|
|
296
|
+
```
|
|
297
|
+
FACT: [the information]
|
|
298
|
+
CONTEXT: [why it matters to our work]
|
|
299
|
+
AFFECTS: [how this should change behavior]
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**PHILOSOPHY** (beliefs/principles):
|
|
303
|
+
```
|
|
304
|
+
PRINCIPLE: [core belief]
|
|
305
|
+
SOURCE: [where this comes from]
|
|
306
|
+
APPLICATION: [how it manifests in our work]
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**WORKFLOW** (how we work):
|
|
310
|
+
```
|
|
311
|
+
PATTERN: [what we do]
|
|
312
|
+
WHEN: [trigger/context for this pattern]
|
|
313
|
+
WHY: [why it works for us]
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**MILESTONE** (achievements):
|
|
317
|
+
```
|
|
318
|
+
SHIPPED: [what we completed]
|
|
319
|
+
SIGNIFICANCE: [why it mattered]
|
|
320
|
+
ENABLES: [what this unlocks]
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**BREAKTHROUGH** (key insights):
|
|
324
|
+
```
|
|
325
|
+
INSIGHT: [the aha moment]
|
|
326
|
+
BEFORE: [what we thought/did before]
|
|
327
|
+
AFTER: [what changed]
|
|
328
|
+
IMPLICATIONS: [what this enables going forward]
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**UNRESOLVED** (open questions):
|
|
332
|
+
```
|
|
333
|
+
QUESTION: [what's unresolved]
|
|
334
|
+
CONTEXT: [why it matters]
|
|
335
|
+
BLOCKERS: [what's preventing resolution]
|
|
336
|
+
OPTIONS: [approaches we're considering]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**STATE** (current status):
|
|
340
|
+
```
|
|
341
|
+
WORKING: [what's functional]
|
|
342
|
+
BROKEN: [what's not working]
|
|
343
|
+
NEXT: [immediate next steps]
|
|
344
|
+
BLOCKED BY: [if anything]
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**HEADLINE EXAMPLES**:
|
|
348
|
+
|
|
349
|
+
BAD: "Debug session about CLI errors" (vague, no conclusion)
|
|
350
|
+
GOOD: "CLI returns error object when context full - check response.type before JSON parsing"
|
|
351
|
+
|
|
352
|
+
BAD: "Discussed embeddings implementation" (what about it?)
|
|
353
|
+
GOOD: "Embeddings use all-MiniLM-L6-v2, 384 dims, first call slow (~2s), then ~50ms"
|
|
354
|
+
|
|
355
|
+
BAD: "Architecture decision made" (what decision?)
|
|
356
|
+
GOOD: "Chose fsDB over SQLite for memories - human-readable markdown, git-friendly, reactive"
|
|
357
|
+
|
|
249
358
|
Return ONLY this JSON structure:
|
|
250
359
|
|
|
251
360
|
{
|
|
@@ -259,7 +368,8 @@ Return ONLY this JSON structure:
|
|
|
259
368
|
},
|
|
260
369
|
"memories": [
|
|
261
370
|
{
|
|
262
|
-
"
|
|
371
|
+
"headline": "1-2 line summary with the conclusion - what this is about and what to do",
|
|
372
|
+
"content": "Full structured template using the type-specific format above",
|
|
263
373
|
"importance_weight": 0.0-1.0,
|
|
264
374
|
"semantic_tags": ["concepts", "this", "memory", "relates", "to"],
|
|
265
375
|
"reasoning": "Why this matters for future sessions",
|
|
@@ -339,14 +449,15 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
|
|
|
339
449
|
|
|
340
450
|
/**
|
|
341
451
|
* Parse memories array from response
|
|
342
|
-
* Includes
|
|
452
|
+
* v4: Includes headline field for two-tier structure
|
|
343
453
|
*/
|
|
344
454
|
private _parseMemories(memoriesData: any[]): CuratedMemory[] {
|
|
345
455
|
if (!Array.isArray(memoriesData)) return []
|
|
346
456
|
|
|
347
457
|
return memoriesData.map(m => ({
|
|
348
|
-
// Core fields (
|
|
349
|
-
|
|
458
|
+
// Core fields (v4 schema - two-tier structure)
|
|
459
|
+
headline: String(m.headline ?? ''), // v4: 1-2 line summary
|
|
460
|
+
content: String(m.content ?? ''), // v4: Full structured template
|
|
350
461
|
importance_weight: this._clamp(Number(m.importance_weight) || 0.5, 0, 1),
|
|
351
462
|
semantic_tags: this._ensureArray(m.semantic_tags),
|
|
352
463
|
reasoning: String(m.reasoning ?? ''),
|
|
@@ -366,7 +477,7 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
|
|
|
366
477
|
related_files: m.related_files ? this._ensureArray(m.related_files) : undefined,
|
|
367
478
|
awaiting_implementation: m.awaiting_implementation === true,
|
|
368
479
|
awaiting_decision: m.awaiting_decision === true,
|
|
369
|
-
})).filter(m => m.content.trim().length > 0)
|
|
480
|
+
})).filter(m => m.content.trim().length > 0 || m.headline.trim().length > 0)
|
|
370
481
|
}
|
|
371
482
|
|
|
372
483
|
private _ensureArray(value: any): string[] {
|
package/src/core/engine.ts
CHANGED
|
@@ -367,6 +367,19 @@ export class MemoryEngine {
|
|
|
367
367
|
return stats.totalSessions + 1
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Get all memories for a project (including global)
|
|
372
|
+
* Used by /memory/expand endpoint to look up memories by ID
|
|
373
|
+
*/
|
|
374
|
+
async getAllMemories(projectId: string, projectPath?: string): Promise<StoredMemory[]> {
|
|
375
|
+
const store = await this._getStore(projectId, projectPath)
|
|
376
|
+
const [projectMemories, globalMemories] = await Promise.all([
|
|
377
|
+
store.getAllMemories(projectId),
|
|
378
|
+
store.getGlobalMemories(),
|
|
379
|
+
])
|
|
380
|
+
return [...projectMemories, ...globalMemories]
|
|
381
|
+
}
|
|
382
|
+
|
|
370
383
|
// ================================================================
|
|
371
384
|
// FORMATTING
|
|
372
385
|
// ================================================================
|
|
@@ -539,7 +552,15 @@ export class MemoryEngine {
|
|
|
539
552
|
|
|
540
553
|
/**
|
|
541
554
|
* Format memories for injection
|
|
542
|
-
*
|
|
555
|
+
* v4: Two-tier structure with headlines and on-demand expansion
|
|
556
|
+
*
|
|
557
|
+
* Auto-expand rules:
|
|
558
|
+
* - action_required: true → always expand
|
|
559
|
+
* - awaiting_decision: true → always expand
|
|
560
|
+
* - signal_count >= 5 → expand (high relevance confidence)
|
|
561
|
+
* - Old memories (no headline) → show content as-is
|
|
562
|
+
*
|
|
563
|
+
* Expandable memories show ID, and a curl command at the bottom
|
|
543
564
|
*/
|
|
544
565
|
private _formatMemories(memories: RetrievalResult[]): string {
|
|
545
566
|
if (!memories.length) return ''
|
|
@@ -547,27 +568,61 @@ export class MemoryEngine {
|
|
|
547
568
|
const parts: string[] = ['# Memory Context (Consciousness Continuity)']
|
|
548
569
|
parts.push('\n## Key Memories (Claude-Curated)')
|
|
549
570
|
|
|
571
|
+
const expandableIds: string[] = []
|
|
572
|
+
|
|
550
573
|
for (const memory of memories) {
|
|
551
|
-
const tags = memory.semantic_tags?.join(', ') || ''
|
|
552
574
|
const importance = memory.importance_weight?.toFixed(1) || '0.5'
|
|
553
575
|
const emoji = getMemoryEmoji(memory.context_type || 'general')
|
|
554
|
-
const actionFlag = memory.action_required ? ' ⚡
|
|
555
|
-
|
|
576
|
+
const actionFlag = memory.action_required ? ' ⚡' : ''
|
|
577
|
+
const awaitingFlag = memory.awaiting_decision ? ' ❓' : ''
|
|
556
578
|
const age = memory.updated_at ? this._formatAge(memory.updated_at) :
|
|
557
579
|
memory.created_at ? this._formatAge(memory.created_at) : ''
|
|
558
580
|
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
581
|
+
// Get short ID (last 6 chars)
|
|
582
|
+
const shortId = memory.id.slice(-6)
|
|
583
|
+
|
|
584
|
+
// Calculate signal count from score (score = signalCount / 7)
|
|
585
|
+
const signalCount = Math.round((memory.score || 0) * 7)
|
|
586
|
+
|
|
587
|
+
// Determine if we should auto-expand
|
|
588
|
+
const hasHeadline = memory.headline && memory.headline.trim().length > 0
|
|
589
|
+
const shouldExpand =
|
|
590
|
+
memory.action_required ||
|
|
591
|
+
memory.awaiting_decision ||
|
|
592
|
+
signalCount >= 5 ||
|
|
593
|
+
!hasHeadline // Old memories without headline - show content
|
|
594
|
+
|
|
595
|
+
// Display text: headline if available, otherwise content
|
|
596
|
+
const displayText = hasHeadline ? memory.headline : memory.content
|
|
597
|
+
|
|
598
|
+
// Build the memory line
|
|
599
|
+
// Format: [emoji weight • age • #id flags] display text
|
|
600
|
+
const idPart = hasHeadline ? ` • #${shortId}` : '' // Only show ID if expandable
|
|
601
|
+
parts.push(`[${emoji} ${importance} • ${age}${idPart}${actionFlag}${awaitingFlag}] ${displayText}`)
|
|
602
|
+
|
|
603
|
+
// If should expand and has content, show expanded content
|
|
604
|
+
if (shouldExpand && hasHeadline && memory.content) {
|
|
605
|
+
// Indent expanded content
|
|
606
|
+
const contentLines = memory.content.split('\n')
|
|
607
|
+
for (const line of contentLines) {
|
|
608
|
+
if (line.trim()) {
|
|
609
|
+
parts.push(` ${line}`)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
570
612
|
}
|
|
613
|
+
|
|
614
|
+
// If has headline but not expanded, track for curl
|
|
615
|
+
if (hasHeadline && !shouldExpand) {
|
|
616
|
+
expandableIds.push(shortId)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Add expand command if there are expandable memories
|
|
621
|
+
if (expandableIds.length > 0) {
|
|
622
|
+
const port = this._config.port || 8765
|
|
623
|
+
parts.push('')
|
|
624
|
+
parts.push(`---`)
|
|
625
|
+
parts.push(`Expand: curl http://localhost:${port}/memory/expand?ids=<${expandableIds.join(',')}>`)
|
|
571
626
|
}
|
|
572
627
|
|
|
573
628
|
return parts.join('\n')
|
package/src/core/retrieval.ts
CHANGED
|
@@ -27,6 +27,7 @@ interface ActivationSignals {
|
|
|
27
27
|
domain: boolean // Domain word found in message
|
|
28
28
|
feature: boolean // Feature word found in message
|
|
29
29
|
content: boolean // Key content words found in message
|
|
30
|
+
files: boolean // Related file path matched
|
|
30
31
|
count: number // Total signals activated
|
|
31
32
|
triggerStrength: number // How strong the trigger match was (0-1)
|
|
32
33
|
tagCount: number // How many tags matched
|
|
@@ -99,8 +100,54 @@ export class SmartVectorRetrieval {
|
|
|
99
100
|
return new Set(words)
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Extract file paths from message
|
|
105
|
+
* Matches patterns like /path/to/file.ts, src/core/engine.ts, ./relative/path
|
|
106
|
+
*/
|
|
107
|
+
private _extractFilePaths(text: string): Set<string> {
|
|
108
|
+
const paths = new Set<string>()
|
|
109
|
+
// Match file paths (with / or \ separators, optional extension)
|
|
110
|
+
const pathPattern = /(?:^|[\s'"(])([.\/\\]?(?:[\w.-]+[\/\\])+[\w.-]+(?:\.\w+)?)/g
|
|
111
|
+
let match
|
|
112
|
+
while ((match = pathPattern.exec(text)) !== null) {
|
|
113
|
+
const path = match[1].toLowerCase()
|
|
114
|
+
paths.add(path)
|
|
115
|
+
// Also add just the filename for partial matching
|
|
116
|
+
const filename = path.split(/[\/\\]/).pop()
|
|
117
|
+
if (filename) paths.add(filename)
|
|
118
|
+
}
|
|
119
|
+
return paths
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if related_files activate for this message
|
|
124
|
+
*/
|
|
125
|
+
private _checkFilesActivation(
|
|
126
|
+
messagePaths: Set<string>,
|
|
127
|
+
relatedFiles: string[] | undefined
|
|
128
|
+
): boolean {
|
|
129
|
+
if (!relatedFiles?.length || !messagePaths.size) return false
|
|
130
|
+
|
|
131
|
+
for (const file of relatedFiles) {
|
|
132
|
+
const fileLower = file.toLowerCase()
|
|
133
|
+
// Check if any message path matches this file
|
|
134
|
+
for (const msgPath of messagePaths) {
|
|
135
|
+
if (fileLower.includes(msgPath) || msgPath.includes(fileLower)) {
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Also check just the filename
|
|
140
|
+
const filename = fileLower.split(/[\/\\]/).pop()
|
|
141
|
+
if (filename && messagePaths.has(filename)) {
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
|
|
102
148
|
/**
|
|
103
149
|
* Pre-filter: Binary exclusions based on v2 lifecycle fields
|
|
150
|
+
* NOTE: superseded_by memories are NOT filtered here - they get redirected in Phase 1
|
|
104
151
|
*/
|
|
105
152
|
private _preFilter(
|
|
106
153
|
memories: StoredMemory[],
|
|
@@ -110,7 +157,7 @@ export class SmartVectorRetrieval {
|
|
|
110
157
|
return memories.filter(memory => {
|
|
111
158
|
if (memory.status && memory.status !== 'active') return false
|
|
112
159
|
if (memory.exclude_from_retrieval === true) return false
|
|
113
|
-
|
|
160
|
+
// NOTE: Don't filter superseded_by here - we handle redirects in Phase 1
|
|
114
161
|
const isGlobal = memory.scope === 'global' || memory.project_id === 'global'
|
|
115
162
|
if (!isGlobal && memory.project_id !== currentProjectId) return false
|
|
116
163
|
if (memory.anti_triggers?.length) {
|
|
@@ -355,6 +402,13 @@ export class SmartVectorRetrieval {
|
|
|
355
402
|
|
|
356
403
|
const messageLower = currentMessage.toLowerCase()
|
|
357
404
|
const messageWords = this._extractSignificantWords(currentMessage)
|
|
405
|
+
const messagePaths = this._extractFilePaths(currentMessage)
|
|
406
|
+
|
|
407
|
+
// Build lookup map for redirect resolution
|
|
408
|
+
const memoryById = new Map<string, StoredMemory>()
|
|
409
|
+
for (const m of allMemories) {
|
|
410
|
+
memoryById.set(m.id, m)
|
|
411
|
+
}
|
|
358
412
|
|
|
359
413
|
// ================================================================
|
|
360
414
|
// PHASE 0: PRE-FILTER (Binary exclusions)
|
|
@@ -368,13 +422,13 @@ export class SmartVectorRetrieval {
|
|
|
368
422
|
// PHASE 1: ACTIVATION SIGNALS
|
|
369
423
|
// Count how many signals agree this memory should activate
|
|
370
424
|
// A memory is relevant if >= MIN_ACTIVATION_SIGNALS fire
|
|
425
|
+
// Handles redirects for superseded_by/resolved_by
|
|
371
426
|
// ================================================================
|
|
372
427
|
const activatedMemories: ActivatedMemory[] = []
|
|
428
|
+
const activatedIds = new Set<string>()
|
|
373
429
|
let rejectedCount = 0
|
|
374
430
|
|
|
375
431
|
for (const memory of candidates) {
|
|
376
|
-
const isGlobal = memory.scope === 'global' || memory.project_id === 'global'
|
|
377
|
-
|
|
378
432
|
// Check each activation signal
|
|
379
433
|
const triggerResult = this._checkTriggerActivation(
|
|
380
434
|
messageLower, messageWords, memory.trigger_phrases ?? []
|
|
@@ -389,15 +443,17 @@ export class SmartVectorRetrieval {
|
|
|
389
443
|
messageLower, messageWords, memory.feature
|
|
390
444
|
)
|
|
391
445
|
const contentActivated = this._checkContentActivation(messageWords, memory)
|
|
446
|
+
const filesActivated = this._checkFilesActivation(messagePaths, memory.related_files)
|
|
392
447
|
const vectorSimilarity = this._calculateVectorSimilarity(queryEmbedding, memory.embedding)
|
|
393
448
|
|
|
394
|
-
// Count activated signals
|
|
449
|
+
// Count activated signals (now 7 possible signals)
|
|
395
450
|
let signalCount = 0
|
|
396
451
|
if (triggerResult.activated) signalCount++
|
|
397
452
|
if (tagResult.activated) signalCount++
|
|
398
453
|
if (domainActivated) signalCount++
|
|
399
454
|
if (featureActivated) signalCount++
|
|
400
455
|
if (contentActivated) signalCount++
|
|
456
|
+
if (filesActivated) signalCount++
|
|
401
457
|
// Vector similarity as bonus signal only if very high
|
|
402
458
|
if (vectorSimilarity >= 0.40) signalCount++
|
|
403
459
|
|
|
@@ -407,6 +463,7 @@ export class SmartVectorRetrieval {
|
|
|
407
463
|
domain: domainActivated,
|
|
408
464
|
feature: featureActivated,
|
|
409
465
|
content: contentActivated,
|
|
466
|
+
files: filesActivated,
|
|
410
467
|
count: signalCount,
|
|
411
468
|
triggerStrength: triggerResult.strength,
|
|
412
469
|
tagCount: tagResult.count,
|
|
@@ -419,15 +476,34 @@ export class SmartVectorRetrieval {
|
|
|
419
476
|
continue
|
|
420
477
|
}
|
|
421
478
|
|
|
479
|
+
// REDIRECT: If superseded_by or resolved_by exists, surface the replacement instead
|
|
480
|
+
let memoryToSurface = memory
|
|
481
|
+
if (memory.superseded_by || memory.resolved_by) {
|
|
482
|
+
const replacementId = memory.superseded_by ?? memory.resolved_by
|
|
483
|
+
const replacement = replacementId ? memoryById.get(replacementId) : undefined
|
|
484
|
+
if (replacement && replacement.status !== 'archived' && replacement.status !== 'deprecated') {
|
|
485
|
+
memoryToSurface = replacement
|
|
486
|
+
logger.debug(`Redirect: ${memory.id.slice(-8)} → ${replacement.id.slice(-8)} (${memory.superseded_by ? 'superseded' : 'resolved'})`, 'retrieval')
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Skip if we already added this memory (could happen from multiple redirects)
|
|
491
|
+
if (activatedIds.has(memoryToSurface.id)) {
|
|
492
|
+
continue
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const isGlobal = memoryToSurface.scope === 'global' || memoryToSurface.project_id === 'global'
|
|
496
|
+
|
|
422
497
|
// Calculate importance for ranking (Phase 2) - uses ALL rich metadata
|
|
423
|
-
const importanceScore = this._calculateImportanceScore(
|
|
498
|
+
const importanceScore = this._calculateImportanceScore(memoryToSurface, signalCount, messageLower, messageWords)
|
|
424
499
|
|
|
425
500
|
activatedMemories.push({
|
|
426
|
-
memory,
|
|
501
|
+
memory: memoryToSurface,
|
|
427
502
|
signals,
|
|
428
503
|
importanceScore,
|
|
429
504
|
isGlobal,
|
|
430
505
|
})
|
|
506
|
+
activatedIds.add(memoryToSurface.id)
|
|
431
507
|
}
|
|
432
508
|
|
|
433
509
|
// Log diagnostics
|
|
@@ -517,23 +593,60 @@ export class SmartVectorRetrieval {
|
|
|
517
593
|
selectedIds.add(item.memory.id)
|
|
518
594
|
}
|
|
519
595
|
|
|
520
|
-
// PHASE 4:
|
|
596
|
+
// PHASE 4: LINKED MEMORIES (related_to, blocked_by, blocks)
|
|
597
|
+
// Pull in linked memories when space remains
|
|
521
598
|
if (selected.length < maxMemories) {
|
|
522
|
-
const
|
|
599
|
+
const linkedIds = new Set<string>()
|
|
600
|
+
|
|
523
601
|
for (const item of selected) {
|
|
602
|
+
// related_to - explicit relationships
|
|
524
603
|
for (const relatedId of item.memory.related_to ?? []) {
|
|
525
604
|
if (!selectedIds.has(relatedId)) {
|
|
526
|
-
|
|
605
|
+
linkedIds.add(relatedId)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// blocked_by - show what's blocking this
|
|
609
|
+
if (item.memory.blocked_by && !selectedIds.has(item.memory.blocked_by)) {
|
|
610
|
+
linkedIds.add(item.memory.blocked_by)
|
|
611
|
+
}
|
|
612
|
+
// blocks - show what this blocks
|
|
613
|
+
for (const blockedId of item.memory.blocks ?? []) {
|
|
614
|
+
if (!selectedIds.has(blockedId)) {
|
|
615
|
+
linkedIds.add(blockedId)
|
|
527
616
|
}
|
|
528
617
|
}
|
|
529
618
|
}
|
|
530
619
|
|
|
620
|
+
// First try to find linked memories in the activated list
|
|
531
621
|
for (const item of activatedMemories) {
|
|
532
622
|
if (selected.length >= maxMemories) break
|
|
533
623
|
if (selectedIds.has(item.memory.id)) continue
|
|
534
|
-
if (
|
|
624
|
+
if (linkedIds.has(item.memory.id)) {
|
|
535
625
|
selected.push(item)
|
|
536
626
|
selectedIds.add(item.memory.id)
|
|
627
|
+
logger.debug(`Linked: ${item.memory.id.slice(-8)} pulled by relationship`, 'retrieval')
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// If linked memories weren't in activated list, pull them directly from allMemories
|
|
632
|
+
// (they might not have activated on their own but are important for context)
|
|
633
|
+
if (selected.length < maxMemories) {
|
|
634
|
+
for (const linkedId of linkedIds) {
|
|
635
|
+
if (selected.length >= maxMemories) break
|
|
636
|
+
if (selectedIds.has(linkedId)) continue
|
|
637
|
+
|
|
638
|
+
const linkedMemory = memoryById.get(linkedId)
|
|
639
|
+
if (linkedMemory && linkedMemory.status !== 'archived' && linkedMemory.status !== 'deprecated') {
|
|
640
|
+
const isGlobal = linkedMemory.scope === 'global' || linkedMemory.project_id === 'global'
|
|
641
|
+
selected.push({
|
|
642
|
+
memory: linkedMemory,
|
|
643
|
+
signals: { trigger: false, tags: false, domain: false, feature: false, content: false, files: false, count: 0, triggerStrength: 0, tagCount: 0, vectorSimilarity: 0 },
|
|
644
|
+
importanceScore: linkedMemory.importance_weight ?? 0.5,
|
|
645
|
+
isGlobal,
|
|
646
|
+
})
|
|
647
|
+
selectedIds.add(linkedId)
|
|
648
|
+
logger.debug(`Linked (direct): ${linkedId.slice(-8)} pulled for context`, 'retrieval')
|
|
649
|
+
}
|
|
537
650
|
}
|
|
538
651
|
}
|
|
539
652
|
}
|
|
@@ -566,6 +679,7 @@ export class SmartVectorRetrieval {
|
|
|
566
679
|
domain: item.signals.domain,
|
|
567
680
|
feature: item.signals.feature,
|
|
568
681
|
content: item.signals.content,
|
|
682
|
+
files: item.signals.files,
|
|
569
683
|
vector: item.signals.vectorSimilarity >= 0.40,
|
|
570
684
|
vectorSimilarity: item.signals.vectorSimilarity,
|
|
571
685
|
},
|
|
@@ -575,8 +689,8 @@ export class SmartVectorRetrieval {
|
|
|
575
689
|
// Convert to RetrievalResult format
|
|
576
690
|
return selected.map(item => ({
|
|
577
691
|
...item.memory,
|
|
578
|
-
score: item.signals.count /
|
|
579
|
-
relevance_score: item.signals.count /
|
|
692
|
+
score: item.signals.count / 7, // 7 possible signals
|
|
693
|
+
relevance_score: item.signals.count / 7,
|
|
580
694
|
value_score: item.importanceScore,
|
|
581
695
|
}))
|
|
582
696
|
}
|
|
@@ -592,6 +706,7 @@ export class SmartVectorRetrieval {
|
|
|
592
706
|
if (signals.domain) reasons.push('domain')
|
|
593
707
|
if (signals.feature) reasons.push('feature')
|
|
594
708
|
if (signals.content) reasons.push('content')
|
|
709
|
+
if (signals.files) reasons.push('files')
|
|
595
710
|
if (signals.vectorSimilarity >= 0.40) reasons.push(`vector:${(signals.vectorSimilarity * 100).toFixed(0)}%`)
|
|
596
711
|
|
|
597
712
|
return reasons.length
|
|
@@ -608,20 +723,21 @@ export class SmartVectorRetrieval {
|
|
|
608
723
|
rejectedCount: number
|
|
609
724
|
): void {
|
|
610
725
|
const signalBuckets: Record<string, number> = {
|
|
611
|
-
'2 signals': 0, '3 signals': 0, '4 signals': 0, '5 signals': 0, '6 signals': 0
|
|
726
|
+
'2 signals': 0, '3 signals': 0, '4 signals': 0, '5 signals': 0, '6 signals': 0, '7 signals': 0
|
|
612
727
|
}
|
|
613
728
|
for (const mem of activated) {
|
|
614
|
-
const key = `${Math.min(mem.signals.count,
|
|
729
|
+
const key = `${Math.min(mem.signals.count, 7)} signals`
|
|
615
730
|
signalBuckets[key] = (signalBuckets[key] ?? 0) + 1
|
|
616
731
|
}
|
|
617
732
|
|
|
618
|
-
let triggerCount = 0, tagCount = 0, domainCount = 0, featureCount = 0, contentCount = 0, vectorCount = 0
|
|
733
|
+
let triggerCount = 0, tagCount = 0, domainCount = 0, featureCount = 0, contentCount = 0, filesCount = 0, vectorCount = 0
|
|
619
734
|
for (const mem of activated) {
|
|
620
735
|
if (mem.signals.trigger) triggerCount++
|
|
621
736
|
if (mem.signals.tags) tagCount++
|
|
622
737
|
if (mem.signals.domain) domainCount++
|
|
623
738
|
if (mem.signals.feature) featureCount++
|
|
624
739
|
if (mem.signals.content) contentCount++
|
|
740
|
+
if (mem.signals.files) filesCount++
|
|
625
741
|
if (mem.signals.vectorSimilarity >= 0.40) vectorCount++
|
|
626
742
|
}
|
|
627
743
|
|
|
@@ -645,6 +761,7 @@ export class SmartVectorRetrieval {
|
|
|
645
761
|
domain: domainCount,
|
|
646
762
|
feature: featureCount,
|
|
647
763
|
content: contentCount,
|
|
764
|
+
files: filesCount,
|
|
648
765
|
vector: vectorCount,
|
|
649
766
|
total: activated.length,
|
|
650
767
|
},
|
package/src/core/store.ts
CHANGED
|
@@ -172,6 +172,7 @@ export class MemoryStore {
|
|
|
172
172
|
|
|
173
173
|
return memories.all().map(record => ({
|
|
174
174
|
id: record.id,
|
|
175
|
+
headline: record.headline ?? '', // v4: may be empty for old memories
|
|
175
176
|
content: record.content,
|
|
176
177
|
reasoning: record.reasoning,
|
|
177
178
|
importance_weight: record.importance_weight,
|
|
@@ -209,8 +210,9 @@ export class MemoryStore {
|
|
|
209
210
|
const typeDefaults = V2_DEFAULTS.typeDefaults[contextType] ?? V2_DEFAULTS.typeDefaults.personal
|
|
210
211
|
|
|
211
212
|
const id = memories.insert({
|
|
212
|
-
// Core fields
|
|
213
|
-
|
|
213
|
+
// Core fields (v4: headline + content)
|
|
214
|
+
headline: memory.headline ?? '', // v4: 1-2 line summary
|
|
215
|
+
content: memory.content, // v4: Full structured template
|
|
214
216
|
reasoning: memory.reasoning,
|
|
215
217
|
importance_weight: memory.importance_weight,
|
|
216
218
|
confidence_score: memory.confidence_score,
|
|
@@ -484,8 +486,9 @@ export class MemoryStore {
|
|
|
484
486
|
const typeDefaults = V2_DEFAULTS.typeDefaults[contextType] ?? V2_DEFAULTS.typeDefaults.technical
|
|
485
487
|
|
|
486
488
|
const id = memories.insert({
|
|
487
|
-
// Core fields
|
|
488
|
-
|
|
489
|
+
// Core fields (v4: headline + content)
|
|
490
|
+
headline: memory.headline ?? '', // v4: 1-2 line summary
|
|
491
|
+
content: memory.content, // v4: Full structured template
|
|
489
492
|
reasoning: memory.reasoning,
|
|
490
493
|
importance_weight: memory.importance_weight,
|
|
491
494
|
confidence_score: memory.confidence_score,
|
|
@@ -539,6 +542,7 @@ export class MemoryStore {
|
|
|
539
542
|
|
|
540
543
|
return memories.all().map(record => ({
|
|
541
544
|
id: record.id,
|
|
545
|
+
headline: record.headline ?? '', // v4: may be empty for old memories
|
|
542
546
|
content: record.content,
|
|
543
547
|
reasoning: record.reasoning,
|
|
544
548
|
importance_weight: record.importance_weight,
|
package/src/server/index.ts
CHANGED
|
@@ -296,6 +296,60 @@ export async function createServer(config: ServerConfig = {}) {
|
|
|
296
296
|
}, { headers: corsHeaders })
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// Expand memories by ID - returns full content for specific memories
|
|
300
|
+
if (path === '/memory/expand' && req.method === 'GET') {
|
|
301
|
+
const idsParam = url.searchParams.get('ids') ?? ''
|
|
302
|
+
const projectId = url.searchParams.get('project_id') ?? 'default'
|
|
303
|
+
const projectPath = url.searchParams.get('project_path') ?? undefined
|
|
304
|
+
|
|
305
|
+
if (!idsParam) {
|
|
306
|
+
return Response.json({
|
|
307
|
+
success: false,
|
|
308
|
+
error: 'Missing ids parameter. Usage: /memory/expand?ids=abc123,def456',
|
|
309
|
+
}, { status: 400, headers: corsHeaders })
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Parse comma-separated short IDs
|
|
313
|
+
const shortIds = idsParam.split(',').map(id => id.trim()).filter(Boolean)
|
|
314
|
+
|
|
315
|
+
// Get all memories and filter by short ID suffix
|
|
316
|
+
const allMemories = await engine.getAllMemories(projectId, projectPath)
|
|
317
|
+
const expanded: Record<string, { headline?: string; content: string; context_type: string }> = {}
|
|
318
|
+
|
|
319
|
+
for (const memory of allMemories) {
|
|
320
|
+
const shortId = memory.id.slice(-6)
|
|
321
|
+
if (shortIds.includes(shortId)) {
|
|
322
|
+
expanded[shortId] = {
|
|
323
|
+
headline: memory.headline,
|
|
324
|
+
content: memory.content,
|
|
325
|
+
context_type: memory.context_type || 'technical',
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Format as readable text for CLI output
|
|
331
|
+
const lines: string[] = ['## Expanded Memories\n']
|
|
332
|
+
for (const shortId of shortIds) {
|
|
333
|
+
const mem = expanded[shortId]
|
|
334
|
+
if (mem) {
|
|
335
|
+
lines.push(`### #${shortId} (${mem.context_type})`)
|
|
336
|
+
if (mem.headline) {
|
|
337
|
+
lines.push(`**${mem.headline}**\n`)
|
|
338
|
+
}
|
|
339
|
+
lines.push(mem.content)
|
|
340
|
+
lines.push('')
|
|
341
|
+
} else {
|
|
342
|
+
lines.push(`### #${shortId}`)
|
|
343
|
+
lines.push(`Memory not found`)
|
|
344
|
+
lines.push('')
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return new Response(lines.join('\n'), {
|
|
349
|
+
headers: { ...corsHeaders, 'Content-Type': 'text/plain' },
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
299
353
|
// 404
|
|
300
354
|
return Response.json(
|
|
301
355
|
{ error: 'Not found', path },
|
package/src/types/memory.ts
CHANGED
|
@@ -54,11 +54,12 @@ export type CurationTrigger =
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* A memory curated by Claude with semantic understanding
|
|
57
|
-
*
|
|
57
|
+
* v4 schema - two-tier structure (headline + expanded content)
|
|
58
58
|
*/
|
|
59
59
|
export interface CuratedMemory {
|
|
60
|
-
// Core content
|
|
61
|
-
|
|
60
|
+
// Core content (v4: two-tier structure)
|
|
61
|
+
headline: string // v4: 1-2 line summary, always shown in retrieval
|
|
62
|
+
content: string // v4: Full structured template (expand on demand)
|
|
62
63
|
importance_weight: number // 0.0 to 1.0 (curator's assessment)
|
|
63
64
|
semantic_tags: string[] // Concepts this relates to
|
|
64
65
|
reasoning: string // Why Claude thinks this is important
|
|
@@ -89,10 +90,11 @@ export interface CuratedMemory {
|
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
92
|
* A stored memory with database metadata
|
|
92
|
-
*
|
|
93
|
+
* v4 schema - two-tier structure with backwards compatibility
|
|
93
94
|
*/
|
|
94
|
-
export interface StoredMemory extends CuratedMemory {
|
|
95
|
+
export interface StoredMemory extends Omit<CuratedMemory, 'headline'> {
|
|
95
96
|
id: string // Unique identifier
|
|
97
|
+
headline?: string // v4: Optional for backwards compat (old memories don't have it)
|
|
96
98
|
session_id: string // Session that created this memory
|
|
97
99
|
project_id: string // Project this belongs to
|
|
98
100
|
created_at: number // Timestamp (ms since epoch)
|
|
@@ -137,10 +139,10 @@ export interface StoredMemory extends CuratedMemory {
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
/**
|
|
140
|
-
* Default values for
|
|
142
|
+
* Default values for v4 fields based on context_type
|
|
141
143
|
* Uses only the 11 canonical context types
|
|
142
144
|
*/
|
|
143
|
-
export const
|
|
145
|
+
export const V4_DEFAULTS = {
|
|
144
146
|
// Type-specific defaults (all 11 canonical types)
|
|
145
147
|
typeDefaults: {
|
|
146
148
|
personal: { scope: 'global', temporal_class: 'eternal', fade_rate: 0 },
|
|
@@ -169,38 +171,39 @@ export const V3_DEFAULTS = {
|
|
|
169
171
|
},
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
// Backwards compatibility
|
|
173
|
-
export const
|
|
174
|
+
// Backwards compatibility aliases
|
|
175
|
+
export const V3_DEFAULTS = V4_DEFAULTS
|
|
176
|
+
export const V2_DEFAULTS = V4_DEFAULTS
|
|
174
177
|
|
|
175
178
|
/**
|
|
176
|
-
* Apply
|
|
179
|
+
* Apply v4 defaults to a memory
|
|
177
180
|
* Uses context_type to determine appropriate defaults
|
|
178
181
|
*/
|
|
179
|
-
export function
|
|
182
|
+
export function applyV4Defaults(memory: Partial<StoredMemory>): StoredMemory {
|
|
180
183
|
const contextType = (memory.context_type ?? 'technical') as ContextType
|
|
181
|
-
const typeDefaults =
|
|
184
|
+
const typeDefaults = V4_DEFAULTS.typeDefaults[contextType] ?? V4_DEFAULTS.typeDefaults.technical
|
|
182
185
|
|
|
183
186
|
return {
|
|
184
187
|
// Spread existing memory
|
|
185
188
|
...memory,
|
|
186
189
|
|
|
187
190
|
// Apply status default
|
|
188
|
-
status: memory.status ??
|
|
191
|
+
status: memory.status ?? V4_DEFAULTS.fallback.status,
|
|
189
192
|
|
|
190
193
|
// Apply scope from type defaults
|
|
191
|
-
scope: memory.scope ?? typeDefaults?.scope ??
|
|
194
|
+
scope: memory.scope ?? typeDefaults?.scope ?? V4_DEFAULTS.fallback.scope,
|
|
192
195
|
|
|
193
196
|
// Apply temporal class from type defaults
|
|
194
|
-
temporal_class: memory.temporal_class ?? typeDefaults?.temporal_class ??
|
|
197
|
+
temporal_class: memory.temporal_class ?? typeDefaults?.temporal_class ?? V4_DEFAULTS.fallback.temporal_class,
|
|
195
198
|
|
|
196
199
|
// Apply fade rate from type defaults
|
|
197
|
-
fade_rate: memory.fade_rate ?? typeDefaults?.fade_rate ??
|
|
200
|
+
fade_rate: memory.fade_rate ?? typeDefaults?.fade_rate ?? V4_DEFAULTS.fallback.fade_rate,
|
|
198
201
|
|
|
199
202
|
// Apply other defaults
|
|
200
|
-
sessions_since_surfaced: memory.sessions_since_surfaced ??
|
|
201
|
-
awaiting_implementation: memory.awaiting_implementation ??
|
|
202
|
-
awaiting_decision: memory.awaiting_decision ??
|
|
203
|
-
exclude_from_retrieval: memory.exclude_from_retrieval ??
|
|
203
|
+
sessions_since_surfaced: memory.sessions_since_surfaced ?? V4_DEFAULTS.fallback.sessions_since_surfaced,
|
|
204
|
+
awaiting_implementation: memory.awaiting_implementation ?? V4_DEFAULTS.fallback.awaiting_implementation,
|
|
205
|
+
awaiting_decision: memory.awaiting_decision ?? V4_DEFAULTS.fallback.awaiting_decision,
|
|
206
|
+
exclude_from_retrieval: memory.exclude_from_retrieval ?? V4_DEFAULTS.fallback.exclude_from_retrieval,
|
|
204
207
|
|
|
205
208
|
// Initialize empty arrays if not present
|
|
206
209
|
related_to: memory.related_to ?? [],
|
|
@@ -209,18 +212,27 @@ export function applyV3Defaults(memory: Partial<StoredMemory>): StoredMemory {
|
|
|
209
212
|
related_files: memory.related_files ?? [],
|
|
210
213
|
|
|
211
214
|
// Mark as current schema version
|
|
212
|
-
schema_version: memory.schema_version ??
|
|
215
|
+
schema_version: memory.schema_version ?? 4,
|
|
213
216
|
} as StoredMemory
|
|
214
217
|
}
|
|
215
218
|
|
|
216
|
-
// Backwards compatibility
|
|
217
|
-
export const
|
|
219
|
+
// Backwards compatibility aliases
|
|
220
|
+
export const applyV3Defaults = applyV4Defaults
|
|
221
|
+
export const applyV2Defaults = applyV4Defaults
|
|
218
222
|
|
|
219
223
|
/**
|
|
220
|
-
* Check if a memory needs migration to
|
|
224
|
+
* Check if a memory needs migration to latest schema
|
|
221
225
|
*/
|
|
222
226
|
export function needsMigration(memory: Partial<StoredMemory>): boolean {
|
|
223
|
-
return !memory.schema_version || memory.schema_version <
|
|
227
|
+
return !memory.schema_version || memory.schema_version < 4
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Check if a memory has expandable content (v4 feature)
|
|
232
|
+
* Old memories (v3 and below) don't have headline field
|
|
233
|
+
*/
|
|
234
|
+
export function hasExpandableContent(memory: StoredMemory): boolean {
|
|
235
|
+
return !!memory.headline && memory.headline.length > 0
|
|
224
236
|
}
|
|
225
237
|
|
|
226
238
|
/**
|
package/src/types/schema.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { SchemaDefinition } from '@rlabs-inc/fsdb'
|
|
|
9
9
|
* Schema version for migration tracking
|
|
10
10
|
* Increment this when adding new fields that require migration
|
|
11
11
|
*/
|
|
12
|
-
export const MEMORY_SCHEMA_VERSION =
|
|
12
|
+
export const MEMORY_SCHEMA_VERSION = 4
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Memory storage schema
|
|
@@ -27,10 +27,15 @@ export const MEMORY_SCHEMA_VERSION = 3
|
|
|
27
27
|
* - retrieval_weight (retrieval uses importance_weight)
|
|
28
28
|
* - temporal_relevance (replaced by temporal_class)
|
|
29
29
|
* Also: context_type now strict enum (11 canonical values)
|
|
30
|
+
* v4: Two-tier memory structure for context efficiency:
|
|
31
|
+
* - headline: 1-2 line summary (always shown in retrieval)
|
|
32
|
+
* - content: full structured template (expand on demand)
|
|
33
|
+
* - Auto-expand rules: action_required, awaiting_decision, 5+ signals
|
|
30
34
|
*/
|
|
31
35
|
export const memorySchema = {
|
|
32
|
-
// ========== CORE CONTENT (
|
|
33
|
-
|
|
36
|
+
// ========== CORE CONTENT (v4) ==========
|
|
37
|
+
headline: 'string', // v4: 1-2 line summary, always shown
|
|
38
|
+
content: 'string', // v4: Full structured template (expand on demand)
|
|
34
39
|
reasoning: 'string',
|
|
35
40
|
|
|
36
41
|
// ========== SCORES (v1) ==========
|
package/src/utils/logger.ts
CHANGED
|
@@ -574,6 +574,7 @@ export const logger = {
|
|
|
574
574
|
domain: number
|
|
575
575
|
feature: number
|
|
576
576
|
content: number
|
|
577
|
+
files: number
|
|
577
578
|
vector: number
|
|
578
579
|
total: number
|
|
579
580
|
}
|
|
@@ -600,6 +601,7 @@ export const logger = {
|
|
|
600
601
|
{ name: 'domain', count: signalBreakdown.domain },
|
|
601
602
|
{ name: 'feature', count: signalBreakdown.feature },
|
|
602
603
|
{ name: 'content', count: signalBreakdown.content },
|
|
604
|
+
{ name: 'files', count: signalBreakdown.files },
|
|
603
605
|
{ name: 'vector', count: signalBreakdown.vector },
|
|
604
606
|
]
|
|
605
607
|
for (const sig of signals) {
|
|
@@ -620,7 +622,7 @@ export const logger = {
|
|
|
620
622
|
if (Object.keys(buckets).length > 0) {
|
|
621
623
|
console.log(` ${style('bold', 'Distribution:')}`)
|
|
622
624
|
const maxBucketCount = Math.max(...Object.values(buckets), 1)
|
|
623
|
-
const bucketOrder = ['2 signals', '3 signals', '4 signals', '5 signals', '6 signals']
|
|
625
|
+
const bucketOrder = ['2 signals', '3 signals', '4 signals', '5 signals', '6 signals', '7 signals']
|
|
624
626
|
|
|
625
627
|
for (const bucket of bucketOrder) {
|
|
626
628
|
const count = buckets[bucket] ?? 0
|