@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,2210 @@
1
+ "use strict";
2
+ /**
3
+ * MAMA (Memory-Augmented MCP Architecture) - Simple Public API
4
+ *
5
+ * Clean wrapper around MAMA's internal functions
6
+ * Follows Claude-First Design: Simple, Transparent, Non-Intrusive
7
+ *
8
+ * Core Principle: MAMA = Librarian, Claude = Researcher
9
+ * - MAMA stores (organize books), retrieves (find books), indexes (catalog)
10
+ * - Claude decides what to save and how to use recalled decisions
11
+ *
12
+ * v1.3 Update: Collaborative Reasoning Graph
13
+ * - Auto-search on save: Find similar decisions before saving
14
+ * - Collaborative invitation: Suggest build-on/debate/synthesize
15
+ * - AX-first: Soft warnings, not hard blocks
16
+ *
17
+ * @module mama-api
18
+ * @version 1.3
19
+ * @date 2025-11-26
20
+ */
21
+ var __importDefault = (this && this.__importDefault) || function (mod) {
22
+ return (mod && mod.__esModule) ? mod : { "default": mod };
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.save = save;
26
+ exports.suggest = suggest;
27
+ exports.list = listDecisions;
28
+ exports.listCheckpoints = listCheckpoints;
29
+ exports.updateOutcome = updateOutcome;
30
+ exports.saveCheckpoint = saveCheckpoint;
31
+ exports.loadCheckpoint = loadCheckpoint;
32
+ exports.recall = recall;
33
+ exports.proposeLink = proposeLink;
34
+ exports.approveLink = approveLink;
35
+ exports.rejectLink = rejectLink;
36
+ exports.getPendingLinks = getPendingLinks;
37
+ exports.deprecateAutoLinks = deprecateAutoLinks;
38
+ exports.calculateCoverage = calculateCoverage;
39
+ exports.calculateQuality = calculateQuality;
40
+ exports.generateQualityReport = generateQualityReport;
41
+ exports.logRestartAttempt = logRestartAttempt;
42
+ exports.calculateRestartSuccessRate = calculateRestartSuccessRate;
43
+ exports.calculateRestartLatency = calculateRestartLatency;
44
+ exports.getRestartMetrics = getRestartMetrics;
45
+ exports.scanAutoLinks = scanAutoLinks;
46
+ exports.createLinkBackup = createLinkBackup;
47
+ exports.generatePreCleanupReport = generatePreCleanupReport;
48
+ exports.restoreLinkBackup = restoreLinkBackup;
49
+ exports.verifyBackupExists = verifyBackupExists;
50
+ exports.deleteAutoLinks = deleteAutoLinks;
51
+ exports.validateCleanupResult = validateCleanupResult;
52
+ // Node built-ins
53
+ const fs_1 = __importDefault(require("fs"));
54
+ const path_1 = __importDefault(require("path"));
55
+ const os_1 = __importDefault(require("os"));
56
+ const crypto_1 = __importDefault(require("crypto"));
57
+ // Internal modules
58
+ const decision_tracker_js_1 = require("./decision-tracker.js");
59
+ const memory_store_js_1 = require("./memory-store.js");
60
+ const decision_formatter_js_1 = require("./decision-formatter.js");
61
+ const progress_indicator_js_1 = require("./progress-indicator.js");
62
+ const embeddings_js_1 = require("./embeddings.js");
63
+ const ollama_client_js_1 = require("./ollama-client.js");
64
+ const debug_logger_js_1 = require("./debug-logger.js");
65
+ // Session-level warning cooldown cache (Story 1.1, 1.2)
66
+ // Prevents spam by tracking warned topics per session
67
+ const warnedTopicsCache = new Map();
68
+ const WARNING_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
69
+ /**
70
+ * Save a decision or insight to MAMA's memory
71
+ *
72
+ * Simple API for Claude to save insights without complex configuration
73
+ * AC #1: Simple API - no complex configuration required
74
+ *
75
+ * @param {Object} params - Decision parameters
76
+ * @param {string} params.topic - Decision topic (e.g., 'auth_strategy', 'date_format')
77
+ * @param {string} params.decision - The decision made (e.g., 'JWT', 'ISO 8601 + Unix')
78
+ * @param {string} params.reasoning - Why this decision was made
79
+ * @param {number} [params.confidence=0.5] - Confidence score 0.0-1.0 (optional)
80
+ * @param {string} [params.type='user_decision'] - 'user_decision' or 'assistant_insight' (optional)
81
+ * @param {string} [params.outcome='pending'] - 'pending', 'success', 'failure', 'partial', 'superseded' (optional)
82
+ * @param {string} [params.failure_reason] - Why this decision failed (optional, used with outcome='failure')
83
+ * @param {string} [params.limitation] - Known limitations of this decision (optional)
84
+ * @returns {Promise<{success: boolean, id: string, similar_decisions?: Array, warning?: string, collaboration_hint?: string, reasoning_graph?: Object}>} Save result with decision ID and metadata
85
+ *
86
+ * @example
87
+ * const decisionId = await mama.save({
88
+ * topic: 'date_calculation_format',
89
+ * decision: 'Support both ISO 8601 and Unix timestamp formats',
90
+ * reasoning: 'Bootstrap data stored as ISO 8601 causing NaN errors',
91
+ * confidence: 0.95,
92
+ * type: 'assistant_insight',
93
+ * outcome: 'success'
94
+ * });
95
+ */
96
+ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user_decision', outcome = 'pending', failure_reason = null, limitation = null, trust_context = null, }) {
97
+ // Validate required fields
98
+ if (!topic || typeof topic !== 'string') {
99
+ throw new Error('mama.save() requires topic (string)');
100
+ }
101
+ if (!decision || typeof decision !== 'string') {
102
+ throw new Error('mama.save() requires decision (string)');
103
+ }
104
+ if (!reasoning || typeof reasoning !== 'string') {
105
+ throw new Error('mama.save() requires reasoning (string)');
106
+ }
107
+ // Validate confidence range
108
+ if (typeof confidence !== 'number' || confidence < 0 || confidence > 1) {
109
+ throw new Error('mama.save() confidence must be a number between 0.0 and 1.0');
110
+ }
111
+ // Validate type
112
+ if (type !== 'user_decision' && type !== 'assistant_insight') {
113
+ throw new Error('mama.save() type must be "user_decision" or "assistant_insight"');
114
+ }
115
+ // Validate outcome
116
+ const validOutcomes = ['pending', 'success', 'failure', 'partial', 'superseded'];
117
+ if (outcome && !validOutcomes.includes(outcome)) {
118
+ throw new Error(`mama.save() outcome must be one of: ${validOutcomes.join(', ')} (got: ${outcome})`);
119
+ }
120
+ // Map type to user_involvement field
121
+ // Note: Current schema uses user_involvement ('requested', 'approved', 'rejected')
122
+ // Future: Will use decision_type column for proper distinction
123
+ const _userInvolvement = type === 'user_decision' ? 'approved' : null;
124
+ // Create detection object for learnDecision()
125
+ // Convert null to undefined for type compatibility
126
+ const detection = {
127
+ topic,
128
+ decision,
129
+ reasoning,
130
+ confidence,
131
+ trust_context: trust_context ?? undefined,
132
+ };
133
+ // Create tool execution context
134
+ // Use current timestamp and generate session ID
135
+ const sessionId = `mama_api_${Date.now()}`;
136
+ const toolExecution = {
137
+ tool_name: 'mama.save',
138
+ tool_input: { topic, decision },
139
+ exit_code: 0,
140
+ session_id: sessionId,
141
+ timestamp: Date.now(),
142
+ };
143
+ // Create session context
144
+ const sessionContext = {
145
+ session_id: sessionId,
146
+ latest_user_message: `Save ${type}: ${topic}`,
147
+ recent_exchange: `Claude: ${reasoning.substring(0, 100)}...`,
148
+ };
149
+ // Call internal learnDecision function
150
+ // Note: learnDecision returns { decisionId, notification }
151
+ (0, progress_indicator_js_1.logProgress)(`Saving decision: ${topic.substring(0, 30)}...`);
152
+ const { decisionId } = await (0, decision_tracker_js_1.learnDecision)(detection, toolExecution, sessionContext);
153
+ (0, progress_indicator_js_1.logComplete)(`Decision saved: ${decisionId.substring(0, 20)}...`);
154
+ // Update user_involvement, outcome, failure_reason, limitation
155
+ // Note: learnDecision always sets 'requested', we need to override it
156
+ const adapter = (0, memory_store_js_1.getAdapter)();
157
+ // Build UPDATE query dynamically based on what fields are provided
158
+ const updates = [];
159
+ const values = [];
160
+ // user_involvement based on type
161
+ if (type === 'assistant_insight') {
162
+ updates.push('user_involvement = NULL');
163
+ }
164
+ else if (type === 'user_decision') {
165
+ updates.push('user_involvement = ?');
166
+ values.push('approved');
167
+ }
168
+ // outcome (always set, default is 'pending')
169
+ // Story M4.1 fix: Map to DB format (uppercase, pending → NULL)
170
+ if (outcome) {
171
+ const outcomeMap = {
172
+ pending: null,
173
+ success: 'SUCCESS',
174
+ failure: 'FAILED',
175
+ partial: 'PARTIAL',
176
+ superseded: null,
177
+ };
178
+ const dbOutcome = outcomeMap[outcome] !== undefined ? outcomeMap[outcome] : outcome;
179
+ updates.push('outcome = ?');
180
+ values.push(dbOutcome);
181
+ }
182
+ // failure_reason (optional)
183
+ if (failure_reason) {
184
+ updates.push('failure_reason = ?');
185
+ values.push(failure_reason);
186
+ }
187
+ // limitation (optional)
188
+ if (limitation) {
189
+ updates.push('limitation = ?');
190
+ values.push(limitation);
191
+ }
192
+ // Execute UPDATE if we have any fields to update
193
+ if (updates.length > 0) {
194
+ values.push(decisionId); // WHERE id = ?
195
+ const stmt = adapter.prepare(`
196
+ UPDATE decisions
197
+ SET ${updates.join(', ')}
198
+ WHERE id = ?
199
+ `);
200
+ await stmt.run(...values);
201
+ }
202
+ // ════════════════════════════════════════════════════════════════════════════
203
+ // Story 1.1: Auto-Search on Save
204
+ // Story 1.2: Response Enhancement
205
+ // ════════════════════════════════════════════════════════════════════════════
206
+ let similar_decisions = [];
207
+ let warning = null;
208
+ let collaboration_hint = null;
209
+ let reasoning_graph = null;
210
+ // Only run auto-search for decisions (not checkpoints) with a topic
211
+ if (topic) {
212
+ try {
213
+ // Story 1.1: Auto-search using suggest()
214
+ (0, progress_indicator_js_1.logSearching)('Searching for related decisions...');
215
+ const searchResults = await suggest(topic, {
216
+ limit: 3,
217
+ threshold: 0.7,
218
+ disableRecency: true, // Pure semantic similarity for comparison
219
+ });
220
+ // Handle suggest() result which can be string | null | object
221
+ if (searchResults && typeof searchResults === 'object' && 'results' in searchResults) {
222
+ // Filter out the decision we just saved
223
+ similar_decisions = searchResults.results
224
+ .filter((d) => d.id !== decisionId)
225
+ .map((d) => ({
226
+ id: d.id,
227
+ topic: d.topic,
228
+ decision: d.decision,
229
+ similarity: d.similarity,
230
+ created_at: d.created_at,
231
+ }));
232
+ if (similar_decisions.length > 0) {
233
+ (0, progress_indicator_js_1.logComplete)(`Found ${similar_decisions.length} related decision(s)`);
234
+ }
235
+ // Story 1.2: Warning logic (similarity >= 0.85)
236
+ const highSimilarity = similar_decisions.find((d) => (d.similarity ?? 0) >= 0.85);
237
+ if (highSimilarity && !_isTopicInCooldown(topic)) {
238
+ warning = `High similarity (${((highSimilarity.similarity ?? 0) * 100).toFixed(0)}%) with existing decision "${highSimilarity.decision.substring(0, 50)}..."`;
239
+ _markTopicWarned(topic);
240
+ }
241
+ // Story 1.2: Collaboration hint
242
+ if (similar_decisions.length > 0) {
243
+ collaboration_hint = _generateCollaborationHint(similar_decisions);
244
+ }
245
+ }
246
+ }
247
+ catch (error) {
248
+ // Story 1.1 AC3: Best-effort - save succeeds even if auto-search fails
249
+ const errMsg = error instanceof Error ? error.message : String(error);
250
+ (0, debug_logger_js_1.error)('Auto-search failed:', errMsg);
251
+ }
252
+ // Story 1.2: Reasoning graph info
253
+ try {
254
+ reasoning_graph = await _getReasoningGraphInfo(topic, decisionId);
255
+ }
256
+ catch (error) {
257
+ const errMsg = error instanceof Error ? error.message : String(error);
258
+ (0, debug_logger_js_1.error)('Reasoning graph query failed:', errMsg);
259
+ }
260
+ // Story 2.2: Parse reasoning for relationship edges (builds_on, debates, synthesizes)
261
+ if (reasoning) {
262
+ try {
263
+ await (0, decision_tracker_js_1.createEdgesFromReasoning)(decisionId, reasoning);
264
+ }
265
+ catch (error) {
266
+ // Best-effort - save succeeds even if edge creation fails
267
+ const errMsg = error instanceof Error ? error.message : String(error);
268
+ (0, debug_logger_js_1.error)('Edge creation from reasoning failed:', errMsg);
269
+ }
270
+ }
271
+ }
272
+ // Story 1.2: Enhanced response (backward compatible)
273
+ return {
274
+ success: true,
275
+ id: decisionId,
276
+ ...(similar_decisions.length > 0 && { similar_decisions }),
277
+ ...(warning && { warning }),
278
+ ...(collaboration_hint && { collaboration_hint }),
279
+ ...(reasoning_graph && { reasoning_graph }),
280
+ };
281
+ }
282
+ // ════════════════════════════════════════════════════════════════════════════
283
+ // Story 1.2: Helper functions for Response Enhancement
284
+ // ════════════════════════════════════════════════════════════════════════════
285
+ /**
286
+ * Check if a topic is in warning cooldown
287
+ * @param {string} topic - Topic to check
288
+ * @returns {boolean} True if topic was warned recently
289
+ */
290
+ function _isTopicInCooldown(topic) {
291
+ const lastWarned = warnedTopicsCache.get(topic);
292
+ if (!lastWarned) {
293
+ return false;
294
+ }
295
+ return Date.now() - lastWarned < WARNING_COOLDOWN_MS;
296
+ }
297
+ /**
298
+ * Mark a topic as warned (start cooldown)
299
+ * @param {string} topic - Topic to mark
300
+ */
301
+ function _markTopicWarned(topic) {
302
+ warnedTopicsCache.set(topic, Date.now());
303
+ }
304
+ /**
305
+ * Generate collaboration hint message
306
+ * @param {Array} similarDecisions - Similar decisions found
307
+ * @returns {string} Collaboration hint message
308
+ */
309
+ function _generateCollaborationHint(similarDecisions) {
310
+ const count = similarDecisions.length;
311
+ if (count === 0) {
312
+ return null;
313
+ }
314
+ return `Found ${count} related decision(s). Consider:
315
+ - SUPERSEDE: Same topic replaces prior (automatic)
316
+ - BUILD-ON: Add "builds_on: <id>" in reasoning to extend
317
+ - DEBATE: Add "debates: <id>" in reasoning for alternative view
318
+ - SYNTHESIZE: Add "synthesizes: [id1, id2]" in reasoning to unify`;
319
+ }
320
+ /**
321
+ * Get reasoning graph info for a topic
322
+ * @param {string} topic - Topic to query
323
+ * @param {string} currentId - Current decision ID
324
+ * @returns {Object} Reasoning graph info
325
+ */
326
+ async function _getReasoningGraphInfo(topic, currentId) {
327
+ try {
328
+ const chain = await (0, memory_store_js_1.queryDecisionGraph)(topic);
329
+ if (!chain || chain.length === 0) {
330
+ return {
331
+ topic,
332
+ depth: 1,
333
+ latest: currentId,
334
+ };
335
+ }
336
+ return {
337
+ topic,
338
+ depth: chain.length,
339
+ latest: chain[0]?.id || currentId,
340
+ };
341
+ }
342
+ catch {
343
+ return {
344
+ topic,
345
+ depth: 1,
346
+ latest: currentId,
347
+ };
348
+ }
349
+ }
350
+ /**
351
+ * Recall decisions by topic
352
+ *
353
+ * DEFAULT: Returns JSON object with decisions and edges (LLM-first design)
354
+ * OPTIONAL: Returns Markdown string if format='markdown' (for human display)
355
+ *
356
+ * @param {string} topic - Decision topic to recall
357
+ * @param {Object} [options] - Options
358
+ * @param {string} [options.format='json'] - Output format: 'json' (default) or 'markdown'
359
+ * @returns {Promise<Object|string>} Decision history as JSON or Markdown
360
+ *
361
+ * @example
362
+ * // LLM usage (default)
363
+ * const data = await mama.recall('auth_strategy');
364
+ * // → { topic, decisions: [...], edges: [...], meta: {...} }
365
+ *
366
+ * // Human display
367
+ * const markdown = await mama.recall('auth_strategy', { format: 'markdown' });
368
+ * // → "📋 Decision History: auth_strategy\n━━━━━━━━..."
369
+ */
370
+ async function recall(topic, options = {}) {
371
+ if (!topic || typeof topic !== 'string') {
372
+ throw new Error('mama.recall() requires topic (string)');
373
+ }
374
+ const { format = 'json' } = options;
375
+ try {
376
+ const decisions = await (0, memory_store_js_1.queryDecisionGraph)(topic);
377
+ if (!decisions || decisions.length === 0) {
378
+ if (format === 'markdown') {
379
+ return `❌ No decisions found for topic: ${topic}`;
380
+ }
381
+ return {
382
+ topic,
383
+ supersedes_chain: [],
384
+ semantic_edges: { refines: [], refined_by: [], contradicts: [], contradicted_by: [] },
385
+ meta: { count: 0 },
386
+ };
387
+ }
388
+ // Query semantic edges for all decisions
389
+ const decisionIds = decisions.map((d) => d.id);
390
+ const rawEdgesResult = await (0, memory_store_js_1.querySemanticEdges)(decisionIds);
391
+ const rawEdges = (rawEdgesResult || {});
392
+ const semanticEdges = {
393
+ refines: (rawEdges.refines || []),
394
+ refined_by: (rawEdges.refined_by || []),
395
+ contradicts: (rawEdges.contradicts || []),
396
+ contradicted_by: (rawEdges.contradicted_by || []),
397
+ };
398
+ // Markdown format (for human display)
399
+ if (format === 'markdown') {
400
+ // Pass semantic edges to formatter - transform to expected format
401
+ const formatterEdges = {
402
+ refines: semanticEdges.refines.map((e) => ({
403
+ topic: e.topic || '',
404
+ decision: e.decision || '',
405
+ })),
406
+ refined_by: semanticEdges.refined_by.map((e) => ({
407
+ topic: e.topic || '',
408
+ decision: e.decision || '',
409
+ })),
410
+ contradicts: semanticEdges.contradicts.map((e) => ({
411
+ topic: e.topic || '',
412
+ decision: e.decision || '',
413
+ })),
414
+ contradicted_by: semanticEdges.contradicted_by.map((e) => ({
415
+ topic: e.topic || '',
416
+ decision: e.decision || '',
417
+ })),
418
+ };
419
+ return (0, decision_formatter_js_1.formatRecall)(decisions, formatterEdges);
420
+ }
421
+ // JSON format (default - LLM-first)
422
+ // Separate supersedes chain from semantic edges
423
+ return {
424
+ topic,
425
+ supersedes_chain: decisions.map((d) => ({
426
+ id: d.id,
427
+ decision: d.decision,
428
+ reasoning: d.reasoning,
429
+ confidence: d.confidence,
430
+ outcome: d.outcome,
431
+ failure_reason: d.failure_reason,
432
+ created_at: d.created_at,
433
+ updated_at: d.updated_at,
434
+ superseded_by: d.superseded_by,
435
+ supersedes: d.supersedes,
436
+ })),
437
+ semantic_edges: {
438
+ refines: semanticEdges.refines.map((e) => ({
439
+ to_topic: e.topic,
440
+ to_decision: e.decision,
441
+ to_id: e.to_id,
442
+ reason: e.reason,
443
+ confidence: e.confidence,
444
+ created_at: e.created_at,
445
+ })),
446
+ refined_by: semanticEdges.refined_by.map((e) => ({
447
+ from_topic: e.topic,
448
+ from_decision: e.decision,
449
+ from_id: e.from_id,
450
+ reason: e.reason,
451
+ confidence: e.confidence,
452
+ created_at: e.created_at,
453
+ })),
454
+ contradicts: semanticEdges.contradicts.map((e) => ({
455
+ to_topic: e.topic,
456
+ to_decision: e.decision,
457
+ to_id: e.to_id,
458
+ reason: e.reason,
459
+ created_at: e.created_at,
460
+ })),
461
+ contradicted_by: semanticEdges.contradicted_by.map((e) => ({
462
+ from_topic: e.topic,
463
+ from_decision: e.decision,
464
+ from_id: e.from_id,
465
+ reason: e.reason,
466
+ created_at: e.created_at,
467
+ })),
468
+ },
469
+ meta: {
470
+ count: decisions.length,
471
+ latest_id: decisions[0]?.id,
472
+ has_supersedes_chain: decisions.some((d) => d.supersedes),
473
+ has_semantic_edges: semanticEdges.refines.length > 0 ||
474
+ semanticEdges.refined_by.length > 0 ||
475
+ semanticEdges.contradicts.length > 0 ||
476
+ semanticEdges.contradicted_by.length > 0,
477
+ semantic_edges_count: {
478
+ refines: semanticEdges.refines.length,
479
+ refined_by: semanticEdges.refined_by.length,
480
+ contradicts: semanticEdges.contradicts.length,
481
+ contradicted_by: semanticEdges.contradicted_by.length,
482
+ },
483
+ },
484
+ };
485
+ }
486
+ catch (error) {
487
+ throw new Error(`mama.recall() failed: ${error instanceof Error ? error.message : String(error)}`);
488
+ }
489
+ }
490
+ async function updateOutcome(decisionId, { outcome, failure_reason, limitation }) {
491
+ if (!decisionId || typeof decisionId !== 'string') {
492
+ throw new Error('mama.updateOutcome() requires decisionId (string)');
493
+ }
494
+ // AX Improvement: Be forgiving with case sensitivity
495
+ const normalizedOutcome = outcome ? outcome.toUpperCase() : null;
496
+ if (!normalizedOutcome || !['SUCCESS', 'FAILED', 'PARTIAL'].includes(normalizedOutcome)) {
497
+ throw new Error('mama.updateOutcome() outcome must be "SUCCESS", "FAILED", or "PARTIAL"');
498
+ }
499
+ try {
500
+ const adapter = (0, memory_store_js_1.getAdapter)();
501
+ // Update outcome and related fields
502
+ const stmt = adapter.prepare(`
503
+ UPDATE decisions
504
+ SET
505
+ outcome = ?,
506
+ failure_reason = ?,
507
+ limitation = ?,
508
+ updated_at = ?
509
+ WHERE id = ?
510
+ `);
511
+ const result = stmt.run(normalizedOutcome, failure_reason || null, limitation || null, Date.now(), decisionId);
512
+ // Check if decision was found and updated
513
+ if (result.changes === 0) {
514
+ throw new Error(`Decision not found: ${decisionId}`);
515
+ }
516
+ return;
517
+ }
518
+ catch (error) {
519
+ throw new Error(`mama.updateOutcome() failed: ${error instanceof Error ? error.message : String(error)}`);
520
+ }
521
+ }
522
+ async function expandWithGraph(candidates) {
523
+ const graphEnhanced = new Map(); // Use Map for deduplication by ID
524
+ const primaryIds = new Set(candidates.map((c) => c.id)); // Track primary candidates
525
+ // Process each candidate
526
+ for (const candidate of candidates) {
527
+ // Add primary candidate with higher rank
528
+ if (!graphEnhanced.has(candidate.id)) {
529
+ graphEnhanced.set(candidate.id, {
530
+ ...candidate,
531
+ graph_source: 'primary', // Mark as primary result
532
+ graph_rank: 1.0, // Highest rank
533
+ });
534
+ }
535
+ // 1. Add supersedes chain (evolution history)
536
+ try {
537
+ const chain = await (0, memory_store_js_1.queryDecisionGraph)(candidate.topic);
538
+ for (const decision of chain) {
539
+ if (!graphEnhanced.has(decision.id)) {
540
+ graphEnhanced.set(decision.id, {
541
+ ...decision,
542
+ graph_source: 'supersedes_chain',
543
+ graph_rank: 0.8, // Lower rank than primary
544
+ similarity: (candidate.similarity ?? 0) * 0.9, // Inherit similarity, slightly reduced
545
+ related_to: candidate.id, // Track relationship
546
+ });
547
+ }
548
+ }
549
+ }
550
+ catch (error) {
551
+ (0, debug_logger_js_1.warn)(`Failed to get supersedes chain for ${candidate.topic}: ${error instanceof Error ? error.message : String(error)}`);
552
+ }
553
+ // 2. Add semantic edges (refines, contradicts, builds_on, debates, synthesizes)
554
+ try {
555
+ const rawEdges = (await (0, memory_store_js_1.querySemanticEdges)([candidate.id])) || {};
556
+ const edges = {
557
+ refines: rawEdges.refines || [],
558
+ refined_by: rawEdges.refined_by || [],
559
+ contradicts: rawEdges.contradicts || [],
560
+ contradicted_by: rawEdges.contradicted_by || [],
561
+ builds_on: rawEdges.builds_on || [],
562
+ built_on_by: rawEdges.built_on_by || [],
563
+ debates: rawEdges.debates || [],
564
+ debated_by: rawEdges.debated_by || [],
565
+ synthesizes: rawEdges.synthesizes || [],
566
+ synthesized_by: rawEdges.synthesized_by || [],
567
+ };
568
+ // Helper to add edge to graph
569
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic property access with idField
570
+ const addEdge = (edge, idField, source, rank, simFactor) => {
571
+ const id = edge[idField];
572
+ if (!graphEnhanced.has(id)) {
573
+ graphEnhanced.set(id, {
574
+ id: id,
575
+ topic: edge.topic,
576
+ decision: edge.decision,
577
+ confidence: edge.confidence,
578
+ created_at: edge.created_at,
579
+ graph_source: source,
580
+ graph_rank: rank,
581
+ similarity: (candidate.similarity ?? 0) * simFactor,
582
+ related_to: candidate.id,
583
+ edge_reason: edge.reason,
584
+ });
585
+ }
586
+ };
587
+ // Add refines edges
588
+ for (const edge of edges.refines) {
589
+ addEdge(edge, 'to_id', 'refines', 0.7, 0.85);
590
+ }
591
+ // Add refined_by edges
592
+ for (const edge of edges.refined_by) {
593
+ addEdge(edge, 'from_id', 'refined_by', 0.7, 0.85);
594
+ }
595
+ // Add contradicts edges (lower rank, but still relevant)
596
+ for (const edge of edges.contradicts) {
597
+ addEdge(edge, 'to_id', 'contradicts', 0.6, 0.8);
598
+ }
599
+ // Story 2.1: Add builds_on edges (high relevance - extending prior work)
600
+ for (const edge of edges.builds_on) {
601
+ addEdge(edge, 'to_id', 'builds_on', 0.75, 0.9);
602
+ }
603
+ // Add built_on_by edges (someone built on this decision)
604
+ for (const edge of edges.built_on_by) {
605
+ addEdge(edge, 'from_id', 'built_on_by', 0.75, 0.9);
606
+ }
607
+ // Add debates edges (alternative view)
608
+ for (const edge of edges.debates) {
609
+ addEdge(edge, 'to_id', 'debates', 0.65, 0.85);
610
+ }
611
+ // Add debated_by edges
612
+ for (const edge of edges.debated_by) {
613
+ addEdge(edge, 'from_id', 'debated_by', 0.65, 0.85);
614
+ }
615
+ // Add synthesizes edges (unified approach)
616
+ for (const edge of edges.synthesizes) {
617
+ addEdge(edge, 'to_id', 'synthesizes', 0.7, 0.88);
618
+ }
619
+ // Add synthesized_by edges
620
+ for (const edge of edges.synthesized_by) {
621
+ addEdge(edge, 'from_id', 'synthesized_by', 0.7, 0.88);
622
+ }
623
+ }
624
+ catch (error) {
625
+ (0, debug_logger_js_1.warn)(`Failed to get semantic edges for ${candidate.id}: ${error instanceof Error ? error.message : String(error)}`);
626
+ }
627
+ }
628
+ // 3. Convert Map to Array
629
+ const allResults = Array.from(graphEnhanced.values());
630
+ // 4. Sort: Interleave expanded results after their related primary
631
+ // This ensures edge-connected decisions appear near their source
632
+ const primaryResults = allResults
633
+ .filter((r) => primaryIds.has(r.id))
634
+ .sort((a, b) => {
635
+ const scoreA = a.final_score || a.similarity || 0;
636
+ const scoreB = b.final_score || b.similarity || 0;
637
+ return scoreB - scoreA;
638
+ });
639
+ const expandedResults = allResults.filter((r) => !primaryIds.has(r.id));
640
+ // Build final results: each primary followed by its related expanded results
641
+ const results = [];
642
+ for (const primary of primaryResults) {
643
+ results.push(primary);
644
+ // Find expanded results related to this primary
645
+ const relatedExpanded = expandedResults.filter((e) => e.related_to === primary.id);
646
+ // Sort related by graph_rank (higher first)
647
+ relatedExpanded.sort((a, b) => (b.graph_rank || 0) - (a.graph_rank || 0));
648
+ // Add related expanded results right after their primary
649
+ results.push(...relatedExpanded);
650
+ }
651
+ // Add any orphaned expanded results (shouldn't happen, but safety net)
652
+ const includedIds = new Set(results.map((r) => r.id));
653
+ const orphaned = expandedResults.filter((e) => !includedIds.has(e.id));
654
+ results.push(...orphaned);
655
+ return results;
656
+ }
657
+ function applyRecencyBoost(results, options = {}) {
658
+ const { recencyWeight = 0.3, recencyScale = 7, recencyDecay = 0.5, disableRecency = false, } = options;
659
+ if (disableRecency || recencyWeight === 0) {
660
+ return results;
661
+ }
662
+ const now = Date.now(); // Current timestamp in milliseconds
663
+ return results
664
+ .map((r) => {
665
+ // created_at is stored in milliseconds in the database
666
+ const createdAt = typeof r.created_at === 'number' ? r.created_at : Date.parse(r.created_at || '0');
667
+ const ageInDays = (now - createdAt) / (86400 * 1000);
668
+ // Gaussian Decay: exp(-((age / scale)^2) / (2 * ln(1 / decay)))
669
+ // At scale days: score = decay (e.g., 7 days = 50%)
670
+ const gaussianDecay = Math.exp(-Math.pow(ageInDays / recencyScale, 2) / (2 * Math.log(1 / recencyDecay)));
671
+ // Combine semantic similarity with recency
672
+ const similarity = r.similarity ?? 0;
673
+ const finalScore = similarity * (1 - recencyWeight) + gaussianDecay * recencyWeight;
674
+ return {
675
+ ...r,
676
+ recency_score: gaussianDecay,
677
+ recency_age_days: Math.round(ageInDays * 10) / 10,
678
+ final_score: finalScore,
679
+ };
680
+ })
681
+ .sort((a, b) => (b.final_score ?? 0) - (a.final_score ?? 0));
682
+ }
683
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
684
+ async function suggest(userQuestion, options = {}) {
685
+ if (!userQuestion || typeof userQuestion !== 'string') {
686
+ throw new Error('mama.suggest() requires userQuestion (string)');
687
+ }
688
+ const { format = 'json', limit = 5, threshold, useReranking = false,
689
+ // Recency boosting parameters (Gaussian Decay - Elasticsearch style)
690
+ recencyWeight = 0.3, // 0-1: How much to weight recency (0.3 = 70% semantic, 30% recency)
691
+ recencyScale = 7, // Days until recency score drops to 50%
692
+ recencyDecay = 0.5, // Score at scale point (0.5 = 50%)
693
+ disableRecency = false, // Set true to disable recency boosting entirely
694
+ } = options;
695
+ try {
696
+ // 1. Try vector search first (if sqlite-vss is available)
697
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-explicit-any
698
+ let results = [];
699
+ let searchMethod = 'vector';
700
+ try {
701
+ // Check if vectorSearch prepared statement exists
702
+ (0, memory_store_js_1.getPreparedStmt)('vectorSearch');
703
+ // Generate query embedding
704
+ const queryEmbedding = await (0, embeddings_js_1.generateEmbedding)(userQuestion);
705
+ // Adaptive threshold (shorter queries need higher confidence)
706
+ const wordCount = userQuestion.split(/\s+/).length;
707
+ const adaptiveThreshold = threshold !== undefined ? threshold : wordCount < 3 ? 0.7 : 0.6;
708
+ // Vector search
709
+ results = await (0, memory_store_js_1.vectorSearch)(queryEmbedding, limit * 2, 0.5); // Get more candidates
710
+ // Filter by adaptive threshold
711
+ results = results.filter((r) => r.similarity >= adaptiveThreshold);
712
+ // Stage 1.5: Apply recency boosting (Gaussian Decay)
713
+ // Allows Claude to adjust search strategy (recent vs historical)
714
+ if (results.length > 0 && !disableRecency) {
715
+ results = applyRecencyBoost(results, {
716
+ recencyWeight,
717
+ recencyScale,
718
+ recencyDecay,
719
+ disableRecency,
720
+ });
721
+ searchMethod = 'vector+recency';
722
+ }
723
+ // Stage 2: Graph expansion (NEW - Phase 1)
724
+ // Expand candidates with supersedes chain and semantic edges
725
+ if (results.length > 0) {
726
+ const graphEnhanced = await expandWithGraph(results);
727
+ results = graphEnhanced;
728
+ searchMethod = disableRecency ? 'vector+graph' : 'vector+recency+graph';
729
+ }
730
+ }
731
+ catch (vectorError) {
732
+ // Fallback to keyword search if vector search unavailable
733
+ (0, debug_logger_js_1.warn)(`Vector search failed: ${vectorError instanceof Error ? vectorError.message : String(vectorError)}, falling back to keyword search`);
734
+ searchMethod = 'keyword';
735
+ // Keyword search fallback
736
+ const adapter = (0, memory_store_js_1.getAdapter)();
737
+ const keywords = userQuestion
738
+ .toLowerCase()
739
+ .split(/\s+/)
740
+ .filter((w) => w.length > 2); // Filter short words
741
+ if (keywords.length === 0) {
742
+ if (format === 'markdown') {
743
+ return `💡 Hint: Please be more specific.\nExample: "Railway Volume settings" or "mesh parameter optimization"`;
744
+ }
745
+ return null; // JSON mode returns null for empty/invalid queries
746
+ }
747
+ // Build LIKE query for each keyword
748
+ const likeConditions = keywords.map(() => '(topic LIKE ? OR decision LIKE ?)').join(' OR ');
749
+ const likeParams = keywords.flatMap((k) => [`%${k}%`, `%${k}%`]);
750
+ const stmt = adapter.prepare(`
751
+ SELECT * FROM decisions
752
+ WHERE ${likeConditions}
753
+ AND superseded_by IS NULL
754
+ ORDER BY created_at DESC
755
+ LIMIT ?
756
+ `);
757
+ const rows = (await stmt.all(...likeParams, limit));
758
+ results = rows.map((row) => ({
759
+ ...row,
760
+ similarity: 0.75, // Assign moderate similarity for keyword matches
761
+ }));
762
+ // Stage 2: Graph expansion for keyword results (Phase 1)
763
+ if (results.length > 0) {
764
+ const graphEnhanced = await expandWithGraph(results);
765
+ results = graphEnhanced;
766
+ searchMethod = 'keyword+graph';
767
+ }
768
+ }
769
+ if (results.length === 0) {
770
+ if (format === 'markdown') {
771
+ const wordCount = userQuestion.split(/\s+/).length;
772
+ if (wordCount < 3) {
773
+ return `💡 Hint: Please be more specific.\nExample: "Why did we choose COMPLEX mesh structure?" or "What parameters are used for large layers?"`;
774
+ }
775
+ }
776
+ return null;
777
+ }
778
+ // 5. Optional: LLM re-ranking (only if requested)
779
+ if (useReranking) {
780
+ results = await rerankWithLLM(userQuestion, results);
781
+ }
782
+ // Slice to limit
783
+ const finalResults = results.slice(0, limit);
784
+ // Markdown format (for human display)
785
+ if (format === 'markdown') {
786
+ const context = (0, decision_formatter_js_1.formatContext)(finalResults, { maxTokens: 500 });
787
+ // Add graph expansion summary if applicable
788
+ let graphSummary = '';
789
+ if (searchMethod.includes('graph')) {
790
+ const primaryCount = finalResults.filter((r) => r.graph_source === 'primary').length;
791
+ const expandedCount = finalResults.filter((r) => r.graph_source !== 'primary').length;
792
+ graphSummary = `\n📊 Graph expansion: ${primaryCount} primary + ${expandedCount} related (supersedes/refines/contradicts)\n`;
793
+ }
794
+ return `🔍 Search method: ${searchMethod}${graphSummary}\n${context}`;
795
+ }
796
+ // Calculate graph expansion stats
797
+ const graphStats = {
798
+ total_results: finalResults.length,
799
+ primary_count: finalResults.filter((r) => r.graph_source === 'primary').length,
800
+ expanded_count: finalResults.filter((r) => r.graph_source !== 'primary').length,
801
+ sources: {
802
+ primary: finalResults.filter((r) => r.graph_source === 'primary').length,
803
+ supersedes_chain: finalResults.filter((r) => r.graph_source === 'supersedes_chain').length,
804
+ refines: finalResults.filter((r) => r.graph_source === 'refines').length,
805
+ refined_by: finalResults.filter((r) => r.graph_source === 'refined_by').length,
806
+ contradicts: finalResults.filter((r) => r.graph_source === 'contradicts').length,
807
+ },
808
+ };
809
+ // JSON format (default - LLM-first)
810
+ return {
811
+ query: userQuestion,
812
+ results: finalResults.map((r) => ({
813
+ id: r.id,
814
+ topic: r.topic,
815
+ decision: r.decision,
816
+ reasoning: r.reasoning,
817
+ confidence: r.confidence,
818
+ similarity: r.similarity,
819
+ created_at: r.created_at,
820
+ // Recency metadata (NEW - Gaussian Decay)
821
+ recency_score: r.recency_score,
822
+ recency_age_days: r.recency_age_days,
823
+ final_score: r.final_score || r.similarity, // Falls back to similarity if no recency
824
+ // Graph metadata (NEW - Phase 1)
825
+ graph_source: r.graph_source || 'primary',
826
+ graph_rank: r.graph_rank || 1.0,
827
+ related_to: r.related_to || null,
828
+ edge_reason: r.edge_reason || null,
829
+ })),
830
+ meta: {
831
+ count: finalResults.length,
832
+ search_method: searchMethod,
833
+ threshold: threshold || 'adaptive',
834
+ // Recency boosting config (NEW - Gaussian Decay)
835
+ recency_boost: disableRecency
836
+ ? null
837
+ : {
838
+ weight: recencyWeight,
839
+ scale: recencyScale,
840
+ decay: recencyDecay,
841
+ },
842
+ // Graph expansion stats (NEW - Phase 1)
843
+ graph_expansion: searchMethod.includes('graph') ? graphStats : null,
844
+ },
845
+ };
846
+ }
847
+ catch (error) {
848
+ // Graceful degradation
849
+ (0, debug_logger_js_1.warn)(`mama.suggest() failed: ${error instanceof Error ? error.message : String(error)}`);
850
+ return null;
851
+ }
852
+ }
853
+ /**
854
+ * Re-rank search results using local LLM (optional enhancement)
855
+ *
856
+ * @param {string} userQuestion - User's question
857
+ * @param {Array} results - Vector search results
858
+ * @returns {Promise<Array>} Re-ranked results
859
+ */
860
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
861
+ async function rerankWithLLM(userQuestion, results) {
862
+ try {
863
+ const prompt = `User asked: "${userQuestion}"
864
+
865
+ Found decisions (ranked by vector similarity):
866
+ ${results.map((r, i) => `${i + 1}. [${(r.similarity ?? 0).toFixed(3)}] ${r.topic}: ${r.decision.substring(0, 60)}...`).join('\n')}
867
+
868
+ Re-rank these by actual relevance to the user's intent (not just keyword similarity).
869
+ Return JSON: { "ranking": [index1, index2, ...] } (0-based indices)
870
+
871
+ Example: { "ranking": [2, 0, 4, 1, 3] } means 3rd is most relevant, then 1st, then 5th...`;
872
+ const response = await (0, ollama_client_js_1.generate)(prompt, {
873
+ format: 'json',
874
+ temperature: 0.3,
875
+ max_tokens: 100,
876
+ timeout: 3000,
877
+ });
878
+ const parsed = typeof response === 'string' ? JSON.parse(response) : response;
879
+ // Reorder results based on LLM ranking
880
+ return parsed.ranking.map((idx) => results[idx]).filter(Boolean);
881
+ }
882
+ catch (error) {
883
+ (0, debug_logger_js_1.warn)(`Re-ranking failed: ${error instanceof Error ? error.message : String(error)}, using vector ranking`);
884
+ return results; // Fallback to vector ranking
885
+ }
886
+ }
887
+ async function listDecisions(options = {}) {
888
+ const { limit = 10, format = 'json' } = options;
889
+ try {
890
+ const adapter = (0, memory_store_js_1.getAdapter)();
891
+ const stmt = adapter.prepare(`
892
+ SELECT * FROM decisions
893
+ WHERE superseded_by IS NULL
894
+ ORDER BY created_at DESC
895
+ LIMIT ?
896
+ `);
897
+ const decisions = await stmt.all(limit);
898
+ if (format === 'markdown') {
899
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
900
+ return (0, decision_formatter_js_1.formatList)(decisions);
901
+ }
902
+ return decisions;
903
+ }
904
+ catch (error) {
905
+ throw new Error(`mama.listDecisions() failed: ${error instanceof Error ? error.message : String(error)}`);
906
+ }
907
+ }
908
+ /**
909
+ * Save current session checkpoint (New Feature: Session Continuity)
910
+ *
911
+ * @param {string} summary - Summary of current session state
912
+ * @param {Array<string>} openFiles - List of currently open files
913
+ * @param {string} nextSteps - Next steps to be taken
914
+ * @returns {Promise<number>} Checkpoint ID
915
+ */
916
+ async function saveCheckpoint(summary, openFiles = [], nextSteps = '',
917
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
918
+ recentConversation = []) {
919
+ if (!summary) {
920
+ throw new Error('Summary is required for checkpoint');
921
+ }
922
+ try {
923
+ const adapter = (0, memory_store_js_1.getAdapter)();
924
+ const stmt = adapter.prepare(`
925
+ INSERT INTO checkpoints (timestamp, summary, open_files, next_steps, recent_conversation, status)
926
+ VALUES (?, ?, ?, ?, ?, 'active')
927
+ `);
928
+ const result = stmt.run(Date.now(), summary, JSON.stringify(openFiles), nextSteps, JSON.stringify(recentConversation || []));
929
+ return result.lastInsertRowid;
930
+ }
931
+ catch (error) {
932
+ throw new Error(`Failed to save checkpoint: ${error instanceof Error ? error.message : String(error)}`);
933
+ }
934
+ }
935
+ async function loadCheckpoint() {
936
+ try {
937
+ const adapter = (0, memory_store_js_1.getAdapter)();
938
+ const stmt = adapter.prepare(`
939
+ SELECT * FROM checkpoints
940
+ WHERE status = 'active'
941
+ ORDER BY timestamp DESC
942
+ LIMIT 1
943
+ `);
944
+ const checkpoint = stmt.get();
945
+ if (checkpoint) {
946
+ try {
947
+ checkpoint.open_files =
948
+ typeof checkpoint.open_files === 'string'
949
+ ? JSON.parse(checkpoint.open_files)
950
+ : checkpoint.open_files || [];
951
+ }
952
+ catch {
953
+ checkpoint.open_files = [];
954
+ }
955
+ try {
956
+ checkpoint.recent_conversation =
957
+ typeof checkpoint.recent_conversation === 'string'
958
+ ? JSON.parse(checkpoint.recent_conversation || '[]')
959
+ : checkpoint.recent_conversation || [];
960
+ }
961
+ catch {
962
+ checkpoint.recent_conversation = [];
963
+ }
964
+ }
965
+ return checkpoint || null;
966
+ }
967
+ catch (error) {
968
+ throw new Error(`Failed to load checkpoint: ${error instanceof Error ? error.message : String(error)}`);
969
+ }
970
+ }
971
+ /**
972
+ * List recent checkpoints (New Feature: Session Continuity)
973
+ *
974
+ * @param {number} limit - Max number of checkpoints to return
975
+ * @returns {Promise<Array>} Recent checkpoints
976
+ */
977
+ async function listCheckpoints(limit = 10) {
978
+ try {
979
+ const adapter = (0, memory_store_js_1.getAdapter)();
980
+ const stmt = adapter.prepare(`
981
+ SELECT * FROM checkpoints
982
+ ORDER BY timestamp DESC
983
+ LIMIT ?
984
+ `);
985
+ const checkpoints = stmt.all(limit);
986
+ return checkpoints.map((c) => {
987
+ try {
988
+ c.open_files =
989
+ typeof c.open_files === 'string' ? JSON.parse(c.open_files) : c.open_files || [];
990
+ }
991
+ catch {
992
+ c.open_files = [];
993
+ }
994
+ try {
995
+ c.recent_conversation =
996
+ typeof c.recent_conversation === 'string'
997
+ ? JSON.parse(c.recent_conversation)
998
+ : c.recent_conversation || [];
999
+ }
1000
+ catch {
1001
+ c.recent_conversation = [];
1002
+ }
1003
+ return c;
1004
+ });
1005
+ }
1006
+ catch (error) {
1007
+ throw new Error(`Failed to list checkpoints: ${error instanceof Error ? error.message : String(error)}`);
1008
+ }
1009
+ }
1010
+ async function proposeLink({ from_id, to_id, relationship, reason, decision_id, evidence, }) {
1011
+ if (!from_id || !to_id || !relationship || !reason) {
1012
+ throw new Error('proposeLink() requires from_id, to_id, relationship, and reason');
1013
+ }
1014
+ if (!['refines', 'contradicts'].includes(relationship)) {
1015
+ throw new Error('proposeLink() relationship must be "refines" or "contradicts"');
1016
+ }
1017
+ try {
1018
+ const adapter = (0, memory_store_js_1.getAdapter)();
1019
+ // Use transaction to ensure atomicity (link + audit log)
1020
+ adapter.transaction(() => {
1021
+ // Insert link with pending approval
1022
+ const stmt = adapter.prepare(`
1023
+ INSERT INTO decision_edges
1024
+ (from_id, to_id, relationship, reason, created_by, approved_by_user, decision_id, evidence, created_at)
1025
+ VALUES (?, ?, ?, ?, 'llm', 0, ?, ?, ?)
1026
+ `);
1027
+ stmt.run(from_id, to_id, relationship, reason, decision_id || null, evidence || null, Date.now());
1028
+ // Log to audit trail
1029
+ const auditStmt = adapter.prepare(`
1030
+ INSERT INTO link_audit_log (from_id, to_id, relationship, action, actor, reason, created_at)
1031
+ VALUES (?, ?, ?, 'proposed', 'llm', ?, ?)
1032
+ `);
1033
+ auditStmt.run(from_id, to_id, relationship, reason, Date.now());
1034
+ });
1035
+ return;
1036
+ }
1037
+ catch (error) {
1038
+ throw new Error(`proposeLink() failed: ${error instanceof Error ? error.message : String(error)}`);
1039
+ }
1040
+ }
1041
+ /**
1042
+ * Approve a proposed link (Epic 3 - Story 3.1)
1043
+ *
1044
+ * User approves a pending link, making it active.
1045
+ *
1046
+ * @param {string} from_id - Source decision ID
1047
+ * @param {string} to_id - Target decision ID
1048
+ * @param {string} relationship - Link relationship type
1049
+ * @returns {Promise<void>}
1050
+ */
1051
+ async function approveLink(from_id, to_id, relationship) {
1052
+ if (!from_id || !to_id || !relationship) {
1053
+ throw new Error('approveLink() requires from_id, to_id, and relationship');
1054
+ }
1055
+ try {
1056
+ const adapter = (0, memory_store_js_1.getAdapter)();
1057
+ // Update link to approved with timestamp
1058
+ const stmt = adapter.prepare(`
1059
+ UPDATE decision_edges
1060
+ SET approved_by_user = 1, approved_at = ?
1061
+ WHERE from_id = ? AND to_id = ? AND relationship = ?
1062
+ `);
1063
+ stmt.run(Date.now(), from_id, to_id, relationship);
1064
+ // Log approval
1065
+ const auditStmt = adapter.prepare(`
1066
+ INSERT INTO link_audit_log (from_id, to_id, relationship, action, actor, created_at)
1067
+ VALUES (?, ?, ?, 'approved', 'user', ?)
1068
+ `);
1069
+ auditStmt.run(from_id, to_id, relationship, Date.now());
1070
+ return;
1071
+ }
1072
+ catch (error) {
1073
+ throw new Error(`approveLink() failed: ${error instanceof Error ? error.message : String(error)}`);
1074
+ }
1075
+ }
1076
+ /**
1077
+ * Reject a proposed link (Epic 3 - Story 3.1)
1078
+ *
1079
+ * User rejects a pending link, removing it from the database.
1080
+ *
1081
+ * @param {string} from_id - Source decision ID
1082
+ * @param {string} to_id - Target decision ID
1083
+ * @param {string} relationship - Link relationship type
1084
+ * @param {string} [reason] - Optional reason for rejection
1085
+ * @returns {Promise<void>}
1086
+ */
1087
+ async function rejectLink(from_id, to_id, relationship, reason) {
1088
+ if (!from_id || !to_id || !relationship) {
1089
+ throw new Error('rejectLink() requires from_id, to_id, and relationship');
1090
+ }
1091
+ try {
1092
+ const adapter = (0, memory_store_js_1.getAdapter)();
1093
+ // Log rejection before deletion
1094
+ const auditStmt = adapter.prepare(`
1095
+ INSERT INTO link_audit_log (from_id, to_id, relationship, action, actor, reason, created_at)
1096
+ VALUES (?, ?, ?, 'rejected', 'user', ?, ?)
1097
+ `);
1098
+ auditStmt.run(from_id, to_id, relationship, reason || 'User rejected', Date.now());
1099
+ // Delete the link
1100
+ const stmt = adapter.prepare(`
1101
+ DELETE FROM decision_edges
1102
+ WHERE from_id = ? AND to_id = ? AND relationship = ?
1103
+ `);
1104
+ stmt.run(from_id, to_id, relationship);
1105
+ return;
1106
+ }
1107
+ catch (error) {
1108
+ throw new Error(`rejectLink() failed: ${error instanceof Error ? error.message : String(error)}`);
1109
+ }
1110
+ }
1111
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1112
+ async function getPendingLinks(options = {}) {
1113
+ try {
1114
+ const adapter = (0, memory_store_js_1.getAdapter)();
1115
+ const { from_id, to_id } = options;
1116
+ let query = `
1117
+ SELECT
1118
+ e.*,
1119
+ d_from.topic as from_topic,
1120
+ d_from.decision as from_decision,
1121
+ d_to.topic as to_topic,
1122
+ d_to.decision as to_decision
1123
+ FROM decision_edges e
1124
+ LEFT JOIN decisions d_from ON e.from_id = d_from.id
1125
+ LEFT JOIN decisions d_to ON e.to_id = d_to.id
1126
+ WHERE e.approved_by_user = 0
1127
+ `;
1128
+ const params = [];
1129
+ if (from_id) {
1130
+ query += ' AND e.from_id = ?';
1131
+ params.push(from_id);
1132
+ }
1133
+ if (to_id) {
1134
+ query += ' AND e.to_id = ?';
1135
+ params.push(to_id);
1136
+ }
1137
+ query += ' ORDER BY e.created_at DESC';
1138
+ const stmt = adapter.prepare(query);
1139
+ const links = await stmt.all(...params);
1140
+ return links;
1141
+ }
1142
+ catch (error) {
1143
+ throw new Error(`getPendingLinks() failed: ${error instanceof Error ? error.message : String(error)}`);
1144
+ }
1145
+ }
1146
+ async function deprecateAutoLinks(options = {}) {
1147
+ const { dryRun = true } = options;
1148
+ try {
1149
+ const adapter = (0, memory_store_js_1.getAdapter)();
1150
+ // Identify auto-generated links (v0 legacy)
1151
+ // Criteria: created_by='user' (default) AND decision_id IS NULL (no proposal context)
1152
+ // Protected: decision_id IS NOT NULL OR created_by='llm' (explicitly proposed)
1153
+ const identifyStmt = adapter.prepare(`
1154
+ SELECT * FROM decision_edges
1155
+ WHERE created_by = 'user' AND decision_id IS NULL
1156
+ `);
1157
+ const autoLinks = (await identifyStmt.all());
1158
+ // Identify protected links for comparison
1159
+ const protectedStmt = adapter.prepare(`
1160
+ SELECT COUNT(*) as count FROM decision_edges
1161
+ WHERE decision_id IS NOT NULL OR created_by = 'llm'
1162
+ `);
1163
+ const protectedResult = (await protectedStmt.get());
1164
+ const protectedCount = protectedResult?.count ?? 0;
1165
+ const totalLinks = autoLinks.length + protectedCount;
1166
+ const autoLinkRatio = totalLinks > 0 ? (autoLinks.length / totalLinks) * 100 : 0;
1167
+ if (!dryRun && autoLinks.length > 0) {
1168
+ // Delete auto-generated links
1169
+ const deleteStmt = adapter.prepare(`
1170
+ DELETE FROM decision_edges
1171
+ WHERE created_by = 'user' AND decision_id IS NULL
1172
+ `);
1173
+ await deleteStmt.run();
1174
+ // Log deprecation to audit trail
1175
+ const timestamp = Date.now();
1176
+ const auditStmt = adapter.prepare(`
1177
+ INSERT INTO link_audit_log (from_id, to_id, relationship, action, actor, reason, created_at)
1178
+ VALUES (?, ?, ?, 'deprecated', 'system', ?, ?)
1179
+ `);
1180
+ for (const link of autoLinks) {
1181
+ await auditStmt.run(link.from_id, link.to_id, link.relationship, 'v0 auto-generated link removed during governance migration', timestamp);
1182
+ }
1183
+ }
1184
+ return {
1185
+ dryRun,
1186
+ deprecated: autoLinks.length,
1187
+ protected: protectedCount,
1188
+ total: totalLinks,
1189
+ autoLinkRatio: autoLinkRatio.toFixed(2) + '%',
1190
+ links: autoLinks.map((l) => ({
1191
+ from_id: l.from_id,
1192
+ to_id: l.to_id,
1193
+ relationship: l.relationship,
1194
+ reason: l.reason,
1195
+ created_at: l.created_at,
1196
+ })),
1197
+ };
1198
+ }
1199
+ catch (error) {
1200
+ throw new Error(`deprecateAutoLinks() failed: ${error instanceof Error ? error.message : String(error)}`);
1201
+ }
1202
+ }
1203
+ /**
1204
+ * Scan and identify auto-generated links for cleanup (Epic 5 - Story 5.1)
1205
+ *
1206
+ * Identifies auto-generated links lacking proper approval metadata.
1207
+ * Separates deletion targets from protected links.
1208
+ *
1209
+ * Identification criteria:
1210
+ * - approved_by_user = 0 OR (created_by IS NULL AND decision_id IS NULL)
1211
+ *
1212
+ * Protected (excluded from deletion):
1213
+ * - approved_by_user = 1 AND (decision_id IS NOT NULL OR evidence IS NOT NULL)
1214
+ *
1215
+ * @returns {Object} Scan results with counts and link details
1216
+ */
1217
+ function scanAutoLinks() {
1218
+ const adapter = (0, memory_store_js_1.getAdapter)();
1219
+ // Total links
1220
+ const totalStmt = adapter.prepare(`SELECT COUNT(*) as count FROM decision_edges`);
1221
+ const totalResult = totalStmt.get();
1222
+ const totalLinks = totalResult?.count ?? 0;
1223
+ // Auto-generated links (lacking proper metadata)
1224
+ const autoStmt = adapter.prepare(`
1225
+ SELECT * FROM decision_edges
1226
+ WHERE approved_by_user = 0
1227
+ OR (created_by IS NULL AND decision_id IS NULL)
1228
+ `);
1229
+ const autoLinks = autoStmt.all();
1230
+ // Protected links (approved or has complete metadata)
1231
+ const protectedStmt = adapter.prepare(`
1232
+ SELECT COUNT(*) as count FROM decision_edges
1233
+ WHERE approved_by_user = 1
1234
+ OR (decision_id IS NOT NULL AND evidence IS NOT NULL)
1235
+ `);
1236
+ const protectedResult = protectedStmt.get();
1237
+ const protectedLinks = protectedResult?.count ?? 0;
1238
+ // Filter deletion targets (exclude protected links)
1239
+ const deletionTargets = autoLinks.filter((link) => {
1240
+ // Exclude protected links
1241
+ return !(link.approved_by_user === 1 || (link.decision_id && link.evidence));
1242
+ });
1243
+ return {
1244
+ total_links: totalLinks,
1245
+ auto_links: autoLinks.length,
1246
+ protected_links: protectedLinks,
1247
+ deletion_targets: deletionTargets.length,
1248
+ deletion_target_list: deletionTargets,
1249
+ };
1250
+ }
1251
+ /**
1252
+ * Create backup of links before cleanup (Epic 5 - Story 5.1)
1253
+ *
1254
+ * Backs up deletion target links with full metadata to JSON file.
1255
+ * Generates SHA-256 checksum for data integrity verification.
1256
+ * Creates backup manifest with timestamp and metadata.
1257
+ *
1258
+ * @param {Array} targetLinks - Links to back up
1259
+ * @returns {Object} Backup result with file paths and checksum
1260
+ */
1261
+ function createLinkBackup(targetLinks) {
1262
+ const backupDir = path_1.default.join(os_1.default.homedir(), '.claude', 'mama-backups');
1263
+ // Create backup directory if not exists
1264
+ if (!fs_1.default.existsSync(backupDir)) {
1265
+ fs_1.default.mkdirSync(backupDir, { recursive: true });
1266
+ }
1267
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1268
+ const backupFile = path_1.default.join(backupDir, `links-backup-${timestamp}.json`);
1269
+ // Serialize target links with full metadata
1270
+ const backupData = {
1271
+ timestamp: new Date().toISOString(),
1272
+ link_count: targetLinks.length,
1273
+ links: targetLinks,
1274
+ };
1275
+ const backupJson = JSON.stringify(backupData, null, 2);
1276
+ // Calculate SHA-256 checksum
1277
+ const checksum = crypto_1.default.createHash('sha256').update(backupJson).digest('hex');
1278
+ // Save backup file
1279
+ fs_1.default.writeFileSync(backupFile, backupJson, 'utf8');
1280
+ // Save manifest
1281
+ const manifest = {
1282
+ timestamp: backupData.timestamp,
1283
+ backup_file: backupFile,
1284
+ checksum,
1285
+ link_count: targetLinks.length,
1286
+ };
1287
+ const manifestFile = path_1.default.join(backupDir, `backup-manifest-${timestamp}.json`);
1288
+ fs_1.default.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2), 'utf8');
1289
+ return {
1290
+ backup_file: backupFile,
1291
+ manifest_file: manifestFile,
1292
+ checksum,
1293
+ link_count: targetLinks.length,
1294
+ };
1295
+ }
1296
+ /**
1297
+ * Generate pre-cleanup report with risk assessment (Epic 5 - Story 5.1)
1298
+ *
1299
+ * Creates comprehensive report with statistics, risk level, and samples.
1300
+ * Risk assessment based on deletion ratio:
1301
+ * - HIGH: > 50% deletion
1302
+ * - MEDIUM: 30-50% deletion
1303
+ * - LOW: < 30% deletion
1304
+ *
1305
+ * @returns {Object} Report data with markdown output and file path
1306
+ */
1307
+ function generatePreCleanupReport() {
1308
+ const scanResult = scanAutoLinks();
1309
+ // Calculate risk level (guard against division by zero)
1310
+ const deletionRatio = scanResult.total_links > 0 ? scanResult.deletion_targets / scanResult.total_links : 0;
1311
+ let riskLevel;
1312
+ if (deletionRatio > 0.5) {
1313
+ riskLevel = 'HIGH';
1314
+ }
1315
+ else if (deletionRatio > 0.3) {
1316
+ riskLevel = 'MEDIUM';
1317
+ }
1318
+ else {
1319
+ riskLevel = 'LOW';
1320
+ }
1321
+ // Sample deletion targets (max 10)
1322
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1323
+ const samples = scanResult.deletion_target_list.slice(0, 10).map((link) => ({
1324
+ from_id: link.from_id,
1325
+ to_id: link.to_id,
1326
+ relationship: link.relationship,
1327
+ reason: link.reason,
1328
+ created_by: link.created_by,
1329
+ approved_by_user: link.approved_by_user,
1330
+ }));
1331
+ const report = {
1332
+ generated_at: new Date().toISOString(),
1333
+ statistics: {
1334
+ total_links: scanResult.total_links,
1335
+ auto_links: scanResult.auto_links,
1336
+ protected_links: scanResult.protected_links,
1337
+ deletion_targets: scanResult.deletion_targets,
1338
+ deletion_ratio: `${(deletionRatio * 100).toFixed(1)}%`,
1339
+ },
1340
+ risk_assessment: {
1341
+ level: riskLevel,
1342
+ message: riskLevel === 'HIGH'
1343
+ ? '⚠️ HIGH RISK: Deletion targets exceed 50%. Create backup before proceeding.'
1344
+ : riskLevel === 'MEDIUM'
1345
+ ? '⚡ MEDIUM RISK: Deletion targets 30-50%. Verify backup recommended.'
1346
+ : '✅ LOW RISK: Deletion targets under 30%. Safe to proceed.',
1347
+ },
1348
+ deletion_target_samples: samples,
1349
+ };
1350
+ // Generate markdown report
1351
+ const markdown = `# Pre-Cleanup Report
1352
+
1353
+ **Generated:** ${report.generated_at}
1354
+
1355
+ ## Statistics
1356
+
1357
+ - **Total Links:** ${report.statistics.total_links}
1358
+ - **Auto Links:** ${report.statistics.auto_links}
1359
+ - **Protected Links:** ${report.statistics.protected_links}
1360
+ - **Deletion Targets:** ${report.statistics.deletion_targets} (${report.statistics.deletion_ratio})
1361
+
1362
+ ## Risk Assessment
1363
+
1364
+ **Level:** ${report.risk_assessment.level}
1365
+
1366
+ ${report.risk_assessment.message}
1367
+
1368
+ ## Sample Deletion Targets (First 10)
1369
+
1370
+ ${samples
1371
+ .map(
1372
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1373
+ (link, idx) => `
1374
+ ### ${idx + 1}. ${link.from_id} → ${link.to_id}
1375
+
1376
+ - **Relationship:** ${link.relationship}
1377
+ - **Reason:** ${link.reason || 'N/A'}
1378
+ - **Created By:** ${link.created_by || 'N/A'}
1379
+ - **Approved:** ${link.approved_by_user ? 'Yes' : 'No'}
1380
+ `)
1381
+ .join('\n')}
1382
+
1383
+ ---
1384
+
1385
+ **Next Steps:**
1386
+
1387
+ 1. Review the deletion targets above
1388
+ 2. Run \`create_link_backup\` to create a backup
1389
+ 3. Proceed with cleanup using Story 5.2 tools
1390
+ 4. If needed, restore from backup using \`restore_link_backup\`
1391
+ `;
1392
+ const backupDir = path_1.default.join(os_1.default.homedir(), '.claude', 'mama-backups');
1393
+ // Ensure backup directory exists
1394
+ if (!fs_1.default.existsSync(backupDir)) {
1395
+ fs_1.default.mkdirSync(backupDir, { recursive: true });
1396
+ }
1397
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1398
+ const reportFile = path_1.default.join(backupDir, `pre-cleanup-report-${timestamp}.md`);
1399
+ fs_1.default.writeFileSync(reportFile, markdown, 'utf8');
1400
+ return {
1401
+ report: report,
1402
+ report_file: reportFile,
1403
+ markdown,
1404
+ };
1405
+ }
1406
+ /**
1407
+ * Restore links from backup file (Epic 5 - Story 5.1)
1408
+ *
1409
+ * Restores previously backed-up links to the database.
1410
+ * Verifies checksum before restoration to ensure data integrity.
1411
+ * Reports number of restored and failed links.
1412
+ *
1413
+ * @param {string} backupFile - Path to backup file
1414
+ * @returns {Object} Restoration result with counts
1415
+ */
1416
+ function restoreLinkBackup(backupFile) {
1417
+ // Read backup file
1418
+ const backupJson = fs_1.default.readFileSync(backupFile, 'utf8');
1419
+ const backupData = JSON.parse(backupJson);
1420
+ // Read manifest for checksum verification
1421
+ const manifestFile = backupFile.replace('links-backup', 'backup-manifest');
1422
+ const manifest = JSON.parse(fs_1.default.readFileSync(manifestFile, 'utf8'));
1423
+ // Verify checksum
1424
+ const calculatedChecksum = crypto_1.default.createHash('sha256').update(backupJson).digest('hex');
1425
+ if (calculatedChecksum !== manifest.checksum) {
1426
+ throw new Error('Backup file checksum mismatch. File may be corrupted.');
1427
+ }
1428
+ // Restore links to database
1429
+ const adapter = (0, memory_store_js_1.getAdapter)();
1430
+ let restored = 0;
1431
+ let failed = 0;
1432
+ const insertStmt = adapter.prepare(`
1433
+ INSERT OR REPLACE INTO decision_edges
1434
+ (from_id, to_id, relationship, reason, created_by, approved_by_user, decision_id, evidence, created_at)
1435
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1436
+ `);
1437
+ for (const link of backupData.links) {
1438
+ try {
1439
+ insertStmt.run(link.from_id, link.to_id, link.relationship, link.reason, link.created_by, link.approved_by_user, link.decision_id, link.evidence, link.created_at);
1440
+ restored++;
1441
+ }
1442
+ catch (error) {
1443
+ const msg = error instanceof Error ? error.message : String(error);
1444
+ (0, debug_logger_js_1.error)(`Failed to restore link ${link.from_id} -> ${link.to_id}:`, msg);
1445
+ failed++;
1446
+ }
1447
+ }
1448
+ return {
1449
+ total_links: backupData.link_count,
1450
+ restored,
1451
+ failed,
1452
+ backup_file: backupFile,
1453
+ };
1454
+ }
1455
+ /**
1456
+ * Verify backup file exists and is recent (Epic 5 - Story 5.2)
1457
+ *
1458
+ * Checks for backup files in backup directory and verifies they are recent enough.
1459
+ * Required as safety check before executing link deletion.
1460
+ *
1461
+ * @param {number} maxAgeHours - Maximum age of backup in hours (default: 24)
1462
+ * @returns {Object} Backup verification result with latest backup info
1463
+ */
1464
+ function verifyBackupExists(maxAgeHours = 24) {
1465
+ const backupDir = path_1.default.join(os_1.default.homedir(), '.claude', 'mama-backups');
1466
+ if (!fs_1.default.existsSync(backupDir)) {
1467
+ throw new Error('Backup directory not found. Please create a backup first using create_link_backup.');
1468
+ }
1469
+ const backupFiles = fs_1.default
1470
+ .readdirSync(backupDir)
1471
+ .filter((f) => f.startsWith('links-backup-'))
1472
+ .map((f) => ({
1473
+ name: f,
1474
+ path: path_1.default.join(backupDir, f),
1475
+ mtime: fs_1.default.statSync(path_1.default.join(backupDir, f)).mtime,
1476
+ }))
1477
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1478
+ if (backupFiles.length === 0) {
1479
+ throw new Error('No recent backup found. Please run create_link_backup first.');
1480
+ }
1481
+ const latestBackup = backupFiles[0];
1482
+ const backupAge = Date.now() - latestBackup.mtime.getTime();
1483
+ const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
1484
+ if (backupAge > maxAgeMs) {
1485
+ throw new Error(`Most recent backup is too old (${(backupAge / (60 * 60 * 1000)).toFixed(1)} hours). Max age: ${maxAgeHours} hours.`);
1486
+ }
1487
+ // Read backup metadata
1488
+ const backupJson = fs_1.default.readFileSync(latestBackup.path, 'utf8');
1489
+ const backupData = JSON.parse(backupJson);
1490
+ return {
1491
+ backup_file: latestBackup.path,
1492
+ age_hours: backupAge / (60 * 60 * 1000),
1493
+ link_count: backupData.link_count || 0,
1494
+ };
1495
+ }
1496
+ /**
1497
+ * Delete auto-generated links with batch processing (Epic 5 - Story 5.2)
1498
+ *
1499
+ * Executes batch deletion of auto-generated links with transaction support.
1500
+ * Requires recent backup (within 24 hours) before execution.
1501
+ * Logs all deletions to audit trail.
1502
+ *
1503
+ * Safety features:
1504
+ * - Backup verification before deletion
1505
+ * - Batch processing with transaction support
1506
+ * - Dry-run mode for simulation
1507
+ * - Large deletion warning (> 1000 links)
1508
+ *
1509
+ * @param {number} batchSize - Number of links to delete per batch (default: 100)
1510
+ * @param {boolean} dryRun - If true, simulate deletion without actual changes (default: true)
1511
+ * @returns {Object} Deletion result with counts and backup info
1512
+ */
1513
+ function deleteAutoLinks(batchSize = 100, dryRun = true) {
1514
+ const adapter = (0, memory_store_js_1.getAdapter)();
1515
+ // Safety check: Verify recent backup exists
1516
+ const backupInfo = verifyBackupExists(24);
1517
+ // Scan for deletion targets
1518
+ const scanResult = scanAutoLinks();
1519
+ const deletionTargets = scanResult.deletion_target_list;
1520
+ if (deletionTargets.length === 0) {
1521
+ return {
1522
+ dry_run: dryRun,
1523
+ deleted: 0,
1524
+ failed: 0,
1525
+ total_targets: 0,
1526
+ backup_file: backupInfo.backup_file,
1527
+ message: 'No auto-generated links found. Nothing to delete.',
1528
+ };
1529
+ }
1530
+ // Large deletion warning
1531
+ const largeDelection = deletionTargets.length > 1000;
1532
+ if (largeDelection) {
1533
+ (0, debug_logger_js_1.warn)(`⚠️ LARGE DELETION: ${deletionTargets.length} links will be deleted. Consider running in dry-run mode first.`);
1534
+ }
1535
+ if (dryRun) {
1536
+ return {
1537
+ dry_run: true,
1538
+ would_delete: deletionTargets.length,
1539
+ deleted: 0,
1540
+ backup_file: backupInfo.backup_file,
1541
+ large_deletion_warning: largeDelection,
1542
+ warning_message: largeDelection
1543
+ ? `Warning: More than 1000 links (${deletionTargets.length}) will be deleted.`
1544
+ : null,
1545
+ sample_links: deletionTargets.slice(0, 5).map((l) => ({
1546
+ from_id: l.from_id,
1547
+ to_id: l.to_id,
1548
+ relationship: l.relationship,
1549
+ })),
1550
+ message: 'Dry-run mode: No links were actually deleted.',
1551
+ };
1552
+ }
1553
+ // Execute batch deletion with transaction support
1554
+ let deleted = 0;
1555
+ let failed = 0;
1556
+ let batchesProcessed = 0;
1557
+ const errors = [];
1558
+ const deleteStmt = adapter.prepare(`
1559
+ DELETE FROM decision_edges
1560
+ WHERE from_id = ? AND to_id = ? AND relationship = ?
1561
+ `);
1562
+ const auditStmt = adapter.prepare(`
1563
+ INSERT INTO link_audit_log (from_id, to_id, relationship, action, actor, reason, created_at)
1564
+ VALUES (?, ?, ?, 'deprecated', 'system', ?, ?)
1565
+ `);
1566
+ // Process in batches
1567
+ for (let i = 0; i < deletionTargets.length; i += batchSize) {
1568
+ const batch = deletionTargets.slice(i, i + batchSize);
1569
+ try {
1570
+ const processBatch = () => {
1571
+ for (const link of batch) {
1572
+ try {
1573
+ deleteStmt.run(link.from_id, link.to_id, link.relationship);
1574
+ auditStmt.run(link.from_id, link.to_id, link.relationship, 'Auto-link cleanup - v1.1 migration', Date.now());
1575
+ deleted++;
1576
+ }
1577
+ catch (error) {
1578
+ failed++;
1579
+ errors.push({
1580
+ link: `${link.from_id}->${link.to_id}`,
1581
+ error: error instanceof Error ? error.message : String(error),
1582
+ });
1583
+ }
1584
+ }
1585
+ };
1586
+ // Use transaction if available, otherwise run directly
1587
+ if (adapter.transaction) {
1588
+ adapter.transaction(processBatch);
1589
+ }
1590
+ else {
1591
+ processBatch();
1592
+ }
1593
+ batchesProcessed++;
1594
+ }
1595
+ catch (error) {
1596
+ (0, debug_logger_js_1.error)(`Batch deletion failed at index ${i}:`, error);
1597
+ failed += batch.length;
1598
+ errors.push({
1599
+ batch_index: i,
1600
+ batch_size: batch.length,
1601
+ error: error instanceof Error ? error.message : String(error),
1602
+ });
1603
+ break; // Stop on batch failure
1604
+ }
1605
+ }
1606
+ const successRate = deletionTargets.length > 0 ? (deleted / deletionTargets.length) * 100 : 0;
1607
+ return {
1608
+ dry_run: false,
1609
+ deleted,
1610
+ failed,
1611
+ total_targets: deletionTargets.length,
1612
+ backup_file: backupInfo.backup_file,
1613
+ batches_processed: batchesProcessed,
1614
+ errors: errors.slice(0, 10), // Return first 10 errors
1615
+ success_rate: successRate,
1616
+ };
1617
+ }
1618
+ /**
1619
+ * Validate cleanup result and generate post-cleanup report (Epic 5 - Story 5.2)
1620
+ *
1621
+ * Re-scans for remaining auto-generated links and evaluates cleanup success.
1622
+ * Generates comprehensive report with statistics and recommendations.
1623
+ *
1624
+ * Success criteria:
1625
+ * - SUCCESS: Remaining auto links < 5%
1626
+ * - PARTIAL: Remaining auto links 5-10%
1627
+ * - FAILED: Remaining auto links > 10%
1628
+ *
1629
+ * @returns {Object} Validation result with report and file path
1630
+ */
1631
+ function validateCleanupResult() {
1632
+ // Re-scan for remaining auto links
1633
+ const scanResult = scanAutoLinks();
1634
+ // Calculate remaining ratio
1635
+ const totalLinks = scanResult.total_links;
1636
+ const remainingAutoLinks = scanResult.auto_links;
1637
+ const remainingRatio = totalLinks > 0 ? remainingAutoLinks / totalLinks : 0;
1638
+ // Evaluate cleanup success
1639
+ let status;
1640
+ let message;
1641
+ let recommendation;
1642
+ if (remainingRatio < 0.05) {
1643
+ status = 'SUCCESS';
1644
+ message = '✅ SUCCESS: Remaining auto-links under 5%. Target achieved!';
1645
+ recommendation = 'Cleanup completed successfully. You can proceed with migration.';
1646
+ }
1647
+ else if (remainingRatio < 0.1) {
1648
+ status = 'PARTIAL';
1649
+ message = '⚡ PARTIAL: Remaining auto-links 5-10%. Additional cleanup recommended.';
1650
+ recommendation = 'Run execute_link_cleanup again to clean up more auto-links.';
1651
+ }
1652
+ else {
1653
+ status = 'FAILED';
1654
+ message = '⚠️ FAILED: Remaining auto-links exceed 10%. Rollback or re-run needed.';
1655
+ recommendation = 'Significantly missed target. Consider restoring from backup and retry.';
1656
+ }
1657
+ // Generate post-cleanup report
1658
+ const report = {
1659
+ validated_at: new Date().toISOString(),
1660
+ status,
1661
+ message,
1662
+ statistics: {
1663
+ total_links: totalLinks,
1664
+ remaining_auto_links: remainingAutoLinks,
1665
+ remaining_ratio: `${(remainingRatio * 100).toFixed(1)}%`,
1666
+ protected_links: scanResult.protected_links,
1667
+ deletion_targets: scanResult.deletion_targets,
1668
+ },
1669
+ recommendation,
1670
+ };
1671
+ // Generate markdown report
1672
+ const markdown = generatePostCleanupReportMarkdown(report);
1673
+ // Save report to file
1674
+ const backupDir = path_1.default.join(os_1.default.homedir(), '.claude', 'mama-backups');
1675
+ if (!fs_1.default.existsSync(backupDir)) {
1676
+ fs_1.default.mkdirSync(backupDir, { recursive: true });
1677
+ }
1678
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1679
+ const reportFile = path_1.default.join(backupDir, `post-cleanup-report-${timestamp}.md`);
1680
+ fs_1.default.writeFileSync(reportFile, markdown, 'utf8');
1681
+ return {
1682
+ status,
1683
+ total_links_before: totalLinks,
1684
+ auto_links_remaining: remainingAutoLinks,
1685
+ remaining_ratio: remainingRatio * 100,
1686
+ protected_links: scanResult.protected_links,
1687
+ report,
1688
+ report_file: reportFile,
1689
+ markdown,
1690
+ };
1691
+ }
1692
+ /**
1693
+ * Generate post-cleanup report in Markdown format (Epic 5 - Story 5.2)
1694
+ *
1695
+ * @param {Object} report - Validation report data
1696
+ * @returns {string} Markdown formatted report
1697
+ */
1698
+ function generatePostCleanupReportMarkdown(report) {
1699
+ let markdown = `# Post-Cleanup Validation Report
1700
+
1701
+ **Generated:** ${report.validated_at}
1702
+ **Status:** ${report.status}
1703
+
1704
+ ${report.message}
1705
+
1706
+ ## Statistics
1707
+
1708
+ - **Total Links:** ${report.statistics.total_links}
1709
+ - **Remaining Auto Links:** ${report.statistics.remaining_auto_links}
1710
+ - **Remaining Ratio:** ${report.statistics.remaining_ratio}
1711
+ - **Protected Links:** ${report.statistics.protected_links}
1712
+ - **Deletion Targets (if any):** ${report.statistics.deletion_targets}
1713
+
1714
+ ## Recommendation
1715
+
1716
+ ${report.recommendation}
1717
+ `;
1718
+ // Add rollback instructions for non-SUCCESS statuses
1719
+ if (report.status !== 'SUCCESS') {
1720
+ markdown += `
1721
+
1722
+ ---
1723
+
1724
+ ## Rollback Instructions
1725
+
1726
+ If you need to restore the deleted links:
1727
+
1728
+ 1. Find the latest backup file in \`~/.claude/mama-backups/\`
1729
+ 2. Run: \`restore_link_backup <backup_file_path>\`
1730
+ 3. Re-run validation: \`validate_cleanup_result\`
1731
+
1732
+ **Next Steps:**
1733
+
1734
+ - For PARTIAL status: Review remaining auto links and run cleanup again if needed
1735
+ - For FAILED status: Consider rollback and investigate why many links remain
1736
+ `;
1737
+ }
1738
+ return markdown;
1739
+ }
1740
+ /**
1741
+ * Calculate coverage metrics (Epic 4 - Story 4.1)
1742
+ *
1743
+ * Measures narrative coverage (% of decisions with narrative fields)
1744
+ * and link coverage (% of decisions with at least one link).
1745
+ *
1746
+ * @returns {Object} Coverage metrics
1747
+ */
1748
+ function calculateCoverage() {
1749
+ const adapter = (0, memory_store_js_1.getAdapter)();
1750
+ // Total decisions
1751
+ const totalDecisions = adapter.prepare(`SELECT COUNT(*) as count FROM decisions`).get().count;
1752
+ if (totalDecisions === 0) {
1753
+ return {
1754
+ narrativeCoverage: '0.0%',
1755
+ linkCoverage: '0.0%',
1756
+ totalDecisions: 0,
1757
+ completeNarratives: 0,
1758
+ decisionsWithLinks: 0,
1759
+ };
1760
+ }
1761
+ // Narrative coverage: Decisions with evidence, alternatives, and risks filled
1762
+ // Note: Using existing schema fields (evidence, alternatives, risks) instead of
1763
+ // 5-layer fields (specificity, evidence, reasoning, tension, continuity) mentioned in story
1764
+ const completeNarratives = adapter
1765
+ .prepare(`
1766
+ SELECT COUNT(*) as count FROM decisions
1767
+ WHERE evidence IS NOT NULL AND evidence != ''
1768
+ AND alternatives IS NOT NULL AND alternatives != ''
1769
+ AND risks IS NOT NULL AND risks != ''
1770
+ `)
1771
+ .get().count;
1772
+ const narrativeCoverage = (completeNarratives / totalDecisions) * 100;
1773
+ // Link coverage: Decisions with at least one link
1774
+ const decisionsWithLinks = adapter
1775
+ .prepare(`
1776
+ SELECT COUNT(DISTINCT d.id) as count FROM decisions d
1777
+ WHERE EXISTS (
1778
+ SELECT 1 FROM decision_edges e
1779
+ WHERE e.from_id = d.id OR e.to_id = d.id
1780
+ )
1781
+ `)
1782
+ .get().count;
1783
+ const linkCoverage = (decisionsWithLinks / totalDecisions) * 100;
1784
+ return {
1785
+ narrativeCoverage: `${narrativeCoverage.toFixed(1)}%`,
1786
+ linkCoverage: `${linkCoverage.toFixed(1)}%`,
1787
+ totalDecisions,
1788
+ completeNarratives,
1789
+ decisionsWithLinks,
1790
+ };
1791
+ }
1792
+ /**
1793
+ * Log restart attempt (Epic 4 - Story 4.2)
1794
+ *
1795
+ * Records restart attempt with success/failure status, latency, and mode.
1796
+ * Replaces in-memory restart-metrics.js with SQLite-backed storage.
1797
+ *
1798
+ * @param {string} sessionId - Session identifier
1799
+ * @param {string} status - 'success' or 'failure'
1800
+ * @param {string|null} failureReason - 'NO_CHECKPOINT', 'LOAD_ERROR', 'CONTEXT_INCOMPLETE', or null
1801
+ * @param {number} latencyMs - Latency in milliseconds
1802
+ * @param {string} mode - 'full' (narrative+links) or 'summary' (summary only)
1803
+ * @returns {void}
1804
+ */
1805
+ function logRestartAttempt(sessionId, status, failureReason, latencyMs, mode = 'full') {
1806
+ const adapter = (0, memory_store_js_1.getAdapter)();
1807
+ const timestamp = new Date().toISOString();
1808
+ adapter
1809
+ .prepare(`
1810
+ INSERT INTO restart_metrics (timestamp, session_id, status, failure_reason, latency_ms, mode)
1811
+ VALUES (?, ?, ?, ?, ?, ?)
1812
+ `)
1813
+ .run(timestamp, sessionId, status, failureReason, latencyMs, mode);
1814
+ // Performance warning if exceeds threshold
1815
+ // Note: Using console.warn directly (not logWarn) because performance warnings
1816
+ // should always be visible regardless of MAMA_LOG_LEVEL setting
1817
+ const threshold = mode === 'summary' ? 1000 : 2500;
1818
+ if (latencyMs > threshold) {
1819
+ console.warn(JSON.stringify({
1820
+ performance_warning: true,
1821
+ message: `Restart latency exceeded threshold: ${latencyMs}ms > ${threshold}ms`,
1822
+ session_id: sessionId,
1823
+ mode,
1824
+ latency_ms: latencyMs,
1825
+ threshold_ms: threshold,
1826
+ }));
1827
+ }
1828
+ }
1829
+ /**
1830
+ * Calculate restart success rate (Epic 4 - Story 4.2)
1831
+ *
1832
+ * Calculates success rate over a given period (24h, 7d, 30d).
1833
+ *
1834
+ * @param {string} period - '24h', '7d', or '30d'
1835
+ * @returns {Object} Success rate metrics
1836
+ */
1837
+ function calculateRestartSuccessRate(period = '7d') {
1838
+ const adapter = (0, memory_store_js_1.getAdapter)();
1839
+ const periodMap = {
1840
+ '24h': 1,
1841
+ '7d': 7,
1842
+ '30d': 30,
1843
+ };
1844
+ const days = periodMap[period] || 7;
1845
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
1846
+ const stats = adapter
1847
+ .prepare(`
1848
+ SELECT
1849
+ COUNT(*) as total,
1850
+ COUNT(CASE WHEN status = 'success' THEN 1 END) as success,
1851
+ COUNT(CASE WHEN status = 'failure' THEN 1 END) as failure
1852
+ FROM restart_metrics
1853
+ WHERE timestamp >= ?
1854
+ `)
1855
+ .get(since);
1856
+ const successRate = stats.total > 0 ? stats.success / stats.total : 0;
1857
+ return {
1858
+ period,
1859
+ total: stats.total,
1860
+ success: stats.success,
1861
+ failure: stats.failure,
1862
+ successRate: `${(successRate * 100).toFixed(1)}%`,
1863
+ meetsTarget: successRate >= 0.95,
1864
+ };
1865
+ }
1866
+ /**
1867
+ * Calculate restart latency percentiles (Epic 4 - Story 4.2)
1868
+ *
1869
+ * Calculates p50, p95, p99 latencies for successful restarts.
1870
+ * Optionally filters by mode (full/summary).
1871
+ *
1872
+ * @param {string} period - '24h', '7d', or '30d'
1873
+ * @param {string|null} mode - 'full', 'summary', or null (all modes)
1874
+ * @returns {Object} Latency percentile metrics
1875
+ */
1876
+ function calculateRestartLatency(period = '7d', mode = null) {
1877
+ const adapter = (0, memory_store_js_1.getAdapter)();
1878
+ const periodMap = { '24h': 1, '7d': 7, '30d': 30 };
1879
+ const days = periodMap[period] || 7;
1880
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
1881
+ let query = `
1882
+ SELECT latency_ms
1883
+ FROM restart_metrics
1884
+ WHERE timestamp >= ? AND status = 'success'
1885
+ `;
1886
+ const params = [since];
1887
+ if (mode) {
1888
+ query += ` AND mode = ?`;
1889
+ params.push(mode);
1890
+ }
1891
+ query += ` ORDER BY latency_ms ASC`;
1892
+ const rows = adapter.prepare(query).all(...params);
1893
+ const latencies = rows.map((r) => r.latency_ms);
1894
+ if (latencies.length === 0) {
1895
+ return { p50: 0, p95: 0, p99: 0, count: 0, mode: mode || 'all' };
1896
+ }
1897
+ const percentile = (arr, p) => {
1898
+ const index = Math.ceil((p / 100) * arr.length) - 1;
1899
+ return arr[Math.max(0, index)];
1900
+ };
1901
+ return {
1902
+ p50: percentile(latencies, 50),
1903
+ p95: percentile(latencies, 95),
1904
+ p99: percentile(latencies, 99),
1905
+ count: latencies.length,
1906
+ mode: mode || 'all',
1907
+ };
1908
+ }
1909
+ /**
1910
+ * Get restart metrics (Epic 4 - Story 4.2)
1911
+ *
1912
+ * Combines success rate and latency metrics for a given period.
1913
+ *
1914
+ * @param {string} period - '24h', '7d', or '30d'
1915
+ * @param {boolean} includeLatency - Whether to include latency percentiles
1916
+ * @returns {Object} Combined restart metrics
1917
+ */
1918
+ function getRestartMetrics(period = '7d', includeLatency = true) {
1919
+ const successRate = calculateRestartSuccessRate(period);
1920
+ const result = { successRate };
1921
+ if (includeLatency) {
1922
+ result.latency = {
1923
+ full: calculateRestartLatency(period, 'full'),
1924
+ summary: calculateRestartLatency(period, 'summary'),
1925
+ };
1926
+ }
1927
+ return result;
1928
+ }
1929
+ /**
1930
+ * Calculate quality metrics (Epic 4 - Story 4.1)
1931
+ *
1932
+ * Measures narrative quality (field completeness per layer)
1933
+ * and link quality (rich reason ratio, approved link ratio).
1934
+ *
1935
+ * @returns {Object} Quality metrics
1936
+ */
1937
+ function calculateQuality() {
1938
+ const adapter = (0, memory_store_js_1.getAdapter)();
1939
+ // Narrative quality: Average completeness for each narrative field
1940
+ const narrativeQuality = adapter
1941
+ .prepare(`
1942
+ SELECT
1943
+ AVG(CASE WHEN evidence IS NOT NULL AND evidence != '' THEN 1 ELSE 0 END) as evidence,
1944
+ AVG(CASE WHEN alternatives IS NOT NULL AND alternatives != '' THEN 1 ELSE 0 END) as alternatives,
1945
+ AVG(CASE WHEN risks IS NOT NULL AND risks != '' THEN 1 ELSE 0 END) as risks
1946
+ FROM decisions
1947
+ `)
1948
+ .get();
1949
+ // Link quality: "Rich" reason ratio (reason > 50 chars) and approved link ratio
1950
+ const linkStats = adapter
1951
+ .prepare(`
1952
+ SELECT
1953
+ COUNT(*) as total,
1954
+ COUNT(CASE WHEN reason IS NOT NULL AND LENGTH(reason) > 50 THEN 1 END) as rich,
1955
+ COUNT(CASE WHEN approved_by_user = 1 THEN 1 END) as approved
1956
+ FROM decision_edges
1957
+ `)
1958
+ .get();
1959
+ const linkQuality = linkStats.total > 0 ? ((linkStats.rich / linkStats.total) * 100).toFixed(1) : '0.0';
1960
+ const approvedRatio = linkStats.total > 0 ? ((linkStats.approved / linkStats.total) * 100).toFixed(1) : '0.0';
1961
+ return {
1962
+ narrativeQuality: {
1963
+ evidence: `${(narrativeQuality.evidence * 100).toFixed(1)}%`,
1964
+ alternatives: `${(narrativeQuality.alternatives * 100).toFixed(1)}%`,
1965
+ risks: `${(narrativeQuality.risks * 100).toFixed(1)}%`,
1966
+ },
1967
+ linkQuality: {
1968
+ richReasonRatio: `${linkQuality}%`,
1969
+ approvedRatio: `${approvedRatio}%`,
1970
+ totalLinks: linkStats.total,
1971
+ richLinks: linkStats.rich,
1972
+ approvedLinks: linkStats.approved,
1973
+ },
1974
+ };
1975
+ }
1976
+ /**
1977
+ * Generate quality report with recommendations (Epic 4 - Story 4.1 + 4.2)
1978
+ *
1979
+ * Generates a comprehensive quality report with coverage, quality metrics,
1980
+ * restart metrics, and recommendations for improvement.
1981
+ *
1982
+ * @param {Object} options - Report options
1983
+ * @param {string} [options.format='json'] - Output format: 'json' or 'markdown'
1984
+ * @param {string} [options.period='7d'] - Period for restart metrics: '24h', '7d', or '30d'
1985
+ * @param {Object} [options.thresholds] - Custom thresholds
1986
+ * @param {number} [options.thresholds.narrativeCoverage=0.8] - Narrative coverage threshold (0-1)
1987
+ * @param {number} [options.thresholds.linkCoverage=0.7] - Link coverage threshold (0-1)
1988
+ * @param {number} [options.thresholds.richReasonRatio=0.7] - Rich reason ratio threshold (0-1)
1989
+ * @param {number} [options.thresholds.restartSuccessRate=0.95] - Restart success rate threshold (0-1)
1990
+ * @param {number} [options.thresholds.restartLatencyP95Full=2500] - Full mode p95 latency threshold (ms)
1991
+ * @param {number} [options.thresholds.restartLatencyP95Summary=1000] - Summary mode p95 latency threshold (ms)
1992
+ * @returns {Object|string} Quality report as JSON or Markdown
1993
+ */
1994
+ function generateQualityReport(options = {}) {
1995
+ const { format = 'json', period = '7d', thresholds = {} } = options;
1996
+ const defaultThresholds = {
1997
+ narrativeCoverage: 0.8,
1998
+ linkCoverage: 0.7,
1999
+ richReasonRatio: 0.7,
2000
+ restartSuccessRate: 0.95,
2001
+ restartLatencyP95Full: 2500,
2002
+ restartLatencyP95Summary: 1000,
2003
+ ...thresholds,
2004
+ };
2005
+ const coverage = calculateCoverage();
2006
+ const quality = calculateQuality();
2007
+ // Story 4.2: Add restart metrics
2008
+ const validPeriod = (['24h', '7d', '30d'].includes(period || '7d') ? period : '7d');
2009
+ const restartMetrics = getRestartMetrics(validPeriod, true);
2010
+ const recommendations = [];
2011
+ // Check narrative coverage threshold
2012
+ const narrativeCoveragePct = parseFloat(coverage.narrativeCoverage);
2013
+ if (narrativeCoveragePct < defaultThresholds.narrativeCoverage * 100) {
2014
+ recommendations.push({
2015
+ type: 'narrative_coverage',
2016
+ message: `Narrative coverage below target (${defaultThresholds.narrativeCoverage * 100}%). Add narrative to decisions missing evidence, alternatives, or risks fields.`,
2017
+ target: `${defaultThresholds.narrativeCoverage * 100}%`,
2018
+ current: coverage.narrativeCoverage,
2019
+ });
2020
+ }
2021
+ // Check link coverage threshold
2022
+ const linkCoveragePct = parseFloat(coverage.linkCoverage);
2023
+ if (linkCoveragePct < defaultThresholds.linkCoverage * 100) {
2024
+ recommendations.push({
2025
+ type: 'link_coverage',
2026
+ message: `Link coverage below target (${defaultThresholds.linkCoverage * 100}%). Add links between related decisions.`,
2027
+ target: `${defaultThresholds.linkCoverage * 100}%`,
2028
+ current: coverage.linkCoverage,
2029
+ });
2030
+ }
2031
+ // Check link quality threshold
2032
+ const richReasonRatioPct = parseFloat(quality.linkQuality.richReasonRatio);
2033
+ if (richReasonRatioPct < defaultThresholds.richReasonRatio * 100) {
2034
+ recommendations.push({
2035
+ type: 'link_quality',
2036
+ message: `Link quality below target (${defaultThresholds.richReasonRatio * 100}%). Add specific causality and evidence to link reason fields.`,
2037
+ target: `${defaultThresholds.richReasonRatio * 100}%`,
2038
+ current: quality.linkQuality.richReasonRatio,
2039
+ });
2040
+ }
2041
+ // Story 4.2: Check restart success rate threshold (only if there's data)
2042
+ if (restartMetrics.successRate.total > 0 && !restartMetrics.successRate.meetsTarget) {
2043
+ const _successRatePct = parseFloat(restartMetrics.successRate.successRate);
2044
+ recommendations.push({
2045
+ type: 'restart_success_rate',
2046
+ message: `Restart success rate below target (95%). Analyze failure reasons and improve checkpoint quality.`,
2047
+ target: '95%',
2048
+ current: restartMetrics.successRate.successRate,
2049
+ });
2050
+ }
2051
+ // Story 4.2: Check restart latency thresholds (only if there's data)
2052
+ if (restartMetrics.latency) {
2053
+ const fullP95 = restartMetrics.latency.full.p95;
2054
+ if (restartMetrics.latency.full.count > 0 &&
2055
+ fullP95 > defaultThresholds.restartLatencyP95Full) {
2056
+ recommendations.push({
2057
+ type: 'restart_latency_full',
2058
+ message: `Narrative+link expansion p95 latency exceeds target (2.5s). Consider limiting link expansion depth or adding caching.`,
2059
+ target: `${defaultThresholds.restartLatencyP95Full}ms`,
2060
+ current: `${fullP95}ms`,
2061
+ });
2062
+ }
2063
+ const summaryP95 = restartMetrics.latency.summary.p95;
2064
+ if (restartMetrics.latency.summary.count > 0 &&
2065
+ summaryP95 > defaultThresholds.restartLatencyP95Summary) {
2066
+ recommendations.push({
2067
+ type: 'restart_latency_summary',
2068
+ message: `Summary mode p95 latency exceeds target (1.0s). Review query optimization or add indexes.`,
2069
+ target: `${defaultThresholds.restartLatencyP95Summary}ms`,
2070
+ current: `${summaryP95}ms`,
2071
+ });
2072
+ }
2073
+ }
2074
+ const report = {
2075
+ generated_at: new Date().toISOString(),
2076
+ period,
2077
+ coverage,
2078
+ quality,
2079
+ restart: restartMetrics,
2080
+ thresholds: defaultThresholds,
2081
+ recommendations,
2082
+ format,
2083
+ };
2084
+ if (format === 'markdown') {
2085
+ return formatQualityReportMarkdown(report);
2086
+ }
2087
+ return report;
2088
+ }
2089
+ /**
2090
+ * Format quality report as Markdown
2091
+ *
2092
+ * @param {Object} report - Quality report data
2093
+ * @returns {string} Markdown-formatted report
2094
+ */
2095
+ function formatQualityReportMarkdown(report) {
2096
+ const { generated_at, period, coverage, quality, restart, thresholds, recommendations } = report;
2097
+ let markdown = `# 📊 MAMA Quality Report\n\n`;
2098
+ markdown += `Generated: ${generated_at}\n`;
2099
+ markdown += `Period: ${period}\n\n`;
2100
+ markdown += `## Coverage Metrics\n\n`;
2101
+ markdown += `- **Narrative Coverage**: ${coverage.narrativeCoverage} (${coverage.completeNarratives}/${coverage.totalDecisions} decisions)\n`;
2102
+ markdown += `- **Link Coverage**: ${coverage.linkCoverage} (${coverage.decisionsWithLinks}/${coverage.totalDecisions} decisions)\n\n`;
2103
+ markdown += `## Quality Metrics\n\n`;
2104
+ markdown += `### Narrative Quality\n`;
2105
+ markdown += `- Evidence: ${quality.narrativeQuality.evidence}\n`;
2106
+ markdown += `- Alternatives: ${quality.narrativeQuality.alternatives}\n`;
2107
+ markdown += `- Risks: ${quality.narrativeQuality.risks}\n\n`;
2108
+ markdown += `### Link Quality\n`;
2109
+ markdown += `- Rich Reason Ratio: ${quality.linkQuality.richReasonRatio} (${quality.linkQuality.richLinks}/${quality.linkQuality.totalLinks} links)\n`;
2110
+ markdown += `- Approved Link Ratio: ${quality.linkQuality.approvedRatio} (${quality.linkQuality.approvedLinks}/${quality.linkQuality.totalLinks} links)\n\n`;
2111
+ // Story 4.2: Add restart metrics section
2112
+ if (restart) {
2113
+ markdown += `## Restart Metrics\n\n`;
2114
+ markdown += `### Success Rate\n`;
2115
+ markdown += `- **Success Rate**: ${restart.successRate.successRate} (${restart.successRate.success}/${restart.successRate.total} attempts)\n`;
2116
+ markdown += `- **Meets Target**: ${restart.successRate.meetsTarget ? '✅ Yes' : '❌ No'}\n`;
2117
+ markdown += `- Failures: ${restart.successRate.failure}\n\n`;
2118
+ if (restart.latency) {
2119
+ markdown += `### Latency (Percentiles)\n\n`;
2120
+ markdown += `**Full Mode (Narrative + Links)**\n`;
2121
+ markdown += `- p50: ${restart.latency.full.p50}ms\n`;
2122
+ markdown += `- p95: ${restart.latency.full.p95}ms\n`;
2123
+ markdown += `- p99: ${restart.latency.full.p99}ms\n`;
2124
+ markdown += `- Count: ${restart.latency.full.count}\n\n`;
2125
+ markdown += `**Summary Mode**\n`;
2126
+ markdown += `- p50: ${restart.latency.summary.p50}ms\n`;
2127
+ markdown += `- p95: ${restart.latency.summary.p95}ms\n`;
2128
+ markdown += `- p99: ${restart.latency.summary.p99}ms\n`;
2129
+ markdown += `- Count: ${restart.latency.summary.count}\n\n`;
2130
+ }
2131
+ }
2132
+ markdown += `## Thresholds\n\n`;
2133
+ markdown += `- Narrative Coverage: ≥ ${thresholds.narrativeCoverage * 100}%\n`;
2134
+ markdown += `- Link Coverage: ≥ ${thresholds.linkCoverage * 100}%\n`;
2135
+ markdown += `- Rich Reason Ratio: ≥ ${thresholds.richReasonRatio * 100}%\n`;
2136
+ markdown += `- Restart Success Rate: ≥ ${thresholds.restartSuccessRate * 100}%\n`;
2137
+ markdown += `- Restart Latency p95 (Full): ≤ ${thresholds.restartLatencyP95Full}ms\n`;
2138
+ markdown += `- Restart Latency p95 (Summary): ≤ ${thresholds.restartLatencyP95Summary}ms\n\n`;
2139
+ if (recommendations.length > 0) {
2140
+ markdown += `## ⚠️ Recommendations\n\n`;
2141
+ recommendations.forEach((rec, idx) => {
2142
+ markdown += `${idx + 1}. **${rec.type}**: ${rec.message}\n`;
2143
+ markdown += ` - Target: ${rec.target}, Current: ${rec.current}\n\n`;
2144
+ });
2145
+ }
2146
+ else {
2147
+ markdown += `## ✅ All quality targets met!\n\n`;
2148
+ }
2149
+ return markdown;
2150
+ }
2151
+ /**
2152
+ * MAMA Public API
2153
+ *
2154
+ * Simple, clean interface for Claude to interact with MAMA
2155
+ * Hides complex implementation details (embeddings, vector search, graph queries)
2156
+ *
2157
+ * Key Principles:
2158
+ * 1. Simple API First - No complex configuration
2159
+ * 2. Transparent Process - Each step is visible
2160
+ * 3. Claude-First Design - Claude decides what to save
2161
+ * 4. Non-Intrusive - Silent failures for helpers (suggest)
2162
+ */
2163
+ // ════════════════════════════════════════════════════════════════════════════
2164
+ // MAMA API - Simplified to 4 MCP tools (2025-11-25)
2165
+ //
2166
+ // Design: LLM can infer decision evolution from time-ordered search results
2167
+ // More tools = more constraints = less LLM flexibility
2168
+ //
2169
+ // Retained internal functions for future use, but MCP exposes only:
2170
+ // save, search, update, load_checkpoint
2171
+ // ════════════════════════════════════════════════════════════════════════════
2172
+ const mama = {
2173
+ // Core functions (used by 4 MCP tools)
2174
+ save,
2175
+ suggest,
2176
+ list: listDecisions,
2177
+ listCheckpoints,
2178
+ updateOutcome,
2179
+ saveCheckpoint,
2180
+ loadCheckpoint,
2181
+ // Legacy functions (retained for internal use, not exposed via MCP)
2182
+ recall,
2183
+ proposeLink,
2184
+ approveLink,
2185
+ rejectLink,
2186
+ getPendingLinks,
2187
+ deprecateAutoLinks,
2188
+ calculateCoverage,
2189
+ calculateQuality,
2190
+ generateQualityReport,
2191
+ logRestartAttempt,
2192
+ calculateRestartSuccessRate,
2193
+ calculateRestartLatency,
2194
+ getRestartMetrics,
2195
+ scanAutoLinks,
2196
+ createLinkBackup,
2197
+ generatePreCleanupReport,
2198
+ restoreLinkBackup,
2199
+ verifyBackupExists,
2200
+ deleteAutoLinks,
2201
+ validateCleanupResult,
2202
+ };
2203
+ // Default export for backward compatibility
2204
+ exports.default = mama;
2205
+ // CommonJS compatibility - allows require('@jungjaehoon/mama-core/mama-api').save()
2206
+ if (typeof module !== 'undefined' && module.exports) {
2207
+ module.exports = mama;
2208
+ module.exports.default = mama;
2209
+ }
2210
+ //# sourceMappingURL=mama-api.js.map