@jungjaehoon/mama-core 1.0.1

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.
@@ -0,0 +1,621 @@
1
+ /**
2
+ * MAMA (Memory-Augmented MCP Architecture) - Decision Tracker
3
+ *
4
+ * Learn and store decisions with graph relationships
5
+ * Tasks: 3.1-3.9 (Learn decision, ID generation, supersedes edges, refinement, embeddings)
6
+ * AC #1: Decision stored with outcome=NULL, confidence from LLM
7
+ * AC #2: Supersedes relationship creation
8
+ * AC #5: Multi-parent refinement with confidence calculation
9
+ *
10
+ * Updated for PostgreSQL compatibility via db-manager
11
+ *
12
+ * @module decision-tracker
13
+ * @version 2.0
14
+ * @date 2025-11-17
15
+ */
16
+
17
+ // eslint-disable-next-line no-unused-vars
18
+ const { info, error: logError } = require('./debug-logger');
19
+ const {
20
+ initDB,
21
+ insertDecisionWithEmbedding,
22
+ // eslint-disable-next-line no-unused-vars
23
+ queryDecisionGraph,
24
+ getAdapter,
25
+ } = require('./memory-store');
26
+
27
+ // ════════════════════════════════════════════════════════════════════════════
28
+ // Story 2.1: Extended Edge Types
29
+ // ════════════════════════════════════════════════════════════════════════════
30
+ // Valid relationship types for decision_edges
31
+ // Original: supersedes, refines, contradicts
32
+ // v1.3 Extension: builds_on, debates, synthesizes
33
+ const VALID_EDGE_TYPES = [
34
+ 'supersedes', // Original: New decision replaces old one
35
+ 'refines', // Original: Decision refines another
36
+ 'contradicts', // Original: Decision contradicts another
37
+ 'builds_on', // v1.3: Extends existing decision with new insights
38
+ 'debates', // v1.3: Presents counter-argument with evidence
39
+ 'synthesizes', // v1.3: Merges multiple decisions into unified approach
40
+ ];
41
+
42
+ /**
43
+ * Generate decision ID
44
+ *
45
+ * Task 3.2: Generate decision ID: `decision_${topic}_${timestamp}`
46
+ *
47
+ * @param {string} topic - Decision topic
48
+ * @returns {string} Decision ID
49
+ */
50
+ function generateDecisionId(topic) {
51
+ // Sanitize topic: remove spaces, lowercase, max 50 chars
52
+ const sanitized = topic
53
+ .toLowerCase()
54
+ .replace(/\s+/g, '_')
55
+ .replace(/[^a-z0-9_]/g, '')
56
+ .substring(0, 50);
57
+
58
+ const timestamp = Date.now();
59
+ const random = Math.random().toString(36).substr(2, 4);
60
+
61
+ return `decision_${sanitized}_${timestamp}_${random}`;
62
+ }
63
+
64
+ /**
65
+ * Check for previous decision on same topic
66
+ *
67
+ * Task 3.3: Query decisions table WHERE topic=? AND superseded_by IS NULL
68
+ * AC #2: Find previous decision to create supersedes relationship
69
+ *
70
+ * @param {string} topic - Decision topic
71
+ * @returns {Promise<Object|null>} Previous decision or null
72
+ */
73
+ async function getPreviousDecision(topic) {
74
+ const adapter = getAdapter();
75
+
76
+ try {
77
+ const stmt = adapter.prepare(`
78
+ SELECT * FROM decisions
79
+ WHERE topic = ? AND superseded_by IS NULL
80
+ ORDER BY created_at DESC
81
+ LIMIT 1
82
+ `);
83
+
84
+ const previous = await stmt.get(topic);
85
+ return previous || null;
86
+ } catch (error) {
87
+ throw new Error(`Failed to query previous decision: ${error.message}`);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Create a decision edge with specified relationship type
93
+ *
94
+ * Story 2.1: Generic edge creation supporting all relationship types
95
+ *
96
+ * @param {string} fromId - Source decision ID
97
+ * @param {string} toId - Target decision ID
98
+ * @param {string} relationship - Edge type (supersedes, builds_on, debates, synthesizes, etc.)
99
+ * @param {string} reason - Reason for the relationship
100
+ * @returns {Promise<boolean>} Success status
101
+ */
102
+ async function createEdge(fromId, toId, relationship, reason) {
103
+ const adapter = getAdapter();
104
+
105
+ // Story 2.1: Runtime validation of edge types
106
+ if (!VALID_EDGE_TYPES.includes(relationship)) {
107
+ throw new Error(
108
+ `Invalid edge type: "${relationship}". Valid types: ${VALID_EDGE_TYPES.join(', ')}`
109
+ );
110
+ }
111
+
112
+ try {
113
+ // Note: SQLite CHECK constraint only allows supersedes/refines/contradicts
114
+ // New types (builds_on, debates, synthesizes) bypass CHECK via runtime validation
115
+ // The INSERT will fail for new types due to CHECK constraint
116
+ // WORKAROUND: Use PRAGMA ignore_check_constraints or recreate table
117
+ // For now, we'll catch the error and handle gracefully
118
+
119
+ // Story 2.1: LLM auto-detected edges are approved by default (approved_by_user=1)
120
+ // This allows them to appear in search results via querySemanticEdges
121
+ const stmt = adapter.prepare(`
122
+ INSERT OR REPLACE INTO decision_edges (from_id, to_id, relationship, reason, created_at, created_by, approved_by_user)
123
+ VALUES (?, ?, ?, ?, ?, 'llm', 1)
124
+ `);
125
+
126
+ await stmt.run(fromId, toId, relationship, reason, Date.now());
127
+ return true;
128
+ } catch (error) {
129
+ // Handle CHECK constraint failure for new edge types
130
+ if (error.message.includes('CHECK constraint failed')) {
131
+ info(
132
+ `[decision-tracker] Edge type "${relationship}" not yet supported in schema, skipping edge creation`
133
+ );
134
+ return false;
135
+ }
136
+ throw new Error(`Failed to create ${relationship} edge: ${error.message}`);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Create supersedes edge
142
+ *
143
+ * Task 3.5: Create supersedes edge (INSERT INTO decision_edges)
144
+ * AC #2: Supersedes relationship creation
145
+ *
146
+ * @param {string} fromId - New decision ID
147
+ * @param {string} toId - Previous decision ID
148
+ * @param {string} reason - Reason for superseding
149
+ */
150
+ async function createSupersedesEdge(fromId, toId, reason) {
151
+ return createEdge(fromId, toId, 'supersedes', reason);
152
+ }
153
+
154
+ /**
155
+ * Update previous decision's superseded_by field
156
+ *
157
+ * Task 3.5: Update previous decision's superseded_by field
158
+ * AC #2: Previous decision's superseded_by field updated
159
+ *
160
+ * @param {string} previousId - Previous decision ID
161
+ * @param {string} newId - New decision ID
162
+ */
163
+ async function markSuperseded(previousId, newId) {
164
+ const adapter = getAdapter();
165
+
166
+ try {
167
+ const stmt = adapter.prepare(`
168
+ UPDATE decisions
169
+ SET superseded_by = ?, updated_at = ?
170
+ WHERE id = ?
171
+ `);
172
+
173
+ await stmt.run(newId, Date.now(), previousId);
174
+ } catch (error) {
175
+ throw new Error(`Failed to mark decision as superseded: ${error.message}`);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Calculate combined confidence (Bayesian update)
181
+ *
182
+ * Task 3.6: Calculate combined confidence for multi-parent refinement
183
+ * AC #5: Confidence score calculated based on history
184
+ *
185
+ * @param {number} prior - Prior confidence
186
+ * @param {Array<Object>} parents - Parent decisions
187
+ * @returns {number} Updated confidence (0.0-1.0)
188
+ */
189
+ function calculateCombinedConfidence(prior, parents) {
190
+ if (!parents || parents.length === 0) {
191
+ return prior;
192
+ }
193
+
194
+ // Bayesian update: Average parent confidences + prior
195
+ const parentConfidences = parents.map((p) => p.confidence || 0.5);
196
+ const avgParentConfidence =
197
+ parentConfidences.reduce((a, b) => a + b, 0) / parentConfidences.length;
198
+
199
+ // Weighted average: 60% prior, 40% parent history
200
+ const combined = prior * 0.6 + avgParentConfidence * 0.4;
201
+
202
+ // Clamp to [0.0, 1.0]
203
+ return Math.max(0, Math.min(1, combined));
204
+ }
205
+
206
+ /**
207
+ * Detect multi-parent refinement
208
+ *
209
+ * Task 3.6: Detect if new decision refines multiple previous decisions
210
+ * AC #5: Multi-parent refinement
211
+ *
212
+ * @param {Object} detection - Decision detection result
213
+ * @param {Object} sessionContext - Session context
214
+ * @returns {Array<string>|null} Array of parent decision IDs or null
215
+ */
216
+ function detectRefinement(_detection, _sessionContext) {
217
+ // TODO: Implement refinement detection heuristics
218
+ // For now, return null (single-parent only)
219
+ // Future: Analyze session context for references to multiple decisions
220
+
221
+ // Example heuristics:
222
+ // 1. User message mentions "combine", "merge", "refine"
223
+ // 2. Recent exchange references multiple topics
224
+ // 3. Decision reasoning mentions multiple approaches
225
+
226
+ return null;
227
+ }
228
+
229
+ // ════════════════════════════════════════════════════════════════════════════
230
+ // Story 2.2: Reasoning Field Parsing
231
+ // ════════════════════════════════════════════════════════════════════════════
232
+
233
+ /**
234
+ * Parse reasoning field for relationship references
235
+ *
236
+ * Story 2.2: Detect patterns like:
237
+ * - builds_on: <decision_id>
238
+ * - debates: <decision_id>
239
+ * - synthesizes: [id1, id2]
240
+ *
241
+ * @param {string} reasoning - Decision reasoning text
242
+ * @returns {Array<{type: string, targetIds: string[]}>} Detected relationships
243
+ */
244
+ function parseReasoningForRelationships(reasoning) {
245
+ if (!reasoning || typeof reasoning !== 'string') {
246
+ return [];
247
+ }
248
+
249
+ const relationships = [];
250
+
251
+ // Pattern 1: builds_on: <id> (allows optional markdown **bold**)
252
+ const buildsOnMatch = reasoning.match(
253
+ /\*{0,2}builds_on\*{0,2}:\*{0,2}\s*(decision_[a-z0-9_]+)/gi
254
+ );
255
+ if (buildsOnMatch) {
256
+ buildsOnMatch.forEach((match) => {
257
+ const id = match.replace(/\*{0,2}builds_on\*{0,2}:\*{0,2}\s*/i, '').trim();
258
+ if (id) {
259
+ relationships.push({ type: 'builds_on', targetIds: [id] });
260
+ }
261
+ });
262
+ }
263
+
264
+ // Pattern 2: debates: <id> (allows optional markdown **bold**)
265
+ const debatesMatch = reasoning.match(/\*{0,2}debates\*{0,2}:\*{0,2}\s*(decision_[a-z0-9_]+)/gi);
266
+ if (debatesMatch) {
267
+ debatesMatch.forEach((match) => {
268
+ const id = match.replace(/\*{0,2}debates\*{0,2}:\*{0,2}\s*/i, '').trim();
269
+ if (id) {
270
+ relationships.push({ type: 'debates', targetIds: [id] });
271
+ }
272
+ });
273
+ }
274
+
275
+ // Pattern 3: synthesizes: [id1, id2] (allows optional markdown **bold**)
276
+ const synthesizesMatch = reasoning.match(
277
+ /\*{0,2}synthesizes\*{0,2}:\*{0,2}\s*\[?\s*(decision_[a-z0-9_]+(?:\s*,\s*decision_[a-z0-9_]+)*)\s*\]?/gi
278
+ );
279
+ if (synthesizesMatch) {
280
+ synthesizesMatch.forEach((match) => {
281
+ const idsStr = match
282
+ .replace(/\*{0,2}synthesizes\*{0,2}:\*{0,2}\s*\[?\s*/i, '')
283
+ .replace(/\s*\]?\s*$/, '');
284
+ const ids = idsStr.split(/\s*,\s*/).filter((id) => id.startsWith('decision_'));
285
+ if (ids.length > 0) {
286
+ relationships.push({ type: 'synthesizes', targetIds: ids });
287
+ }
288
+ });
289
+ }
290
+
291
+ return relationships;
292
+ }
293
+
294
+ /**
295
+ * Create edges from parsed reasoning relationships
296
+ *
297
+ * Story 2.2: Auto-create edges when reasoning references other decisions
298
+ *
299
+ * @param {string} fromId - Source decision ID
300
+ * @param {string} reasoning - Decision reasoning text
301
+ * @returns {Promise<{created: number, failed: number}>} Edge creation stats
302
+ */
303
+ async function createEdgesFromReasoning(fromId, reasoning) {
304
+ const relationships = parseReasoningForRelationships(reasoning);
305
+ let created = 0;
306
+ let failed = 0;
307
+
308
+ for (const rel of relationships) {
309
+ for (const targetId of rel.targetIds) {
310
+ try {
311
+ // Verify target decision exists
312
+ const adapter = getAdapter();
313
+ const stmt = adapter.prepare('SELECT id FROM decisions WHERE id = ?');
314
+ const target = await stmt.get(targetId);
315
+
316
+ if (!target) {
317
+ info(`[decision-tracker] Referenced decision not found: ${targetId}, skipping edge`);
318
+ failed++;
319
+ continue;
320
+ }
321
+
322
+ // Create the edge
323
+ const reason = `Auto-detected from reasoning: ${rel.type} reference`;
324
+ const success = await createEdge(fromId, targetId, rel.type, reason);
325
+
326
+ if (success) {
327
+ created++;
328
+ info(`[decision-tracker] Created ${rel.type} edge: ${fromId} -> ${targetId}`);
329
+ } else {
330
+ failed++;
331
+ }
332
+ } catch (error) {
333
+ info(`[decision-tracker] Failed to create edge to ${targetId}: ${error.message}`);
334
+ failed++;
335
+ }
336
+ }
337
+ }
338
+
339
+ return { created, failed };
340
+ }
341
+
342
+ /**
343
+ * Get supersedes chain depth for a topic
344
+ *
345
+ * Story 2.2: Calculate how many times a topic has been superseded
346
+ *
347
+ * @param {string} topic - Decision topic
348
+ * @returns {Promise<{depth: number, chain: string[]}>} Chain depth and decision IDs
349
+ */
350
+ async function getSupersededChainDepth(topic) {
351
+ const adapter = getAdapter();
352
+ const chain = [];
353
+
354
+ try {
355
+ // Start from the latest decision (superseded_by IS NULL)
356
+ let stmt = adapter.prepare(`
357
+ SELECT id, supersedes FROM decisions
358
+ WHERE topic = ? AND superseded_by IS NULL
359
+ ORDER BY created_at DESC
360
+ LIMIT 1
361
+ `);
362
+
363
+ let current = await stmt.get(topic);
364
+
365
+ if (!current) {
366
+ return { depth: 0, chain: [] };
367
+ }
368
+
369
+ chain.push(current.id);
370
+
371
+ // Walk back through supersedes chain
372
+ while (current && current.supersedes) {
373
+ stmt = adapter.prepare('SELECT id, supersedes FROM decisions WHERE id = ?');
374
+ current = await stmt.get(current.supersedes);
375
+
376
+ if (current) {
377
+ chain.push(current.id);
378
+ }
379
+ }
380
+
381
+ return {
382
+ depth: chain.length - 1, // depth = number of supersedes edges
383
+ chain: chain.reverse(), // oldest to newest
384
+ };
385
+ } catch (error) {
386
+ throw new Error(`Failed to get supersedes chain: ${error.message}`);
387
+ }
388
+ }
389
+
390
+ // ════════════════════════════════════════════════════════════════════════════
391
+ // NOTE: Auto-link functions REMOVED in v1.2.0
392
+ //
393
+ // Removed functions:
394
+ // - createRefinesEdge
395
+ // - detectConflicts
396
+ // - createContradictsEdge
397
+ // - findRelatedDecisions
398
+ // - isConflicting
399
+ //
400
+ // Reason: LLM can infer decision evolution from time-ordered search results.
401
+ // Auto-links created 366 noise edges (100% cross-topic).
402
+ // Only supersedes (same topic) is reliable.
403
+ //
404
+ // See: CHANGELOG.md v1.2.0 - 2025-11-25
405
+ // ════════════════════════════════════════════════════════════════════════════
406
+
407
+ /**
408
+ * Learn Decision Function (Main API)
409
+ *
410
+ * Task 3.1: Create Learn Decision Function
411
+ * Task 3.2: Generate decision ID
412
+ * Task 3.3: Check for previous decision on same topic
413
+ * Task 3.4: Insert new decision with outcome=NULL, confidence from LLM
414
+ * Task 3.5: If previous exists: Create supersedes edge, Update previous superseded_by
415
+ * Task 3.6: If multi-parent refinement: Store refined_from, Calculate combined confidence
416
+ * Task 3.7: Generate enhanced embedding
417
+ * Task 3.8: Store in vss_memories (link via rowid)
418
+ *
419
+ * AC #1: Decision stored with outcome=NULL, confidence from LLM
420
+ * AC #2: Supersedes relationship creation
421
+ * AC #5: Multi-parent refinement with confidence calculation
422
+ *
423
+ * @param {Object} detection - Decision detection result
424
+ * @param {string} detection.topic - Decision topic
425
+ * @param {string} detection.decision - Decision value
426
+ * @param {string} detection.reasoning - Decision reasoning
427
+ * @param {number} detection.confidence - Confidence score (0.0-1.0)
428
+ * @param {Object} toolExecution - Tool execution data
429
+ * @param {Object} sessionContext - Session context
430
+ * @returns {Promise<Object>} { decisionId, notification }
431
+ */
432
+ async function learnDecision(detection, toolExecution, sessionContext) {
433
+ try {
434
+ // Ensure database is initialized
435
+ await initDB();
436
+
437
+ // ════════════════════════════════════════════════════════
438
+ // Task 3.2: Generate Decision ID
439
+ // ════════════════════════════════════════════════════════
440
+ const decisionId = generateDecisionId(detection.topic);
441
+
442
+ // ════════════════════════════════════════════════════════
443
+ // Task 3.3: Check for Previous Decision on Same Topic
444
+ // ════════════════════════════════════════════════════════
445
+ const previous = await getPreviousDecision(detection.topic);
446
+
447
+ // ════════════════════════════════════════════════════════
448
+ // Task 3.6: Detect Multi-Parent Refinement
449
+ // ════════════════════════════════════════════════════════
450
+ const refinedFrom = detectRefinement(detection, sessionContext);
451
+ let finalConfidence = detection.confidence;
452
+
453
+ if (refinedFrom && refinedFrom.length > 0) {
454
+ // AC #5: Multi-parent refinement
455
+ // Get parent decisions
456
+ const adapter = getAdapter();
457
+ const stmt = adapter.prepare('SELECT * FROM decisions WHERE id = ?');
458
+
459
+ const parents = await Promise.all(
460
+ refinedFrom.map(async (parentId) => await stmt.get(parentId))
461
+ );
462
+ const validParents = parents.filter(Boolean);
463
+
464
+ // Calculate combined confidence
465
+ finalConfidence = calculateCombinedConfidence(detection.confidence, validParents);
466
+ }
467
+
468
+ // ════════════════════════════════════════════════════════
469
+ // Task 3.4: Insert New Decision
470
+ // ════════════════════════════════════════════════════════
471
+ // ════════════════════════════════════════════════════════
472
+ // Story 014.7.6: Set needs_validation for assistant insights
473
+ // ════════════════════════════════════════════════════════
474
+ const isAssistantInsight = detection.type === 'assistant_insight';
475
+ const needsValidation = isAssistantInsight ? 1 : 0;
476
+
477
+ // AC #1: Decision stored with outcome=NULL, confidence from LLM
478
+ const decision = {
479
+ id: decisionId,
480
+ topic: detection.topic,
481
+ decision: detection.decision,
482
+ reasoning: detection.reasoning,
483
+ outcome: null, // AC #1: outcome=NULL (not yet tracked)
484
+ failure_reason: null,
485
+ limitation: null,
486
+ user_involvement: 'requested', // Inferred from tool execution
487
+ session_id: sessionContext.session_id,
488
+ supersedes: previous ? previous.id : null,
489
+ superseded_by: null,
490
+ refined_from: refinedFrom, // AC #5: Multi-parent refinement
491
+ confidence: finalConfidence, // AC #1, AC #5: Confidence from LLM
492
+ needs_validation: needsValidation, // Story 014.7.6: AC #1 - Validation for assistant insights
493
+ validation_attempts: 0, // Story 014.7.6: Track skip count
494
+ usage_count: 0, // Story 014.7.6: Track usage for periodic review
495
+ created_at: toolExecution.timestamp || Date.now(),
496
+ updated_at: Date.now(),
497
+ // Story 014.7.10: Add trust_context for Claude-Friendly Context Formatting
498
+ trust_context: detection.trust_context ? JSON.stringify(detection.trust_context) : null,
499
+ // Story 2.1: 5-layer narrative fields
500
+ evidence: detection.evidence
501
+ ? Array.isArray(detection.evidence)
502
+ ? JSON.stringify(detection.evidence)
503
+ : detection.evidence
504
+ : null,
505
+ alternatives: detection.alternatives
506
+ ? Array.isArray(detection.alternatives)
507
+ ? JSON.stringify(detection.alternatives)
508
+ : detection.alternatives
509
+ : null,
510
+ risks: detection.risks || null,
511
+ };
512
+
513
+ // Task 3.7, 3.8: Generate enhanced embedding and store in vss_memories
514
+ // (Handled by insertDecisionWithEmbedding function)
515
+ await insertDecisionWithEmbedding(decision);
516
+
517
+ // ════════════════════════════════════════════════════════
518
+ // Task 3.5: Create Supersedes Relationship (if previous exists)
519
+ // ════════════════════════════════════════════════════════
520
+ if (previous) {
521
+ // AC #2: Supersedes relationship creation
522
+ const reason = `User changed from "${previous.decision}" to "${detection.decision}"`;
523
+
524
+ // Create edge: new decision → previous decision
525
+ await createSupersedesEdge(decisionId, previous.id, reason);
526
+
527
+ // Update previous decision's superseded_by field
528
+ await markSuperseded(previous.id, decisionId);
529
+ }
530
+
531
+ // ════════════════════════════════════════════════════════
532
+ // NOTE: Auto-link generation (refines, contradicts) REMOVED
533
+ //
534
+ // Reason: LLM can infer decision evolution from time-ordered
535
+ // search results. Auto-links created 366 noise edges (100%
536
+ // cross-topic). Only supersedes (same topic) is reliable.
537
+ //
538
+ // See: 2025-11-25 discussion on decision tracking algorithm
539
+ // ════════════════════════════════════════════════════════
540
+
541
+ // ════════════════════════════════════════════════════════
542
+ // Story 014.7.6: Generate notification if needs validation
543
+ // ════════════════════════════════════════════════════════
544
+ let notification = null;
545
+ if (needsValidation) {
546
+ const { notifyInsight } = require('./notification-manager');
547
+ notification = notifyInsight({
548
+ id: decisionId,
549
+ topic: decision.topic,
550
+ decision: decision.decision,
551
+ reasoning: decision.reasoning,
552
+ confidence: decision.confidence,
553
+ needs_validation: true,
554
+ validation_attempts: 0,
555
+ });
556
+ }
557
+
558
+ // ════════════════════════════════════════════════════════
559
+ // Task 3.9: Return decision ID (+ notification for Story 014.7.6)
560
+ // ════════════════════════════════════════════════════════
561
+ return {
562
+ decisionId,
563
+ notification, // null if no validation needed, notification object otherwise
564
+ };
565
+ } catch (error) {
566
+ // CLAUDE.md Rule #1: No silent failures
567
+ throw new Error(`Failed to learn decision: ${error.message}`);
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Update confidence score
573
+ *
574
+ * Task 6: Confidence evolution (used in outcome tracking)
575
+ * AC #5: Confidence score calculated based on history
576
+ *
577
+ * @param {number} prior - Prior confidence
578
+ * @param {Array<Object>} evidence - Evidence items
579
+ * @param {string} evidence[].type - Evidence type (success, failure, partial)
580
+ * @param {number} evidence[].impact - Impact on confidence
581
+ * @returns {number} Updated confidence (0.0-1.0)
582
+ */
583
+ function updateConfidence(prior, evidence) {
584
+ if (!evidence || evidence.length === 0) {
585
+ return prior;
586
+ }
587
+
588
+ // Calculate total impact
589
+ const totalImpact = evidence.reduce((acc, e) => acc + e.impact, 0);
590
+
591
+ // Update confidence
592
+ const updated = prior + totalImpact;
593
+
594
+ // Clamp to [0.0, 1.0]
595
+ return Math.max(0, Math.min(1, updated));
596
+ }
597
+
598
+ // Export API
599
+ // NOTE: Auto-link functions (createRefinesEdge, createContradictsEdge,
600
+ // findRelatedDecisions, isConflicting, detectConflicts) removed from exports.
601
+ // LLM infers relationships from search results instead.
602
+ //
603
+ // Story 2.1/2.2: Added new edge type support and reasoning parsing
604
+ module.exports = {
605
+ // Core functions
606
+ learnDecision,
607
+ generateDecisionId,
608
+ getPreviousDecision,
609
+ createSupersedesEdge,
610
+ markSuperseded,
611
+ calculateCombinedConfidence,
612
+ detectRefinement,
613
+ updateConfidence,
614
+ // Story 2.1: Edge type extension
615
+ VALID_EDGE_TYPES,
616
+ createEdge,
617
+ // Story 2.2: Reasoning field parsing
618
+ parseReasoningForRelationships,
619
+ createEdgesFromReasoning,
620
+ getSupersededChainDepth,
621
+ };