1use crate::{Edge, GraphData, Node, Relationship};
13use fabryk_core::Result;
14use petgraph::Direction;
15use petgraph::algo::toposort;
16use petgraph::graph::NodeIndex;
17use petgraph::visit::EdgeRef;
18use std::collections::{HashMap, HashSet, VecDeque};
19
20const MAX_BFS_DEPTH: usize = 10;
22
23#[derive(Clone, Debug)]
29pub struct NeighborhoodResult {
30 pub center: Node,
32 pub nodes: Vec<Node>,
34 pub edges: Vec<Edge>,
36 pub distances: HashMap<String, usize>,
38}
39
40#[derive(Clone, Debug)]
42pub struct PathResult {
43 pub path: Vec<Node>,
45 pub edges: Vec<Edge>,
47 pub total_weight: f32,
49 pub found: bool,
51}
52
53impl PathResult {
54 pub fn not_found() -> Self {
56 Self {
57 path: Vec::new(),
58 edges: Vec::new(),
59 total_weight: 0.0,
60 found: false,
61 }
62 }
63}
64
65#[derive(Clone, Debug)]
67pub struct PrerequisitesResult {
68 pub ordered: Vec<Node>,
70 pub target: Node,
72 pub has_cycles: bool,
74}
75
76#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
78pub struct CentralityScore {
79 pub node_id: String,
81 pub degree: f32,
83 pub in_degree: f32,
85 pub out_degree: f32,
87}
88
89pub fn neighborhood(
106 graph: &GraphData,
107 center_id: &str,
108 radius: usize,
109 relationship_filter: Option<&[Relationship]>,
110) -> Result<NeighborhoodResult> {
111 let center_node = graph
112 .get_node(center_id)
113 .ok_or_else(|| fabryk_core::Error::not_found("node", center_id))?
114 .clone();
115
116 let center_idx = graph
117 .get_index(center_id)
118 .ok_or_else(|| fabryk_core::Error::not_found("node index", center_id))?;
119
120 let radius = radius.min(MAX_BFS_DEPTH);
121
122 let mut visited: HashSet<NodeIndex> = HashSet::new();
123 let mut distances: HashMap<String, usize> = HashMap::new();
124 let mut queue: VecDeque<(NodeIndex, usize)> = VecDeque::new();
125 let mut result_nodes: Vec<Node> = Vec::new();
126 let mut result_edges: Vec<Edge> = Vec::new();
127
128 visited.insert(center_idx);
129 distances.insert(center_id.to_string(), 0);
130 queue.push_back((center_idx, 0));
131
132 while let Some((current_idx, current_dist)) = queue.pop_front() {
133 if current_dist >= radius {
134 continue;
135 }
136
137 for edge_ref in graph.graph.edges_directed(current_idx, Direction::Outgoing) {
139 let edge_weight = edge_ref.weight();
140
141 if let Some(filter) = relationship_filter
142 && !filter.contains(&edge_weight.relationship)
143 {
144 continue;
145 }
146
147 let neighbor_idx = edge_ref.target();
148 if visited.insert(neighbor_idx) {
149 let neighbor = &graph.graph[neighbor_idx];
150 distances.insert(neighbor.id.clone(), current_dist + 1);
151 result_nodes.push(neighbor.clone());
152 result_edges.push(edge_weight.clone());
153 queue.push_back((neighbor_idx, current_dist + 1));
154 }
155 }
156
157 for edge_ref in graph.graph.edges_directed(current_idx, Direction::Incoming) {
159 let edge_weight = edge_ref.weight();
160
161 if let Some(filter) = relationship_filter
162 && !filter.contains(&edge_weight.relationship)
163 {
164 continue;
165 }
166
167 let neighbor_idx = edge_ref.source();
168 if visited.insert(neighbor_idx) {
169 let neighbor = &graph.graph[neighbor_idx];
170 distances.insert(neighbor.id.clone(), current_dist + 1);
171 result_nodes.push(neighbor.clone());
172 result_edges.push(edge_weight.clone());
173 queue.push_back((neighbor_idx, current_dist + 1));
174 }
175 }
176 }
177
178 Ok(NeighborhoodResult {
179 center: center_node,
180 nodes: result_nodes,
181 edges: result_edges,
182 distances,
183 })
184}
185
186pub fn shortest_path(graph: &GraphData, from_id: &str, to_id: &str) -> Result<PathResult> {
190 let from_idx = match graph.get_index(from_id) {
191 Some(idx) => idx,
192 None => return Ok(PathResult::not_found()),
193 };
194
195 let to_idx = match graph.get_index(to_id) {
196 Some(idx) => idx,
197 None => return Ok(PathResult::not_found()),
198 };
199
200 if from_idx == to_idx {
201 let node = graph.graph[from_idx].clone();
202 return Ok(PathResult {
203 path: vec![node],
204 edges: Vec::new(),
205 total_weight: 0.0,
206 found: true,
207 });
208 }
209
210 let result = petgraph::algo::astar(
211 &graph.graph,
212 from_idx,
213 |n| n == to_idx,
214 |e| (1.0 / e.weight().weight.max(0.01)) as u32,
215 |_| 0,
216 );
217
218 match result {
219 Some((_cost, path_indices)) => {
220 let path: Vec<Node> = path_indices
221 .iter()
222 .map(|&idx| graph.graph[idx].clone())
223 .collect();
224
225 let mut edges = Vec::new();
226 for window in path_indices.windows(2) {
227 if let [from, to] = window
228 && let Some(edge_idx) = graph.graph.find_edge(*from, *to)
229 {
230 edges.push(graph.graph[edge_idx].clone());
231 }
232 }
233
234 let total_weight = edges.iter().map(|e| e.weight).sum();
235
236 Ok(PathResult {
237 path,
238 edges,
239 total_weight,
240 found: true,
241 })
242 }
243 None => Ok(PathResult::not_found()),
244 }
245}
246
247pub fn prerequisites_sorted(graph: &GraphData, target_id: &str) -> Result<PrerequisitesResult> {
252 let target_node = graph
253 .get_node(target_id)
254 .ok_or_else(|| fabryk_core::Error::not_found("node", target_id))?
255 .clone();
256
257 let target_idx = graph.get_index(target_id).unwrap();
258
259 let mut prereq_indices: HashSet<NodeIndex> = HashSet::new();
261 let mut queue: VecDeque<NodeIndex> = VecDeque::new();
262 queue.push_back(target_idx);
263
264 while let Some(current) = queue.pop_front() {
265 for edge_ref in graph.graph.edges_directed(current, Direction::Incoming) {
266 if edge_ref.weight().relationship == Relationship::Prerequisite {
267 let source = edge_ref.source();
268 if prereq_indices.insert(source) {
269 queue.push_back(source);
270 }
271 }
272 }
273 }
274
275 if prereq_indices.is_empty() {
276 return Ok(PrerequisitesResult {
277 ordered: Vec::new(),
278 target: target_node,
279 has_cycles: false,
280 });
281 }
282
283 let sorted = toposort(&graph.graph, None);
285
286 let (ordered, has_cycles) = match sorted {
287 Ok(all_sorted) => {
288 let ordered: Vec<Node> = all_sorted
290 .into_iter()
291 .filter(|idx| prereq_indices.contains(idx))
292 .map(|idx| graph.graph[idx].clone())
293 .collect();
294 (ordered, false)
295 }
296 Err(_) => {
297 let ordered: Vec<Node> = prereq_indices
299 .iter()
300 .map(|idx| graph.graph[*idx].clone())
301 .collect();
302 (ordered, true)
303 }
304 };
305
306 Ok(PrerequisitesResult {
307 ordered,
308 target: target_node,
309 has_cycles,
310 })
311}
312
313pub fn calculate_centrality(graph: &GraphData) -> Vec<CentralityScore> {
318 let n = graph.node_count() as f32;
319 if n < 2.0 {
320 return Vec::new();
321 }
322
323 let mut scores: Vec<CentralityScore> = graph
324 .iter_nodes()
325 .map(|node| {
326 let idx = graph.get_index(&node.id).unwrap();
327
328 let in_deg = graph.graph.edges_directed(idx, Direction::Incoming).count() as f32;
329 let out_deg = graph.graph.edges_directed(idx, Direction::Outgoing).count() as f32;
330 let degree = in_deg + out_deg;
331
332 CentralityScore {
333 node_id: node.id.clone(),
334 degree: degree / (2.0 * (n - 1.0)),
335 in_degree: in_deg / (n - 1.0),
336 out_degree: out_deg / (n - 1.0),
337 }
338 })
339 .collect();
340
341 scores.sort_by(|a, b| {
342 b.degree
343 .partial_cmp(&a.degree)
344 .unwrap_or(std::cmp::Ordering::Equal)
345 });
346 scores
347}
348
349pub fn find_bridges(graph: &GraphData, limit: usize) -> Vec<Node> {
354 let mut bridge_scores: Vec<(String, f32)> = graph
355 .iter_nodes()
356 .map(|node| {
357 let idx = graph.get_index(&node.id).unwrap();
358
359 let in_deg = graph.graph.edges_directed(idx, Direction::Incoming).count() as f32;
360 let out_deg = graph.graph.edges_directed(idx, Direction::Outgoing).count() as f32;
361
362 let mut neighbor_categories: HashSet<String> = HashSet::new();
364 for edge_ref in graph.graph.edges(idx) {
365 let neighbor = &graph.graph[edge_ref.target()];
366 if let Some(ref cat) = neighbor.category {
367 neighbor_categories.insert(cat.clone());
368 }
369 }
370
371 let diversity = neighbor_categories.len() as f32;
373 let score = (in_deg.min(out_deg) + 1.0) * (diversity + 1.0);
374
375 (node.id.clone(), score)
376 })
377 .collect();
378
379 bridge_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
380
381 bridge_scores
382 .into_iter()
383 .take(limit)
384 .filter_map(|(id, _)| graph.get_node(&id).cloned())
385 .collect()
386}
387
388pub fn get_related(
390 graph: &GraphData,
391 node_id: &str,
392 relationships: &[Relationship],
393 direction: Direction,
394) -> Result<Vec<(Node, Relationship)>> {
395 let idx = graph
396 .get_index(node_id)
397 .ok_or_else(|| fabryk_core::Error::not_found("node", node_id))?;
398
399 let mut results = Vec::new();
400
401 let edges: Box<dyn Iterator<Item = _>> = match direction {
402 Direction::Outgoing => Box::new(graph.graph.edges_directed(idx, Direction::Outgoing)),
403 Direction::Incoming => Box::new(graph.graph.edges_directed(idx, Direction::Incoming)),
404 };
405
406 for edge_ref in edges {
407 let edge = edge_ref.weight();
408 if relationships.contains(&edge.relationship) {
409 let neighbor_idx = match direction {
410 Direction::Outgoing => edge_ref.target(),
411 Direction::Incoming => edge_ref.source(),
412 };
413 let neighbor = graph.graph[neighbor_idx].clone();
414 results.push((neighbor, edge.relationship.clone()));
415 }
416 }
417
418 Ok(results)
419}
420
421impl GraphData {
426 pub fn prerequisites(&self, node_id: &str) -> Result<Vec<Node>> {
428 let results = get_related(
429 self,
430 node_id,
431 &[Relationship::Prerequisite],
432 Direction::Outgoing,
433 )?;
434 Ok(results.into_iter().map(|(node, _)| node).collect())
435 }
436
437 pub fn dependents(&self, node_id: &str) -> Result<Vec<Node>> {
439 let results = get_related(
440 self,
441 node_id,
442 &[Relationship::Prerequisite],
443 Direction::Incoming,
444 )?;
445 Ok(results.into_iter().map(|(node, _)| node).collect())
446 }
447
448 pub fn related_by(&self, node_id: &str, relationship: &Relationship) -> Result<Vec<Node>> {
450 let results = get_related(
451 self,
452 node_id,
453 std::slice::from_ref(relationship),
454 Direction::Outgoing,
455 )?;
456 Ok(results.into_iter().map(|(node, _)| node).collect())
457 }
458}
459
460#[cfg(test)]
465mod tests {
466 use super::*;
467 use crate::types::*;
468
469 fn create_test_graph() -> GraphData {
470 let mut graph = GraphData::new();
471
472 let nodes = vec![
474 Node::new("a", "Node A").with_category("cat1"),
475 Node::new("b", "Node B").with_category("cat1"),
476 Node::new("c", "Node C").with_category("cat2"),
477 Node::new("d", "Node D").with_category("cat2"),
478 ];
479
480 for node in &nodes {
481 graph.add_node(node.clone());
482 }
483
484 let edges = vec![
485 Edge::new("a", "b", Relationship::Prerequisite),
486 Edge::new("b", "c", Relationship::Prerequisite),
487 Edge::new("a", "d", Relationship::RelatesTo),
488 Edge::new("b", "d", Relationship::LeadsTo),
489 ];
490
491 for edge in edges {
492 graph.add_edge(edge).unwrap();
493 }
494
495 graph
496 }
497
498 #[test]
503 fn test_neighborhood_basic() {
504 let graph = create_test_graph();
505 let result = neighborhood(&graph, "a", 1, None).unwrap();
506
507 assert_eq!(result.center.id, "a");
508 assert_eq!(result.nodes.len(), 2); assert!(result.nodes.iter().any(|n| n.id == "b"));
510 assert!(result.nodes.iter().any(|n| n.id == "d"));
511 }
512
513 #[test]
514 fn test_neighborhood_radius_2() {
515 let graph = create_test_graph();
516 let result = neighborhood(&graph, "a", 2, None).unwrap();
517
518 assert!(result.nodes.iter().any(|n| n.id == "c"));
519 assert_eq!(result.distances["c"], 2);
520 }
521
522 #[test]
523 fn test_neighborhood_with_filter() {
524 let graph = create_test_graph();
525 let result = neighborhood(&graph, "a", 2, Some(&[Relationship::Prerequisite])).unwrap();
526
527 assert!(result.nodes.iter().any(|n| n.id == "b"));
528 assert!(result.nodes.iter().any(|n| n.id == "c"));
529 assert!(!result.nodes.iter().any(|n| n.id == "d"));
531 }
532
533 #[test]
534 fn test_neighborhood_not_found() {
535 let graph = create_test_graph();
536 let result = neighborhood(&graph, "nonexistent", 1, None);
537 assert!(result.is_err());
538 }
539
540 #[test]
541 fn test_neighborhood_empty_graph() {
542 let graph = GraphData::new();
543 let result = neighborhood(&graph, "x", 1, None);
544 assert!(result.is_err());
545 }
546
547 #[test]
548 fn test_neighborhood_radius_zero() {
549 let graph = create_test_graph();
550 let result = neighborhood(&graph, "a", 0, None).unwrap();
551
552 assert_eq!(result.center.id, "a");
553 assert!(result.nodes.is_empty());
554 }
555
556 #[test]
557 fn test_neighborhood_depth_capped() {
558 let graph = create_test_graph();
559 let result = neighborhood(&graph, "a", 100, None).unwrap();
561 assert!(!result.nodes.is_empty());
563 }
564
565 #[test]
570 fn test_shortest_path_found() {
571 let graph = create_test_graph();
572 let result = shortest_path(&graph, "a", "c").unwrap();
573
574 assert!(result.found);
575 assert!(!result.path.is_empty());
576 assert_eq!(result.path.first().unwrap().id, "a");
577 assert_eq!(result.path.last().unwrap().id, "c");
578 }
579
580 #[test]
581 fn test_shortest_path_not_found() {
582 let graph = create_test_graph();
583 let result = shortest_path(&graph, "c", "a").unwrap();
584 assert!(!result.found);
585 }
586
587 #[test]
588 fn test_shortest_path_same_node() {
589 let graph = create_test_graph();
590 let result = shortest_path(&graph, "a", "a").unwrap();
591 assert!(result.found);
592 assert_eq!(result.path.len(), 1);
593 assert_eq!(result.total_weight, 0.0);
594 }
595
596 #[test]
597 fn test_shortest_path_missing_from() {
598 let graph = create_test_graph();
599 let result = shortest_path(&graph, "missing", "a").unwrap();
600 assert!(!result.found);
601 }
602
603 #[test]
604 fn test_shortest_path_missing_to() {
605 let graph = create_test_graph();
606 let result = shortest_path(&graph, "a", "missing").unwrap();
607 assert!(!result.found);
608 }
609
610 #[test]
615 fn test_prerequisites_sorted() {
616 let graph = create_test_graph();
617 let result = prerequisites_sorted(&graph, "c").unwrap();
618
619 assert_eq!(result.target.id, "c");
620 assert!(!result.has_cycles);
621 assert!(result.ordered.iter().any(|n| n.id == "a"));
622 assert!(result.ordered.iter().any(|n| n.id == "b"));
623 let a_pos = result.ordered.iter().position(|n| n.id == "a").unwrap();
625 let b_pos = result.ordered.iter().position(|n| n.id == "b").unwrap();
626 assert!(a_pos < b_pos);
627 }
628
629 #[test]
630 fn test_prerequisites_no_deps() {
631 let graph = create_test_graph();
632 let result = prerequisites_sorted(&graph, "a").unwrap();
633
634 assert_eq!(result.target.id, "a");
635 assert!(result.ordered.is_empty());
636 }
637
638 #[test]
639 fn test_prerequisites_not_found() {
640 let graph = create_test_graph();
641 let result = prerequisites_sorted(&graph, "nonexistent");
642 assert!(result.is_err());
643 }
644
645 #[test]
650 fn test_calculate_centrality() {
651 let graph = create_test_graph();
652 let scores = calculate_centrality(&graph);
653
654 assert_eq!(scores.len(), 4);
655 for score in &scores {
656 assert!(score.degree >= 0.0 && score.degree <= 1.0);
657 assert!(score.in_degree >= 0.0 && score.in_degree <= 1.0);
658 assert!(score.out_degree >= 0.0 && score.out_degree <= 1.0);
659 }
660 }
661
662 #[test]
663 fn test_calculate_centrality_empty() {
664 let graph = GraphData::new();
665 let scores = calculate_centrality(&graph);
666 assert!(scores.is_empty());
667 }
668
669 #[test]
670 fn test_calculate_centrality_single_node() {
671 let mut graph = GraphData::new();
672 graph.add_node(Node::new("a", "A"));
673 let scores = calculate_centrality(&graph);
674 assert!(scores.is_empty());
676 }
677
678 #[test]
683 fn test_find_bridges() {
684 let graph = create_test_graph();
685 let bridges = find_bridges(&graph, 2);
686
687 assert!(!bridges.is_empty());
688 assert!(bridges.len() <= 2);
689 }
690
691 #[test]
692 fn test_find_bridges_empty() {
693 let graph = GraphData::new();
694 let bridges = find_bridges(&graph, 5);
695 assert!(bridges.is_empty());
696 }
697
698 #[test]
703 fn test_get_related_outgoing() {
704 let graph = create_test_graph();
705 let related = get_related(
706 &graph,
707 "a",
708 &[Relationship::Prerequisite],
709 Direction::Outgoing,
710 )
711 .unwrap();
712
713 assert_eq!(related.len(), 1);
714 assert_eq!(related[0].0.id, "b");
715 assert_eq!(related[0].1, Relationship::Prerequisite);
716 }
717
718 #[test]
719 fn test_get_related_incoming() {
720 let graph = create_test_graph();
721 let related = get_related(
722 &graph,
723 "b",
724 &[Relationship::Prerequisite],
725 Direction::Incoming,
726 )
727 .unwrap();
728
729 assert_eq!(related.len(), 1);
730 assert_eq!(related[0].0.id, "a");
731 }
732
733 #[test]
734 fn test_get_related_not_found() {
735 let graph = create_test_graph();
736 let result = get_related(
737 &graph,
738 "nonexistent",
739 &[Relationship::Prerequisite],
740 Direction::Outgoing,
741 );
742 assert!(result.is_err());
743 }
744
745 #[test]
750 fn test_graph_data_prerequisites() {
751 let graph = create_test_graph();
752 let prereqs = graph.prerequisites("a").unwrap();
753 assert_eq!(prereqs.len(), 1);
755 assert_eq!(prereqs[0].id, "b");
756 }
757
758 #[test]
759 fn test_graph_data_dependents() {
760 let graph = create_test_graph();
761 let deps = graph.dependents("b").unwrap();
763 assert_eq!(deps.len(), 1);
764 assert_eq!(deps[0].id, "a");
765 }
766
767 #[test]
768 fn test_graph_data_related_by() {
769 let graph = create_test_graph();
770 let related = graph.related_by("a", &Relationship::RelatesTo).unwrap();
771 assert_eq!(related.len(), 1);
772 assert_eq!(related[0].id, "d");
773 }
774}