@jungjaehoon/mama-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +53 -0
- package/src/mama/config-loader.js +218 -0
- package/src/mama/db-adapter/README.md +105 -0
- package/src/mama/db-adapter/base-adapter.js +91 -0
- package/src/mama/db-adapter/index.js +31 -0
- package/src/mama/db-adapter/sqlite-adapter.js +342 -0
- package/src/mama/db-adapter/statement.js +127 -0
- package/src/mama/db-manager.js +584 -0
- package/src/mama/debug-logger.js +78 -0
- package/src/mama/decision-formatter.js +1180 -0
- package/src/mama/decision-tracker.js +565 -0
- package/src/mama/embedding-cache.js +221 -0
- package/src/mama/embeddings.js +265 -0
- package/src/mama/hook-metrics.js +403 -0
- package/src/mama/mama-api.js +913 -0
- package/src/mama/memory-inject.js +243 -0
- package/src/mama/memory-store.js +89 -0
- package/src/mama/ollama-client.js +387 -0
- package/src/mama/outcome-tracker.js +349 -0
- package/src/mama/query-intent.js +236 -0
- package/src/mama/relevance-scorer.js +283 -0
- package/src/mama/time-formatter.js +82 -0
- package/src/mama/transparency-banner.js +301 -0
- package/src/server.js +290 -0
- package/src/tools/checkpoint-tools.js +76 -0
- package/src/tools/index.js +54 -0
- package/src/tools/list-decisions.js +76 -0
- package/src/tools/recall-decision.js +75 -0
- package/src/tools/save-decision.js +113 -0
- package/src/tools/suggest-decision.js +84 -0
- package/src/tools/update-outcome.js +128 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAMA (Memory-Augmented MCP Architecture) - Decision Context Formatter
|
|
3
|
+
*
|
|
4
|
+
* Formats decision history with token budget enforcement and top-N selection
|
|
5
|
+
* Tasks: 6.1-6.6, 8.1-8.5 (Context formatting with top-N selection)
|
|
6
|
+
* AC #1: Context under 500 tokens
|
|
7
|
+
* AC #4: Rolling summary for large histories
|
|
8
|
+
* AC #5: Top-N selection with summary
|
|
9
|
+
*
|
|
10
|
+
* @module decision-formatter
|
|
11
|
+
* @version 2.0
|
|
12
|
+
* @date 2025-11-14
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { info, error: logError } = require('./debug-logger');
|
|
16
|
+
const { formatTopNContext } = require('./relevance-scorer');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format decision context for Claude injection with top-N selection
|
|
20
|
+
*
|
|
21
|
+
* Task 6.1-6.2, 8.1-8.5: Build context format template with top-N selection
|
|
22
|
+
* AC #1: Format decision history
|
|
23
|
+
* AC #4: Handle large histories with rolling summary
|
|
24
|
+
* AC #5: Top-N selection with summary (top 3 full detail, rest summarized)
|
|
25
|
+
*
|
|
26
|
+
* Story 014.7.10 - Task 5: Fallback Formatting
|
|
27
|
+
* Tries Instant Answer format first (if trust_context available), falls back to legacy
|
|
28
|
+
*
|
|
29
|
+
* @param {Array<Object>} decisions - Decision chain (sorted by relevance)
|
|
30
|
+
* @param {Object} options - Formatting options
|
|
31
|
+
* @param {number} options.maxTokens - Token budget (default: 500)
|
|
32
|
+
* @param {boolean} options.useTopN - Use top-N selection (default: true for 4+ decisions)
|
|
33
|
+
* @param {number} options.topN - Number of decisions for full detail (default: 3)
|
|
34
|
+
* @returns {string} Formatted context for injection
|
|
35
|
+
*/
|
|
36
|
+
function formatContext(decisions, options = {}) {
|
|
37
|
+
const {
|
|
38
|
+
maxTokens = 500,
|
|
39
|
+
useTopN = decisions.length >= 4, // Auto-enable for 4+ decisions
|
|
40
|
+
topN = 3,
|
|
41
|
+
useTeaser = true, // New: Use Teaser format to encourage interaction
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
44
|
+
if (!decisions || decisions.length === 0) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// New approach: Teaser format (curiosity-driven)
|
|
49
|
+
// MAMA = Librarian: Shows book previews, Claude decides to read
|
|
50
|
+
if (useTeaser) {
|
|
51
|
+
// Show top 3 results (Google-style)
|
|
52
|
+
const teaserList = formatTeaserList(decisions, topN);
|
|
53
|
+
|
|
54
|
+
if (teaserList) {
|
|
55
|
+
return teaserList;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fallback: Legacy format
|
|
60
|
+
return formatLegacyContext(decisions, { maxTokens, useTopN, topN });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Format decisions using legacy format (no trust context)
|
|
65
|
+
*
|
|
66
|
+
* Story 014.7.10 - Task 5.1: Fallback formatting
|
|
67
|
+
* AC #3: Graceful degradation for decisions without trust_context
|
|
68
|
+
*
|
|
69
|
+
* @param {Array<Object>} decisions - Decision chain (sorted by relevance)
|
|
70
|
+
* @param {Object} options - Formatting options
|
|
71
|
+
* @param {number} options.maxTokens - Token budget (default: 500)
|
|
72
|
+
* @param {boolean} options.useTopN - Use top-N selection (default: true for 4+ decisions)
|
|
73
|
+
* @param {number} options.topN - Number of decisions for full detail (default: 3)
|
|
74
|
+
* @returns {string} Formatted context (legacy format)
|
|
75
|
+
*/
|
|
76
|
+
function formatLegacyContext(decisions, options = {}) {
|
|
77
|
+
if (!decisions || decisions.length === 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { maxTokens = 500, useTopN = decisions.length >= 4, topN = 3 } = options;
|
|
82
|
+
|
|
83
|
+
// Task 8.1: Use top-N selection for 4+ decisions (AC #5)
|
|
84
|
+
let context;
|
|
85
|
+
|
|
86
|
+
if (useTopN && decisions.length > topN) {
|
|
87
|
+
// Task 8.1: Modify to use top-N selection
|
|
88
|
+
context = formatWithTopN(decisions, topN);
|
|
89
|
+
} else {
|
|
90
|
+
// Find current decision (superseded_by = NULL or missing)
|
|
91
|
+
const current = decisions.find((d) => !d.superseded_by) || decisions[0];
|
|
92
|
+
const history = decisions.filter((d) => d.id !== current.id);
|
|
93
|
+
|
|
94
|
+
// Task 6.2: Build context format template (legacy)
|
|
95
|
+
if (decisions.length <= 3) {
|
|
96
|
+
// Small history: Full details
|
|
97
|
+
context = formatSmallHistory(current, history);
|
|
98
|
+
} else {
|
|
99
|
+
// Large history: Rolling summary
|
|
100
|
+
context = formatLargeHistory(current, history);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Task 6.3, 8.4: Ensure token budget stays under 500 tokens
|
|
105
|
+
return ensureTokenBudget(context, maxTokens);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Format with top-N selection
|
|
110
|
+
*
|
|
111
|
+
* Task 8.2-8.3: Full detail for top 3, summary for rest
|
|
112
|
+
* AC #5: Top-N selection with summary
|
|
113
|
+
*
|
|
114
|
+
* @param {Array<Object>} decisions - All decisions (sorted by relevance)
|
|
115
|
+
* @param {number} topN - Number of decisions for full detail
|
|
116
|
+
* @returns {string} Formatted context
|
|
117
|
+
*/
|
|
118
|
+
function formatWithTopN(decisions, topN) {
|
|
119
|
+
// Use formatTopNContext from relevance-scorer.js
|
|
120
|
+
const { full, summary } = formatTopNContext(decisions, topN);
|
|
121
|
+
|
|
122
|
+
const current = full[0]; // Highest relevance
|
|
123
|
+
const topic = current.topic;
|
|
124
|
+
|
|
125
|
+
// Task 8.2: Full detail for top 3 decisions
|
|
126
|
+
let context = `
|
|
127
|
+
š§ DECISION HISTORY: ${topic}
|
|
128
|
+
|
|
129
|
+
Top ${full.length} Most Relevant Decisions:
|
|
130
|
+
`.trim();
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < full.length; i++) {
|
|
133
|
+
const d = full[i];
|
|
134
|
+
const duration = calculateDuration(d.created_at);
|
|
135
|
+
const outcomeEmoji = getOutcomeEmoji(d.outcome);
|
|
136
|
+
const relevancePercent = Math.round((d.relevanceScore || 0) * 100);
|
|
137
|
+
|
|
138
|
+
context += `\n\n${i + 1}. ${d.decision} (${duration}, relevance: ${relevancePercent}%) ${outcomeEmoji}`;
|
|
139
|
+
context += `\n Reasoning: ${d.reasoning || 'N/A'}`;
|
|
140
|
+
|
|
141
|
+
if (d.outcome === 'FAILED') {
|
|
142
|
+
context += `\n ā ļø Failure: ${d.failure_reason || 'Unknown reason'}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Task 8.3: Summary for rest (count, duration, key failures only)
|
|
147
|
+
if (summary && summary.count > 0) {
|
|
148
|
+
context += `\n\nāāāāāāāāāāāāāāāāāāāāāāā`;
|
|
149
|
+
context += `\nHistory: ${summary.count} additional decisions over ${summary.duration_days} days`;
|
|
150
|
+
|
|
151
|
+
if (summary.failures && summary.failures.length > 0) {
|
|
152
|
+
context += `\n\nā ļø Other Failures:`;
|
|
153
|
+
for (const failure of summary.failures) {
|
|
154
|
+
context += `\n- ${failure.decision}: ${failure.reason || 'Unknown'}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return context;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Format small decision history (3 or fewer)
|
|
164
|
+
*
|
|
165
|
+
* @param {Object} current - Current decision
|
|
166
|
+
* @param {Array<Object>} history - Previous decisions
|
|
167
|
+
* @returns {string} Formatted context
|
|
168
|
+
*/
|
|
169
|
+
function formatSmallHistory(current, history) {
|
|
170
|
+
const duration = calculateDuration(current.created_at);
|
|
171
|
+
|
|
172
|
+
let context = `
|
|
173
|
+
š§ DECISION HISTORY: ${current.topic}
|
|
174
|
+
|
|
175
|
+
Current: ${current.decision} (${duration}, confidence: ${current.confidence})
|
|
176
|
+
Reasoning: ${current.reasoning || 'N/A'}
|
|
177
|
+
`.trim();
|
|
178
|
+
|
|
179
|
+
// Add history details
|
|
180
|
+
if (history.length > 0) {
|
|
181
|
+
context += '\n\nPrevious Decisions:\n';
|
|
182
|
+
|
|
183
|
+
for (const decision of history) {
|
|
184
|
+
const durationDays = calculateDurationDays(
|
|
185
|
+
decision.created_at,
|
|
186
|
+
decision.updated_at || Date.now()
|
|
187
|
+
);
|
|
188
|
+
const outcomeEmoji = getOutcomeEmoji(decision.outcome);
|
|
189
|
+
|
|
190
|
+
context += `- ${decision.decision} (${durationDays} days) ${outcomeEmoji}\n`;
|
|
191
|
+
|
|
192
|
+
if (decision.outcome === 'FAILED') {
|
|
193
|
+
context += ` Reason: ${decision.failure_reason || 'Unknown'}\n`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return context;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Format large decision history (4+ decisions)
|
|
203
|
+
*
|
|
204
|
+
* Task 6.2: Rolling summary for large histories
|
|
205
|
+
* AC #4: Highlight top 3 failures
|
|
206
|
+
*
|
|
207
|
+
* @param {Object} current - Current decision
|
|
208
|
+
* @param {Array<Object>} history - Previous decisions
|
|
209
|
+
* @returns {string} Formatted context with rolling summary
|
|
210
|
+
*/
|
|
211
|
+
function formatLargeHistory(current, history) {
|
|
212
|
+
const duration = calculateDuration(current.created_at);
|
|
213
|
+
// Include current decision in total duration calculation
|
|
214
|
+
const allDecisions = [current, ...history];
|
|
215
|
+
const totalDuration = calculateTotalDuration(allDecisions);
|
|
216
|
+
|
|
217
|
+
// Extract failures
|
|
218
|
+
const failures = history.filter((d) => d.outcome === 'FAILED');
|
|
219
|
+
const topFailures = failures.slice(0, 3);
|
|
220
|
+
|
|
221
|
+
// Get last evolution
|
|
222
|
+
const lastEvolution = history.length > 0 ? history[0] : null;
|
|
223
|
+
|
|
224
|
+
let context = `
|
|
225
|
+
š§ DECISION HISTORY: ${current.topic}
|
|
226
|
+
|
|
227
|
+
Current: ${current.decision} (confidence: ${current.confidence})
|
|
228
|
+
Reasoning: ${current.reasoning || 'N/A'}
|
|
229
|
+
|
|
230
|
+
History: ${history.length + 1} decisions over ${totalDuration}
|
|
231
|
+
`.trim();
|
|
232
|
+
|
|
233
|
+
// Add key failures
|
|
234
|
+
if (topFailures.length > 0) {
|
|
235
|
+
context += '\n\nā ļø Key Failures (avoid these):\n';
|
|
236
|
+
|
|
237
|
+
for (const failure of topFailures) {
|
|
238
|
+
context += `- ${failure.decision}: ${failure.failure_reason || 'Unknown reason'}\n`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Add last evolution
|
|
243
|
+
if (lastEvolution) {
|
|
244
|
+
context += `\nLast evolution: ${lastEvolution.decision} ā ${current.decision}`;
|
|
245
|
+
|
|
246
|
+
if (current.reasoning) {
|
|
247
|
+
const reasonSummary = current.reasoning.substring(0, 100);
|
|
248
|
+
context += ` (${reasonSummary}${current.reasoning.length > 100 ? '...' : ''})`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return context;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Ensure token budget is enforced
|
|
257
|
+
*
|
|
258
|
+
* Task 6.3-6.5: Token budget enforcement
|
|
259
|
+
* AC #1: Context stays under 500 tokens
|
|
260
|
+
*
|
|
261
|
+
* @param {string} text - Context text
|
|
262
|
+
* @param {number} maxTokens - Maximum tokens allowed
|
|
263
|
+
* @returns {string} Truncated text if needed
|
|
264
|
+
*/
|
|
265
|
+
function ensureTokenBudget(text, maxTokens) {
|
|
266
|
+
// Task 6.4: Token estimation (~1 token per 4 characters)
|
|
267
|
+
const estimatedTokens = estimateTokens(text);
|
|
268
|
+
|
|
269
|
+
if (estimatedTokens <= maxTokens) {
|
|
270
|
+
return text;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Task 6.5: Truncate to fit budget
|
|
274
|
+
const ratio = maxTokens / estimatedTokens;
|
|
275
|
+
const truncated = text.substring(0, Math.floor(text.length * ratio));
|
|
276
|
+
|
|
277
|
+
return truncated + '\n\n... (truncated to fit token budget)';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Estimate token count from text
|
|
282
|
+
*
|
|
283
|
+
* Task 6.4: Simple token estimation
|
|
284
|
+
* Heuristic: ~1 token per 4 characters
|
|
285
|
+
*
|
|
286
|
+
* @param {string} text - Text to estimate
|
|
287
|
+
* @returns {number} Estimated token count
|
|
288
|
+
*/
|
|
289
|
+
function estimateTokens(text) {
|
|
290
|
+
// Task 6.4: ~1 token per 4 characters
|
|
291
|
+
return Math.ceil(text.length / 4);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Calculate human-readable duration
|
|
296
|
+
*
|
|
297
|
+
* @param {number|string} timestamp - Unix timestamp (ms) or ISO 8601 string
|
|
298
|
+
* @returns {string} Human-readable duration (e.g., "3 days ago")
|
|
299
|
+
*/
|
|
300
|
+
function calculateDuration(timestamp) {
|
|
301
|
+
// Handle Unix timestamp (number or numeric string) and ISO 8601 string
|
|
302
|
+
let ts;
|
|
303
|
+
if (typeof timestamp === 'string') {
|
|
304
|
+
// Try parsing as number first (e.g., "1763971277689")
|
|
305
|
+
const num = Number(timestamp);
|
|
306
|
+
ts = isNaN(num) ? Date.parse(timestamp) : num;
|
|
307
|
+
} else {
|
|
308
|
+
ts = timestamp;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (isNaN(ts) || ts === null || ts === undefined) {
|
|
312
|
+
return 'unknown';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
const diffMs = now - ts;
|
|
317
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
318
|
+
|
|
319
|
+
if (diffDays === 0) {
|
|
320
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
321
|
+
if (diffHours === 0) {
|
|
322
|
+
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
323
|
+
return `${diffMins} min${diffMins !== 1 ? 's' : ''} ago`;
|
|
324
|
+
}
|
|
325
|
+
return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Calculate duration between two timestamps
|
|
333
|
+
*
|
|
334
|
+
* @param {number|string} start - Start timestamp (ms) or ISO 8601 string
|
|
335
|
+
* @param {number|string} end - End timestamp (ms) or ISO 8601 string
|
|
336
|
+
* @returns {number} Duration in days
|
|
337
|
+
*/
|
|
338
|
+
function calculateDurationDays(start, end) {
|
|
339
|
+
// Handle Unix timestamp (number or numeric string) and ISO 8601 string
|
|
340
|
+
let startTs, endTs;
|
|
341
|
+
|
|
342
|
+
if (typeof start === 'string') {
|
|
343
|
+
const num = Number(start);
|
|
344
|
+
startTs = isNaN(num) ? Date.parse(start) : num;
|
|
345
|
+
} else {
|
|
346
|
+
startTs = start;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (typeof end === 'string') {
|
|
350
|
+
const num = Number(end);
|
|
351
|
+
endTs = isNaN(num) ? Date.parse(end) : num;
|
|
352
|
+
} else {
|
|
353
|
+
endTs = end;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (isNaN(startTs) || isNaN(endTs)) {
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const diffMs = endTs - startTs;
|
|
361
|
+
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Calculate total duration across decision history
|
|
366
|
+
*
|
|
367
|
+
* @param {Array<Object>} history - Decision history
|
|
368
|
+
* @returns {string} Human-readable total duration
|
|
369
|
+
*/
|
|
370
|
+
function calculateTotalDuration(history) {
|
|
371
|
+
if (history.length === 0) {
|
|
372
|
+
return 'N/A';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Convert all timestamps to numbers for comparison
|
|
376
|
+
const timestamps = history
|
|
377
|
+
.map((d) => {
|
|
378
|
+
const created = typeof d.created_at === 'string' ? Date.parse(d.created_at) : d.created_at;
|
|
379
|
+
const updated = d.updated_at
|
|
380
|
+
? typeof d.updated_at === 'string'
|
|
381
|
+
? Date.parse(d.updated_at)
|
|
382
|
+
: d.updated_at
|
|
383
|
+
: created;
|
|
384
|
+
return { created, updated };
|
|
385
|
+
})
|
|
386
|
+
.filter((t) => !isNaN(t.created) && !isNaN(t.updated));
|
|
387
|
+
|
|
388
|
+
if (timestamps.length === 0) {
|
|
389
|
+
return 'N/A';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const earliest = Math.min(...timestamps.map((t) => t.created));
|
|
393
|
+
const latest = Math.max(...timestamps.map((t) => t.updated));
|
|
394
|
+
|
|
395
|
+
const durationDays = calculateDurationDays(earliest, latest);
|
|
396
|
+
|
|
397
|
+
if (durationDays < 7) {
|
|
398
|
+
return `${durationDays} days`;
|
|
399
|
+
} else if (durationDays < 30) {
|
|
400
|
+
const weeks = Math.floor(durationDays / 7);
|
|
401
|
+
return `${weeks} week${weeks !== 1 ? 's' : ''}`;
|
|
402
|
+
} else {
|
|
403
|
+
const months = Math.floor(durationDays / 30);
|
|
404
|
+
return `${months} month${months !== 1 ? 's' : ''}`;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get emoji for outcome
|
|
410
|
+
*
|
|
411
|
+
* @param {string} outcome - Decision outcome
|
|
412
|
+
* @returns {string} Emoji representation
|
|
413
|
+
*/
|
|
414
|
+
function getOutcomeEmoji(outcome) {
|
|
415
|
+
const emojiMap = {
|
|
416
|
+
SUCCESS: 'ā
',
|
|
417
|
+
FAILED: 'ā',
|
|
418
|
+
PARTIAL: 'ā ļø',
|
|
419
|
+
ONGOING: 'ā³',
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
return emojiMap[outcome] || '';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Format context in Claude-friendly Instant Answer format
|
|
427
|
+
*
|
|
428
|
+
* Story 014.7.10: Claude-Friendly Context Formatting
|
|
429
|
+
* AC #1: Instant Answer format with trust components
|
|
430
|
+
*
|
|
431
|
+
* Prioritizes:
|
|
432
|
+
* 1. Quick answer (one line)
|
|
433
|
+
* 2. Code example (if available)
|
|
434
|
+
* 3. Trust evidence (5 components)
|
|
435
|
+
* 4. Minimal reasoning (< 150 chars)
|
|
436
|
+
*
|
|
437
|
+
* @param {Object} decision - Decision object
|
|
438
|
+
* @param {Object} options - Formatting options
|
|
439
|
+
* @param {number} options.maxTokens - Token budget (default: 500)
|
|
440
|
+
* @returns {string|null} Formatted instant answer or null
|
|
441
|
+
*/
|
|
442
|
+
function formatInstantAnswer(decision, options = {}) {
|
|
443
|
+
const { maxTokens = 500 } = options;
|
|
444
|
+
|
|
445
|
+
if (!decision) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Extract quick answer (first line of decision)
|
|
450
|
+
const quickAnswer = extractQuickAnswer(decision);
|
|
451
|
+
|
|
452
|
+
if (!quickAnswer) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Extract code example (from reasoning)
|
|
457
|
+
const codeExample = extractCodeExample(decision);
|
|
458
|
+
|
|
459
|
+
// Format trust context
|
|
460
|
+
const trustSection = formatTrustContext(decision.trust_context);
|
|
461
|
+
|
|
462
|
+
// Build output
|
|
463
|
+
let output = `ā” INSTANT ANSWER\n\n${quickAnswer}`;
|
|
464
|
+
|
|
465
|
+
if (codeExample) {
|
|
466
|
+
output += `\n\n${codeExample}`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (trustSection) {
|
|
470
|
+
output += `\n\n${trustSection}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Token budget check
|
|
474
|
+
if (estimateTokens(output) > maxTokens) {
|
|
475
|
+
output = truncateToFit(output, maxTokens);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return output;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Extract quick answer from decision
|
|
483
|
+
*
|
|
484
|
+
* Returns first line or sentence from decision field
|
|
485
|
+
*
|
|
486
|
+
* @param {Object} decision - Decision object
|
|
487
|
+
* @returns {string|null} Quick answer or null
|
|
488
|
+
*/
|
|
489
|
+
function extractQuickAnswer(decision) {
|
|
490
|
+
if (!decision.decision || typeof decision.decision !== 'string') {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const text = decision.decision.trim();
|
|
495
|
+
|
|
496
|
+
if (text.length === 0) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Extract first line
|
|
501
|
+
const lines = text.split('\n');
|
|
502
|
+
const firstLine = lines[0].trim();
|
|
503
|
+
|
|
504
|
+
// Check if first line contains multiple real sentences
|
|
505
|
+
// Match period/exclamation/question mark followed by space and capital letter
|
|
506
|
+
const sentenceMatch = firstLine.match(/^.+?[.!?](?=\s+[A-Z])/);
|
|
507
|
+
if (sentenceMatch) {
|
|
508
|
+
// Multiple sentences detected - return first sentence
|
|
509
|
+
return sentenceMatch[0].trim();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Single sentence or no sentence boundary - use full first line if reasonable
|
|
513
|
+
if (firstLine.length <= 150) {
|
|
514
|
+
return firstLine;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// First line too long - truncate to 100 chars
|
|
518
|
+
return firstLine.substring(0, 100) + '...';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Extract code example from reasoning
|
|
523
|
+
*
|
|
524
|
+
* Looks for markdown code blocks (```...```)
|
|
525
|
+
*
|
|
526
|
+
* @param {Object} decision - Decision object
|
|
527
|
+
* @returns {string|null} Code example or null
|
|
528
|
+
*/
|
|
529
|
+
function extractCodeExample(decision) {
|
|
530
|
+
if (!decision.reasoning || typeof decision.reasoning !== 'string') {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Match markdown code blocks
|
|
535
|
+
const codeBlockRegex = /```[\s\S]*?```/;
|
|
536
|
+
const match = decision.reasoning.match(codeBlockRegex);
|
|
537
|
+
|
|
538
|
+
if (match) {
|
|
539
|
+
return match[0];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Check if decision field contains code patterns
|
|
543
|
+
if (decision.decision && typeof decision.decision === 'string') {
|
|
544
|
+
const hasCode =
|
|
545
|
+
decision.decision.includes('mama.save(') ||
|
|
546
|
+
decision.decision.includes('await ') ||
|
|
547
|
+
decision.decision.includes('=>');
|
|
548
|
+
|
|
549
|
+
if (hasCode) {
|
|
550
|
+
// Wrap in code block
|
|
551
|
+
return `\`\`\`javascript\n${decision.decision}\n\`\`\``;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Format trust context section
|
|
560
|
+
*
|
|
561
|
+
* Story 014.7.10 AC #2: Trust Context display
|
|
562
|
+
*
|
|
563
|
+
* Shows 5 trust components:
|
|
564
|
+
* 1. Source transparency
|
|
565
|
+
* 2. Causality
|
|
566
|
+
* 3. Verifiability
|
|
567
|
+
* 4. Context relevance
|
|
568
|
+
* 5. Track record
|
|
569
|
+
*
|
|
570
|
+
* @param {Object} trustCtx - Trust context object
|
|
571
|
+
* @returns {string|null} Formatted trust section or null
|
|
572
|
+
*/
|
|
573
|
+
function formatTrustContext(trustCtx) {
|
|
574
|
+
if (!trustCtx) {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const lines = ['ā'.repeat(40), 'š WHY TRUST THIS?', ''];
|
|
579
|
+
|
|
580
|
+
let hasContent = false;
|
|
581
|
+
|
|
582
|
+
// 1. Source transparency
|
|
583
|
+
if (trustCtx.source) {
|
|
584
|
+
const { file, line, author, timestamp } = trustCtx.source;
|
|
585
|
+
const timeAgo = calculateDuration(timestamp);
|
|
586
|
+
lines.push(`š Source: ${file}:${line} (${timeAgo}, by ${author})`);
|
|
587
|
+
hasContent = true;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// 2. Causality
|
|
591
|
+
if (trustCtx.causality && trustCtx.causality.impact) {
|
|
592
|
+
lines.push(`š Reason: ${trustCtx.causality.impact}`);
|
|
593
|
+
hasContent = true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// 3. Verifiability
|
|
597
|
+
if (trustCtx.verification) {
|
|
598
|
+
const { test_file, result } = trustCtx.verification;
|
|
599
|
+
const status = result === 'success' ? 'passed' : result;
|
|
600
|
+
lines.push(`ā
Verified: ${test_file} ${status}`);
|
|
601
|
+
hasContent = true;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 4. Context relevance
|
|
605
|
+
if (trustCtx.context_match && trustCtx.context_match.user_intent) {
|
|
606
|
+
lines.push(`šÆ Applies to: ${trustCtx.context_match.user_intent}`);
|
|
607
|
+
hasContent = true;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// 5. Track record
|
|
611
|
+
if (trustCtx.track_record) {
|
|
612
|
+
const { recent_successes, recent_failures } = trustCtx.track_record;
|
|
613
|
+
const successCount = recent_successes?.length || 0;
|
|
614
|
+
const failureCount = recent_failures?.length || 0;
|
|
615
|
+
const total = successCount + failureCount;
|
|
616
|
+
|
|
617
|
+
if (total > 0) {
|
|
618
|
+
lines.push(`š Track record: ${successCount}/${total} recent successes`);
|
|
619
|
+
hasContent = true;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!hasContent) {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
lines.push('ā'.repeat(40));
|
|
628
|
+
|
|
629
|
+
return lines.join('\n');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Truncate output to fit token budget
|
|
634
|
+
*
|
|
635
|
+
* Prioritizes:
|
|
636
|
+
* 1. Keep quick answer (always)
|
|
637
|
+
* 2. Keep code example (if fits)
|
|
638
|
+
* 3. Trim trust section (if needed)
|
|
639
|
+
*
|
|
640
|
+
* @param {string} output - Full output
|
|
641
|
+
* @param {number} maxTokens - Maximum tokens
|
|
642
|
+
* @returns {string} Truncated output
|
|
643
|
+
*/
|
|
644
|
+
function truncateToFit(output, maxTokens) {
|
|
645
|
+
// Split sections
|
|
646
|
+
const sections = output.split('\n\n');
|
|
647
|
+
const quickAnswer = sections[0]; // "ā” INSTANT ANSWER\n\n[answer]"
|
|
648
|
+
|
|
649
|
+
// Always keep quick answer
|
|
650
|
+
let result = quickAnswer;
|
|
651
|
+
let remainingTokens = maxTokens - estimateTokens(result);
|
|
652
|
+
|
|
653
|
+
// Try to add code example
|
|
654
|
+
const codeIndex = sections.findIndex((s) => s.startsWith('```'));
|
|
655
|
+
if (codeIndex > 0) {
|
|
656
|
+
const codeSection = sections[codeIndex];
|
|
657
|
+
const codeTokens = estimateTokens(codeSection);
|
|
658
|
+
|
|
659
|
+
if (codeTokens <= remainingTokens) {
|
|
660
|
+
result += '\n\n' + codeSection;
|
|
661
|
+
remainingTokens -= codeTokens;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Try to add trust section (trimmed if needed)
|
|
666
|
+
const trustIndex = sections.findIndex((s) => s.startsWith('ā'));
|
|
667
|
+
if (trustIndex > 0 && remainingTokens > 50) {
|
|
668
|
+
const trustSection = sections[trustIndex];
|
|
669
|
+
const trustTokens = estimateTokens(trustSection);
|
|
670
|
+
|
|
671
|
+
if (trustTokens <= remainingTokens) {
|
|
672
|
+
result += '\n\n' + trustSection;
|
|
673
|
+
} else {
|
|
674
|
+
// Trim trust section to fit
|
|
675
|
+
const trustLines = trustSection.split('\n');
|
|
676
|
+
let trimmed = trustLines[0] + '\n' + trustLines[1] + '\n'; // Header
|
|
677
|
+
|
|
678
|
+
for (let i = 2; i < trustLines.length - 1; i++) {
|
|
679
|
+
const line = trustLines[i] + '\n';
|
|
680
|
+
if (estimateTokens(trimmed + line) <= remainingTokens - 10) {
|
|
681
|
+
trimmed += line;
|
|
682
|
+
} else {
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
trimmed += trustLines[trustLines.length - 1]; // Footer
|
|
688
|
+
result += '\n\n' + trimmed;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return result;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Format multiple decisions as Google-style search results
|
|
697
|
+
*
|
|
698
|
+
* Shows top N results with relevance scores, allowing user to choose
|
|
699
|
+
* Story: Google-style teaser list for better UX
|
|
700
|
+
*
|
|
701
|
+
* @param {Array<Object>} decisions - Decision objects (sorted by relevance)
|
|
702
|
+
* @param {number} topN - Number of results to show (default: 3)
|
|
703
|
+
* @returns {string|null} Formatted teaser list or null
|
|
704
|
+
*/
|
|
705
|
+
function formatTeaserList(decisions, topN = 3) {
|
|
706
|
+
if (!decisions || decisions.length === 0) {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const topDecisions = decisions.slice(0, topN);
|
|
711
|
+
const count = topDecisions.length;
|
|
712
|
+
|
|
713
|
+
let output = `š” MAMA found ${count} related topic${count > 1 ? 's' : ''}:\n`;
|
|
714
|
+
|
|
715
|
+
for (let i = 0; i < topDecisions.length; i++) {
|
|
716
|
+
const d = topDecisions[i];
|
|
717
|
+
const relevance = Math.round((d.similarity || d.confidence || 0) * 100);
|
|
718
|
+
|
|
719
|
+
// Preview (max 60 chars)
|
|
720
|
+
const preview = d.decision.length > 60 ? d.decision.substring(0, 60) + '...' : d.decision;
|
|
721
|
+
|
|
722
|
+
output += `\n${i + 1}. ${d.topic} (${relevance}% match)`;
|
|
723
|
+
output += `\n "${preview}"`;
|
|
724
|
+
|
|
725
|
+
// Recency metadata (NEW - Gaussian Decay)
|
|
726
|
+
// Shows age and recency impact to help Claude adjust parameters
|
|
727
|
+
if (d.recency_age_days !== undefined && d.created_at) {
|
|
728
|
+
const timeAgo = calculateDuration(d.created_at); // Use human-readable time (mins/hours/days)
|
|
729
|
+
const recencyScore = d.recency_score ? Math.round(d.recency_score * 100) : null;
|
|
730
|
+
const finalScore = d.final_score ? Math.round(d.final_score * 100) : null;
|
|
731
|
+
|
|
732
|
+
output += `\n ā° ${timeAgo}`;
|
|
733
|
+
if (recencyScore !== null && finalScore !== null) {
|
|
734
|
+
output += ` | Recency: ${recencyScore}% | Final: ${finalScore}%`;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
output += `\n š mama.recall('${d.topic}')`;
|
|
739
|
+
|
|
740
|
+
if (i < topDecisions.length - 1) {
|
|
741
|
+
output += '\n';
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return output;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Format decision as curiosity-inducing teaser
|
|
750
|
+
*
|
|
751
|
+
* MAMA = Librarian: Shows book preview, Claude decides to read
|
|
752
|
+
* "Just enough context to spark curiosity" - makes Claude want to learn more
|
|
753
|
+
*
|
|
754
|
+
* @param {Object} decision - Decision object
|
|
755
|
+
* @returns {string|null} Formatted teaser or null
|
|
756
|
+
*/
|
|
757
|
+
function formatTeaser(decision) {
|
|
758
|
+
if (!decision) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const timeAgo = calculateDuration(decision.created_at);
|
|
763
|
+
|
|
764
|
+
// Extract preview (first 60 chars)
|
|
765
|
+
const preview =
|
|
766
|
+
decision.decision.length > 60 ? decision.decision.substring(0, 60) + '...' : decision.decision;
|
|
767
|
+
|
|
768
|
+
// Extract files from trust_context or show generic
|
|
769
|
+
let files = 'Multiple files';
|
|
770
|
+
if (decision.trust_context?.source?.file) {
|
|
771
|
+
const fileStr = decision.trust_context.source.file;
|
|
772
|
+
const fileList = fileStr.split(',').map((f) => f.trim());
|
|
773
|
+
|
|
774
|
+
if (fileList.length === 1) {
|
|
775
|
+
files = fileList[0];
|
|
776
|
+
} else if (fileList.length === 2) {
|
|
777
|
+
files = fileList.join(', ');
|
|
778
|
+
} else {
|
|
779
|
+
files = `${fileList[0]}, ${fileList[1]} (+${fileList.length - 2})`;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Build teaser
|
|
784
|
+
const teaser = `
|
|
785
|
+
š” MAMA has related info
|
|
786
|
+
|
|
787
|
+
š Topic: ${decision.topic}
|
|
788
|
+
š Preview: "${preview}"
|
|
789
|
+
š Files: ${files}
|
|
790
|
+
ā° Updated: ${timeAgo}
|
|
791
|
+
|
|
792
|
+
š Read more: mama.recall('${decision.topic}')
|
|
793
|
+
`.trim();
|
|
794
|
+
|
|
795
|
+
return teaser;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Extract files from source string
|
|
800
|
+
* Helper for formatTeaser
|
|
801
|
+
*
|
|
802
|
+
* @param {string} source - Source file string (may be comma-separated)
|
|
803
|
+
* @returns {string} Formatted file list
|
|
804
|
+
*/
|
|
805
|
+
function extractFiles(source) {
|
|
806
|
+
if (!source) {
|
|
807
|
+
return 'Multiple files';
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const files = source.split(',').map((f) => f.trim());
|
|
811
|
+
|
|
812
|
+
if (files.length === 1) {
|
|
813
|
+
return files[0];
|
|
814
|
+
} else if (files.length === 2) {
|
|
815
|
+
return files.join(', ');
|
|
816
|
+
} else {
|
|
817
|
+
return `${files[0]}, ${files[1]} (+${files.length - 2})`;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Format mama.recall() results in readable format
|
|
823
|
+
*
|
|
824
|
+
* Transforms raw JSON into readable markdown with:
|
|
825
|
+
* - Properly formatted reasoning (markdown preserved)
|
|
826
|
+
* - Parsed trust_context (not JSON string)
|
|
827
|
+
* - Clean metadata display
|
|
828
|
+
*
|
|
829
|
+
* @param {Array<Object>} decisions - Decision history from recall()
|
|
830
|
+
* @returns {string} Formatted output for human reading
|
|
831
|
+
*/
|
|
832
|
+
function formatRecall(decisions, semanticEdges = null) {
|
|
833
|
+
if (!decisions || decisions.length === 0) {
|
|
834
|
+
return 'ā No decisions found';
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Single decision: full detail
|
|
838
|
+
if (decisions.length === 1) {
|
|
839
|
+
return formatSingleDecision(decisions[0], semanticEdges);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Multiple decisions: history view
|
|
843
|
+
return formatDecisionHistory(decisions, semanticEdges);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Format single decision with full detail
|
|
848
|
+
*
|
|
849
|
+
* @param {Object} decision - Single decision object
|
|
850
|
+
* @returns {string} Formatted decision
|
|
851
|
+
*/
|
|
852
|
+
function formatSingleDecision(decision) {
|
|
853
|
+
const timeAgo = calculateDuration(decision.created_at);
|
|
854
|
+
const confidencePercent = Math.round((decision.confidence || 0) * 100);
|
|
855
|
+
const outcomeEmoji = getOutcomeEmoji(decision.outcome);
|
|
856
|
+
const outcomeText = decision.outcome || 'Not yet tracked';
|
|
857
|
+
|
|
858
|
+
let output = `
|
|
859
|
+
š Decision: ${decision.topic}
|
|
860
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
861
|
+
|
|
862
|
+
${decision.reasoning || decision.decision}
|
|
863
|
+
`.trim();
|
|
864
|
+
|
|
865
|
+
// Metadata section
|
|
866
|
+
output += `\n\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`;
|
|
867
|
+
output += `\nš Confidence: ${confidencePercent}%`;
|
|
868
|
+
output += `\nā° Created: ${timeAgo}`;
|
|
869
|
+
output += `\n${outcomeEmoji} Outcome: ${outcomeText}`;
|
|
870
|
+
|
|
871
|
+
if (decision.outcome === 'FAILED' && decision.failure_reason) {
|
|
872
|
+
output += `\nā ļø Failure reason: ${decision.failure_reason}`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Trust context section (if available)
|
|
876
|
+
const trustCtx = parseTrustContext(decision.trust_context);
|
|
877
|
+
if (trustCtx) {
|
|
878
|
+
output += '\n\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā';
|
|
879
|
+
output += '\nš Trust Context\n';
|
|
880
|
+
|
|
881
|
+
if (trustCtx.source) {
|
|
882
|
+
const { file, line, author } = trustCtx.source;
|
|
883
|
+
output += `\nš Source: ${file}${line ? ':' + line : ''} (by ${author || 'unknown'})`;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (trustCtx.causality?.impact) {
|
|
887
|
+
output += `\nš Impact: ${trustCtx.causality.impact}`;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (trustCtx.verification) {
|
|
891
|
+
const { test_file, result } = trustCtx.verification;
|
|
892
|
+
const status = result === 'success' ? 'ā
passed' : `ā ļø ${result}`;
|
|
893
|
+
output += `\n${status}: ${test_file || 'Verified'}`;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (trustCtx.track_record) {
|
|
897
|
+
const { success_rate, sample_size } = trustCtx.track_record;
|
|
898
|
+
if (sample_size > 0) {
|
|
899
|
+
const rate = Math.round(success_rate * 100);
|
|
900
|
+
output += `\nš Track record: ${rate}% success (${sample_size} samples)`;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return output;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Format decision history (multiple decisions)
|
|
910
|
+
*
|
|
911
|
+
* @param {Array<Object>} decisions - Decision array
|
|
912
|
+
* @param {Object} [semanticEdges] - Semantic edges { refines, refined_by, contradicts, contradicted_by }
|
|
913
|
+
* @returns {string} Formatted history
|
|
914
|
+
*/
|
|
915
|
+
function formatDecisionHistory(decisions, semanticEdges = null) {
|
|
916
|
+
const topic = decisions[0].topic;
|
|
917
|
+
const latest = decisions[0];
|
|
918
|
+
const older = decisions.slice(1);
|
|
919
|
+
|
|
920
|
+
let output = `
|
|
921
|
+
š Decision History: ${topic}
|
|
922
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
923
|
+
|
|
924
|
+
Latest Decision (${calculateDuration(latest.created_at)}):
|
|
925
|
+
${latest.decision}
|
|
926
|
+
`.trim();
|
|
927
|
+
|
|
928
|
+
// Show brief reasoning if available
|
|
929
|
+
if (latest.reasoning) {
|
|
930
|
+
const briefReasoning = latest.reasoning.split('\n')[0].substring(0, 150);
|
|
931
|
+
output += `\n\nReasoning: ${briefReasoning}${latest.reasoning.length > 150 ? '...' : ''}`;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
output += `\n\nConfidence: ${Math.round(latest.confidence * 100)}%`;
|
|
935
|
+
|
|
936
|
+
// Show older decisions (supersedes chain)
|
|
937
|
+
if (older.length > 0) {
|
|
938
|
+
output += '\n\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā';
|
|
939
|
+
output += `\nPrevious Decisions (${older.length}):\n`;
|
|
940
|
+
|
|
941
|
+
for (let i = 0; i < Math.min(older.length, 5); i++) {
|
|
942
|
+
const d = older[i];
|
|
943
|
+
const timeAgo = calculateDuration(d.created_at);
|
|
944
|
+
const emoji = getOutcomeEmoji(d.outcome);
|
|
945
|
+
output += `\n${i + 2}. ${d.decision} (${timeAgo}) ${emoji}`;
|
|
946
|
+
|
|
947
|
+
if (d.outcome === 'FAILED' && d.failure_reason) {
|
|
948
|
+
output += `\n ā ļø ${d.failure_reason}`;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (older.length > 5) {
|
|
953
|
+
output += `\n\n... and ${older.length - 5} more`;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Show semantic edges (related decisions)
|
|
958
|
+
if (semanticEdges) {
|
|
959
|
+
const totalEdges =
|
|
960
|
+
(semanticEdges.refines?.length || 0) +
|
|
961
|
+
(semanticEdges.refined_by?.length || 0) +
|
|
962
|
+
(semanticEdges.contradicts?.length || 0) +
|
|
963
|
+
(semanticEdges.contradicted_by?.length || 0);
|
|
964
|
+
|
|
965
|
+
if (totalEdges > 0) {
|
|
966
|
+
output += '\n\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā';
|
|
967
|
+
output += `\nš Related Decisions (${totalEdges}):\n`;
|
|
968
|
+
|
|
969
|
+
// Refines (builds upon)
|
|
970
|
+
if (semanticEdges.refines && semanticEdges.refines.length > 0) {
|
|
971
|
+
output += '\n⨠Refines (builds upon):';
|
|
972
|
+
semanticEdges.refines.slice(0, 3).forEach((e) => {
|
|
973
|
+
const preview = e.decision.substring(0, 60);
|
|
974
|
+
output += `\n ⢠${e.topic}: ${preview}${e.decision.length > 60 ? '...' : ''}`;
|
|
975
|
+
});
|
|
976
|
+
if (semanticEdges.refines.length > 3) {
|
|
977
|
+
output += `\n ... and ${semanticEdges.refines.length - 3} more`;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Refined by (later improvements)
|
|
982
|
+
if (semanticEdges.refined_by && semanticEdges.refined_by.length > 0) {
|
|
983
|
+
output += '\n\nš Refined by (later improvements):';
|
|
984
|
+
semanticEdges.refined_by.slice(0, 3).forEach((e) => {
|
|
985
|
+
const preview = e.decision.substring(0, 60);
|
|
986
|
+
output += `\n ⢠${e.topic}: ${preview}${e.decision.length > 60 ? '...' : ''}`;
|
|
987
|
+
});
|
|
988
|
+
if (semanticEdges.refined_by.length > 3) {
|
|
989
|
+
output += `\n ... and ${semanticEdges.refined_by.length - 3} more`;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Contradicts
|
|
994
|
+
if (semanticEdges.contradicts && semanticEdges.contradicts.length > 0) {
|
|
995
|
+
output += '\n\nā” Contradicts:';
|
|
996
|
+
semanticEdges.contradicts.forEach((e) => {
|
|
997
|
+
const preview = e.decision.substring(0, 60);
|
|
998
|
+
output += `\n ⢠${e.topic}: ${preview}${e.decision.length > 60 ? '...' : ''}`;
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Contradicted by
|
|
1003
|
+
if (semanticEdges.contradicted_by && semanticEdges.contradicted_by.length > 0) {
|
|
1004
|
+
output += '\n\nā Contradicted by:';
|
|
1005
|
+
semanticEdges.contradicted_by.forEach((e) => {
|
|
1006
|
+
const preview = e.decision.substring(0, 60);
|
|
1007
|
+
output += `\n ⢠${e.topic}: ${preview}${e.decision.length > 60 ? '...' : ''}`;
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
output += '\n\nš” Tip: Review individual decisions for full context';
|
|
1014
|
+
|
|
1015
|
+
return output;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Parse trust_context (might be JSON string)
|
|
1020
|
+
*
|
|
1021
|
+
* @param {Object|string} trustContext - Trust context (object or JSON string)
|
|
1022
|
+
* @returns {Object|null} Parsed trust context
|
|
1023
|
+
*/
|
|
1024
|
+
function parseTrustContext(trustContext) {
|
|
1025
|
+
if (!trustContext) {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Already parsed
|
|
1030
|
+
if (typeof trustContext === 'object') {
|
|
1031
|
+
return trustContext;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Parse JSON string
|
|
1035
|
+
if (typeof trustContext === 'string') {
|
|
1036
|
+
try {
|
|
1037
|
+
return JSON.parse(trustContext);
|
|
1038
|
+
} catch (e) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Format recent decisions list (all topics, chronological)
|
|
1048
|
+
*
|
|
1049
|
+
* Readable format for Claude - no raw JSON
|
|
1050
|
+
* Shows: time, type (user/assistant), topic, preview, confidence, status
|
|
1051
|
+
*
|
|
1052
|
+
* @param {Array<Object>} decisions - Recent decisions (sorted by created_at DESC)
|
|
1053
|
+
* @param {Object} options - Formatting options
|
|
1054
|
+
* @param {number} options.limit - Max decisions to show (default: 20)
|
|
1055
|
+
* @returns {string} Formatted list
|
|
1056
|
+
*/
|
|
1057
|
+
function formatList(decisions, options = {}) {
|
|
1058
|
+
const { limit = 20 } = options;
|
|
1059
|
+
|
|
1060
|
+
if (!decisions || decisions.length === 0) {
|
|
1061
|
+
return 'ā No decisions found';
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Limit results
|
|
1065
|
+
const items = decisions.slice(0, limit);
|
|
1066
|
+
|
|
1067
|
+
let output = `š Recent Decisions (Last ${items.length})\n`;
|
|
1068
|
+
output += 'ā'.repeat(60) + '\n';
|
|
1069
|
+
|
|
1070
|
+
for (let i = 0; i < items.length; i++) {
|
|
1071
|
+
const d = items[i];
|
|
1072
|
+
const timeAgo = calculateDuration(d.created_at);
|
|
1073
|
+
const type = d.user_involvement === 'approved' ? 'š¤ User' : 'š¤ Assistant';
|
|
1074
|
+
const status = d.outcome ? getOutcomeEmoji(d.outcome) + ' ' + d.outcome : 'ā³ Pending';
|
|
1075
|
+
const confidence = Math.round((d.confidence || 0) * 100);
|
|
1076
|
+
|
|
1077
|
+
// Preview (max 60 chars)
|
|
1078
|
+
const preview = d.decision.length > 60 ? d.decision.substring(0, 60) + '...' : d.decision;
|
|
1079
|
+
|
|
1080
|
+
output += `\n${i + 1}. [${timeAgo}] ${type}\n`;
|
|
1081
|
+
output += ` š ${d.topic}\n`;
|
|
1082
|
+
output += ` š” ${preview}\n`;
|
|
1083
|
+
output += ` š ${confidence}% confidence | ${status}\n`;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
output += '\n' + 'ā'.repeat(60);
|
|
1087
|
+
output += `\nš” Tip: Use mama.recall('topic') for full details\n`;
|
|
1088
|
+
|
|
1089
|
+
return output;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Export API
|
|
1093
|
+
module.exports = {
|
|
1094
|
+
formatContext,
|
|
1095
|
+
formatInstantAnswer,
|
|
1096
|
+
formatLegacyContext,
|
|
1097
|
+
formatTeaser,
|
|
1098
|
+
formatRecall,
|
|
1099
|
+
formatList,
|
|
1100
|
+
ensureTokenBudget,
|
|
1101
|
+
estimateTokens,
|
|
1102
|
+
extractQuickAnswer,
|
|
1103
|
+
extractCodeExample,
|
|
1104
|
+
formatTrustContext,
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
// CLI execution for testing
|
|
1108
|
+
if (require.main === module) {
|
|
1109
|
+
info('š§ MAMA Decision Formatter - Test\n');
|
|
1110
|
+
|
|
1111
|
+
// Task 6.6: Test token budget enforcement
|
|
1112
|
+
const mockDecisions = [
|
|
1113
|
+
{
|
|
1114
|
+
id: 'decision_mesh_structure_003',
|
|
1115
|
+
topic: 'mesh_structure',
|
|
1116
|
+
decision: 'MODERATE',
|
|
1117
|
+
reasoning: 'Balance between performance and completeness',
|
|
1118
|
+
confidence: 0.8,
|
|
1119
|
+
outcome: null,
|
|
1120
|
+
created_at: Date.now() - 5 * 24 * 60 * 60 * 1000, // 5 days ago
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
id: 'decision_mesh_structure_002',
|
|
1124
|
+
topic: 'mesh_structure',
|
|
1125
|
+
decision: 'SIMPLE',
|
|
1126
|
+
reasoning: 'Learned from 001 performance failure',
|
|
1127
|
+
confidence: 0.6,
|
|
1128
|
+
outcome: 'PARTIAL',
|
|
1129
|
+
limitation: 'Missing layer information',
|
|
1130
|
+
created_at: Date.now() - 10 * 24 * 60 * 60 * 1000, // 10 days ago
|
|
1131
|
+
updated_at: Date.now() - 5 * 24 * 60 * 60 * 1000,
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
id: 'decision_mesh_structure_001',
|
|
1135
|
+
topic: 'mesh_structure',
|
|
1136
|
+
decision: 'COMPLEX',
|
|
1137
|
+
reasoning: 'Initial choice for flexibility',
|
|
1138
|
+
confidence: 0.5,
|
|
1139
|
+
outcome: 'FAILED',
|
|
1140
|
+
failure_reason: 'Performance bottleneck at 10K+ meshes',
|
|
1141
|
+
created_at: Date.now() - 15 * 24 * 60 * 60 * 1000, // 15 days ago
|
|
1142
|
+
updated_at: Date.now() - 10 * 24 * 60 * 60 * 1000,
|
|
1143
|
+
},
|
|
1144
|
+
];
|
|
1145
|
+
|
|
1146
|
+
info('š Test 1: Format small history (3 decisions)...');
|
|
1147
|
+
const context1 = formatContext(mockDecisions.slice(0, 3), { maxTokens: 500 });
|
|
1148
|
+
info(context1);
|
|
1149
|
+
info(`\nTokens: ${estimateTokens(context1)}/500\n`);
|
|
1150
|
+
|
|
1151
|
+
info('āāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1152
|
+
info('š Test 2: Format large history (10+ decisions)...');
|
|
1153
|
+
|
|
1154
|
+
// Generate large history
|
|
1155
|
+
const largeHistory = [mockDecisions[0]];
|
|
1156
|
+
for (let i = 1; i <= 10; i++) {
|
|
1157
|
+
largeHistory.push({
|
|
1158
|
+
...mockDecisions[1],
|
|
1159
|
+
id: `decision_mesh_structure_${String(i).padStart(3, '0')}`,
|
|
1160
|
+
created_at: Date.now() - i * 5 * 24 * 60 * 60 * 1000,
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const context2 = formatContext(largeHistory, { maxTokens: 500 });
|
|
1165
|
+
info(context2);
|
|
1166
|
+
info(`\nTokens: ${estimateTokens(context2)}/500\n`);
|
|
1167
|
+
|
|
1168
|
+
info('āāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1169
|
+
info('š Test 3: Token budget enforcement (truncation)...');
|
|
1170
|
+
|
|
1171
|
+
// Create very long context
|
|
1172
|
+
const longDecisions = largeHistory.concat(largeHistory);
|
|
1173
|
+
const context3 = formatContext(longDecisions, { maxTokens: 300 });
|
|
1174
|
+
info(context3);
|
|
1175
|
+
info(`\nTokens: ${estimateTokens(context3)}/300 (enforced)\n`);
|
|
1176
|
+
|
|
1177
|
+
info('āāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1178
|
+
info('ā
Decision formatter tests complete');
|
|
1179
|
+
info('āāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
1180
|
+
}
|