@rlabs-inc/memory 0.4.1 → 0.4.3

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.
@@ -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 (v3)**: These fields enable intelligent memory management:
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,93 @@ 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
+ WHAT: [mechanism/feature in 1 sentence]
264
+ WHERE: [file:line or module path]
265
+ HOW: [usage - actual code/command if relevant]
266
+ WHY: [design choice, trade-off]
267
+ GOTCHA: [non-obvious caveat, if any]
268
+
269
+ **DEBUG** (problems and solutions):
270
+ SYMPTOM: [what went wrong - error message, behavior]
271
+ CAUSE: [why it happened]
272
+ FIX: [what solved it - specific code/config]
273
+ PREVENT: [how to avoid in future]
274
+
275
+ **ARCHITECTURE** (system design):
276
+ PATTERN: [what we chose]
277
+ COMPONENTS: [how pieces connect]
278
+ WHY: [reasoning, trade-offs]
279
+ REJECTED: [alternatives we didn't choose and why]
280
+
281
+ **DECISION** (choices made):
282
+ DECISION: [what we chose]
283
+ OPTIONS: [what we considered]
284
+ REASONING: [why this one]
285
+ REVISIT WHEN: [conditions that would change this]
286
+
287
+ **PERSONAL** (relationship context):
288
+ FACT: [the information]
289
+ CONTEXT: [why it matters to our work]
290
+ AFFECTS: [how this should change behavior]
291
+
292
+ **PHILOSOPHY** (beliefs/principles):
293
+ PRINCIPLE: [core belief]
294
+ SOURCE: [where this comes from]
295
+ APPLICATION: [how it manifests in our work]
296
+
297
+ **WORKFLOW** (how we work):
298
+ PATTERN: [what we do]
299
+ WHEN: [trigger/context for this pattern]
300
+ WHY: [why it works for us]
301
+
302
+ **MILESTONE** (achievements):
303
+ SHIPPED: [what we completed]
304
+ SIGNIFICANCE: [why it mattered]
305
+ ENABLES: [what this unlocks]
306
+
307
+ **BREAKTHROUGH** (key insights):
308
+ INSIGHT: [the aha moment]
309
+ BEFORE: [what we thought/did before]
310
+ AFTER: [what changed]
311
+ IMPLICATIONS: [what this enables going forward]
312
+
313
+ **UNRESOLVED** (open questions):
314
+ QUESTION: [what's unresolved]
315
+ CONTEXT: [why it matters]
316
+ BLOCKERS: [what's preventing resolution]
317
+ OPTIONS: [approaches we're considering]
318
+
319
+ **STATE** (current status):
320
+ WORKING: [what's functional]
321
+ BROKEN: [what's not working]
322
+ NEXT: [immediate next steps]
323
+ BLOCKED BY: [if anything]
324
+
325
+ **HEADLINE EXAMPLES**:
326
+
327
+ BAD: "Debug session about CLI errors" (vague, no conclusion)
328
+ GOOD: "CLI returns error object when context full - check response.type before JSON parsing"
329
+
330
+ BAD: "Discussed embeddings implementation" (what about it?)
331
+ GOOD: "Embeddings use all-MiniLM-L6-v2, 384 dims, first call slow (~2s), then ~50ms"
332
+
333
+ BAD: "Architecture decision made" (what decision?)
334
+ GOOD: "Chose fsDB over SQLite for memories - human-readable markdown, git-friendly, reactive"
335
+
249
336
  Return ONLY this JSON structure:
250
337
 
251
338
  {
@@ -259,7 +346,8 @@ Return ONLY this JSON structure:
259
346
  },
260
347
  "memories": [
261
348
  {
262
- "content": "The distilled insight itself",
349
+ "headline": "1-2 line summary with the conclusion - what this is about and what to do",
350
+ "content": "Full structured template using the type-specific format above",
263
351
  "importance_weight": 0.0-1.0,
264
352
  "semantic_tags": ["concepts", "this", "memory", "relates", "to"],
265
353
  "reasoning": "Why this matters for future sessions",
@@ -339,14 +427,15 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
339
427
 
340
428
  /**
341
429
  * Parse memories array from response
342
- * Includes v2 lifecycle metadata fields
430
+ * v4: Includes headline field for two-tier structure
343
431
  */
344
432
  private _parseMemories(memoriesData: any[]): CuratedMemory[] {
345
433
  if (!Array.isArray(memoriesData)) return []
346
434
 
347
435
  return memoriesData.map(m => ({
348
- // Core fields (v3 schema)
349
- content: String(m.content ?? ''),
436
+ // Core fields (v4 schema - two-tier structure)
437
+ headline: String(m.headline ?? ''), // v4: 1-2 line summary
438
+ content: String(m.content ?? ''), // v4: Full structured template
350
439
  importance_weight: this._clamp(Number(m.importance_weight) || 0.5, 0, 1),
351
440
  semantic_tags: this._ensureArray(m.semantic_tags),
352
441
  reasoning: String(m.reasoning ?? ''),
@@ -366,7 +455,7 @@ Focus ONLY on technical, architectural, debugging, decision, workflow, and proje
366
455
  related_files: m.related_files ? this._ensureArray(m.related_files) : undefined,
367
456
  awaiting_implementation: m.awaiting_implementation === true,
368
457
  awaiting_decision: m.awaiting_decision === true,
369
- })).filter(m => m.content.trim().length > 0)
458
+ })).filter(m => m.content.trim().length > 0 || m.headline.trim().length > 0)
370
459
  }
371
460
 
372
461
  private _ensureArray(value: any): string[] {
@@ -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
- * Uses emoji types for compact, scannable representation
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 ? ' ⚡ACTION' : ''
555
- // Use updated_at for freshness - captures both original age and recent modifications
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
- // Compact format: [emoji weight • age] [tags] content
560
- // Action required memories get ⚡ACTION flag for visibility
561
- parts.push(`[${emoji} • ${importance} • ${age}${actionFlag}] [${tags}] ${memory.content}`)
562
-
563
- // Show related memories: one entry point ID + count of others
564
- // Bidirectional linking means one ID gives access to entire cluster
565
- const related = memory.related_to
566
- if (related && related.length > 0) {
567
- const moreCount = related.length - 1
568
- const moreSuffix = moreCount > 0 ? ` +${moreCount} more` : ''
569
- parts.push(` ↳ ${related[0]}${moreSuffix}`)
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/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
- content: memory.content,
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
- content: memory.content,
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,
@@ -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 },
@@ -54,11 +54,12 @@ export type CurationTrigger =
54
54
 
55
55
  /**
56
56
  * A memory curated by Claude with semantic understanding
57
- * v3 schema - consolidated metadata, strict context types
57
+ * v4 schema - two-tier structure (headline + expanded content)
58
58
  */
59
59
  export interface CuratedMemory {
60
- // Core content
61
- content: string // The memory content itself
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
- * v3 schema - removed unused fields, consolidated metadata
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 v3 fields based on context_type
142
+ * Default values for v4 fields based on context_type
141
143
  * Uses only the 11 canonical context types
142
144
  */
143
- export const V3_DEFAULTS = {
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 alias
173
- export const V2_DEFAULTS = V3_DEFAULTS
174
+ // Backwards compatibility aliases
175
+ export const V3_DEFAULTS = V4_DEFAULTS
176
+ export const V2_DEFAULTS = V4_DEFAULTS
174
177
 
175
178
  /**
176
- * Apply v3 defaults to a memory
179
+ * Apply v4 defaults to a memory
177
180
  * Uses context_type to determine appropriate defaults
178
181
  */
179
- export function applyV3Defaults(memory: Partial<StoredMemory>): StoredMemory {
182
+ export function applyV4Defaults(memory: Partial<StoredMemory>): StoredMemory {
180
183
  const contextType = (memory.context_type ?? 'technical') as ContextType
181
- const typeDefaults = V3_DEFAULTS.typeDefaults[contextType] ?? V3_DEFAULTS.typeDefaults.technical
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 ?? V3_DEFAULTS.fallback.status,
191
+ status: memory.status ?? V4_DEFAULTS.fallback.status,
189
192
 
190
193
  // Apply scope from type defaults
191
- scope: memory.scope ?? typeDefaults?.scope ?? V3_DEFAULTS.fallback.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 ?? V3_DEFAULTS.fallback.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 ?? V3_DEFAULTS.fallback.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 ?? V3_DEFAULTS.fallback.sessions_since_surfaced,
201
- awaiting_implementation: memory.awaiting_implementation ?? V3_DEFAULTS.fallback.awaiting_implementation,
202
- awaiting_decision: memory.awaiting_decision ?? V3_DEFAULTS.fallback.awaiting_decision,
203
- exclude_from_retrieval: memory.exclude_from_retrieval ?? V3_DEFAULTS.fallback.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 ?? 3,
215
+ schema_version: memory.schema_version ?? 4,
213
216
  } as StoredMemory
214
217
  }
215
218
 
216
- // Backwards compatibility alias
217
- export const applyV2Defaults = applyV3Defaults
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 v3
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 < 3
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
  /**
@@ -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 = 3
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 (v1) ==========
33
- content: 'string',
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) ==========