1use std::collections::{HashMap, HashSet};
16
17use uuid::Uuid;
18
19use khive_storage::types::{Direction, Edge, LinkId, NeighborQuery, TraversalOptions};
20
21use crate::error::{RuntimeError, RuntimeResult};
22use crate::runtime::{KhiveRuntime, NamespaceToken};
23
24#[derive(Debug, Clone)]
26pub struct PathNode {
27 pub entity_id: Uuid,
29 pub depth: usize,
31 pub via_edge: Option<Edge>,
33}
34
35impl KhiveRuntime {
36 pub async fn bfs_traverse(
44 &self,
45 token: &NamespaceToken,
46 start: Uuid,
47 options: TraversalOptions,
48 ) -> RuntimeResult<Vec<PathNode>> {
49 if !self.substrate_exists_in_ns(token, start).await? {
50 return Ok(Vec::new());
51 }
52
53 let graph = self.graph(token)?;
54 let limit = options.limit.map(|n| n as usize).unwrap_or(usize::MAX);
55
56 let mut visited: HashSet<Uuid> = HashSet::new();
57 let mut results: Vec<PathNode> = Vec::new();
58 let mut frontier: Vec<Uuid> = Vec::new();
59
60 visited.insert(start);
61 results.push(PathNode {
62 entity_id: start,
63 depth: 0,
64 via_edge: None,
65 });
66 frontier.push(start);
67
68 let mut depth = 0usize;
69
70 'bfs: while !frontier.is_empty() && depth < options.max_depth {
71 let query = NeighborQuery {
72 direction: options.direction.clone(),
73 relations: options.relations.clone(),
74 limit: None,
75 min_weight: None,
76 };
77
78 let all_hits = graph.batch_neighbors(&frontier, query).await?;
80
81 let mut level_new: Vec<(Uuid, Uuid)> = Vec::new();
82 for (_src, hit) in &all_hits {
83 if visited.contains(&hit.node_id) {
84 continue;
85 }
86 if visited.insert(hit.node_id) {
88 level_new.push((hit.node_id, hit.edge_id));
89 }
90 }
91
92 if level_new.is_empty() {
93 break 'bfs;
94 }
95
96 let edge_ids: Vec<LinkId> = level_new
98 .iter()
99 .map(|(_, eid)| LinkId::from(*eid))
100 .collect();
101 let edges = graph.get_edges(&edge_ids).await?;
102 let edge_map: HashMap<Uuid, Edge> =
103 edges.into_iter().map(|e| (Uuid::from(e.id), e)).collect();
104
105 let next_depth = depth + 1;
106 frontier.clear();
107 for (node_id, edge_id) in level_new {
108 let via_edge = edge_map.get(&edge_id).cloned().or(None);
109 if via_edge.is_none() {
112 return Err(RuntimeError::NotFound(format!("edge {} missing", edge_id)));
113 }
114 results.push(PathNode {
115 entity_id: node_id,
116 depth: next_depth,
117 via_edge,
118 });
119 if results.len() >= limit {
120 break 'bfs;
121 }
122 frontier.push(node_id);
123 }
124
125 depth = next_depth;
126 }
127
128 Ok(results)
129 }
130
131 pub async fn shortest_path(
140 &self,
141 token: &NamespaceToken,
142 from: Uuid,
143 to: Uuid,
144 max_depth: usize,
145 ) -> RuntimeResult<Option<Vec<PathNode>>> {
146 if !self.substrate_exists_in_ns(token, from).await?
147 || !self.substrate_exists_in_ns(token, to).await?
148 {
149 return Ok(None);
150 }
151
152 if from == to {
153 return Ok(Some(vec![PathNode {
154 entity_id: from,
155 depth: 0,
156 via_edge: None,
157 }]));
158 }
159
160 let graph = self.graph(token)?;
161
162 let mut fwd: HashMap<Uuid, (usize, Option<Uuid>, Option<Uuid>)> = HashMap::new();
164 let mut fwd_frontier: Vec<Uuid> = vec![from];
165 fwd.insert(from, (0, None, None));
166
167 let mut bwd: HashMap<Uuid, (usize, Option<Uuid>, Option<Uuid>)> = HashMap::new();
169 let mut bwd_frontier: Vec<Uuid> = vec![to];
170 bwd.insert(to, (0, None, None));
171
172 let mut meeting: Option<(Uuid, usize)> = None;
173 let mut current_depth = 0usize;
174
175 while (!fwd_frontier.is_empty() || !bwd_frontier.is_empty()) && current_depth <= max_depth {
176 if !fwd_frontier.is_empty() {
177 let hits = graph
178 .batch_neighbors(
179 &fwd_frontier,
180 NeighborQuery {
181 direction: Direction::Out,
182 relations: None,
183 limit: None,
184 min_weight: None,
185 },
186 )
187 .await?;
188
189 let mut next_fwd: Vec<Uuid> = Vec::new();
190 for (src, hit) in &hits {
191 if fwd.contains_key(&hit.node_id) {
192 continue;
193 }
194 let new_depth = fwd[src].0 + 1;
195 fwd.insert(hit.node_id, (new_depth, Some(*src), Some(hit.edge_id)));
196 next_fwd.push(hit.node_id);
197
198 if let Some(&(bwd_depth, _, _)) = bwd.get(&hit.node_id) {
199 let total = new_depth + bwd_depth;
200 if total <= max_depth
201 && meeting.as_ref().is_none_or(|&(_, best)| total < best)
202 {
203 meeting = Some((hit.node_id, total));
204 }
205 }
206 }
207 fwd_frontier = next_fwd;
208 }
209
210 if meeting.is_some() {
211 break;
212 }
213
214 if !bwd_frontier.is_empty() {
215 let hits = graph
216 .batch_neighbors(
217 &bwd_frontier,
218 NeighborQuery {
219 direction: Direction::In,
220 relations: None,
221 limit: None,
222 min_weight: None,
223 },
224 )
225 .await?;
226
227 let mut next_bwd: Vec<Uuid> = Vec::new();
228 for (src, hit) in &hits {
229 if bwd.contains_key(&hit.node_id) {
230 continue;
231 }
232 let new_depth = bwd[src].0 + 1;
233 bwd.insert(hit.node_id, (new_depth, Some(*src), Some(hit.edge_id)));
234 next_bwd.push(hit.node_id);
235
236 if let Some(&(fwd_depth, _, _)) = fwd.get(&hit.node_id) {
237 let total = fwd_depth + new_depth;
238 if total <= max_depth
239 && meeting.as_ref().is_none_or(|&(_, best)| total < best)
240 {
241 meeting = Some((hit.node_id, total));
242 }
243 }
244 }
245 bwd_frontier = next_bwd;
246 }
247
248 if meeting.is_some() {
249 break;
250 }
251
252 current_depth += 1;
253 }
254
255 let (mid, _) = match meeting {
256 None => return Ok(None),
257 Some(m) => m,
258 };
259
260 let mut fwd_chain: Vec<(Uuid, Option<Uuid>)> = Vec::new();
261 {
262 let mut cur = mid;
263 loop {
264 let (_, parent, edge_id) = fwd[&cur];
265 fwd_chain.push((cur, edge_id));
266 match parent {
267 Some(p) => cur = p,
268 None => break,
269 }
270 }
271 }
272 fwd_chain.reverse();
273
274 let mut bwd_chain: Vec<(Uuid, Option<Uuid>)> = Vec::new();
275 {
276 let mut cur = mid;
277 while let Some(&(_, Some(child), edge_id)) = bwd.get(&cur) {
278 bwd_chain.push((child, edge_id));
279 cur = child;
280 }
281 }
282
283 let path_edge_ids: Vec<LinkId> = fwd_chain
285 .iter()
286 .chain(bwd_chain.iter())
287 .filter_map(|(_, eid)| eid.map(LinkId::from))
288 .collect();
289
290 let path_edges = graph.get_edges(&path_edge_ids).await?;
291 let edge_map: HashMap<Uuid, Edge> = path_edges
292 .into_iter()
293 .map(|e| (Uuid::from(e.id), e))
294 .collect();
295
296 let mut path: Vec<PathNode> = Vec::new();
297 for (i, (node_id, edge_id)) in fwd_chain.iter().enumerate() {
298 let via_edge = if i == 0 {
299 None } else {
301 edge_id.and_then(|eid| edge_map.get(&eid).cloned())
302 };
303 path.push(PathNode {
304 entity_id: *node_id,
305 depth: i,
306 via_edge,
307 });
308 }
309
310 let base = path.len();
311 for (i, (node_id, edge_id)) in bwd_chain.iter().enumerate() {
312 let via_edge = edge_id.and_then(|eid| edge_map.get(&eid).cloned());
313 path.push(PathNode {
314 entity_id: *node_id,
315 depth: base + i,
316 via_edge,
317 });
318 }
319
320 Ok(Some(path))
321 }
322}
323
324#[cfg(test)]
327#[path = "graph_traversal_tests.rs"]
328mod tests;