1use std::collections::{HashMap, HashSet, VecDeque};
8use thiserror::Error;
9
10#[derive(Debug, Error)]
12pub enum KgError {
13 #[error("node not found: {0}")]
15 NodeNotFound(String),
16
17 #[error("duplicate node: {0}")]
19 DuplicateNode(String),
20
21 #[error("edge endpoint missing: from={from}, to={to}")]
23 EdgeEndpointMissing { from: String, to: String },
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub enum NodeType {
29 Predicate,
31 Constant,
33 Variable,
35 Rule,
37}
38
39#[derive(Debug, Clone)]
41pub struct KgNode {
42 pub id: String,
44 pub node_type: NodeType,
46 pub metadata: HashMap<String, String>,
48}
49
50impl KgNode {
51 pub fn new(id: impl Into<String>, node_type: NodeType) -> Self {
53 Self {
54 id: id.into(),
55 node_type,
56 metadata: HashMap::new(),
57 }
58 }
59
60 pub fn with_metadata(
62 id: impl Into<String>,
63 node_type: NodeType,
64 metadata: HashMap<String, String>,
65 ) -> Self {
66 Self {
67 id: id.into(),
68 node_type,
69 metadata,
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Hash)]
76pub enum EdgeType {
77 UsesIn,
79 DefinesIn,
81 DependsOn,
83 Contradicts,
85 Subsumes,
87}
88
89#[derive(Debug, Clone)]
91pub struct KgEdge {
92 pub from_id: String,
94 pub to_id: String,
96 pub edge_type: EdgeType,
98 pub weight: f32,
100}
101
102impl KgEdge {
103 pub fn new(from_id: impl Into<String>, to_id: impl Into<String>, edge_type: EdgeType) -> Self {
105 Self {
106 from_id: from_id.into(),
107 to_id: to_id.into(),
108 edge_type,
109 weight: 1.0,
110 }
111 }
112
113 pub fn with_weight(
115 from_id: impl Into<String>,
116 to_id: impl Into<String>,
117 edge_type: EdgeType,
118 weight: f32,
119 ) -> Self {
120 Self {
121 from_id: from_id.into(),
122 to_id: to_id.into(),
123 edge_type,
124 weight,
125 }
126 }
127}
128
129#[derive(Debug, Clone, Default)]
135pub struct KnowledgeGraph {
136 pub nodes: HashMap<String, KgNode>,
138 pub edges: Vec<KgEdge>,
140 pub adjacency: HashMap<String, Vec<usize>>,
142}
143
144impl KnowledgeGraph {
145 pub fn new() -> Self {
147 Self::default()
148 }
149
150 pub fn add_node(&mut self, node: KgNode) -> Result<(), KgError> {
152 if self.nodes.contains_key(&node.id) {
153 return Err(KgError::DuplicateNode(node.id));
154 }
155 self.adjacency.entry(node.id.clone()).or_default();
157 self.nodes.insert(node.id.clone(), node);
158 Ok(())
159 }
160
161 pub fn add_edge(&mut self, edge: KgEdge) -> Result<(), KgError> {
165 if !self.nodes.contains_key(&edge.from_id) || !self.nodes.contains_key(&edge.to_id) {
166 return Err(KgError::EdgeEndpointMissing {
167 from: edge.from_id.clone(),
168 to: edge.to_id.clone(),
169 });
170 }
171 let idx = self.edges.len();
172 self.adjacency
173 .entry(edge.from_id.clone())
174 .or_default()
175 .push(idx);
176 self.edges.push(edge);
177 Ok(())
178 }
179
180 pub fn node_count(&self) -> usize {
182 self.nodes.len()
183 }
184
185 pub fn edge_count(&self) -> usize {
187 self.edges.len()
188 }
189
190 pub fn neighbors(&self, node_id: &str) -> Vec<&KgNode> {
196 let Some(edge_indices) = self.adjacency.get(node_id) else {
197 return Vec::new();
198 };
199 edge_indices
200 .iter()
201 .filter_map(|&idx| {
202 let edge = self.edges.get(idx)?;
203 self.nodes.get(&edge.to_id)
204 })
205 .collect()
206 }
207}
208
209pub struct KnowledgeGraphTraverser {
215 pub graph: KnowledgeGraph,
217}
218
219impl KnowledgeGraphTraverser {
220 pub fn new(graph: KnowledgeGraph) -> Self {
222 Self { graph }
223 }
224
225 pub fn bfs(&self, start: &str, max_depth: usize) -> Vec<String> {
233 if !self.graph.nodes.contains_key(start) {
234 return Vec::new();
235 }
236
237 let mut visited: HashSet<String> = HashSet::new();
238 let mut order: Vec<String> = Vec::new();
239 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
241
242 visited.insert(start.to_string());
243 queue.push_back((start.to_string(), 0));
244
245 while let Some((current, depth)) = queue.pop_front() {
246 order.push(current.clone());
247
248 if depth >= max_depth {
249 continue;
250 }
251
252 if let Some(edge_indices) = self.graph.adjacency.get(¤t) {
253 for &idx in edge_indices {
254 if let Some(edge) = self.graph.edges.get(idx) {
255 let neighbor = &edge.to_id;
256 if !visited.contains(neighbor) {
257 visited.insert(neighbor.clone());
258 queue.push_back((neighbor.clone(), depth + 1));
259 }
260 }
261 }
262 }
263 }
264
265 order
266 }
267
268 pub fn dfs(&self, start: &str, max_depth: usize) -> Vec<String> {
276 if !self.graph.nodes.contains_key(start) {
277 return Vec::new();
278 }
279
280 let mut visited: HashSet<String> = HashSet::new();
281 let mut order: Vec<String> = Vec::new();
282 let mut stack: Vec<(String, usize)> = vec![(start.to_string(), 0)];
284
285 while let Some((current, depth)) = stack.pop() {
286 if visited.contains(¤t) {
287 continue;
288 }
289 visited.insert(current.clone());
290 order.push(current.clone());
291
292 if depth < max_depth {
293 if let Some(edge_indices) = self.graph.adjacency.get(¤t) {
296 let neighbours: Vec<String> = edge_indices
297 .iter()
298 .filter_map(|&idx| {
299 let edge = self.graph.edges.get(idx)?;
300 if !visited.contains(&edge.to_id) {
301 Some(edge.to_id.clone())
302 } else {
303 None
304 }
305 })
306 .collect();
307 for n in neighbours.into_iter().rev() {
308 stack.push((n, depth + 1));
309 }
310 }
311 }
312 }
313
314 order
315 }
316
317 pub fn find_path(&self, from: &str, to: &str) -> Option<Vec<String>> {
323 if !self.graph.nodes.contains_key(from) || !self.graph.nodes.contains_key(to) {
324 return None;
325 }
326 if from == to {
327 return Some(vec![from.to_string()]);
328 }
329
330 let mut visited: HashSet<String> = HashSet::new();
332 let mut predecessor: HashMap<String, String> = HashMap::new();
333 let mut queue: VecDeque<String> = VecDeque::new();
334
335 visited.insert(from.to_string());
336 queue.push_back(from.to_string());
337
338 let mut found = false;
339
340 'outer: while let Some(current) = queue.pop_front() {
341 if let Some(edge_indices) = self.graph.adjacency.get(¤t) {
342 for &idx in edge_indices {
343 if let Some(edge) = self.graph.edges.get(idx) {
344 let neighbor = &edge.to_id;
345 if !visited.contains(neighbor) {
346 visited.insert(neighbor.clone());
347 predecessor.insert(neighbor.clone(), current.clone());
348 if neighbor == to {
349 found = true;
350 break 'outer;
351 }
352 queue.push_back(neighbor.clone());
353 }
354 }
355 }
356 }
357 }
358
359 if !found {
360 return None;
361 }
362
363 let mut path = vec![to.to_string()];
365 let mut node = to.to_string();
366 while node != from {
367 let prev = predecessor.get(&node)?.clone();
368 path.push(prev.clone());
369 node = prev;
370 }
371 path.reverse();
372 Some(path)
373 }
374
375 pub fn subgraph(&self, roots: &[String], depth: usize) -> KnowledgeGraph {
381 let mut reachable: HashSet<String> = HashSet::new();
383 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
384
385 for root in roots {
386 if self.graph.nodes.contains_key(root.as_str()) && !reachable.contains(root) {
387 reachable.insert(root.clone());
388 queue.push_back((root.clone(), 0));
389 }
390 }
391
392 while let Some((current, d)) = queue.pop_front() {
393 if d >= depth {
394 continue;
395 }
396 if let Some(edge_indices) = self.graph.adjacency.get(¤t) {
397 for &idx in edge_indices {
398 if let Some(edge) = self.graph.edges.get(idx) {
399 let neighbor = &edge.to_id;
400 if !reachable.contains(neighbor) {
401 reachable.insert(neighbor.clone());
402 queue.push_back((neighbor.clone(), d + 1));
403 }
404 }
405 }
406 }
407 }
408
409 let mut sg = KnowledgeGraph::new();
411 for id in &reachable {
412 if let Some(node) = self.graph.nodes.get(id) {
413 let _ = sg.add_node(node.clone());
416 }
417 }
418 for edge in &self.graph.edges {
419 if reachable.contains(&edge.from_id) && reachable.contains(&edge.to_id) {
420 let _ = sg.add_edge(edge.clone());
421 }
422 }
423 sg
424 }
425
426 pub fn connected_components(&self) -> Vec<Vec<String>> {
433 let mut ids: Vec<String> = self.graph.nodes.keys().cloned().collect();
435 ids.sort();
436
437 let index: HashMap<&str, usize> = ids
439 .iter()
440 .enumerate()
441 .map(|(i, id)| (id.as_str(), i))
442 .collect();
443 let n = ids.len();
444
445 let mut parent: Vec<usize> = (0..n).collect();
447 let mut rank: Vec<usize> = vec![0; n];
448
449 fn find(parent: &mut [usize], x: usize) -> usize {
450 if parent[x] != x {
451 parent[x] = find(parent, parent[x]); }
453 parent[x]
454 }
455
456 fn union(parent: &mut [usize], rank: &mut [usize], x: usize, y: usize) {
457 let rx = find(parent, x);
458 let ry = find(parent, y);
459 if rx == ry {
460 return;
461 }
462 match rank[rx].cmp(&rank[ry]) {
463 std::cmp::Ordering::Less => parent[rx] = ry,
464 std::cmp::Ordering::Greater => parent[ry] = rx,
465 std::cmp::Ordering::Equal => {
466 parent[ry] = rx;
467 rank[rx] += 1;
468 }
469 }
470 }
471
472 for edge in &self.graph.edges {
474 if let (Some(&ai), Some(&bi)) = (
475 index.get(edge.from_id.as_str()),
476 index.get(edge.to_id.as_str()),
477 ) {
478 union(&mut parent, &mut rank, ai, bi);
479 }
480 }
481
482 let mut components: HashMap<usize, Vec<String>> = HashMap::new();
484 for (i, id) in ids.iter().enumerate() {
485 let root = find(&mut parent, i);
486 components.entry(root).or_default().push(id.clone());
487 }
488
489 let mut result: Vec<Vec<String>> = components.into_values().collect();
491 result.sort_by(|a, b| a[0].cmp(&b[0]));
493 result
494 }
495
496 pub fn has_cycle(&self) -> bool {
503 let mut color: HashMap<&str, u8> = HashMap::new();
505
506 for start_id in self.graph.nodes.keys() {
507 if color.get(start_id.as_str()).copied().unwrap_or(0) != 0 {
508 continue;
509 }
510 let mut dfs_stack: Vec<(&str, usize)> = vec![(start_id.as_str(), 0)];
513 color.insert(start_id.as_str(), 1); 'dfs: while let Some((node, cursor)) = dfs_stack.last_mut() {
516 let node: &str = node; let edge_indices = match self.graph.adjacency.get(node) {
518 Some(v) => v,
519 None => {
520 color.insert(node, 2);
522 dfs_stack.pop();
523 continue 'dfs;
524 }
525 };
526
527 if *cursor < edge_indices.len() {
528 let idx = edge_indices[*cursor];
529 *cursor += 1;
530 if let Some(edge) = self.graph.edges.get(idx) {
531 let neighbor = edge.to_id.as_str();
532 let c = color.get(neighbor).copied().unwrap_or(0);
533 if c == 1 {
534 return true;
536 }
537 if c == 0 {
538 color.insert(neighbor, 1);
539 dfs_stack.push((neighbor, 0));
540 }
541 }
543 } else {
544 color.insert(node, 2);
546 dfs_stack.pop();
547 }
548 }
549 }
550
551 false
552 }
553}
554
555#[cfg(test)]
558mod tests {
559 use super::*;
560
561 fn diamond_graph() -> KnowledgeGraph {
567 let mut g = KnowledgeGraph::new();
568 for id in ["A", "B", "C", "D"] {
569 g.add_node(KgNode::new(id, NodeType::Predicate))
570 .expect("test: should succeed");
571 }
572 g.add_edge(KgEdge::new("A", "B", EdgeType::DependsOn))
573 .expect("test: should succeed");
574 g.add_edge(KgEdge::new("A", "C", EdgeType::DependsOn))
575 .expect("test: should succeed");
576 g.add_edge(KgEdge::new("B", "D", EdgeType::DependsOn))
577 .expect("test: should succeed");
578 g.add_edge(KgEdge::new("C", "D", EdgeType::DependsOn))
579 .expect("test: should succeed");
580 g
581 }
582
583 #[test]
586 fn test_add_node_and_count() {
587 let mut g = KnowledgeGraph::new();
588 g.add_node(KgNode::new("X", NodeType::Variable))
589 .expect("test: should succeed");
590 g.add_node(KgNode::new("Y", NodeType::Constant))
591 .expect("test: should succeed");
592 assert_eq!(g.node_count(), 2);
593 assert_eq!(g.edge_count(), 0);
594 }
595
596 #[test]
597 fn test_duplicate_node_error() {
598 let mut g = KnowledgeGraph::new();
599 g.add_node(KgNode::new("dup", NodeType::Rule))
600 .expect("test: should succeed");
601 let err = g.add_node(KgNode::new("dup", NodeType::Rule)).unwrap_err();
602 assert!(matches!(err, KgError::DuplicateNode(ref s) if s == "dup"));
603 }
604
605 #[test]
606 fn test_add_edge_and_count() {
607 let mut g = KnowledgeGraph::new();
608 g.add_node(KgNode::new("p", NodeType::Predicate))
609 .expect("test: should succeed");
610 g.add_node(KgNode::new("q", NodeType::Predicate))
611 .expect("test: should succeed");
612 g.add_edge(KgEdge::new("p", "q", EdgeType::UsesIn))
613 .expect("test: should succeed");
614 assert_eq!(g.edge_count(), 1);
615 }
616
617 #[test]
618 fn test_edge_missing_from_endpoint_error() {
619 let mut g = KnowledgeGraph::new();
620 g.add_node(KgNode::new("exists", NodeType::Predicate))
621 .expect("test: should succeed");
622 let err = g
623 .add_edge(KgEdge::new("missing", "exists", EdgeType::DefinesIn))
624 .unwrap_err();
625 assert!(matches!(err, KgError::EdgeEndpointMissing { .. }));
626 }
627
628 #[test]
629 fn test_edge_missing_to_endpoint_error() {
630 let mut g = KnowledgeGraph::new();
631 g.add_node(KgNode::new("exists", NodeType::Predicate))
632 .expect("test: should succeed");
633 let err = g
634 .add_edge(KgEdge::new("exists", "ghost", EdgeType::DefinesIn))
635 .unwrap_err();
636 assert!(matches!(err, KgError::EdgeEndpointMissing { .. }));
637 }
638
639 #[test]
642 fn test_neighbors_listing() {
643 let g = diamond_graph();
644 let mut nb: Vec<String> = g.neighbors("A").iter().map(|n| n.id.clone()).collect();
645 nb.sort();
646 assert_eq!(nb, vec!["B", "C"]);
647 }
648
649 #[test]
650 fn test_neighbors_leaf_node_is_empty() {
651 let g = diamond_graph();
652 assert!(g.neighbors("D").is_empty());
653 }
654
655 #[test]
658 fn test_bfs_visit_order() {
659 let g = diamond_graph();
660 let t = KnowledgeGraphTraverser::new(g);
661 let visited = t.bfs("A", 10);
662 assert_eq!(visited[0], "A");
664 let pos_b = visited
665 .iter()
666 .position(|x| x == "B")
667 .expect("test: should succeed");
668 let pos_c = visited
669 .iter()
670 .position(|x| x == "C")
671 .expect("test: should succeed");
672 let pos_d = visited
673 .iter()
674 .position(|x| x == "D")
675 .expect("test: should succeed");
676 assert!(pos_b < pos_d);
677 assert!(pos_c < pos_d);
678 assert_eq!(visited.len(), 4);
679 }
680
681 #[test]
682 fn test_bfs_max_depth_cutoff() {
683 let g = diamond_graph();
684 let t = KnowledgeGraphTraverser::new(g);
685 let visited = t.bfs("A", 1);
687 assert!(visited.contains(&"A".to_string()));
688 assert!(visited.contains(&"B".to_string()));
689 assert!(visited.contains(&"C".to_string()));
690 assert!(!visited.contains(&"D".to_string()));
691 }
692
693 #[test]
694 fn test_bfs_nonexistent_start_returns_empty() {
695 let g = diamond_graph();
696 let t = KnowledgeGraphTraverser::new(g);
697 assert!(t.bfs("NOPE", 5).is_empty());
698 }
699
700 #[test]
703 fn test_dfs_visit_order_starts_with_root() {
704 let g = diamond_graph();
705 let t = KnowledgeGraphTraverser::new(g);
706 let visited = t.dfs("A", 10);
707 assert_eq!(visited[0], "A");
708 assert_eq!(visited.len(), 4);
709 }
710
711 #[test]
712 fn test_dfs_max_depth_cutoff() {
713 let g = diamond_graph();
714 let t = KnowledgeGraphTraverser::new(g);
715 let visited = t.dfs("A", 1);
716 assert!(visited.contains(&"A".to_string()));
717 assert!(!visited.contains(&"D".to_string()));
718 }
719
720 #[test]
723 fn test_find_path_direct() {
724 let g = diamond_graph();
725 let t = KnowledgeGraphTraverser::new(g);
726 let path = t.find_path("A", "D").expect("test: should succeed");
727 assert_eq!(path[0], "A");
728 assert_eq!(*path.last().expect("test: should succeed"), "D");
729 assert_eq!(path.len(), 3);
731 }
732
733 #[test]
734 fn test_find_path_self() {
735 let g = diamond_graph();
736 let t = KnowledgeGraphTraverser::new(g);
737 let path = t.find_path("A", "A").expect("test: should succeed");
738 assert_eq!(path, vec!["A"]);
739 }
740
741 #[test]
742 fn test_find_path_not_found() {
743 let g = diamond_graph();
744 let t = KnowledgeGraphTraverser::new(g);
745 assert!(t.find_path("D", "A").is_none());
747 }
748
749 #[test]
750 fn test_find_path_missing_node() {
751 let g = diamond_graph();
752 let t = KnowledgeGraphTraverser::new(g);
753 assert!(t.find_path("A", "NOPE").is_none());
754 }
755
756 #[test]
759 fn test_subgraph_contains_correct_nodes() {
760 let g = diamond_graph();
761 let t = KnowledgeGraphTraverser::new(g);
762 let sg = t.subgraph(&["A".to_string()], 1);
763 assert!(sg.nodes.contains_key("A"));
765 assert!(sg.nodes.contains_key("B"));
766 assert!(sg.nodes.contains_key("C"));
767 assert!(!sg.nodes.contains_key("D"));
768 }
769
770 #[test]
771 fn test_subgraph_full_depth_includes_all() {
772 let g = diamond_graph();
773 let t = KnowledgeGraphTraverser::new(g);
774 let sg = t.subgraph(&["A".to_string()], 10);
775 assert_eq!(sg.node_count(), 4);
776 }
777
778 #[test]
781 fn test_connected_components_disconnected() {
782 let mut g = KnowledgeGraph::new();
783 g.add_node(KgNode::new("P1", NodeType::Predicate))
785 .expect("test: should succeed");
786 g.add_node(KgNode::new("P2", NodeType::Predicate))
787 .expect("test: should succeed");
788 g.add_edge(KgEdge::new("P1", "P2", EdgeType::DependsOn))
789 .expect("test: should succeed");
790 g.add_node(KgNode::new("Q1", NodeType::Constant))
792 .expect("test: should succeed");
793
794 let t = KnowledgeGraphTraverser::new(g);
795 let comps = t.connected_components();
796 assert_eq!(comps.len(), 2);
797 let flat: Vec<String> = comps.iter().flatten().cloned().collect();
799 assert_eq!(flat.len(), 3);
800 assert!(flat.contains(&"P1".to_string()));
801 assert!(flat.contains(&"Q1".to_string()));
802 }
803
804 #[test]
805 fn test_connected_components_single_component() {
806 let g = diamond_graph();
807 let t = KnowledgeGraphTraverser::new(g);
808 let comps = t.connected_components();
809 assert_eq!(comps.len(), 1);
810 let mut comp = comps[0].clone();
811 comp.sort();
812 assert_eq!(comp, vec!["A", "B", "C", "D"]);
813 }
814
815 #[test]
818 fn test_has_cycle_detects_cycle() {
819 let mut g = KnowledgeGraph::new();
820 for id in ["X", "Y", "Z"] {
821 g.add_node(KgNode::new(id, NodeType::Rule))
822 .expect("test: should succeed");
823 }
824 g.add_edge(KgEdge::new("X", "Y", EdgeType::DependsOn))
825 .expect("test: should succeed");
826 g.add_edge(KgEdge::new("Y", "Z", EdgeType::DependsOn))
827 .expect("test: should succeed");
828 g.add_edge(KgEdge::new("Z", "X", EdgeType::DependsOn))
829 .expect("test: should succeed"); let t = KnowledgeGraphTraverser::new(g);
832 assert!(t.has_cycle());
833 }
834
835 #[test]
836 fn test_has_cycle_false_on_dag() {
837 let g = diamond_graph(); let t = KnowledgeGraphTraverser::new(g);
839 assert!(!t.has_cycle());
840 }
841
842 #[test]
845 fn test_node_metadata_preserved() {
846 let mut meta = HashMap::new();
847 meta.insert("arity".to_string(), "2".to_string());
848 let node = KgNode::with_metadata("parent", NodeType::Predicate, meta.clone());
849 let mut g = KnowledgeGraph::new();
850 g.add_node(node).expect("test: should succeed");
851 let stored = g.nodes.get("parent").expect("test: should succeed");
852 assert_eq!(stored.metadata.get("arity").map(String::as_str), Some("2"));
853 }
854
855 #[test]
856 fn test_edge_weight_preserved() {
857 let mut g = KnowledgeGraph::new();
858 g.add_node(KgNode::new("a", NodeType::Predicate))
859 .expect("test: should succeed");
860 g.add_node(KgNode::new("b", NodeType::Predicate))
861 .expect("test: should succeed");
862 g.add_edge(KgEdge::with_weight("a", "b", EdgeType::Subsumes, 0.42))
863 .expect("test: should succeed");
864 let edge = &g.edges[0];
865 assert!((edge.weight - 0.42).abs() < f32::EPSILON);
866 }
867}