1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum CausalRelation {
30 Caused,
32 ResolvedBy,
34 InformedBy,
36 SupersededBy,
38 TriggeredBy,
40 BranchedFrom,
42 RelatedTo,
44}
45
46impl CausalRelation {
47 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, CausalRelation::TriggeredBy => CausalRelation::InformedBy,
55 CausalRelation::BranchedFrom => CausalRelation::BranchedFrom,
56 CausalRelation::RelatedTo => CausalRelation::RelatedTo,
57 }
58 }
59
60 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76pub enum LineageSource {
77 Inferred,
79 Confirmed,
81 Explicit,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct LineageEdge {
88 pub id: String,
90 pub from: MemoryId,
92 pub to: MemoryId,
94 pub relation: CausalRelation,
96 pub confidence: f32,
98 pub source: LineageSource,
100 pub branch_id: Option<String>,
102 pub created_at: DateTime<Utc>,
104 pub last_reinforced: DateTime<Utc>,
106 pub reinforcement_count: u32,
108}
109
110impl LineageEdge {
111 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 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, source: LineageSource::Explicit,
143 branch_id: None,
144 created_at: now,
145 last_reinforced: now,
146 reinforcement_count: 1,
147 }
148 }
149
150 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct LineageBranch {
169 pub id: String,
171 pub name: String,
173 pub description: Option<String>,
175 pub parent_branch: Option<String>,
177 pub branch_point: Option<MemoryId>,
179 pub created_at: DateTime<Utc>,
181 pub active: bool,
183 pub tags: Vec<String>,
185}
186
187impl LineageBranch {
188 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct LineageTrace {
225 pub root: MemoryId,
227 pub direction: TraceDirection,
229 pub edges: Vec<LineageEdge>,
231 pub path: Vec<MemoryId>,
233 pub depth: usize,
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
239pub enum TraceDirection {
240 Backward,
242 Forward,
244 Both,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct InferenceConfig {
251 pub max_temporal_gap_days: i64,
253 pub min_entity_overlap: f32,
255 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#[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
291pub struct LineageGraph {
293 db: Arc<DB>,
294 config: InferenceConfig,
295}
296
297impl LineageGraph {
298 pub fn new(db: Arc<DB>) -> Self {
300 Self {
301 db,
302 config: InferenceConfig::default(),
303 }
304 }
305
306 pub fn with_config(db: Arc<DB>, config: InferenceConfig) -> Self {
308 Self { db, config }
309 }
310
311 pub fn store_edge(&self, user_id: &str, edge: &LineageEdge) -> Result<()> {
317 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 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 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 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 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 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 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 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 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 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 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 edges.sort_by(|a, b| b.created_at.cmp(&a.created_at));
437 Ok(edges)
438 }
439
440 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 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 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 branches.sort_by(|a, b| b.created_at.cmp(&a.created_at));
495 Ok(branches)
496 }
497
498 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 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 pub fn infer_relation(&self, from: &Memory, to: &Memory) -> Option<(CausalRelation, f32)> {
526 if from.created_at >= to.created_at {
528 return None;
529 }
530
531 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 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 let (relation, base_confidence) = self.infer_by_types(
546 &from.experience.experience_type,
547 &to.experience.experience_type,
548 )?;
549
550 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 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, Task) => Some((
569 CausalRelation::Caused,
570 *self
571 .config
572 .relation_confidence
573 .get(&CausalRelation::Caused)
574 .unwrap_or(&0.8),
575 )),
576
577 (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) | (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) => Some((
599 CausalRelation::SupersededBy,
600 *self
601 .config
602 .relation_confidence
603 .get(&CausalRelation::SupersededBy)
604 .unwrap_or(&0.6),
605 )),
606
607 (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) | (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) => 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) => Some((
639 CausalRelation::TriggeredBy,
640 *self
641 .config
642 .relation_confidence
643 .get(&CausalRelation::TriggeredBy)
644 .unwrap_or(&0.75),
645 )),
646
647 _ => {
649 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 fn are_types_related(a: &ExperienceType, b: &ExperienceType) -> bool {
668 use ExperienceType::*;
669
670 if std::mem::discriminant(a) == std::mem::discriminant(b) {
672 return true;
673 }
674
675 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 (in_knowledge(a) && in_knowledge(b))
698 || (in_action(a) && in_action(b))
699 || (in_context(a) && in_context(b))
700 }
701
702 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 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 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, ¤t_id)?,
767 TraceDirection::Forward => self.get_edges_from(user_id, ¤t_id)?,
768 TraceDirection::Both => {
769 let mut all = self.get_edges_to(user_id, ¤t_id)?;
770 all.extend(self.get_edges_from(user_id, ¤t_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 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 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 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 pub fn reject_edge(&self, user_id: &str, edge_id: &str) -> Result<bool> {
841 self.delete_edge(user_id, edge_id)
842 }
843
844 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct PostMortem {
909 pub task_id: MemoryId,
911 pub summary: String,
913 pub learnings: Vec<String>,
915 pub decisions: Vec<String>,
917 pub errors_resolved: Vec<String>,
919 pub patterns: Vec<String>,
921 pub related_memories: Vec<MemoryId>,
923 pub generated_at: DateTime<Utc>,
925}
926
927impl PostMortem {
928 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 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, None, None, None, None, )
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 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 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); 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 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 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}