Skip to main content

shodh_memory/memory/
lineage.rs

1//! Decision Lineage Graph - Causal Memory System (SHO-118)
2//!
3//! Transforms Shodh from a memory system into a reasoning system by tracking
4//! causal relationships between memories. Enables:
5//!
6//! 1. "Why" Audit Trail - Trace decisions back to root causes
7//! 2. Lineage Branching - Git-like branches when projects pivot
8//! 3. Automatic Post-Mortems - Synthesize learnings on task completion
9//!
10//! Storage schema:
11//! - `lineage:edges:{user_id}:{edge_id}` - Causal edges between memories
12//! - `lineage:by_from:{user_id}:{from_id}:{edge_id}` - Index by source memory
13//! - `lineage:by_to:{user_id}:{to_id}:{edge_id}` - Index by target memory
14//! - `lineage:branches:{user_id}:{branch_id}` - Branch metadata
15//! - `lineage:branch_members:{user_id}:{branch_id}:{memory_id}` - Memories in branch
16
17use anyhow::Result;
18use chrono::{DateTime, Utc};
19use rocksdb::{IteratorMode, DB};
20use serde::{Deserialize, Serialize};
21use std::collections::{HashMap, HashSet, VecDeque};
22use std::sync::Arc;
23use uuid::Uuid;
24
25use super::types::{ExperienceType, Memory, MemoryId};
26
27/// Causal relationship types between memories
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum CausalRelation {
30    /// Error/Bug caused a Todo to be created
31    Caused,
32    /// Todo was resolved by a Learning/Fix
33    ResolvedBy,
34    /// Decision was informed by a Learning/Discovery
35    InformedBy,
36    /// Old decision/pattern was superseded by new one
37    SupersededBy,
38    /// Discovery/Learning triggered a Todo
39    TriggeredBy,
40    /// Memory branched from another (project pivot)
41    BranchedFrom,
42    /// Generic relation when type is unclear
43    RelatedTo,
44}
45
46impl CausalRelation {
47    /// Get the inverse relation for bidirectional traversal
48    pub fn inverse(&self) -> Self {
49        match self {
50            CausalRelation::Caused => CausalRelation::ResolvedBy,
51            CausalRelation::ResolvedBy => CausalRelation::Caused,
52            CausalRelation::InformedBy => CausalRelation::TriggeredBy,
53            CausalRelation::SupersededBy => CausalRelation::SupersededBy, // symmetric conceptually
54            CausalRelation::TriggeredBy => CausalRelation::InformedBy,
55            CausalRelation::BranchedFrom => CausalRelation::BranchedFrom,
56            CausalRelation::RelatedTo => CausalRelation::RelatedTo,
57        }
58    }
59
60    /// Human-readable description of the relationship
61    pub fn description(&self) -> &'static str {
62        match self {
63            CausalRelation::Caused => "caused",
64            CausalRelation::ResolvedBy => "was resolved by",
65            CausalRelation::InformedBy => "was informed by",
66            CausalRelation::SupersededBy => "was superseded by",
67            CausalRelation::TriggeredBy => "triggered",
68            CausalRelation::BranchedFrom => "branched from",
69            CausalRelation::RelatedTo => "is related to",
70        }
71    }
72}
73
74/// Source of a lineage edge
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76pub enum LineageSource {
77    /// Automatically inferred by the system
78    Inferred,
79    /// Explicitly confirmed by user/agent
80    Confirmed,
81    /// Manually added by user/agent
82    Explicit,
83}
84
85/// A causal edge between two memories
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct LineageEdge {
88    /// Unique edge identifier
89    pub id: String,
90    /// Source memory (cause)
91    pub from: MemoryId,
92    /// Target memory (effect)
93    pub to: MemoryId,
94    /// Type of causal relationship
95    pub relation: CausalRelation,
96    /// Confidence in this causal link (0.0-1.0)
97    pub confidence: f32,
98    /// How this edge was created
99    pub source: LineageSource,
100    /// Branch this edge belongs to (None = main branch)
101    pub branch_id: Option<String>,
102    /// When the edge was created
103    pub created_at: DateTime<Utc>,
104    /// Last time this edge was reinforced/confirmed
105    pub last_reinforced: DateTime<Utc>,
106    /// Number of times this edge was reinforced
107    pub reinforcement_count: u32,
108}
109
110impl LineageEdge {
111    /// Create a new inferred edge
112    pub fn inferred(
113        from: MemoryId,
114        to: MemoryId,
115        relation: CausalRelation,
116        confidence: f32,
117    ) -> Self {
118        let now = Utc::now();
119        Self {
120            id: Uuid::new_v4().to_string(),
121            from,
122            to,
123            relation,
124            confidence,
125            source: LineageSource::Inferred,
126            branch_id: None,
127            created_at: now,
128            last_reinforced: now,
129            reinforcement_count: 1,
130        }
131    }
132
133    /// Create a new explicit edge
134    pub fn explicit(from: MemoryId, to: MemoryId, relation: CausalRelation) -> Self {
135        let now = Utc::now();
136        Self {
137            id: Uuid::new_v4().to_string(),
138            from,
139            to,
140            relation,
141            confidence: 1.0, // Explicit edges have full confidence
142            source: LineageSource::Explicit,
143            branch_id: None,
144            created_at: now,
145            last_reinforced: now,
146            reinforcement_count: 1,
147        }
148    }
149
150    /// Confirm an inferred edge
151    pub fn confirm(&mut self) {
152        self.source = LineageSource::Confirmed;
153        self.confidence = 1.0;
154        self.last_reinforced = Utc::now();
155        self.reinforcement_count += 1;
156    }
157
158    /// Reinforce this edge (increase confidence)
159    pub fn reinforce(&mut self) {
160        self.confidence = (self.confidence + 0.1).min(1.0);
161        self.last_reinforced = Utc::now();
162        self.reinforcement_count += 1;
163    }
164}
165
166/// A branch in the lineage graph (for project pivots)
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct LineageBranch {
169    /// Unique branch identifier
170    pub id: String,
171    /// Human-readable branch name
172    pub name: String,
173    /// Description of what this branch represents
174    pub description: Option<String>,
175    /// Parent branch (None for main branch)
176    pub parent_branch: Option<String>,
177    /// Memory where this branch diverged from parent
178    pub branch_point: Option<MemoryId>,
179    /// When the branch was created
180    pub created_at: DateTime<Utc>,
181    /// Whether this branch is currently active
182    pub active: bool,
183    /// Tags for categorization
184    pub tags: Vec<String>,
185}
186
187impl LineageBranch {
188    /// Create the main branch
189    pub fn main() -> Self {
190        Self {
191            id: "main".to_string(),
192            name: "Main".to_string(),
193            description: Some("Primary project lineage".to_string()),
194            parent_branch: None,
195            branch_point: None,
196            created_at: Utc::now(),
197            active: true,
198            tags: vec![],
199        }
200    }
201
202    /// Create a new branch from a parent
203    pub fn new(
204        name: &str,
205        parent: &str,
206        branch_point: MemoryId,
207        description: Option<&str>,
208    ) -> Self {
209        Self {
210            id: Uuid::new_v4().to_string(),
211            name: name.to_string(),
212            description: description.map(|s| s.to_string()),
213            parent_branch: Some(parent.to_string()),
214            branch_point: Some(branch_point),
215            created_at: Utc::now(),
216            active: true,
217            tags: vec![],
218        }
219    }
220}
221
222/// Result of lineage trace operation
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct LineageTrace {
225    /// The memory we started from
226    pub root: MemoryId,
227    /// Direction of traversal
228    pub direction: TraceDirection,
229    /// Edges in the trace (ordered by distance from root)
230    pub edges: Vec<LineageEdge>,
231    /// Memory IDs in traversal order
232    pub path: Vec<MemoryId>,
233    /// Total depth traversed
234    pub depth: usize,
235}
236
237/// Direction for lineage traversal
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
239pub enum TraceDirection {
240    /// Trace backward to find causes
241    Backward,
242    /// Trace forward to find effects
243    Forward,
244    /// Trace in both directions
245    Both,
246}
247
248/// Configuration for lineage inference
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct InferenceConfig {
251    /// Maximum days between memories for causal inference
252    pub max_temporal_gap_days: i64,
253    /// Minimum entity overlap for causal inference
254    pub min_entity_overlap: f32,
255    /// Confidence thresholds for each relation type
256    pub relation_confidence: HashMap<CausalRelation, f32>,
257}
258
259impl Default for InferenceConfig {
260    fn default() -> Self {
261        let mut relation_confidence = HashMap::new();
262        relation_confidence.insert(CausalRelation::Caused, 0.8);
263        relation_confidence.insert(CausalRelation::ResolvedBy, 0.85);
264        relation_confidence.insert(CausalRelation::InformedBy, 0.7);
265        relation_confidence.insert(CausalRelation::SupersededBy, 0.6);
266        relation_confidence.insert(CausalRelation::TriggeredBy, 0.75);
267        relation_confidence.insert(CausalRelation::BranchedFrom, 0.9);
268        relation_confidence.insert(CausalRelation::RelatedTo, 0.5);
269
270        Self {
271            max_temporal_gap_days: 7,
272            min_entity_overlap: 0.3,
273            relation_confidence,
274        }
275    }
276}
277
278/// Statistics about the lineage graph
279#[derive(Debug, Clone, Default, Serialize, Deserialize)]
280pub struct LineageStats {
281    pub total_edges: usize,
282    pub inferred_edges: usize,
283    pub confirmed_edges: usize,
284    pub explicit_edges: usize,
285    pub total_branches: usize,
286    pub active_branches: usize,
287    pub edges_by_relation: HashMap<String, usize>,
288    pub avg_confidence: f32,
289}
290
291/// The Lineage Graph - stores and infers causal relationships
292pub struct LineageGraph {
293    db: Arc<DB>,
294    config: InferenceConfig,
295}
296
297impl LineageGraph {
298    /// Create a new lineage graph backed by RocksDB
299    pub fn new(db: Arc<DB>) -> Self {
300        Self {
301            db,
302            config: InferenceConfig::default(),
303        }
304    }
305
306    /// Create with custom inference config
307    pub fn with_config(db: Arc<DB>, config: InferenceConfig) -> Self {
308        Self { db, config }
309    }
310
311    // =========================================================================
312    // EDGE STORAGE
313    // =========================================================================
314
315    /// Store a lineage edge
316    pub fn store_edge(&self, user_id: &str, edge: &LineageEdge) -> Result<()> {
317        // Primary storage
318        let key = format!("lineage:edges:{}:{}", user_id, edge.id);
319        let value = bincode::serde::encode_to_vec(edge, bincode::config::standard())?;
320        self.db.put(key.as_bytes(), &value)?;
321
322        // Index by source (from)
323        let from_key = format!("lineage:by_from:{}:{}:{}", user_id, edge.from.0, edge.id);
324        self.db.put(from_key.as_bytes(), edge.id.as_bytes())?;
325
326        // Index by target (to)
327        let to_key = format!("lineage:by_to:{}:{}:{}", user_id, edge.to.0, edge.id);
328        self.db.put(to_key.as_bytes(), edge.id.as_bytes())?;
329
330        Ok(())
331    }
332
333    /// Get an edge by ID
334    pub fn get_edge(&self, user_id: &str, edge_id: &str) -> Result<Option<LineageEdge>> {
335        let key = format!("lineage:edges:{}:{}", user_id, edge_id);
336        match self.db.get(key.as_bytes())? {
337            Some(data) => {
338                let (edge, _): (LineageEdge, _) =
339                    bincode::serde::decode_from_slice(&data, bincode::config::standard())?;
340                Ok(Some(edge))
341            }
342            None => Ok(None),
343        }
344    }
345
346    /// Delete an edge (for rejection)
347    pub fn delete_edge(&self, user_id: &str, edge_id: &str) -> Result<bool> {
348        if let Some(edge) = self.get_edge(user_id, edge_id)? {
349            // Delete indices
350            let from_key = format!("lineage:by_from:{}:{}:{}", user_id, edge.from.0, edge_id);
351            self.db.delete(from_key.as_bytes())?;
352
353            let to_key = format!("lineage:by_to:{}:{}:{}", user_id, edge.to.0, edge_id);
354            self.db.delete(to_key.as_bytes())?;
355
356            // Delete primary
357            let key = format!("lineage:edges:{}:{}", user_id, edge_id);
358            self.db.delete(key.as_bytes())?;
359
360            Ok(true)
361        } else {
362            Ok(false)
363        }
364    }
365
366    /// Get all edges from a memory (outgoing)
367    pub fn get_edges_from(&self, user_id: &str, memory_id: &MemoryId) -> Result<Vec<LineageEdge>> {
368        let prefix = format!("lineage:by_from:{}:{}:", user_id, memory_id.0);
369        self.get_edges_by_prefix(user_id, &prefix)
370    }
371
372    /// Get all edges to a memory (incoming)
373    pub fn get_edges_to(&self, user_id: &str, memory_id: &MemoryId) -> Result<Vec<LineageEdge>> {
374        let prefix = format!("lineage:by_to:{}:{}:", user_id, memory_id.0);
375        self.get_edges_by_prefix(user_id, &prefix)
376    }
377
378    /// Helper to get edges by index prefix
379    fn get_edges_by_prefix(&self, user_id: &str, prefix: &str) -> Result<Vec<LineageEdge>> {
380        let mut edges = Vec::new();
381
382        let iter = self.db.iterator(IteratorMode::From(
383            prefix.as_bytes(),
384            rocksdb::Direction::Forward,
385        ));
386
387        for item in iter {
388            let (key, value) = item?;
389            let key_str = String::from_utf8_lossy(&key);
390
391            if !key_str.starts_with(prefix) {
392                break;
393            }
394
395            let edge_id = String::from_utf8_lossy(&value);
396            if let Some(edge) = self.get_edge(user_id, &edge_id)? {
397                edges.push(edge);
398            }
399        }
400
401        Ok(edges)
402    }
403
404    /// List all edges for a user
405    pub fn list_edges(&self, user_id: &str, limit: usize) -> Result<Vec<LineageEdge>> {
406        let prefix = format!("lineage:edges:{}:", user_id);
407        let mut edges = Vec::new();
408
409        let iter = self.db.iterator(IteratorMode::From(
410            prefix.as_bytes(),
411            rocksdb::Direction::Forward,
412        ));
413
414        for item in iter {
415            let (key, value) = item?;
416            let key_str = String::from_utf8_lossy(&key);
417
418            if !key_str.starts_with(&prefix) {
419                break;
420            }
421
422            if let Ok(edge) = bincode::serde::decode_from_slice::<LineageEdge, _>(
423                &value,
424                bincode::config::standard(),
425            )
426            .map(|(v, _)| v)
427            {
428                edges.push(edge);
429                if edges.len() >= limit {
430                    break;
431                }
432            }
433        }
434
435        // Sort by creation time (newest first)
436        edges.sort_by(|a, b| b.created_at.cmp(&a.created_at));
437        Ok(edges)
438    }
439
440    // =========================================================================
441    // BRANCH MANAGEMENT
442    // =========================================================================
443
444    /// Store a branch
445    pub fn store_branch(&self, user_id: &str, branch: &LineageBranch) -> Result<()> {
446        let key = format!("lineage:branches:{}:{}", user_id, branch.id);
447        let value = bincode::serde::encode_to_vec(branch, bincode::config::standard())?;
448        self.db.put(key.as_bytes(), &value)?;
449        Ok(())
450    }
451
452    /// Get a branch by ID
453    pub fn get_branch(&self, user_id: &str, branch_id: &str) -> Result<Option<LineageBranch>> {
454        let key = format!("lineage:branches:{}:{}", user_id, branch_id);
455        match self.db.get(key.as_bytes())? {
456            Some(data) => {
457                let (branch, _): (LineageBranch, _) =
458                    bincode::serde::decode_from_slice(&data, bincode::config::standard())?;
459                Ok(Some(branch))
460            }
461            None => Ok(None),
462        }
463    }
464
465    /// List all branches for a user
466    pub fn list_branches(&self, user_id: &str) -> Result<Vec<LineageBranch>> {
467        let prefix = format!("lineage:branches:{}:", user_id);
468        let mut branches = Vec::new();
469
470        let iter = self.db.iterator(IteratorMode::From(
471            prefix.as_bytes(),
472            rocksdb::Direction::Forward,
473        ));
474
475        for item in iter {
476            let (key, value) = item?;
477            let key_str = String::from_utf8_lossy(&key);
478
479            if !key_str.starts_with(&prefix) {
480                break;
481            }
482
483            if let Ok(branch) = bincode::serde::decode_from_slice::<LineageBranch, _>(
484                &value,
485                bincode::config::standard(),
486            )
487            .map(|(v, _)| v)
488            {
489                branches.push(branch);
490            }
491        }
492
493        // Sort by creation time (newest first)
494        branches.sort_by(|a, b| b.created_at.cmp(&a.created_at));
495        Ok(branches)
496    }
497
498    /// Create a new branch from current state
499    pub fn create_branch(
500        &self,
501        user_id: &str,
502        name: &str,
503        parent_branch: &str,
504        branch_point: MemoryId,
505        description: Option<&str>,
506    ) -> Result<LineageBranch> {
507        let branch = LineageBranch::new(name, parent_branch, branch_point, description);
508        self.store_branch(user_id, &branch)?;
509        Ok(branch)
510    }
511
512    /// Ensure main branch exists for user
513    pub fn ensure_main_branch(&self, user_id: &str) -> Result<()> {
514        if self.get_branch(user_id, "main")?.is_none() {
515            self.store_branch(user_id, &LineageBranch::main())?;
516        }
517        Ok(())
518    }
519
520    // =========================================================================
521    // LINEAGE INFERENCE ENGINE
522    // =========================================================================
523
524    /// Infer causal relationship between two memories
525    pub fn infer_relation(&self, from: &Memory, to: &Memory) -> Option<(CausalRelation, f32)> {
526        // Must be in temporal order (from before to)
527        if from.created_at >= to.created_at {
528            return None;
529        }
530
531        // Check temporal gap
532        let gap = to.created_at.signed_duration_since(from.created_at);
533        if gap.num_days() > self.config.max_temporal_gap_days {
534            return None;
535        }
536
537        // Calculate entity overlap using experience.entities (tags)
538        let overlap =
539            Self::calculate_entity_overlap(&from.experience.entities, &to.experience.entities);
540        if overlap < self.config.min_entity_overlap {
541            return None;
542        }
543
544        // Infer based on memory types
545        let (relation, base_confidence) = self.infer_by_types(
546            &from.experience.experience_type,
547            &to.experience.experience_type,
548        )?;
549
550        // Adjust confidence based on entity overlap and temporal proximity
551        let temporal_factor =
552            1.0 - (gap.num_days() as f32 / self.config.max_temporal_gap_days as f32);
553        let confidence = base_confidence * overlap * (0.5 + 0.5 * temporal_factor);
554
555        Some((relation, confidence))
556    }
557
558    /// Infer relation based on memory types
559    fn infer_by_types(
560        &self,
561        from_type: &ExperienceType,
562        to_type: &ExperienceType,
563    ) -> Option<(CausalRelation, f32)> {
564        use ExperienceType::*;
565
566        match (from_type, to_type) {
567            // Error → Todo = Caused
568            (Error, Task) => Some((
569                CausalRelation::Caused,
570                *self
571                    .config
572                    .relation_confidence
573                    .get(&CausalRelation::Caused)
574                    .unwrap_or(&0.8),
575            )),
576
577            // Todo → Learning = ResolvedBy (when todo leads to learning)
578            (Task, Learning) => Some((
579                CausalRelation::ResolvedBy,
580                *self
581                    .config
582                    .relation_confidence
583                    .get(&CausalRelation::ResolvedBy)
584                    .unwrap_or(&0.85),
585            )),
586
587            // Learning → Decision = InformedBy
588            (Learning, Decision) | (Discovery, Decision) => Some((
589                CausalRelation::InformedBy,
590                *self
591                    .config
592                    .relation_confidence
593                    .get(&CausalRelation::InformedBy)
594                    .unwrap_or(&0.7),
595            )),
596
597            // Decision → Decision = SupersededBy (newer supersedes older)
598            (Decision, Decision) => Some((
599                CausalRelation::SupersededBy,
600                *self
601                    .config
602                    .relation_confidence
603                    .get(&CausalRelation::SupersededBy)
604                    .unwrap_or(&0.6),
605            )),
606
607            // Discovery/Learning → Todo = TriggeredBy
608            (Discovery, Task) | (Learning, Task) => Some((
609                CausalRelation::TriggeredBy,
610                *self
611                    .config
612                    .relation_confidence
613                    .get(&CausalRelation::TriggeredBy)
614                    .unwrap_or(&0.75),
615            )),
616
617            // Pattern → Learning = InformedBy (patterns inform learnings)
618            (Pattern, Learning) | (Pattern, Decision) => Some((
619                CausalRelation::InformedBy,
620                *self
621                    .config
622                    .relation_confidence
623                    .get(&CausalRelation::InformedBy)
624                    .unwrap_or(&0.7),
625            )),
626
627            // Error → Learning = ResolvedBy (error led to learning)
628            (Error, Learning) => Some((
629                CausalRelation::ResolvedBy,
630                *self
631                    .config
632                    .relation_confidence
633                    .get(&CausalRelation::ResolvedBy)
634                    .unwrap_or(&0.85),
635            )),
636
637            // Observation → Discovery = TriggeredBy
638            (Observation, Discovery) => Some((
639                CausalRelation::TriggeredBy,
640                *self
641                    .config
642                    .relation_confidence
643                    .get(&CausalRelation::TriggeredBy)
644                    .unwrap_or(&0.75),
645            )),
646
647            // Default: RelatedTo if same type or generic relation
648            _ => {
649                // Only suggest RelatedTo for semantically related types
650                if Self::are_types_related(from_type, to_type) {
651                    Some((
652                        CausalRelation::RelatedTo,
653                        *self
654                            .config
655                            .relation_confidence
656                            .get(&CausalRelation::RelatedTo)
657                            .unwrap_or(&0.5),
658                    ))
659                } else {
660                    None
661                }
662            }
663        }
664    }
665
666    /// Check if two experience types are semantically related
667    fn are_types_related(a: &ExperienceType, b: &ExperienceType) -> bool {
668        use ExperienceType::*;
669
670        // Same type is always related
671        if std::mem::discriminant(a) == std::mem::discriminant(b) {
672            return true;
673        }
674
675        // Define related type groups
676        let knowledge_types = [Learning, Discovery, Pattern, Observation];
677        let action_types = [Task, Decision, Command, CodeEdit];
678        let context_types = [Context, Conversation, FileAccess, Search];
679
680        let in_knowledge = |t: &ExperienceType| {
681            knowledge_types
682                .iter()
683                .any(|k| std::mem::discriminant(k) == std::mem::discriminant(t))
684        };
685        let in_action = |t: &ExperienceType| {
686            action_types
687                .iter()
688                .any(|k| std::mem::discriminant(k) == std::mem::discriminant(t))
689        };
690        let in_context = |t: &ExperienceType| {
691            context_types
692                .iter()
693                .any(|k| std::mem::discriminant(k) == std::mem::discriminant(t))
694        };
695
696        // Types in same group are related
697        (in_knowledge(a) && in_knowledge(b))
698            || (in_action(a) && in_action(b))
699            || (in_context(a) && in_context(b))
700    }
701
702    /// Calculate entity overlap using Jaccard similarity
703    fn calculate_entity_overlap(tags_a: &[String], tags_b: &[String]) -> f32 {
704        if tags_a.is_empty() && tags_b.is_empty() {
705            return 0.0;
706        }
707
708        let set_a: HashSet<&str> = tags_a.iter().map(|s| s.as_str()).collect();
709        let set_b: HashSet<&str> = tags_b.iter().map(|s| s.as_str()).collect();
710
711        let intersection = set_a.intersection(&set_b).count();
712        let union = set_a.union(&set_b).count();
713
714        if union == 0 {
715            0.0
716        } else {
717            intersection as f32 / union as f32
718        }
719    }
720
721    /// Detect branch point from memory content (pivot language)
722    pub fn detect_branch_signal(content: &str) -> bool {
723        let pivot_signals = [
724            "actually",
725            "instead",
726            "pivot",
727            "change direction",
728            "new approach",
729            "scrap",
730            "start fresh",
731            "rewrite",
732            "rethink",
733            "different strategy",
734        ];
735
736        let content_lower = content.to_lowercase();
737        pivot_signals.iter().any(|s| content_lower.contains(s))
738    }
739
740    // =========================================================================
741    // LINEAGE TRAVERSAL
742    // =========================================================================
743
744    /// Trace lineage from a memory
745    pub fn trace(
746        &self,
747        user_id: &str,
748        memory_id: &MemoryId,
749        direction: TraceDirection,
750        max_depth: usize,
751    ) -> Result<LineageTrace> {
752        let mut visited = HashSet::new();
753        let mut edges = Vec::new();
754        let mut path = vec![memory_id.clone()];
755        let mut queue: VecDeque<(MemoryId, usize)> = VecDeque::new();
756
757        queue.push_back((memory_id.clone(), 0));
758        visited.insert(memory_id.clone());
759
760        while let Some((current_id, depth)) = queue.pop_front() {
761            if depth >= max_depth {
762                continue;
763            }
764
765            let next_edges = match direction {
766                TraceDirection::Backward => self.get_edges_to(user_id, &current_id)?,
767                TraceDirection::Forward => self.get_edges_from(user_id, &current_id)?,
768                TraceDirection::Both => {
769                    let mut all = self.get_edges_to(user_id, &current_id)?;
770                    all.extend(self.get_edges_from(user_id, &current_id)?);
771                    all
772                }
773            };
774
775            for edge in next_edges {
776                let next_id = match direction {
777                    TraceDirection::Backward => edge.from.clone(),
778                    TraceDirection::Forward => edge.to.clone(),
779                    TraceDirection::Both => {
780                        if edge.from == current_id {
781                            edge.to.clone()
782                        } else {
783                            edge.from.clone()
784                        }
785                    }
786                };
787
788                if !visited.contains(&next_id) {
789                    visited.insert(next_id.clone());
790                    path.push(next_id.clone());
791                    edges.push(edge);
792                    queue.push_back((next_id, depth + 1));
793                }
794            }
795        }
796
797        let depth = path.len().saturating_sub(1);
798        Ok(LineageTrace {
799            root: memory_id.clone(),
800            direction,
801            edges,
802            path,
803            depth,
804        })
805    }
806
807    /// Find the root cause of a memory (trace all the way back)
808    pub fn find_root_cause(&self, user_id: &str, memory_id: &MemoryId) -> Result<Option<MemoryId>> {
809        let trace = self.trace(user_id, memory_id, TraceDirection::Backward, 100)?;
810        Ok(trace.path.last().cloned())
811    }
812
813    /// Find all effects of a memory (trace all the way forward)
814    pub fn find_effects(
815        &self,
816        user_id: &str,
817        memory_id: &MemoryId,
818        max_depth: usize,
819    ) -> Result<Vec<MemoryId>> {
820        let trace = self.trace(user_id, memory_id, TraceDirection::Forward, max_depth)?;
821        Ok(trace.path)
822    }
823
824    // =========================================================================
825    // USER OPERATIONS
826    // =========================================================================
827
828    /// Confirm an inferred edge
829    pub fn confirm_edge(&self, user_id: &str, edge_id: &str) -> Result<bool> {
830        if let Some(mut edge) = self.get_edge(user_id, edge_id)? {
831            edge.confirm();
832            self.store_edge(user_id, &edge)?;
833            Ok(true)
834        } else {
835            Ok(false)
836        }
837    }
838
839    /// Reject (delete) an inferred edge
840    pub fn reject_edge(&self, user_id: &str, edge_id: &str) -> Result<bool> {
841        self.delete_edge(user_id, edge_id)
842    }
843
844    /// Add an explicit edge
845    pub fn add_explicit_edge(
846        &self,
847        user_id: &str,
848        from: MemoryId,
849        to: MemoryId,
850        relation: CausalRelation,
851    ) -> Result<LineageEdge> {
852        let edge = LineageEdge::explicit(from, to, relation);
853        self.store_edge(user_id, &edge)?;
854        Ok(edge)
855    }
856
857    /// Check if an edge already exists between two memories
858    pub fn edge_exists(&self, user_id: &str, from: &MemoryId, to: &MemoryId) -> Result<bool> {
859        let edges = self.get_edges_from(user_id, from)?;
860        Ok(edges.iter().any(|e| &e.to == to))
861    }
862
863    // =========================================================================
864    // STATISTICS
865    // =========================================================================
866
867    /// Get lineage statistics for a user
868    pub fn stats(&self, user_id: &str) -> Result<LineageStats> {
869        let edges = self.list_edges(user_id, 10000)?;
870        let branches = self.list_branches(user_id)?;
871
872        let mut stats = LineageStats {
873            total_edges: edges.len(),
874            total_branches: branches.len(),
875            active_branches: branches.iter().filter(|b| b.active).count(),
876            ..Default::default()
877        };
878
879        let mut total_confidence: f32 = 0.0;
880
881        for edge in &edges {
882            match edge.source {
883                LineageSource::Inferred => stats.inferred_edges += 1,
884                LineageSource::Confirmed => stats.confirmed_edges += 1,
885                LineageSource::Explicit => stats.explicit_edges += 1,
886            }
887
888            let relation_name = format!("{:?}", edge.relation);
889            *stats.edges_by_relation.entry(relation_name).or_insert(0) += 1;
890
891            total_confidence += edge.confidence;
892        }
893
894        if !edges.is_empty() {
895            stats.avg_confidence = total_confidence / edges.len() as f32;
896        }
897
898        Ok(stats)
899    }
900}
901
902// =========================================================================
903// POST-MORTEM GENERATION
904// =========================================================================
905
906/// Summary of learnings from a completed task
907#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct PostMortem {
909    /// The completed task/todo memory ID
910    pub task_id: MemoryId,
911    /// Summary of what was accomplished
912    pub summary: String,
913    /// Learnings extracted from the task chain
914    pub learnings: Vec<String>,
915    /// Decisions made during this task
916    pub decisions: Vec<String>,
917    /// Errors encountered and resolved
918    pub errors_resolved: Vec<String>,
919    /// Patterns discovered
920    pub patterns: Vec<String>,
921    /// Related memory IDs in the lineage
922    pub related_memories: Vec<MemoryId>,
923    /// When the post-mortem was generated
924    pub generated_at: DateTime<Utc>,
925}
926
927impl PostMortem {
928    /// Create a new post-mortem from a lineage trace
929    pub fn from_trace(
930        task_id: MemoryId,
931        task_content: &str,
932        trace: &LineageTrace,
933        memories: &HashMap<MemoryId, Memory>,
934    ) -> Self {
935        let mut learnings = Vec::new();
936        let mut decisions = Vec::new();
937        let mut errors_resolved = Vec::new();
938        let mut patterns = Vec::new();
939
940        for mem_id in &trace.path {
941            if let Some(memory) = memories.get(mem_id) {
942                match memory.experience.experience_type {
943                    ExperienceType::Learning => {
944                        learnings.push(memory.experience.content.clone());
945                    }
946                    ExperienceType::Decision => {
947                        decisions.push(memory.experience.content.clone());
948                    }
949                    ExperienceType::Error => {
950                        errors_resolved.push(memory.experience.content.clone());
951                    }
952                    ExperienceType::Pattern => {
953                        patterns.push(memory.experience.content.clone());
954                    }
955                    _ => {}
956                }
957            }
958        }
959
960        PostMortem {
961            task_id,
962            summary: format!("Completed: {}", task_content),
963            learnings,
964            decisions,
965            errors_resolved,
966            patterns,
967            related_memories: trace.path.clone(),
968            generated_at: Utc::now(),
969        }
970    }
971
972    /// Format as markdown summary
973    pub fn to_markdown(&self) -> String {
974        let mut md = String::new();
975
976        md.push_str("# Project Growth Summary\n\n");
977        md.push_str(&format!("**{}**\n\n", self.summary));
978        md.push_str(&format!(
979            "Generated: {}\n\n",
980            self.generated_at.format("%Y-%m-%d %H:%M UTC")
981        ));
982
983        if !self.learnings.is_empty() {
984            md.push_str("## Learnings\n");
985            for learning in &self.learnings {
986                md.push_str(&format!("- {}\n", learning));
987            }
988            md.push('\n');
989        }
990
991        if !self.decisions.is_empty() {
992            md.push_str("## Decisions Made\n");
993            for decision in &self.decisions {
994                md.push_str(&format!("- {}\n", decision));
995            }
996            md.push('\n');
997        }
998
999        if !self.errors_resolved.is_empty() {
1000            md.push_str("## Errors Resolved\n");
1001            for error in &self.errors_resolved {
1002                md.push_str(&format!("- {}\n", error));
1003            }
1004            md.push('\n');
1005        }
1006
1007        if !self.patterns.is_empty() {
1008            md.push_str("## Patterns Discovered\n");
1009            for pattern in &self.patterns {
1010                md.push_str(&format!("- {}\n", pattern));
1011            }
1012            md.push('\n');
1013        }
1014
1015        md.push_str(&format!(
1016            "---\n*Related memories: {}*\n",
1017            self.related_memories.len()
1018        ));
1019
1020        md
1021    }
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026    use super::*;
1027    use crate::memory::types::Experience;
1028    use chrono::Duration;
1029    use tempfile::TempDir;
1030
1031    fn create_test_graph() -> (LineageGraph, TempDir) {
1032        let temp_dir = TempDir::new().unwrap();
1033        let db = Arc::new(DB::open_default(temp_dir.path()).unwrap());
1034        (LineageGraph::new(db), temp_dir)
1035    }
1036
1037    fn create_test_memory(exp_type: ExperienceType, entities: Vec<&str>) -> Memory {
1038        let experience = Experience {
1039            experience_type: exp_type,
1040            content: "Test memory".to_string(),
1041            entities: entities.into_iter().map(|s| s.to_string()).collect(),
1042            ..Default::default()
1043        };
1044        Memory::new(
1045            MemoryId(Uuid::new_v4()),
1046            experience,
1047            0.5,  // importance
1048            None, // agent_id
1049            None, // run_id
1050            None, // actor_id
1051            None, // created_at (uses Utc::now())
1052        )
1053    }
1054
1055    #[test]
1056    fn test_store_and_get_edge() {
1057        let (graph, _dir) = create_test_graph();
1058        let from = MemoryId(Uuid::new_v4());
1059        let to = MemoryId(Uuid::new_v4());
1060
1061        let edge = LineageEdge::explicit(from.clone(), to.clone(), CausalRelation::Caused);
1062        graph.store_edge("user-1", &edge).unwrap();
1063
1064        let retrieved = graph.get_edge("user-1", &edge.id).unwrap();
1065        assert!(retrieved.is_some());
1066        assert_eq!(retrieved.unwrap().relation, CausalRelation::Caused);
1067    }
1068
1069    #[test]
1070    fn test_get_edges_from_and_to() {
1071        let (graph, _dir) = create_test_graph();
1072        let from = MemoryId(Uuid::new_v4());
1073        let to1 = MemoryId(Uuid::new_v4());
1074        let to2 = MemoryId(Uuid::new_v4());
1075
1076        let edge1 = LineageEdge::explicit(from.clone(), to1.clone(), CausalRelation::Caused);
1077        let edge2 = LineageEdge::explicit(from.clone(), to2.clone(), CausalRelation::TriggeredBy);
1078
1079        graph.store_edge("user-1", &edge1).unwrap();
1080        graph.store_edge("user-1", &edge2).unwrap();
1081
1082        let from_edges = graph.get_edges_from("user-1", &from).unwrap();
1083        assert_eq!(from_edges.len(), 2);
1084
1085        let to_edges = graph.get_edges_to("user-1", &to1).unwrap();
1086        assert_eq!(to_edges.len(), 1);
1087    }
1088
1089    #[test]
1090    fn test_infer_error_to_task() {
1091        let (graph, _dir) = create_test_graph();
1092
1093        // Use same entities for high overlap
1094        let error = create_test_memory(ExperienceType::Error, vec!["auth", "login"]);
1095        let mut task = create_test_memory(ExperienceType::Task, vec!["auth", "login"]);
1096        task.created_at = error.created_at + Duration::days(1);
1097
1098        let result = graph.infer_relation(&error, &task);
1099        assert!(result.is_some());
1100        let (relation, confidence) = result.unwrap();
1101        assert_eq!(relation, CausalRelation::Caused);
1102        // With perfect overlap (1.0) and 1 day gap: 0.8 * 1.0 * 0.93 ≈ 0.74
1103        assert!(confidence > 0.4, "confidence was {}", confidence);
1104    }
1105
1106    #[test]
1107    fn test_infer_learning_to_decision() {
1108        let (graph, _dir) = create_test_graph();
1109
1110        let learning = create_test_memory(ExperienceType::Learning, vec!["react", "hooks"]);
1111        let mut decision =
1112            create_test_memory(ExperienceType::Decision, vec!["react", "hooks", "state"]);
1113        decision.created_at = learning.created_at + Duration::days(2);
1114
1115        let result = graph.infer_relation(&learning, &decision);
1116        assert!(result.is_some());
1117        let (relation, _) = result.unwrap();
1118        assert_eq!(relation, CausalRelation::InformedBy);
1119    }
1120
1121    #[test]
1122    fn test_no_inference_wrong_order() {
1123        let (graph, _dir) = create_test_graph();
1124
1125        let task = create_test_memory(ExperienceType::Task, vec!["auth"]);
1126        let mut error = create_test_memory(ExperienceType::Error, vec!["auth"]);
1127        error.created_at = task.created_at - Duration::days(1); // Error BEFORE task
1128
1129        // Task to Error (wrong causal direction) should not infer Caused
1130        let result = graph.infer_relation(&task, &error);
1131        assert!(result.is_none());
1132    }
1133
1134    #[test]
1135    fn test_branch_creation() {
1136        let (graph, _dir) = create_test_graph();
1137        let branch_point = MemoryId(Uuid::new_v4());
1138
1139        graph.ensure_main_branch("user-1").unwrap();
1140
1141        let branch = graph
1142            .create_branch(
1143                "user-1",
1144                "v2-rewrite",
1145                "main",
1146                branch_point,
1147                Some("Complete rewrite"),
1148            )
1149            .unwrap();
1150
1151        let retrieved = graph.get_branch("user-1", &branch.id).unwrap();
1152        assert!(retrieved.is_some());
1153        assert_eq!(retrieved.unwrap().name, "v2-rewrite");
1154    }
1155
1156    #[test]
1157    fn test_detect_branch_signal() {
1158        assert!(LineageGraph::detect_branch_signal(
1159            "Let's pivot to a new approach"
1160        ));
1161        assert!(LineageGraph::detect_branch_signal(
1162            "Actually, we should rewrite this"
1163        ));
1164        assert!(LineageGraph::detect_branch_signal(
1165            "I think we need to start fresh"
1166        ));
1167        assert!(!LineageGraph::detect_branch_signal("Fixed the bug in auth"));
1168    }
1169
1170    #[test]
1171    fn test_confirm_and_reject_edge() {
1172        let (graph, _dir) = create_test_graph();
1173        let from = MemoryId(Uuid::new_v4());
1174        let to = MemoryId(Uuid::new_v4());
1175
1176        let edge = LineageEdge::inferred(from.clone(), to.clone(), CausalRelation::Caused, 0.7);
1177        graph.store_edge("user-1", &edge).unwrap();
1178
1179        // Confirm
1180        assert!(graph.confirm_edge("user-1", &edge.id).unwrap());
1181        let confirmed = graph.get_edge("user-1", &edge.id).unwrap().unwrap();
1182        assert_eq!(confirmed.source, LineageSource::Confirmed);
1183        assert_eq!(confirmed.confidence, 1.0);
1184
1185        // Reject another edge
1186        let edge2 = LineageEdge::inferred(from, to, CausalRelation::RelatedTo, 0.5);
1187        graph.store_edge("user-1", &edge2).unwrap();
1188        assert!(graph.reject_edge("user-1", &edge2.id).unwrap());
1189        assert!(graph.get_edge("user-1", &edge2.id).unwrap().is_none());
1190    }
1191
1192    #[test]
1193    fn test_lineage_stats() {
1194        let (graph, _dir) = create_test_graph();
1195
1196        let from = MemoryId(Uuid::new_v4());
1197        let to = MemoryId(Uuid::new_v4());
1198
1199        graph
1200            .store_edge(
1201                "user-1",
1202                &LineageEdge::inferred(from.clone(), to.clone(), CausalRelation::Caused, 0.8),
1203            )
1204            .unwrap();
1205        graph
1206            .store_edge(
1207                "user-1",
1208                &LineageEdge::explicit(from.clone(), to.clone(), CausalRelation::InformedBy),
1209            )
1210            .unwrap();
1211        graph.ensure_main_branch("user-1").unwrap();
1212
1213        let stats = graph.stats("user-1").unwrap();
1214        assert_eq!(stats.total_edges, 2);
1215        assert_eq!(stats.inferred_edges, 1);
1216        assert_eq!(stats.explicit_edges, 1);
1217        assert_eq!(stats.total_branches, 1);
1218    }
1219}