Skip to main content

mentedb_core/
edge.rs

1//! MemoryEdge: typed, weighted relationships between memories.
2
3use serde::{Deserialize, Serialize};
4
5use crate::types::MemoryId;
6use crate::types::*;
7
8/// The type of relationship between two memories.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum EdgeType {
11    /// Causal: A caused B.
12    Caused,
13    /// Temporal: A happened before B.
14    Before,
15    /// Semantic: A is related to B.
16    Related,
17    /// Contradicts: A conflicts with B.
18    Contradicts,
19    /// Supports: A provides evidence for B.
20    Supports,
21    /// Supersedes: A replaces B (newer information).
22    Supersedes,
23    /// Derived: A was derived/inferred from B.
24    Derived,
25    /// Part of: A is a component of B.
26    PartOf,
27}
28
29/// A directed, typed, weighted edge between two memory nodes.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct MemoryEdge {
32    /// Source memory ID.
33    pub source: MemoryId,
34    /// Target memory ID.
35    pub target: MemoryId,
36    /// Relationship type.
37    pub edge_type: EdgeType,
38    /// Strength of the relationship (0.0 to 1.0).
39    pub weight: f32,
40    /// When this edge was created.
41    pub created_at: Timestamp,
42    /// When this relationship became valid in the real world.
43    /// None means valid since creation.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub valid_from: Option<Timestamp>,
46    /// When this relationship stopped being valid.
47    /// None means still valid.
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub valid_until: Option<Timestamp>,
50}
51
52impl MemoryEdge {
53    /// Returns true if this edge is temporally valid at the given timestamp.
54    /// An edge with no validity bounds is always valid.
55    pub fn is_valid_at(&self, at: Timestamp) -> bool {
56        let from = self.valid_from.unwrap_or(0);
57        match self.valid_until {
58            Some(until) => at >= from && at < until,
59            None => at >= from,
60        }
61    }
62
63    /// Mark this edge as no longer valid, setting valid_until to the given timestamp.
64    pub fn invalidate(&mut self, at: Timestamp) {
65        self.valid_until = Some(at);
66    }
67
68    /// Returns true if this edge has been invalidated.
69    pub fn is_invalidated(&self) -> bool {
70        self.valid_until.is_some()
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    fn make_edge() -> MemoryEdge {
79        MemoryEdge {
80            source: MemoryId::new(),
81            target: MemoryId::new(),
82            edge_type: EdgeType::Related,
83            weight: 0.8,
84            created_at: 1000,
85            valid_from: None,
86            valid_until: None,
87        }
88    }
89
90    #[test]
91    fn unbounded_edge_always_valid() {
92        let edge = make_edge();
93        assert!(edge.is_valid_at(0));
94        assert!(edge.is_valid_at(u64::MAX));
95        assert!(!edge.is_invalidated());
96    }
97
98    #[test]
99    fn edge_with_valid_from() {
100        let mut edge = make_edge();
101        edge.valid_from = Some(5000);
102        assert!(!edge.is_valid_at(4999));
103        assert!(edge.is_valid_at(5000));
104        assert!(edge.is_valid_at(99999));
105    }
106
107    #[test]
108    fn edge_with_valid_until() {
109        let mut edge = make_edge();
110        edge.valid_until = Some(8000);
111        assert!(edge.is_valid_at(0));
112        assert!(edge.is_valid_at(7999));
113        assert!(!edge.is_valid_at(8000));
114        assert!(edge.is_invalidated());
115    }
116
117    #[test]
118    fn edge_with_both_bounds() {
119        let mut edge = make_edge();
120        edge.valid_from = Some(1000);
121        edge.valid_until = Some(5000);
122        assert!(!edge.is_valid_at(999));
123        assert!(edge.is_valid_at(1000));
124        assert!(edge.is_valid_at(3000));
125        assert!(!edge.is_valid_at(5000));
126    }
127
128    #[test]
129    fn invalidate_sets_valid_until() {
130        let mut edge = make_edge();
131        assert!(!edge.is_invalidated());
132        edge.invalidate(9000);
133        assert!(edge.is_invalidated());
134        assert_eq!(edge.valid_until, Some(9000));
135        assert!(edge.is_valid_at(8999));
136        assert!(!edge.is_valid_at(9000));
137    }
138
139    #[test]
140    fn serde_roundtrip_with_no_bounds() {
141        let edge = make_edge();
142        let json = serde_json::to_string(&edge).unwrap();
143        assert!(!json.contains("valid_from"));
144        assert!(!json.contains("valid_until"));
145        let deserialized: MemoryEdge = serde_json::from_str(&json).unwrap();
146        assert_eq!(deserialized.valid_from, None);
147        assert_eq!(deserialized.valid_until, None);
148    }
149
150    #[test]
151    fn serde_roundtrip_with_bounds() {
152        let mut edge = make_edge();
153        edge.valid_from = Some(1000);
154        edge.valid_until = Some(5000);
155        let json = serde_json::to_string(&edge).unwrap();
156        assert!(json.contains("valid_from"));
157        assert!(json.contains("valid_until"));
158        let deserialized: MemoryEdge = serde_json::from_str(&json).unwrap();
159        assert_eq!(deserialized.valid_from, Some(1000));
160        assert_eq!(deserialized.valid_until, Some(5000));
161    }
162
163    #[test]
164    fn deserialize_old_format_without_temporal_fields() {
165        // Simulates loading data saved before temporal fields existed
166        let json = r#"{"source":"00000000-0000-0000-0000-000000000001","target":"00000000-0000-0000-0000-000000000002","edge_type":"Related","weight":0.8,"created_at":1000}"#;
167        let edge: MemoryEdge = serde_json::from_str(json).unwrap();
168        assert_eq!(edge.valid_from, None);
169        assert_eq!(edge.valid_until, None);
170        assert!(edge.is_valid_at(5000));
171    }
172}