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    /// Semantic label describing the relationship (e.g. "owns", "attends", "uses daily").
51    /// None for edges without a specific semantic meaning.
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub label: Option<String>,
54}
55
56impl MemoryEdge {
57    /// Returns true if this edge is temporally valid at the given timestamp.
58    /// An edge with no validity bounds is always valid.
59    pub fn is_valid_at(&self, at: Timestamp) -> bool {
60        let from = self.valid_from.unwrap_or(0);
61        match self.valid_until {
62            Some(until) => at >= from && at < until,
63            None => at >= from,
64        }
65    }
66
67    /// Mark this edge as no longer valid, setting valid_until to the given timestamp.
68    pub fn invalidate(&mut self, at: Timestamp) {
69        self.valid_until = Some(at);
70    }
71
72    /// Returns true if this edge has been invalidated.
73    pub fn is_invalidated(&self) -> bool {
74        self.valid_until.is_some()
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    fn make_edge() -> MemoryEdge {
83        MemoryEdge {
84            source: MemoryId::new(),
85            target: MemoryId::new(),
86            edge_type: EdgeType::Related,
87            weight: 0.8,
88            created_at: 1000,
89            valid_from: None,
90            valid_until: None,
91            label: None,
92        }
93    }
94
95    #[test]
96    fn unbounded_edge_always_valid() {
97        let edge = make_edge();
98        assert!(edge.is_valid_at(0));
99        assert!(edge.is_valid_at(u64::MAX));
100        assert!(!edge.is_invalidated());
101    }
102
103    #[test]
104    fn edge_with_valid_from() {
105        let mut edge = make_edge();
106        edge.valid_from = Some(5000);
107        assert!(!edge.is_valid_at(4999));
108        assert!(edge.is_valid_at(5000));
109        assert!(edge.is_valid_at(99999));
110    }
111
112    #[test]
113    fn edge_with_valid_until() {
114        let mut edge = make_edge();
115        edge.valid_until = Some(8000);
116        assert!(edge.is_valid_at(0));
117        assert!(edge.is_valid_at(7999));
118        assert!(!edge.is_valid_at(8000));
119        assert!(edge.is_invalidated());
120    }
121
122    #[test]
123    fn edge_with_both_bounds() {
124        let mut edge = make_edge();
125        edge.valid_from = Some(1000);
126        edge.valid_until = Some(5000);
127        assert!(!edge.is_valid_at(999));
128        assert!(edge.is_valid_at(1000));
129        assert!(edge.is_valid_at(3000));
130        assert!(!edge.is_valid_at(5000));
131    }
132
133    #[test]
134    fn invalidate_sets_valid_until() {
135        let mut edge = make_edge();
136        assert!(!edge.is_invalidated());
137        edge.invalidate(9000);
138        assert!(edge.is_invalidated());
139        assert_eq!(edge.valid_until, Some(9000));
140        assert!(edge.is_valid_at(8999));
141        assert!(!edge.is_valid_at(9000));
142    }
143
144    #[test]
145    fn serde_roundtrip_with_no_bounds() {
146        let edge = make_edge();
147        let json = serde_json::to_string(&edge).unwrap();
148        assert!(!json.contains("valid_from"));
149        assert!(!json.contains("valid_until"));
150        let deserialized: MemoryEdge = serde_json::from_str(&json).unwrap();
151        assert_eq!(deserialized.valid_from, None);
152        assert_eq!(deserialized.valid_until, None);
153    }
154
155    #[test]
156    fn serde_roundtrip_with_bounds() {
157        let mut edge = make_edge();
158        edge.valid_from = Some(1000);
159        edge.valid_until = Some(5000);
160        let json = serde_json::to_string(&edge).unwrap();
161        assert!(json.contains("valid_from"));
162        assert!(json.contains("valid_until"));
163        let deserialized: MemoryEdge = serde_json::from_str(&json).unwrap();
164        assert_eq!(deserialized.valid_from, Some(1000));
165        assert_eq!(deserialized.valid_until, Some(5000));
166    }
167
168    #[test]
169    fn deserialize_old_format_without_temporal_fields() {
170        // Simulates loading data saved before temporal fields existed
171        let json = r#"{"source":"00000000-0000-0000-0000-000000000001","target":"00000000-0000-0000-0000-000000000002","edge_type":"Related","weight":0.8,"created_at":1000}"#;
172        let edge: MemoryEdge = serde_json::from_str(json).unwrap();
173        assert_eq!(edge.valid_from, None);
174        assert_eq!(edge.valid_until, None);
175        assert_eq!(edge.label, None);
176        assert!(edge.is_valid_at(5000));
177    }
178
179    #[test]
180    fn edge_with_label() {
181        let mut edge = make_edge();
182        edge.label = Some("owns".to_string());
183        let json = serde_json::to_string(&edge).unwrap();
184        assert!(json.contains("\"label\":\"owns\""));
185        let deserialized: MemoryEdge = serde_json::from_str(&json).unwrap();
186        assert_eq!(deserialized.label, Some("owns".to_string()));
187    }
188}