1use std::io::Write;
4use std::path::{Path, PathBuf};
5
6use mentedb_core::edge::MemoryEdge;
7use mentedb_core::error::{MenteError, MenteResult};
8use mentedb_core::types::MemoryId;
9use parking_lot::{Mutex, RwLock};
10use serde::{Deserialize, Serialize};
11use tracing::warn;
12
13use crate::belief::{PropagationConfig, propagate_delta, propagate_update};
14use crate::contradiction::find_contradictions;
15use crate::csr::CsrGraph;
16use crate::traversal::extract_subgraph;
17
18#[derive(Debug, Serialize, Deserialize)]
20enum LogRecord {
21 AddNode(MemoryId),
22 RemoveNode(MemoryId),
23 AddEdge(MemoryEdge),
24}
25
26struct EdgeLog {
32 file: std::fs::File,
33}
34
35const EDGE_LOG_FILE: &str = "edges.jsonl";
36
37impl EdgeLog {
38 fn append(&mut self, record: &LogRecord) -> MenteResult<()> {
39 let mut line =
40 serde_json::to_vec(record).map_err(|e| MenteError::Serialization(e.to_string()))?;
41 line.push(b'\n');
42 self.file.write_all(&line)?;
43 self.file.sync_data()?;
44 Ok(())
45 }
46}
47
48pub struct GraphManager {
52 graph: RwLock<CsrGraph>,
53 log: Mutex<Option<EdgeLog>>,
56 dir: Option<PathBuf>,
57}
58
59impl GraphManager {
60 pub fn new() -> Self {
62 Self {
63 graph: RwLock::new(CsrGraph::new()),
64 log: Mutex::new(None),
65 dir: None,
66 }
67 }
68
69 pub fn open(dir: &Path) -> MenteResult<Self> {
75 std::fs::create_dir_all(dir)?;
76
77 let snapshot_path = dir.join("graph.json");
78 let mut graph = if snapshot_path.exists() {
79 CsrGraph::load(&snapshot_path)?
80 } else {
81 CsrGraph::new()
82 };
83
84 let log_path = dir.join(EDGE_LOG_FILE);
85 if log_path.exists() {
86 let contents = std::fs::read_to_string(&log_path)?;
87 let lines: Vec<&str> = contents.lines().collect();
88 for (i, line) in lines.iter().enumerate() {
89 if line.trim().is_empty() {
90 continue;
91 }
92 match serde_json::from_str::<LogRecord>(line) {
93 Ok(LogRecord::AddNode(id)) => {
94 graph.add_node(id);
95 }
96 Ok(LogRecord::RemoveNode(id)) => {
97 graph.remove_node(id);
98 }
99 Ok(LogRecord::AddEdge(edge)) => {
100 graph.add_node(edge.source);
101 graph.add_node(edge.target);
102 graph.add_edge(&edge);
103 }
104 Err(e) => {
105 if i + 1 == lines.len() {
108 warn!("dropping torn final edge log line: {e}");
109 } else {
110 return Err(MenteError::Serialization(format!(
111 "corrupt edge log line {}: {e}",
112 i + 1
113 )));
114 }
115 }
116 }
117 }
118 }
119
120 let file = std::fs::OpenOptions::new()
121 .create(true)
122 .append(true)
123 .open(&log_path)?;
124
125 Ok(Self {
126 graph: RwLock::new(graph),
127 log: Mutex::new(Some(EdgeLog { file })),
128 dir: Some(dir.to_path_buf()),
129 })
130 }
131
132 pub fn save(&self, dir: &Path) -> MenteResult<()> {
134 std::fs::create_dir_all(dir)?;
135 self.graph.read().save(&dir.join("graph.json"))?;
136
137 let mut log = self.log.lock();
139 if log.is_some() && self.dir.as_deref() == Some(dir) {
140 let log_path = dir.join(EDGE_LOG_FILE);
141 let file = std::fs::OpenOptions::new()
142 .create(true)
143 .write(true)
144 .truncate(true)
145 .open(&log_path)?;
146 file.sync_data()?;
147 drop(file);
148 let file = std::fs::OpenOptions::new()
149 .create(true)
150 .append(true)
151 .open(&log_path)?;
152 *log = Some(EdgeLog { file });
153 }
154 Ok(())
155 }
156
157 pub fn load(dir: &Path) -> MenteResult<Self> {
161 let graph = CsrGraph::load(&dir.join("graph.json"))?;
162 Ok(Self {
163 graph: RwLock::new(graph),
164 log: Mutex::new(None),
165 dir: None,
166 })
167 }
168
169 fn log_record(&self, record: &LogRecord) {
170 if let Some(log) = self.log.lock().as_mut()
171 && let Err(e) = log.append(record)
172 {
173 warn!("failed to append to graph edge log: {e}");
174 }
175 }
176
177 pub fn add_memory(&self, id: MemoryId) {
179 let is_new = {
180 let mut g = self.graph.write();
181 let existed = g.contains_node(id);
182 g.add_node(id);
183 !existed
184 };
185 if is_new {
186 self.log_record(&LogRecord::AddNode(id));
187 }
188 }
189
190 pub fn remove_memory(&self, id: MemoryId) {
192 self.graph.write().remove_node(id);
193 self.log_record(&LogRecord::RemoveNode(id));
194 }
195
196 pub fn add_relationship(&self, edge: &MemoryEdge) -> MenteResult<()> {
198 {
199 let mut g = self.graph.write();
200 if !g.contains_node(edge.source) {
201 return Err(MenteError::MemoryNotFound(edge.source));
202 }
203 if !g.contains_node(edge.target) {
204 return Err(MenteError::MemoryNotFound(edge.target));
205 }
206 g.add_edge(edge);
207 }
208 self.log_record(&LogRecord::AddEdge(edge.clone()));
209 Ok(())
210 }
211
212 pub fn get_context_subgraph(
214 &self,
215 center: MemoryId,
216 depth: usize,
217 ) -> (Vec<MemoryId>, Vec<MemoryEdge>) {
218 extract_subgraph(&self.graph.read(), center, depth)
219 }
220
221 pub fn propagate_belief_change(
223 &self,
224 id: MemoryId,
225 new_confidence: f32,
226 ) -> Vec<(MemoryId, f32)> {
227 propagate_update(&self.graph.read(), id, new_confidence)
228 }
229
230 pub fn propagate_belief_delta(
234 &self,
235 root: MemoryId,
236 delta: f32,
237 config: &PropagationConfig,
238 ) -> Vec<(MemoryId, f32)> {
239 propagate_delta(&self.graph.read(), root, delta, config)
240 }
241
242 pub fn find_all_contradictions(&self, id: MemoryId) -> Vec<MemoryId> {
244 find_contradictions(&self.graph.read(), id)
245 }
246
247 pub fn compact(&self) {
249 self.graph.write().compact();
250 }
251
252 pub fn remove_edges_of_types(&self, types: &[mentedb_core::edge::EdgeType]) -> usize {
260 self.graph.write().remove_edges_of_types(types)
261 }
262
263 pub fn strengthen_edge(&self, source: MemoryId, target: MemoryId, delta: f32) {
265 self.graph.write().strengthen_edge(source, target, delta);
266 }
267
268 pub fn read_graph(&self) -> parking_lot::RwLockReadGuard<'_, CsrGraph> {
272 self.graph.read()
273 }
274
275 pub fn graph(&self) -> parking_lot::RwLockReadGuard<'_, CsrGraph> {
277 self.graph.read()
278 }
279
280 pub fn conflict_edge_counts(&self) -> (usize, usize) {
284 self.graph.read().conflict_edge_counts()
285 }
286
287 pub fn conflict_edges(
291 &self,
292 ) -> Vec<(
293 mentedb_core::types::MemoryId,
294 mentedb_core::types::MemoryId,
295 mentedb_core::edge::EdgeType,
296 f32,
297 )> {
298 self.graph.read().conflict_edges()
299 }
300}
301
302impl Default for GraphManager {
303 fn default() -> Self {
304 Self::new()
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use mentedb_core::edge::EdgeType;
312
313 fn make_edge(src: MemoryId, tgt: MemoryId, etype: EdgeType) -> MemoryEdge {
314 MemoryEdge {
315 source: src,
316 target: tgt,
317 edge_type: etype,
318 weight: 0.8,
319 created_at: 1000,
320 valid_from: None,
321 valid_until: None,
322 label: None,
323 }
324 }
325
326 #[test]
327 fn test_add_memory_and_relationship() {
328 let mgr = GraphManager::new();
329 let a = MemoryId::new();
330 let b = MemoryId::new();
331 mgr.add_memory(a);
332 mgr.add_memory(b);
333 assert!(
334 mgr.add_relationship(&make_edge(a, b, EdgeType::Caused))
335 .is_ok()
336 );
337 }
338
339 #[test]
340 fn test_relationship_missing_node() {
341 let mgr = GraphManager::new();
342 let a = MemoryId::new();
343 let b = MemoryId::new();
344 mgr.add_memory(a);
345 assert!(
347 mgr.add_relationship(&make_edge(a, b, EdgeType::Caused))
348 .is_err()
349 );
350 }
351
352 #[test]
353 fn test_context_subgraph() {
354 let mgr = GraphManager::new();
355 let a = MemoryId::new();
356 let b = MemoryId::new();
357 let c = MemoryId::new();
358 mgr.add_memory(a);
359 mgr.add_memory(b);
360 mgr.add_memory(c);
361 mgr.add_relationship(&make_edge(a, b, EdgeType::Caused))
362 .unwrap();
363 mgr.add_relationship(&make_edge(b, c, EdgeType::Related))
364 .unwrap();
365
366 let (nodes, edges) = mgr.get_context_subgraph(a, 2);
367 assert_eq!(nodes.len(), 3);
368 assert_eq!(edges.len(), 2);
369 }
370
371 #[test]
372 fn test_compact() {
373 let mgr = GraphManager::new();
374 let a = MemoryId::new();
375 let b = MemoryId::new();
376 mgr.add_memory(a);
377 mgr.add_memory(b);
378 mgr.add_relationship(&make_edge(a, b, EdgeType::Caused))
379 .unwrap();
380 mgr.compact();
381
382 let out = mgr.graph().outgoing(a);
383 assert_eq!(out.len(), 1);
384 }
385
386 #[test]
387 fn test_edge_log_replay_without_snapshot() {
388 let dir = tempfile::tempdir().unwrap();
389 let a = MemoryId::new();
390 let b = MemoryId::new();
391 {
392 let mgr = GraphManager::open(dir.path()).unwrap();
393 mgr.add_memory(a);
394 mgr.add_memory(b);
395 mgr.add_relationship(&make_edge(a, b, EdgeType::Caused))
396 .unwrap();
397 }
399 {
400 let mgr = GraphManager::open(dir.path()).unwrap();
401 assert!(mgr.read_graph().contains_node(a));
402 let out = mgr.read_graph().outgoing(a);
403 assert_eq!(out.len(), 1);
404 assert_eq!(out[0].0, b);
405 }
406 }
407
408 #[test]
409 fn test_save_truncates_edge_log() {
410 let dir = tempfile::tempdir().unwrap();
411 let a = MemoryId::new();
412 let b = MemoryId::new();
413 {
414 let mgr = GraphManager::open(dir.path()).unwrap();
415 mgr.add_memory(a);
416 mgr.add_memory(b);
417 mgr.add_relationship(&make_edge(a, b, EdgeType::Related))
418 .unwrap();
419 mgr.save(dir.path()).unwrap();
420 let log_len = std::fs::metadata(dir.path().join(super::EDGE_LOG_FILE))
421 .unwrap()
422 .len();
423 assert_eq!(log_len, 0, "save must truncate the edge log");
424 }
425 {
426 let mgr = GraphManager::open(dir.path()).unwrap();
428 assert_eq!(mgr.read_graph().outgoing(a).len(), 1);
429 let c = MemoryId::new();
430 mgr.add_memory(c);
431 mgr.add_relationship(&make_edge(a, c, EdgeType::Supports))
432 .unwrap();
433 }
434 {
435 let mgr = GraphManager::open(dir.path()).unwrap();
436 assert_eq!(
437 mgr.read_graph().outgoing(a).len(),
438 2,
439 "post-save mutations must replay from the fresh log"
440 );
441 }
442 }
443
444 #[test]
445 fn test_belief_propagation() {
446 let mgr = GraphManager::new();
447 let a = MemoryId::new();
448 let b = MemoryId::new();
449 mgr.add_memory(a);
450 mgr.add_memory(b);
451 mgr.add_relationship(&MemoryEdge {
452 source: a,
453 target: b,
454 edge_type: EdgeType::Caused,
455 weight: 1.0,
456 created_at: 1000,
457 valid_from: None,
458 valid_until: None,
459 label: None,
460 })
461 .unwrap();
462
463 let results = mgr.propagate_belief_change(a, 0.5);
464 assert!(results.len() >= 2);
465 }
466}