Skip to main content

hippmem_engine/
traverse_api.rs

1//! Engine::traverse — BFS graph traversal API.
2
3use crate::{EngineResult, TraverseDirection, TraverseInput, TraverseNode, TraverseOutput};
4use hippmem_core::ids::MemoryId;
5use std::collections::{HashMap, HashSet, VecDeque};
6
7impl crate::Engine {
8    /// BFS graph traversal: starts from start_id and explores the memory network along association edges.
9    pub fn traverse(&self, input: TraverseInput) -> EngineResult<TraverseOutput> {
10        let max_depth = input.max_depth.clamp(1, 5);
11
12        let units = crate::retrieve_api::load_all_units(self.store.db_arc());
13        let unit_map: HashMap<MemoryId, &hippmem_core::model::unit::MemoryUnit> =
14            units.iter().map(|u| (u.id, u)).collect();
15
16        // Validate that the start node exists
17        if !unit_map.contains_key(&input.start_id) {
18            return Err(crate::EngineError::NotFound(input.start_id));
19        }
20
21        let mut visited: HashSet<MemoryId> = HashSet::new();
22        let mut nodes: Vec<TraverseNode> = Vec::new();
23        let mut edges: Vec<crate::EdgeView> = Vec::new();
24        let mut queue: VecDeque<(MemoryId, u8)> = VecDeque::new();
25
26        visited.insert(input.start_id);
27        queue.push_back((input.start_id, 0));
28
29        while let Some((current_id, depth)) = queue.pop_front() {
30            if depth >= max_depth {
31                continue;
32            }
33
34            let current_unit = match unit_map.get(&current_id) {
35                Some(u) => *u,
36                None => continue,
37            };
38
39            // Collect candidate edges starting from current
40            let candidate_edges: Vec<(MemoryId, &hippmem_core::model::links::AssociationLink)> =
41                match input.direction {
42                    TraverseDirection::Outgoing => current_unit
43                        .links
44                        .iter()
45                        .map(|l| (l.target_id, l))
46                        .collect(),
47                    TraverseDirection::Incoming => units
48                        .iter()
49                        .flat_map(|other| {
50                            other.links.iter().filter_map(move |l| {
51                                if l.target_id == current_id {
52                                    Some((other.id, l))
53                                } else {
54                                    None
55                                }
56                            })
57                        })
58                        .collect(),
59                    TraverseDirection::Both => {
60                        let mut both: Vec<(
61                            MemoryId,
62                            &hippmem_core::model::links::AssociationLink,
63                        )> = current_unit
64                            .links
65                            .iter()
66                            .map(|l| (l.target_id, l))
67                            .collect();
68                        // In-edges
69                        for other in &units {
70                            for l in &other.links {
71                                if l.target_id == current_id {
72                                    both.push((other.id, l));
73                                }
74                            }
75                        }
76                        both
77                    }
78                };
79
80            for (neighbor_id, link) in &candidate_edges {
81                // Filter by link_types (API-layer filter, not exposed in CLI)
82                if let Some(ref types) = input.link_types {
83                    if !types.contains(&link.link_type) {
84                        continue;
85                    }
86                }
87
88                // Record edge
89                edges.push(crate::EdgeView {
90                    from: current_id,
91                    to: *neighbor_id,
92                    link_type: link.link_type,
93                    strength: link.strength.value(),
94                    confidence: link.confidence.value(),
95                    activation_count: link.activation_count,
96                    evidence: link.evidence.note.clone().unwrap_or_default(),
97                });
98
99                // If not visited, enqueue and record node
100                if visited.insert(*neighbor_id) {
101                    if let Some(nu) = unit_map.get(neighbor_id) {
102                        nodes.push(TraverseNode {
103                            id: *neighbor_id,
104                            depth: depth + 1,
105                            content_preview: nu.content.raw.chars().take(100).collect(),
106                            content_type: nu.content.content_type,
107                            importance: nu.understanding.importance.value(),
108                        });
109                    }
110                    queue.push_back((*neighbor_id, depth + 1));
111                }
112            }
113        }
114
115        Ok(TraverseOutput { nodes, edges })
116    }
117}