@jungjaehoon/mama-core 1.1.2 → 1.1.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.
Files changed (67) hide show
  1. package/dist/config-loader.d.ts +61 -0
  2. package/dist/config-loader.js +187 -0
  3. package/dist/db-adapter/base-adapter.d.ts +76 -0
  4. package/dist/db-adapter/base-adapter.js +11 -0
  5. package/dist/db-adapter/index.d.ts +24 -0
  6. package/dist/db-adapter/index.js +29 -0
  7. package/dist/db-adapter/sqlite-adapter.d.ts +73 -0
  8. package/dist/db-adapter/sqlite-adapter.js +330 -0
  9. package/dist/db-adapter/statement.d.ts +119 -0
  10. package/dist/db-adapter/statement.js +113 -0
  11. package/dist/db-manager.d.ts +252 -0
  12. package/dist/db-manager.js +622 -0
  13. package/dist/debug-logger.d.ts +32 -0
  14. package/dist/debug-logger.js +83 -0
  15. package/dist/decision-formatter.d.ts +172 -0
  16. package/dist/decision-formatter.js +894 -0
  17. package/dist/decision-tracker.d.ts +218 -0
  18. package/dist/decision-tracker.js +531 -0
  19. package/dist/embedding-cache.d.ts +83 -0
  20. package/dist/embedding-cache.js +182 -0
  21. package/dist/embedding-client.d.ts +43 -0
  22. package/dist/embedding-client.js +132 -0
  23. package/dist/embedding-server/index.d.ts +65 -0
  24. package/dist/embedding-server/index.js +397 -0
  25. package/dist/embedding-server/mobile/auth.d.ts +53 -0
  26. package/dist/embedding-server/mobile/auth.js +140 -0
  27. package/dist/embedding-server/mobile/daemon.d.ts +128 -0
  28. package/dist/embedding-server/mobile/daemon.js +303 -0
  29. package/dist/embedding-server/mobile/output-parser.d.ts +115 -0
  30. package/dist/embedding-server/mobile/output-parser.js +241 -0
  31. package/dist/embedding-server/mobile/session-api.d.ts +57 -0
  32. package/dist/embedding-server/mobile/session-api.js +261 -0
  33. package/dist/embedding-server/mobile/session-manager.d.ts +135 -0
  34. package/dist/embedding-server/mobile/session-manager.js +333 -0
  35. package/dist/embedding-server/mobile/websocket-handler.d.ts +127 -0
  36. package/dist/embedding-server/mobile/websocket-handler.js +435 -0
  37. package/dist/embeddings.d.ts +75 -0
  38. package/dist/embeddings.js +262 -0
  39. package/dist/errors.d.ts +131 -0
  40. package/dist/errors.js +225 -0
  41. package/dist/index.d.ts +32 -0
  42. package/dist/index.js +193 -0
  43. package/dist/mama-api.d.ts +954 -0
  44. package/dist/mama-api.js +2210 -0
  45. package/dist/memory-inject.d.ts +24 -0
  46. package/dist/memory-inject.js +116 -0
  47. package/dist/memory-store.d.ts +103 -0
  48. package/dist/memory-store.js +129 -0
  49. package/dist/notification-manager.d.ts +7 -0
  50. package/dist/notification-manager.js +12 -0
  51. package/dist/ollama-client.d.ts +51 -0
  52. package/dist/ollama-client.js +308 -0
  53. package/dist/outcome-tracker.d.ts +165 -0
  54. package/dist/outcome-tracker.js +315 -0
  55. package/dist/progress-indicator.d.ts +48 -0
  56. package/dist/progress-indicator.js +82 -0
  57. package/dist/query-intent.d.ts +27 -0
  58. package/dist/query-intent.js +144 -0
  59. package/dist/relevance-scorer.d.ts +124 -0
  60. package/dist/relevance-scorer.js +243 -0
  61. package/dist/test-utils.d.ts +66 -0
  62. package/dist/test-utils.js +166 -0
  63. package/dist/tier-validator.d.ts +55 -0
  64. package/dist/tier-validator.js +216 -0
  65. package/dist/time-formatter.d.ts +25 -0
  66. package/dist/time-formatter.js +93 -0
  67. package/package.json +3 -2
@@ -0,0 +1,894 @@
1
+ "use strict";
2
+ /**
3
+ * MAMA (Memory-Augmented MCP Architecture) - Decision Context Formatter
4
+ *
5
+ * Formats decision history with token budget enforcement and top-N selection
6
+ * Tasks: 6.1-6.6, 8.1-8.5 (Context formatting with top-N selection)
7
+ * AC #1: Context under 500 tokens
8
+ * AC #4: Rolling summary for large histories
9
+ * AC #5: Top-N selection with summary
10
+ *
11
+ * @module decision-formatter
12
+ * @version 2.0
13
+ * @date 2025-11-14
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.formatContext = formatContext;
17
+ exports.formatLegacyContext = formatLegacyContext;
18
+ exports.ensureTokenBudget = ensureTokenBudget;
19
+ exports.estimateTokens = estimateTokens;
20
+ exports.formatInstantAnswer = formatInstantAnswer;
21
+ exports.extractQuickAnswer = extractQuickAnswer;
22
+ exports.extractCodeExample = extractCodeExample;
23
+ exports.formatTrustContext = formatTrustContext;
24
+ exports.formatTeaser = formatTeaser;
25
+ exports.formatRecall = formatRecall;
26
+ exports.formatList = formatList;
27
+ const relevance_scorer_js_1 = require("./relevance-scorer.js");
28
+ /**
29
+ * Safely parse JSON string, returning fallback on error
30
+ */
31
+ function safeParseJson(str, fallback) {
32
+ try {
33
+ return JSON.parse(str);
34
+ }
35
+ catch {
36
+ return fallback;
37
+ }
38
+ }
39
+ /**
40
+ * Format decision context for Claude injection with top-N selection
41
+ *
42
+ * Task 6.1-6.2, 8.1-8.5: Build context format template with top-N selection
43
+ * AC #1: Format decision history
44
+ * AC #4: Handle large histories with rolling summary
45
+ * AC #5: Top-N selection with summary (top 3 full detail, rest summarized)
46
+ *
47
+ * Story 014.7.10 - Task 5: Fallback Formatting
48
+ * Tries Instant Answer format first (if trust_context available), falls back to legacy
49
+ *
50
+ * @param decisions - Decision chain (sorted by relevance)
51
+ * @param options - Formatting options
52
+ * @returns Formatted context for injection
53
+ */
54
+ function formatContext(decisions, options = {}) {
55
+ const { maxTokens = 500, useTopN = decisions.length >= 4, // Auto-enable for 4+ decisions
56
+ topN = 3, useTeaser = true, // New: Use Teaser format to encourage interaction
57
+ } = options;
58
+ if (!decisions || decisions.length === 0) {
59
+ return null;
60
+ }
61
+ // New approach: Teaser format (curiosity-driven)
62
+ // MAMA = Librarian: Shows book previews, Claude decides to read
63
+ if (useTeaser) {
64
+ // Show top 3 results (Google-style)
65
+ const teaserList = formatTeaserList(decisions, topN);
66
+ if (teaserList) {
67
+ return teaserList;
68
+ }
69
+ }
70
+ // Fallback: Legacy format
71
+ return formatLegacyContext(decisions, { maxTokens, useTopN, topN });
72
+ }
73
+ /**
74
+ * Format decisions using legacy format (no trust context)
75
+ *
76
+ * Story 014.7.10 - Task 5.1: Fallback formatting
77
+ * AC #3: Graceful degradation for decisions without trust_context
78
+ *
79
+ * @param decisions - Decision chain (sorted by relevance)
80
+ * @param options - Formatting options
81
+ * @returns Formatted context (legacy format)
82
+ */
83
+ function formatLegacyContext(decisions, options = {}) {
84
+ if (!decisions || decisions.length === 0) {
85
+ return null;
86
+ }
87
+ const { maxTokens = 500, useTopN = decisions.length >= 4, topN = 3 } = options;
88
+ // Task 8.1: Use top-N selection for 4+ decisions (AC #5)
89
+ let context;
90
+ if (useTopN && decisions.length > topN) {
91
+ // Task 8.1: Modify to use top-N selection
92
+ context = formatWithTopN(decisions, topN);
93
+ }
94
+ else {
95
+ // Find current decision (superseded_by = NULL or missing)
96
+ const current = decisions.find((d) => !d.superseded_by) || decisions[0];
97
+ const history = decisions.filter((d) => d.id !== current.id);
98
+ // Task 6.2: Build context format template (legacy)
99
+ if (decisions.length <= 3) {
100
+ // Small history: Full details
101
+ context = formatSmallHistory(current, history);
102
+ }
103
+ else {
104
+ // Large history: Rolling summary
105
+ context = formatLargeHistory(current, history);
106
+ }
107
+ }
108
+ // Task 6.3, 8.4: Ensure token budget stays under 500 tokens
109
+ return ensureTokenBudget(context, maxTokens);
110
+ }
111
+ /**
112
+ * Format with top-N selection
113
+ *
114
+ * Task 8.2-8.3: Full detail for top 3, summary for rest
115
+ * AC #5: Top-N selection with summary
116
+ *
117
+ * @param decisions - All decisions (sorted by relevance)
118
+ * @param topN - Number of decisions for full detail
119
+ * @returns Formatted context
120
+ */
121
+ function formatWithTopN(decisions, topN) {
122
+ // Use formatTopNContext from relevance-scorer.js
123
+ const { full, summary } = (0, relevance_scorer_js_1.formatTopNContext)(decisions.map((d) => ({
124
+ ...d,
125
+ created_at: typeof d.created_at === 'string' ? Date.parse(d.created_at) : d.created_at,
126
+ updated_at: typeof d.updated_at === 'string'
127
+ ? Date.parse(d.updated_at)
128
+ : typeof d.updated_at === 'number'
129
+ ? d.updated_at
130
+ : undefined,
131
+ reasoning: d.reasoning === null ? undefined : d.reasoning,
132
+ })), topN);
133
+ const current = full[0]; // Highest relevance
134
+ const topic = current?.topic || 'Unknown';
135
+ // Task 8.2: Full detail for top 3 decisions
136
+ let context = `
137
+ 🧠 DECISION HISTORY: ${topic}
138
+
139
+ Top ${full.length} Most Relevant Decisions:
140
+ `.trim();
141
+ for (let i = 0; i < full.length; i++) {
142
+ const d = full[i];
143
+ const duration = calculateDuration(d.created_at);
144
+ const outcomeEmoji = getOutcomeEmoji(d.outcome ?? null);
145
+ const relevancePercent = Math.round((d.relevanceScore || 0) * 100);
146
+ context += `\n\n${i + 1}. ${d.decision} (${duration}, relevance: ${relevancePercent}%) ${outcomeEmoji}`;
147
+ context += `\n Reasoning: ${d.reasoning || 'N/A'}`;
148
+ if (d.outcome === 'FAILED') {
149
+ context += `\n āš ļø Failure: ${d.failure_reason || 'Unknown reason'}`;
150
+ }
151
+ }
152
+ // Task 8.3: Summary for rest (count, duration, key failures only)
153
+ if (summary && summary.count > 0) {
154
+ context += `\n\n━━━━━━━━━━━━━━━━━━━━━━━`;
155
+ context += `\nHistory: ${summary.count} additional decisions over ${summary.duration_days} days`;
156
+ if (summary.failures && summary.failures.length > 0) {
157
+ context += `\n\nāš ļø Other Failures:`;
158
+ for (const failure of summary.failures) {
159
+ context += `\n- ${failure.decision}: ${failure.reason || 'Unknown'}`;
160
+ }
161
+ }
162
+ }
163
+ return context;
164
+ }
165
+ /**
166
+ * Format small decision history (3 or fewer)
167
+ */
168
+ function formatSmallHistory(current, history) {
169
+ const duration = calculateDuration(current.created_at);
170
+ let context = `
171
+ 🧠 DECISION HISTORY: ${current.topic}
172
+
173
+ Current: ${current.decision} (${duration}, confidence: ${current.confidence})
174
+ Reasoning: ${current.reasoning || 'N/A'}
175
+ `.trim();
176
+ // Add history details
177
+ if (history.length > 0) {
178
+ context += '\n\nPrevious Decisions:\n';
179
+ for (const decision of history) {
180
+ const durationDays = calculateDurationDays(decision.created_at, decision.updated_at || Date.now());
181
+ const outcomeEmoji = getOutcomeEmoji(decision.outcome ?? null);
182
+ context += `- ${decision.decision} (${durationDays} days) ${outcomeEmoji}\n`;
183
+ if (decision.outcome === 'FAILED') {
184
+ context += ` Reason: ${decision.failure_reason || 'Unknown'}\n`;
185
+ }
186
+ }
187
+ }
188
+ return context;
189
+ }
190
+ /**
191
+ * Format large decision history (4+ decisions)
192
+ *
193
+ * Task 6.2: Rolling summary for large histories
194
+ * AC #4: Highlight top 3 failures
195
+ */
196
+ function formatLargeHistory(current, history) {
197
+ // Include current decision in total duration calculation
198
+ const allDecisions = [current, ...history];
199
+ const totalDuration = calculateTotalDuration(allDecisions);
200
+ // Extract failures
201
+ const failures = history.filter((d) => d.outcome === 'FAILED');
202
+ const topFailures = failures.slice(0, 3);
203
+ // Get last evolution
204
+ const lastEvolution = history.length > 0 ? history[0] : null;
205
+ let context = `
206
+ 🧠 DECISION HISTORY: ${current.topic}
207
+
208
+ Current: ${current.decision} (confidence: ${current.confidence})
209
+ Reasoning: ${current.reasoning || 'N/A'}
210
+
211
+ History: ${history.length + 1} decisions over ${totalDuration}
212
+ `.trim();
213
+ // Add key failures
214
+ if (topFailures.length > 0) {
215
+ context += '\n\nāš ļø Key Failures (avoid these):\n';
216
+ for (const failure of topFailures) {
217
+ context += `- ${failure.decision}: ${failure.failure_reason || 'Unknown reason'}\n`;
218
+ }
219
+ }
220
+ // Add last evolution
221
+ if (lastEvolution) {
222
+ context += `\nLast evolution: ${lastEvolution.decision} → ${current.decision}`;
223
+ if (current.reasoning) {
224
+ const reasonSummary = current.reasoning.substring(0, 100);
225
+ context += ` (${reasonSummary}${current.reasoning.length > 100 ? '...' : ''})`;
226
+ }
227
+ }
228
+ return context;
229
+ }
230
+ /**
231
+ * Ensure token budget is enforced
232
+ *
233
+ * Task 6.3-6.5: Token budget enforcement
234
+ * AC #1: Context stays under 500 tokens
235
+ */
236
+ function ensureTokenBudget(text, maxTokens) {
237
+ // Task 6.4: Token estimation (~1 token per 4 characters)
238
+ const estimatedTokens = estimateTokens(text);
239
+ if (estimatedTokens <= maxTokens) {
240
+ return text;
241
+ }
242
+ // Task 6.5: Truncate to fit budget
243
+ const ratio = maxTokens / estimatedTokens;
244
+ const truncated = text.substring(0, Math.floor(text.length * ratio));
245
+ return truncated + '\n\n... (truncated to fit token budget)';
246
+ }
247
+ /**
248
+ * Estimate token count from text
249
+ *
250
+ * Task 6.4: Simple token estimation
251
+ * Heuristic: ~1 token per 4 characters
252
+ */
253
+ function estimateTokens(text) {
254
+ // Task 6.4: ~1 token per 4 characters
255
+ return Math.ceil(text.length / 4);
256
+ }
257
+ /**
258
+ * Calculate human-readable duration
259
+ */
260
+ function calculateDuration(timestamp) {
261
+ // Handle Unix timestamp (number or numeric string) and ISO 8601 string
262
+ let ts;
263
+ if (typeof timestamp === 'string') {
264
+ // Try parsing as number first (e.g., "1763971277689")
265
+ const num = Number(timestamp);
266
+ ts = isNaN(num) ? Date.parse(timestamp) : num;
267
+ }
268
+ else {
269
+ ts = timestamp;
270
+ }
271
+ if (isNaN(ts) || ts === null || ts === undefined) {
272
+ return 'unknown';
273
+ }
274
+ const now = Date.now();
275
+ const diffMs = now - ts;
276
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
277
+ if (diffDays === 0) {
278
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
279
+ if (diffHours === 0) {
280
+ const diffMins = Math.floor(diffMs / (1000 * 60));
281
+ return `${diffMins} min${diffMins !== 1 ? 's' : ''} ago`;
282
+ }
283
+ return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
284
+ }
285
+ return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
286
+ }
287
+ /**
288
+ * Calculate duration between two timestamps in days
289
+ */
290
+ function calculateDurationDays(start, end) {
291
+ let startTs;
292
+ let endTs;
293
+ if (typeof start === 'string') {
294
+ const num = Number(start);
295
+ startTs = isNaN(num) ? Date.parse(start) : num;
296
+ }
297
+ else {
298
+ startTs = start;
299
+ }
300
+ if (typeof end === 'string') {
301
+ const num = Number(end);
302
+ endTs = isNaN(num) ? Date.parse(end) : num;
303
+ }
304
+ else {
305
+ endTs = end;
306
+ }
307
+ if (isNaN(startTs) || isNaN(endTs)) {
308
+ return 0;
309
+ }
310
+ const diffMs = endTs - startTs;
311
+ return Math.floor(diffMs / (1000 * 60 * 60 * 24));
312
+ }
313
+ /**
314
+ * Calculate total duration across decision history
315
+ */
316
+ function calculateTotalDuration(history) {
317
+ if (history.length === 0) {
318
+ return 'N/A';
319
+ }
320
+ // Convert all timestamps to numbers for comparison
321
+ const timestamps = history
322
+ .map((d) => {
323
+ const created = typeof d.created_at === 'string' ? Date.parse(d.created_at) : d.created_at;
324
+ const updated = d.updated_at
325
+ ? typeof d.updated_at === 'string'
326
+ ? Date.parse(d.updated_at)
327
+ : d.updated_at
328
+ : created;
329
+ return { created, updated };
330
+ })
331
+ .filter((t) => !isNaN(t.created) && !isNaN(t.updated));
332
+ if (timestamps.length === 0) {
333
+ return 'N/A';
334
+ }
335
+ const earliest = Math.min(...timestamps.map((t) => t.created));
336
+ const latest = Math.max(...timestamps.map((t) => t.updated));
337
+ const durationDays = calculateDurationDays(earliest, latest);
338
+ if (durationDays < 7) {
339
+ return `${durationDays} days`;
340
+ }
341
+ else if (durationDays < 30) {
342
+ const weeks = Math.floor(durationDays / 7);
343
+ return `${weeks} week${weeks !== 1 ? 's' : ''}`;
344
+ }
345
+ else {
346
+ const months = Math.floor(durationDays / 30);
347
+ return `${months} month${months !== 1 ? 's' : ''}`;
348
+ }
349
+ }
350
+ /**
351
+ * Get emoji for outcome
352
+ */
353
+ function getOutcomeEmoji(outcome) {
354
+ const emojiMap = {
355
+ SUCCESS: 'āœ…',
356
+ FAILED: 'āŒ',
357
+ PARTIAL: 'āš ļø',
358
+ ONGOING: 'ā³',
359
+ };
360
+ return outcome ? emojiMap[outcome] || '' : '';
361
+ }
362
+ /**
363
+ * Format context in Claude-friendly Instant Answer format
364
+ *
365
+ * Story 014.7.10: Claude-Friendly Context Formatting
366
+ * AC #1: Instant Answer format with trust components
367
+ */
368
+ function formatInstantAnswer(decision, options = {}) {
369
+ const { maxTokens = 500 } = options;
370
+ if (!decision) {
371
+ return null;
372
+ }
373
+ // Extract quick answer (first line of decision)
374
+ const quickAnswer = extractQuickAnswer(decision);
375
+ if (!quickAnswer) {
376
+ return null;
377
+ }
378
+ // Extract code example (from reasoning)
379
+ const codeExample = extractCodeExample(decision);
380
+ // Format trust context
381
+ const trustSection = formatTrustContext(typeof decision.trust_context === 'string'
382
+ ? parseTrustContext(decision.trust_context)
383
+ : decision.trust_context);
384
+ // Build output
385
+ let output = `⚔ INSTANT ANSWER\n\n${quickAnswer}`;
386
+ if (codeExample) {
387
+ output += `\n\n${codeExample}`;
388
+ }
389
+ if (trustSection) {
390
+ output += `\n\n${trustSection}`;
391
+ }
392
+ // Token budget check
393
+ if (estimateTokens(output) > maxTokens) {
394
+ output = truncateToFit(output, maxTokens);
395
+ }
396
+ return output;
397
+ }
398
+ /**
399
+ * Extract quick answer from decision
400
+ */
401
+ function extractQuickAnswer(decision) {
402
+ if (!decision.decision || typeof decision.decision !== 'string') {
403
+ return null;
404
+ }
405
+ const text = decision.decision.trim();
406
+ if (text.length === 0) {
407
+ return null;
408
+ }
409
+ // Extract first line
410
+ const lines = text.split('\n');
411
+ const firstLine = lines[0].trim();
412
+ // Check if first line contains multiple real sentences
413
+ const sentenceMatch = firstLine.match(/^.+?[.!?](?=\s+[A-Z])/);
414
+ if (sentenceMatch) {
415
+ return sentenceMatch[0].trim();
416
+ }
417
+ // Single sentence or no sentence boundary - use full first line if reasonable
418
+ if (firstLine.length <= 150) {
419
+ return firstLine;
420
+ }
421
+ // First line too long - truncate to 100 chars
422
+ return firstLine.substring(0, 100) + '...';
423
+ }
424
+ /**
425
+ * Extract code example from reasoning
426
+ */
427
+ function extractCodeExample(decision) {
428
+ if (!decision.reasoning || typeof decision.reasoning !== 'string') {
429
+ return null;
430
+ }
431
+ // Match markdown code blocks
432
+ const codeBlockRegex = /```[\s\S]*?```/;
433
+ const match = decision.reasoning.match(codeBlockRegex);
434
+ if (match) {
435
+ return match[0];
436
+ }
437
+ // Check if decision field contains code patterns
438
+ if (decision.decision && typeof decision.decision === 'string') {
439
+ const hasCode = decision.decision.includes('mama.save(') ||
440
+ decision.decision.includes('await ') ||
441
+ decision.decision.includes('=>');
442
+ if (hasCode) {
443
+ return `\`\`\`javascript\n${decision.decision}\n\`\`\``;
444
+ }
445
+ }
446
+ return null;
447
+ }
448
+ /**
449
+ * Format trust context section
450
+ *
451
+ * Story 014.7.10 AC #2: Trust Context display
452
+ */
453
+ function formatTrustContext(trustCtx) {
454
+ if (!trustCtx) {
455
+ return null;
456
+ }
457
+ const lines = ['━'.repeat(40), 'šŸ” WHY TRUST THIS?', ''];
458
+ let hasContent = false;
459
+ // 1. Source transparency
460
+ if (trustCtx.source) {
461
+ const { file, line, author, timestamp } = trustCtx.source;
462
+ const timeAgo = timestamp ? calculateDuration(timestamp) : 'unknown';
463
+ lines.push(`šŸ“ Source: ${file}:${line} (${timeAgo}, by ${author})`);
464
+ hasContent = true;
465
+ }
466
+ // 2. Causality
467
+ if (trustCtx.causality && trustCtx.causality.impact) {
468
+ lines.push(`šŸ”— Reason: ${trustCtx.causality.impact}`);
469
+ hasContent = true;
470
+ }
471
+ // 3. Verifiability
472
+ if (trustCtx.verification) {
473
+ const { test_file, result } = trustCtx.verification;
474
+ const status = result === 'success' ? 'passed' : result;
475
+ lines.push(`āœ… Verified: ${test_file} ${status}`);
476
+ hasContent = true;
477
+ }
478
+ // 4. Context relevance
479
+ if (trustCtx.context_match && trustCtx.context_match.user_intent) {
480
+ lines.push(`šŸŽÆ Applies to: ${trustCtx.context_match.user_intent}`);
481
+ hasContent = true;
482
+ }
483
+ // 5. Track record
484
+ if (trustCtx.track_record) {
485
+ const { recent_successes, recent_failures } = trustCtx.track_record;
486
+ const successCount = recent_successes?.length || 0;
487
+ const failureCount = recent_failures?.length || 0;
488
+ const total = successCount + failureCount;
489
+ if (total > 0) {
490
+ lines.push(`šŸ“Š Track record: ${successCount}/${total} recent successes`);
491
+ hasContent = true;
492
+ }
493
+ }
494
+ if (!hasContent) {
495
+ return null;
496
+ }
497
+ lines.push('━'.repeat(40));
498
+ return lines.join('\n');
499
+ }
500
+ /**
501
+ * Truncate output to fit token budget
502
+ */
503
+ function truncateToFit(output, maxTokens) {
504
+ const sections = output.split('\n\n');
505
+ const quickAnswer = sections[0];
506
+ let result = quickAnswer;
507
+ let remainingTokens = maxTokens - estimateTokens(result);
508
+ // Try to add code example
509
+ const codeIndex = sections.findIndex((s) => s.startsWith('```'));
510
+ if (codeIndex > 0) {
511
+ const codeSection = sections[codeIndex];
512
+ const codeTokens = estimateTokens(codeSection);
513
+ if (codeTokens <= remainingTokens) {
514
+ result += '\n\n' + codeSection;
515
+ remainingTokens -= codeTokens;
516
+ }
517
+ }
518
+ // Try to add trust section (trimmed if needed)
519
+ const trustIndex = sections.findIndex((s) => s.startsWith('━'));
520
+ if (trustIndex > 0 && remainingTokens > 50) {
521
+ const trustSection = sections[trustIndex];
522
+ const trustTokens = estimateTokens(trustSection);
523
+ if (trustTokens <= remainingTokens) {
524
+ result += '\n\n' + trustSection;
525
+ }
526
+ else {
527
+ const trustLines = trustSection.split('\n');
528
+ let trimmed = trustLines[0] + '\n' + trustLines[1] + '\n';
529
+ for (let i = 2; i < trustLines.length - 1; i++) {
530
+ const line = trustLines[i] + '\n';
531
+ if (estimateTokens(trimmed + line) <= remainingTokens - 10) {
532
+ trimmed += line;
533
+ }
534
+ else {
535
+ break;
536
+ }
537
+ }
538
+ trimmed += trustLines[trustLines.length - 1];
539
+ result += '\n\n' + trimmed;
540
+ }
541
+ }
542
+ return result;
543
+ }
544
+ /**
545
+ * Format multiple decisions as Google-style search results
546
+ */
547
+ function formatTeaserList(decisions, topN = 3) {
548
+ if (!decisions || decisions.length === 0) {
549
+ return null;
550
+ }
551
+ const topDecisions = decisions.slice(0, topN);
552
+ const count = topDecisions.length;
553
+ let output = `šŸ’” MAMA found ${count} related topic${count > 1 ? 's' : ''}:\n`;
554
+ for (let i = 0; i < topDecisions.length; i++) {
555
+ const d = topDecisions[i];
556
+ const relevance = Math.round((d.similarity || d.confidence || 0) * 100);
557
+ // Preview (max 60 chars)
558
+ const preview = d.decision.length > 60 ? d.decision.substring(0, 60) + '...' : d.decision;
559
+ output += `\n${i + 1}. ${d.topic} (${relevance}% match)`;
560
+ output += `\n "${preview}"`;
561
+ // Recency metadata (NEW - Gaussian Decay)
562
+ if (d.recency_age_days !== undefined && d.created_at) {
563
+ const timeAgo = calculateDuration(d.created_at);
564
+ const recencyScore = d.recency_score ? Math.round(d.recency_score * 100) : null;
565
+ const finalScore = d.final_score ? Math.round(d.final_score * 100) : null;
566
+ output += `\n ā° ${timeAgo}`;
567
+ if (recencyScore !== null && finalScore !== null) {
568
+ output += ` | Recency: ${recencyScore}% | Final: ${finalScore}%`;
569
+ }
570
+ }
571
+ output += `\n šŸ” mama.recall('${d.topic}')`;
572
+ if (i < topDecisions.length - 1) {
573
+ output += '\n';
574
+ }
575
+ }
576
+ return output;
577
+ }
578
+ /**
579
+ * Format decision as curiosity-inducing teaser
580
+ */
581
+ function formatTeaser(decision) {
582
+ if (!decision) {
583
+ return null;
584
+ }
585
+ const timeAgo = calculateDuration(decision.created_at);
586
+ // Extract preview (first 60 chars)
587
+ const preview = decision.decision.length > 60 ? decision.decision.substring(0, 60) + '...' : decision.decision;
588
+ // Extract files from trust_context or show generic
589
+ let files = 'Multiple files';
590
+ const trustCtx = typeof decision.trust_context === 'string'
591
+ ? parseTrustContext(decision.trust_context)
592
+ : decision.trust_context;
593
+ if (trustCtx?.source?.file) {
594
+ const fileStr = trustCtx.source.file;
595
+ const fileList = fileStr.split(',').map((f) => f.trim());
596
+ if (fileList.length === 1) {
597
+ files = fileList[0];
598
+ }
599
+ else if (fileList.length === 2) {
600
+ files = fileList.join(', ');
601
+ }
602
+ else {
603
+ files = `${fileList[0]}, ${fileList[1]} (+${fileList.length - 2})`;
604
+ }
605
+ }
606
+ const teaser = `
607
+ šŸ’” MAMA has related info
608
+
609
+ šŸ“š Topic: ${decision.topic}
610
+ šŸ“– Preview: "${preview}"
611
+ šŸ“ Files: ${files}
612
+ ā° Updated: ${timeAgo}
613
+
614
+ šŸ” Read more: mama.recall('${decision.topic}')
615
+ `.trim();
616
+ return teaser;
617
+ }
618
+ /**
619
+ * Format mama.recall() results in readable format
620
+ */
621
+ function formatRecall(decisions, semanticEdges = null) {
622
+ if (!decisions || decisions.length === 0) {
623
+ return 'āŒ No decisions found';
624
+ }
625
+ // Single decision: full detail
626
+ if (decisions.length === 1) {
627
+ return formatSingleDecision(decisions[0]);
628
+ }
629
+ // Multiple decisions: history view
630
+ return formatDecisionHistory(decisions, semanticEdges);
631
+ }
632
+ /**
633
+ * Format single decision with full detail
634
+ */
635
+ function formatSingleDecision(decision) {
636
+ const timeAgo = calculateDuration(decision.created_at);
637
+ const confidencePercent = Math.round((decision.confidence || 0) * 100);
638
+ const outcomeEmoji = getOutcomeEmoji(decision.outcome ?? null);
639
+ const outcomeText = decision.outcome || 'Not yet tracked';
640
+ let output = `
641
+ šŸ“‹ Decision: ${decision.topic}
642
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
643
+
644
+ ${decision.reasoning || decision.decision}
645
+ `.trim();
646
+ // Metadata section
647
+ output += `\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`;
648
+ output += `\nšŸ“Š Confidence: ${confidencePercent}%`;
649
+ output += `\nā° Created: ${timeAgo}`;
650
+ output += `\n${outcomeEmoji} Outcome: ${outcomeText}`;
651
+ if (decision.outcome === 'FAILED' && decision.failure_reason) {
652
+ output += `\nāš ļø Failure reason: ${decision.failure_reason}`;
653
+ }
654
+ // Narrative fields section (Story 2.2)
655
+ if (decision.evidence || decision.alternatives || decision.risks) {
656
+ output += '\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
657
+ output += '\nšŸ“ Narrative Details\n';
658
+ if (decision.evidence) {
659
+ const evidenceList = Array.isArray(decision.evidence)
660
+ ? decision.evidence
661
+ : typeof decision.evidence === 'string'
662
+ ? safeParseJson(decision.evidence, [decision.evidence])
663
+ : [decision.evidence];
664
+ if (evidenceList.length > 0) {
665
+ output += '\nšŸ” Evidence:';
666
+ evidenceList.forEach((item, idx) => {
667
+ output += `\n ${idx + 1}. ${item}`;
668
+ });
669
+ }
670
+ }
671
+ if (decision.alternatives) {
672
+ const altList = Array.isArray(decision.alternatives)
673
+ ? decision.alternatives
674
+ : typeof decision.alternatives === 'string'
675
+ ? safeParseJson(decision.alternatives, [decision.alternatives])
676
+ : [decision.alternatives];
677
+ if (altList.length > 0) {
678
+ output += '\n\nšŸ”€ Alternatives Considered:';
679
+ altList.forEach((item, idx) => {
680
+ output += `\n ${idx + 1}. ${item}`;
681
+ });
682
+ }
683
+ }
684
+ if (decision.risks) {
685
+ output += `\n\nāš ļø Risks: ${decision.risks}`;
686
+ }
687
+ }
688
+ // Trust context section (if available)
689
+ const trustCtx = typeof decision.trust_context === 'string'
690
+ ? parseTrustContext(decision.trust_context)
691
+ : decision.trust_context;
692
+ if (trustCtx) {
693
+ output += '\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
694
+ output += '\nšŸ” Trust Context\n';
695
+ if (trustCtx.source) {
696
+ const { file, line, author } = trustCtx.source;
697
+ output += `\nšŸ“ Source: ${file}${line ? ':' + line : ''} (by ${author || 'unknown'})`;
698
+ }
699
+ if (trustCtx.causality?.impact) {
700
+ output += `\nšŸ”— Impact: ${trustCtx.causality.impact}`;
701
+ }
702
+ if (trustCtx.verification) {
703
+ const { test_file, result } = trustCtx.verification;
704
+ const status = result === 'success' ? 'āœ… passed' : `āš ļø ${result}`;
705
+ output += `\n${status}: ${test_file || 'Verified'}`;
706
+ }
707
+ if (trustCtx.track_record) {
708
+ const { success_rate, sample_size } = trustCtx.track_record;
709
+ if (sample_size && sample_size > 0) {
710
+ const rate = Math.round((success_rate || 0) * 100);
711
+ output += `\nšŸ“Š Track record: ${rate}% success (${sample_size} samples)`;
712
+ }
713
+ }
714
+ }
715
+ return output;
716
+ }
717
+ /**
718
+ * Format decision history (multiple decisions)
719
+ */
720
+ function formatDecisionHistory(decisions, semanticEdges = null) {
721
+ const topic = decisions[0].topic;
722
+ const latest = decisions[0];
723
+ const older = decisions.slice(1);
724
+ let output = `
725
+ šŸ“‹ Decision History: ${topic}
726
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
727
+
728
+ Latest Decision (${calculateDuration(latest.created_at)}):
729
+ ${latest.decision}
730
+ `.trim();
731
+ // Show brief reasoning if available
732
+ if (latest.reasoning) {
733
+ const briefReasoning = latest.reasoning.split('\n')[0].substring(0, 150);
734
+ output += `\n\nReasoning: ${briefReasoning}${latest.reasoning.length > 150 ? '...' : ''}`;
735
+ }
736
+ output += `\n\nConfidence: ${Math.round((latest.confidence || 0) * 100)}%`;
737
+ // Show narrative fields for latest decision (Story 2.2)
738
+ if (latest.evidence || latest.alternatives || latest.risks) {
739
+ if (latest.evidence) {
740
+ const evidenceList = Array.isArray(latest.evidence)
741
+ ? latest.evidence
742
+ : typeof latest.evidence === 'string'
743
+ ? safeParseJson(latest.evidence, [latest.evidence])
744
+ : [latest.evidence];
745
+ if (evidenceList.length > 0) {
746
+ output += '\n\nšŸ” Evidence:';
747
+ evidenceList.slice(0, 3).forEach((item, idx) => {
748
+ output += `\n ${idx + 1}. ${item}`;
749
+ });
750
+ if (evidenceList.length > 3) {
751
+ output += `\n ... and ${evidenceList.length - 3} more`;
752
+ }
753
+ }
754
+ }
755
+ if (latest.alternatives) {
756
+ const altList = Array.isArray(latest.alternatives)
757
+ ? latest.alternatives
758
+ : typeof latest.alternatives === 'string'
759
+ ? safeParseJson(latest.alternatives, [latest.alternatives])
760
+ : [latest.alternatives];
761
+ if (altList.length > 0) {
762
+ output += '\n\nšŸ”€ Alternatives: ';
763
+ output += altList.slice(0, 2).join('; ');
764
+ if (altList.length > 2) {
765
+ output += `... (+${altList.length - 2} more)`;
766
+ }
767
+ }
768
+ }
769
+ if (latest.risks) {
770
+ const risksPreview = latest.risks.length > 100 ? latest.risks.substring(0, 100) + '...' : latest.risks;
771
+ output += `\n\nāš ļø Risks: ${risksPreview}`;
772
+ }
773
+ }
774
+ // Show older decisions (supersedes chain)
775
+ if (older.length > 0) {
776
+ output += '\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
777
+ output += `\nPrevious Decisions (${older.length}):\n`;
778
+ for (let i = 0; i < Math.min(older.length, 5); i++) {
779
+ const d = older[i];
780
+ const timeAgo = calculateDuration(d.created_at);
781
+ const emoji = getOutcomeEmoji(d.outcome ?? null);
782
+ output += `\n${i + 2}. ${d.decision} (${timeAgo}) ${emoji}`;
783
+ if (d.outcome === 'FAILED' && d.failure_reason) {
784
+ output += `\n āš ļø ${d.failure_reason}`;
785
+ }
786
+ }
787
+ if (older.length > 5) {
788
+ output += `\n\n... and ${older.length - 5} more`;
789
+ }
790
+ }
791
+ // Show semantic edges (related decisions)
792
+ if (semanticEdges) {
793
+ const totalEdges = (semanticEdges.refines?.length || 0) +
794
+ (semanticEdges.refined_by?.length || 0) +
795
+ (semanticEdges.contradicts?.length || 0) +
796
+ (semanticEdges.contradicted_by?.length || 0);
797
+ if (totalEdges > 0) {
798
+ output += '\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
799
+ output += `\nšŸ”— Related Decisions (${totalEdges}):\n`;
800
+ // Refines (builds upon)
801
+ if (semanticEdges.refines && semanticEdges.refines.length > 0) {
802
+ output += '\n✨ Refines (builds upon):';
803
+ semanticEdges.refines.slice(0, 3).forEach((e) => {
804
+ const preview = e.decision.substring(0, 60);
805
+ output += `\n • ${e.topic}: ${preview}${e.decision.length > 60 ? '...' : ''}`;
806
+ });
807
+ if (semanticEdges.refines.length > 3) {
808
+ output += `\n ... and ${semanticEdges.refines.length - 3} more`;
809
+ }
810
+ }
811
+ // Refined by (later improvements)
812
+ if (semanticEdges.refined_by && semanticEdges.refined_by.length > 0) {
813
+ output += '\n\nšŸ”„ Refined by (later improvements):';
814
+ semanticEdges.refined_by.slice(0, 3).forEach((e) => {
815
+ const preview = e.decision.substring(0, 60);
816
+ output += `\n • ${e.topic}: ${preview}${e.decision.length > 60 ? '...' : ''}`;
817
+ });
818
+ if (semanticEdges.refined_by.length > 3) {
819
+ output += `\n ... and ${semanticEdges.refined_by.length - 3} more`;
820
+ }
821
+ }
822
+ // Contradicts
823
+ if (semanticEdges.contradicts && semanticEdges.contradicts.length > 0) {
824
+ output += '\n\n⚔ Contradicts:';
825
+ semanticEdges.contradicts.forEach((e) => {
826
+ const preview = e.decision.substring(0, 60);
827
+ output += `\n • ${e.topic}: ${preview}${e.decision.length > 60 ? '...' : ''}`;
828
+ });
829
+ }
830
+ // Contradicted by
831
+ if (semanticEdges.contradicted_by && semanticEdges.contradicted_by.length > 0) {
832
+ output += '\n\nāŒ Contradicted by:';
833
+ semanticEdges.contradicted_by.forEach((e) => {
834
+ const preview = e.decision.substring(0, 60);
835
+ output += `\n • ${e.topic}: ${preview}${e.decision.length > 60 ? '...' : ''}`;
836
+ });
837
+ }
838
+ }
839
+ }
840
+ output += '\n\nšŸ’” Tip: Review individual decisions for full context';
841
+ return output;
842
+ }
843
+ /**
844
+ * Parse trust_context (might be JSON string)
845
+ */
846
+ function parseTrustContext(trustContext) {
847
+ if (!trustContext) {
848
+ return null;
849
+ }
850
+ // Already parsed
851
+ if (typeof trustContext === 'object') {
852
+ return trustContext;
853
+ }
854
+ // Parse JSON string
855
+ if (typeof trustContext === 'string') {
856
+ try {
857
+ return JSON.parse(trustContext);
858
+ }
859
+ catch {
860
+ return null;
861
+ }
862
+ }
863
+ return null;
864
+ }
865
+ /**
866
+ * Format recent decisions list (all topics, chronological)
867
+ */
868
+ function formatList(decisions, options = {}) {
869
+ const { limit = 20 } = options;
870
+ if (!decisions || decisions.length === 0) {
871
+ return 'āŒ No decisions found';
872
+ }
873
+ // Limit results
874
+ const items = decisions.slice(0, limit);
875
+ let output = `šŸ“‹ Recent Decisions (Last ${items.length})\n`;
876
+ output += '━'.repeat(60) + '\n';
877
+ for (let i = 0; i < items.length; i++) {
878
+ const d = items[i];
879
+ const timeAgo = calculateDuration(d.created_at);
880
+ const type = d.user_involvement === 'approved' ? 'šŸ‘¤ User' : 'šŸ¤– Assistant';
881
+ const status = d.outcome ? getOutcomeEmoji(d.outcome) + ' ' + d.outcome : 'ā³ Pending';
882
+ const confidence = Math.round((d.confidence || 0) * 100);
883
+ // Preview (max 60 chars)
884
+ const preview = d.decision.length > 60 ? d.decision.substring(0, 60) + '...' : d.decision;
885
+ output += `\n${i + 1}. [${timeAgo}] ${type}\n`;
886
+ output += ` šŸ“š ${d.topic}\n`;
887
+ output += ` šŸ’” ${preview}\n`;
888
+ output += ` šŸ“Š ${confidence}% confidence | ${status}\n`;
889
+ }
890
+ output += '\n' + '━'.repeat(60);
891
+ output += `\nšŸ’” Tip: Use mama.recall('topic') for full details\n`;
892
+ return output;
893
+ }
894
+ //# sourceMappingURL=decision-formatter.js.map