1use crate::knowledge::entity::{Entity, EntityStore, EntityType, Relationship};
7use std::collections::{HashMap, HashSet, VecDeque};
8
9pub use brainwires_core::graph::{EdgeType, GraphEdge, GraphNode};
11
12#[derive(Debug, Default)]
14pub struct RelationshipGraph {
15 nodes: HashMap<String, GraphNode>,
16 edges: Vec<GraphEdge>,
17 adjacency: HashMap<String, Vec<usize>>, }
19
20impl RelationshipGraph {
21 pub fn new() -> Self {
23 Self::default()
24 }
25
26 pub fn from_entity_store(store: &EntityStore) -> Self {
28 let mut graph = Self::new();
29
30 for entity in store.get_top_entities(100) {
32 graph.add_node(GraphNode {
33 entity_name: entity.name.clone(),
34 entity_type: entity.entity_type.clone(),
35 message_ids: entity.message_ids.clone(),
36 mention_count: entity.mention_count,
37 importance: Self::calculate_importance(entity),
38 });
39 }
40
41 graph
42 }
43
44 fn calculate_importance(entity: &Entity) -> f32 {
46 let mut score = 0.0;
47
48 score += (entity.mention_count as f32).ln().max(0.0) * 0.3;
50
51 score += match entity.entity_type {
53 EntityType::File => 0.4,
54 EntityType::Function => 0.3,
55 EntityType::Type => 0.35,
56 EntityType::Error => 0.25,
57 EntityType::Concept => 0.2,
58 EntityType::Variable => 0.1,
59 EntityType::Command => 0.15,
60 };
61
62 score += (entity.message_ids.len() as f32 * 0.05).min(0.2);
65
66 score.clamp(0.0, 1.0)
67 }
68
69 pub fn add_node(&mut self, node: GraphNode) {
71 let name = node.entity_name.clone();
72 if !self.adjacency.contains_key(&name) {
73 self.adjacency.insert(name.clone(), Vec::new());
74 }
75 self.nodes.insert(name, node);
76 }
77
78 pub fn add_edge(&mut self, edge: GraphEdge) {
80 let idx = self.edges.len();
81
82 if let Some(adj) = self.adjacency.get_mut(&edge.from) {
84 adj.push(idx);
85 }
86 if let Some(adj) = self.adjacency.get_mut(&edge.to) {
87 adj.push(idx);
88 }
89
90 self.edges.push(edge);
91 }
92
93 pub fn add_relationship(&mut self, rel: &Relationship) {
95 let (from, to, edge_type, message_id) = match rel {
96 Relationship::CoOccurs {
97 entity_a,
98 entity_b,
99 message_id,
100 } => (
101 entity_a.clone(),
102 entity_b.clone(),
103 EdgeType::CoOccurs,
104 Some(message_id.clone()),
105 ),
106 Relationship::Contains {
107 container,
108 contained,
109 } => (
110 container.clone(),
111 contained.clone(),
112 EdgeType::Contains,
113 None,
114 ),
115 Relationship::References { from, to } => {
116 (from.clone(), to.clone(), EdgeType::References, None)
117 }
118 Relationship::DependsOn {
119 dependent,
120 dependency,
121 } => (
122 dependent.clone(),
123 dependency.clone(),
124 EdgeType::DependsOn,
125 None,
126 ),
127 Relationship::Modifies {
128 modifier, modified, ..
129 } => (modifier.clone(), modified.clone(), EdgeType::Modifies, None),
130 Relationship::Defines {
131 definer, defined, ..
132 } => (definer.clone(), defined.clone(), EdgeType::Defines, None),
133 };
134
135 if self.nodes.contains_key(&from) && self.nodes.contains_key(&to) {
137 self.add_edge(GraphEdge {
138 from,
139 to,
140 weight: edge_type.weight(),
141 edge_type,
142 message_id,
143 });
144 }
145 }
146
147 pub fn get_node(&self, name: &str) -> Option<&GraphNode> {
149 self.nodes.get(name)
150 }
151
152 pub fn get_neighbors(&self, name: &str) -> Vec<&GraphNode> {
154 let mut neighbors = Vec::new();
155
156 if let Some(edge_indices) = self.adjacency.get(name) {
157 for &idx in edge_indices {
158 if let Some(edge) = self.edges.get(idx) {
159 let neighbor_name = if edge.from == name {
160 &edge.to
161 } else {
162 &edge.from
163 };
164 if let Some(node) = self.nodes.get(neighbor_name) {
165 neighbors.push(node);
166 }
167 }
168 }
169 }
170
171 neighbors
172 }
173
174 pub fn get_edges(&self, name: &str) -> Vec<&GraphEdge> {
176 self.adjacency
177 .get(name)
178 .map(|indices| {
179 indices
180 .iter()
181 .filter_map(|&idx| self.edges.get(idx))
182 .collect()
183 })
184 .unwrap_or_default()
185 }
186
187 pub fn find_path(&self, from: &str, to: &str) -> Option<Vec<String>> {
189 if !self.nodes.contains_key(from) || !self.nodes.contains_key(to) {
190 return None;
191 }
192
193 if from == to {
194 return Some(vec![from.to_string()]);
195 }
196
197 let mut visited = HashSet::new();
198 let mut queue = VecDeque::new();
199 let mut parent: HashMap<String, String> = HashMap::new();
200
201 queue.push_back(from.to_string());
202 visited.insert(from.to_string());
203
204 while let Some(current) = queue.pop_front() {
205 for neighbor in self.get_neighbors(¤t) {
206 if !visited.contains(&neighbor.entity_name) {
207 visited.insert(neighbor.entity_name.clone());
208 parent.insert(neighbor.entity_name.clone(), current.clone());
209
210 if neighbor.entity_name == to {
211 let mut path = vec![to.to_string()];
213 let mut node = to.to_string();
214 while let Some(p) = parent.get(&node) {
215 path.push(p.clone());
216 node = p.clone();
217 }
218 path.reverse();
219 return Some(path);
220 }
221
222 queue.push_back(neighbor.entity_name.clone());
223 }
224 }
225 }
226
227 None
228 }
229
230 pub fn get_entity_context(&self, entity: &str, max_depth: usize) -> EntityContext {
232 let mut context = EntityContext {
233 root: entity.to_string(),
234 related_entities: Vec::new(),
235 message_ids: HashSet::new(),
236 };
237
238 if let Some(node) = self.nodes.get(entity) {
239 context.message_ids.extend(node.message_ids.clone());
240 }
241
242 let mut visited = HashSet::new();
243 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
244
245 queue.push_back((entity.to_string(), 0));
246 visited.insert(entity.to_string());
247
248 while let Some((current, depth)) = queue.pop_front() {
249 if depth >= max_depth {
250 continue;
251 }
252
253 for edge in self.get_edges(¤t) {
254 let neighbor = if edge.from == current {
255 &edge.to
256 } else {
257 &edge.from
258 };
259
260 if !visited.contains(neighbor) {
261 visited.insert(neighbor.clone());
262
263 if let Some(node) = self.nodes.get(neighbor) {
264 context.related_entities.push(RelatedEntity {
265 name: neighbor.clone(),
266 entity_type: node.entity_type.clone(),
267 relationship: edge.edge_type.clone(),
268 distance: depth + 1,
269 relevance: edge.weight * (0.8_f32).powi((depth + 1) as i32),
270 });
271 context.message_ids.extend(node.message_ids.clone());
272 }
273
274 queue.push_back((neighbor.clone(), depth + 1));
275 }
276 }
277 }
278
279 context.related_entities.sort_by(|a, b| {
281 b.relevance
282 .partial_cmp(&a.relevance)
283 .unwrap_or(std::cmp::Ordering::Equal)
284 });
285
286 context
287 }
288
289 pub fn search(&self, query: &str, limit: usize) -> Vec<&GraphNode> {
291 let query_lower = query.to_lowercase();
292 let query_words: HashSet<_> = query_lower.split_whitespace().collect();
293
294 let mut scored: Vec<_> = self
295 .nodes
296 .values()
297 .map(|node| {
298 let name_lower = node.entity_name.to_lowercase();
299 let mut score = 0.0;
300
301 if name_lower == query_lower {
303 score += 1.0;
304 }
305 else if name_lower.contains(&query_lower) {
307 score += 0.7;
308 }
309 else if query_lower.contains(&name_lower) {
311 score += 0.5;
312 }
313 else {
315 let name_words: HashSet<_> =
316 name_lower.split(|c: char| !c.is_alphanumeric()).collect();
317 let overlap = query_words.intersection(&name_words).count();
318 score += overlap as f32 * 0.3;
319 }
320
321 score *= 1.0 + node.importance * 0.5;
323
324 (node, score)
325 })
326 .filter(|(_, score)| *score > 0.0)
327 .collect();
328
329 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
330
331 scored
332 .into_iter()
333 .take(limit)
334 .map(|(node, _)| node)
335 .collect()
336 }
337
338 pub fn stats(&self) -> GraphStats {
340 let mut type_counts = HashMap::new();
341 for node in self.nodes.values() {
342 *type_counts.entry(node.entity_type.as_str()).or_insert(0) += 1;
343 }
344
345 let mut edge_type_counts = HashMap::new();
346 for edge in &self.edges {
347 *edge_type_counts
348 .entry(format!("{:?}", edge.edge_type))
349 .or_insert(0) += 1;
350 }
351
352 GraphStats {
353 node_count: self.nodes.len(),
354 edge_count: self.edges.len(),
355 nodes_by_type: type_counts,
356 edges_by_type: edge_type_counts,
357 }
358 }
359
360 pub fn get_impact_set(&self, entity: &str, depth: usize) -> Vec<ImpactedEntity> {
365 let mut impacts = Vec::new();
366 let mut visited = HashSet::new();
367 let mut queue: VecDeque<(String, usize, f32)> = VecDeque::new();
368
369 if !self.nodes.contains_key(entity) {
370 return impacts;
371 }
372
373 queue.push_back((entity.to_string(), 0, 1.0));
374 visited.insert(entity.to_string());
375
376 while let Some((current, current_depth, current_impact)) = queue.pop_front() {
377 if current_depth >= depth {
378 continue;
379 }
380
381 for edge in self.get_edges(¤t) {
382 let neighbor = if edge.from == current {
383 &edge.to
384 } else {
385 &edge.from
386 };
387
388 if !visited.contains(neighbor) {
389 visited.insert(neighbor.clone());
390
391 let impact_factor = match edge.edge_type {
393 EdgeType::DependsOn => 0.9,
394 EdgeType::Contains => 0.8,
395 EdgeType::Modifies => 0.7,
396 EdgeType::References => 0.5,
397 EdgeType::Defines => 0.6,
398 EdgeType::CoOccurs => 0.3,
399 };
400
401 let propagated_impact = current_impact * impact_factor * edge.weight;
402
403 if let Some(node) = self.nodes.get(neighbor) {
404 impacts.push(ImpactedEntity {
405 name: neighbor.clone(),
406 entity_type: node.entity_type.clone(),
407 distance: current_depth + 1,
408 impact_score: propagated_impact,
409 impact_path: vec![current.clone(), neighbor.clone()],
410 });
411 }
412
413 queue.push_back((neighbor.clone(), current_depth + 1, propagated_impact));
414 }
415 }
416 }
417
418 impacts.sort_by(|a, b| {
420 b.impact_score
421 .partial_cmp(&a.impact_score)
422 .unwrap_or(std::cmp::Ordering::Equal)
423 });
424
425 impacts
426 }
427
428 pub fn find_clusters(&self) -> Vec<EntityCluster> {
430 let mut clusters = Vec::new();
431 let mut visited = HashSet::new();
432
433 for node_name in self.nodes.keys() {
434 if visited.contains(node_name) {
435 continue;
436 }
437
438 let mut cluster_nodes = Vec::new();
440 let mut queue = VecDeque::new();
441 queue.push_back(node_name.clone());
442 visited.insert(node_name.clone());
443
444 while let Some(current) = queue.pop_front() {
445 if let Some(node) = self.nodes.get(¤t) {
446 cluster_nodes.push(node.clone());
447 }
448
449 for neighbor in self.get_neighbors(¤t) {
450 if !visited.contains(&neighbor.entity_name) {
451 visited.insert(neighbor.entity_name.clone());
452 queue.push_back(neighbor.entity_name.clone());
453 }
454 }
455 }
456
457 if !cluster_nodes.is_empty() {
458 let total_importance: f32 = cluster_nodes.iter().map(|n| n.importance).sum();
460 let avg_importance = total_importance / cluster_nodes.len() as f32;
461
462 let mut type_counts = HashMap::new();
464 for node in &cluster_nodes {
465 *type_counts.entry(node.entity_type.clone()).or_insert(0) += 1;
466 }
467 let dominant_type = type_counts
468 .into_iter()
469 .max_by_key(|(_, count)| *count)
470 .map(|(t, _)| t);
471
472 clusters.push(EntityCluster {
473 id: clusters.len(),
474 nodes: cluster_nodes,
475 avg_importance,
476 dominant_type,
477 });
478 }
479 }
480
481 clusters.sort_by(|a, b| b.nodes.len().cmp(&a.nodes.len()));
483
484 clusters
485 }
486
487 pub fn suggest_related(&self, entities: &[&str]) -> Vec<SuggestedEntity> {
490 let mut scores: HashMap<String, f32> = HashMap::new();
491 let entity_set: HashSet<_> = entities.iter().copied().collect();
492
493 for entity in entities {
494 for neighbor in self.get_neighbors(entity) {
496 if !entity_set.contains(neighbor.entity_name.as_str()) {
497 *scores.entry(neighbor.entity_name.clone()).or_default() += neighbor.importance;
498 }
499 }
500
501 for first_neighbor in self.get_neighbors(entity) {
503 if entity_set.contains(first_neighbor.entity_name.as_str()) {
504 continue;
505 }
506 for second_neighbor in self.get_neighbors(&first_neighbor.entity_name) {
507 if !entity_set.contains(second_neighbor.entity_name.as_str())
508 && second_neighbor.entity_name != *entity
509 {
510 *scores
511 .entry(second_neighbor.entity_name.clone())
512 .or_default() += second_neighbor.importance * 0.5;
513 }
514 }
515 }
516 }
517
518 let mut suggestions: Vec<_> = scores
520 .into_iter()
521 .filter_map(|(name, score)| {
522 self.nodes.get(&name).map(|node| SuggestedEntity {
523 name: name.clone(),
524 entity_type: node.entity_type.clone(),
525 relevance_score: score,
526 reason: self.get_suggestion_reason(&name, entities),
527 })
528 })
529 .collect();
530
531 suggestions.sort_by(|a, b| {
533 b.relevance_score
534 .partial_cmp(&a.relevance_score)
535 .unwrap_or(std::cmp::Ordering::Equal)
536 });
537
538 suggestions.truncate(10);
539 suggestions
540 }
541
542 fn get_suggestion_reason(&self, suggested: &str, source_entities: &[&str]) -> String {
544 for source in source_entities {
545 let edges = self.get_edges(source);
547 for edge in edges {
548 let other = if edge.from == *source {
549 &edge.to
550 } else {
551 &edge.from
552 };
553 if other == suggested {
554 return format!("{:?} by {}", edge.edge_type, source);
555 }
556 }
557 }
558 "Related through graph".to_string()
559 }
560
561 pub fn get_central_nodes(&self, limit: usize) -> Vec<&GraphNode> {
563 let mut centrality: Vec<_> = self
564 .nodes
565 .iter()
566 .map(|(name, node)| {
567 let degree = self.adjacency.get(name).map(|v| v.len()).unwrap_or(0);
568 let weighted_score = node.importance * 0.7 + (degree as f32 / 10.0).min(0.3);
569 (node, weighted_score)
570 })
571 .collect();
572
573 centrality.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
574
575 centrality.into_iter().take(limit).map(|(n, _)| n).collect()
576 }
577
578 #[cfg(feature = "spectral")]
586 fn to_adjacency_matrix(&self) -> (ndarray::Array2<f32>, Vec<String>) {
587 let names: Vec<String> = self.nodes.keys().cloned().collect();
588 let n = names.len();
589 let idx: HashMap<&str, usize> = names
590 .iter()
591 .enumerate()
592 .map(|(i, s)| (s.as_str(), i))
593 .collect();
594
595 let mut adj = ndarray::Array2::<f32>::zeros((n, n));
596 for edge in &self.edges {
597 if let (Some(&i), Some(&j)) = (idx.get(edge.from.as_str()), idx.get(edge.to.as_str())) {
598 adj[[i, j]] += edge.weight;
599 adj[[j, i]] += edge.weight;
600 }
601 }
602
603 (adj, names)
604 }
605
606 #[cfg(feature = "spectral")]
617 pub fn spectral_clusters(&self, k: usize) -> Vec<EntityCluster> {
618 if self.nodes.is_empty() || k == 0 {
619 return Vec::new();
620 }
621
622 let (adj, names) = self.to_adjacency_matrix();
623 let assignments = match crate::spectral::graph_ops::spectral_cluster(&adj, k) {
624 Some(a) => a,
625 None => return self.find_clusters(), };
627
628 let max_cluster = assignments.iter().copied().max().unwrap_or(0);
630 let mut cluster_nodes: Vec<Vec<GraphNode>> = vec![Vec::new(); max_cluster + 1];
631
632 for (i, &cluster_id) in assignments.iter().enumerate() {
633 if let Some(node) = self.nodes.get(&names[i]) {
634 cluster_nodes[cluster_id].push(node.clone());
635 }
636 }
637
638 cluster_nodes
640 .into_iter()
641 .enumerate()
642 .filter(|(_, nodes)| !nodes.is_empty())
643 .map(|(id, nodes)| {
644 let avg_importance =
645 nodes.iter().map(|n| n.importance).sum::<f32>() / nodes.len() as f32;
646 let mut type_counts = HashMap::new();
647 for node in &nodes {
648 *type_counts.entry(node.entity_type.clone()).or_insert(0) += 1;
649 }
650 let dominant_type = type_counts
651 .into_iter()
652 .max_by_key(|(_, c)| *c)
653 .map(|(t, _)| t);
654
655 EntityCluster {
656 id,
657 nodes,
658 avg_importance,
659 dominant_type,
660 }
661 })
662 .collect()
663 }
664
665 #[cfg(feature = "spectral")]
675 pub fn spectral_central_nodes(&self, limit: usize) -> Vec<(&GraphNode, f32)> {
676 if self.nodes.is_empty() {
677 return Vec::new();
678 }
679
680 let (adj, names) = self.to_adjacency_matrix();
681 let scores = crate::spectral::graph_ops::spectral_centrality(&adj);
682
683 scores
684 .into_iter()
685 .filter_map(|(i, score)| self.nodes.get(&names[i]).map(|node| (node, score)))
686 .take(limit)
687 .collect()
688 }
689
690 #[cfg(feature = "spectral")]
700 pub fn connectivity(&self) -> f32 {
701 if self.nodes.len() < 2 {
702 return 0.0;
703 }
704 let (adj, _) = self.to_adjacency_matrix();
705 crate::spectral::graph_ops::algebraic_connectivity(&adj)
706 }
707
708 #[cfg(feature = "spectral")]
720 pub fn sparsify(&mut self, epsilon: f32) {
721 if self.nodes.len() < 4 {
722 return; }
724
725 let (adj, names) = self.to_adjacency_matrix();
726 let sparse_adj = crate::spectral::graph_ops::sparsify(&adj, epsilon);
727
728 let idx: HashMap<&str, usize> = names
729 .iter()
730 .enumerate()
731 .map(|(i, s)| (s.as_str(), i))
732 .collect();
733
734 let mut new_edges = Vec::new();
736 let mut new_adjacency: HashMap<String, Vec<usize>> = HashMap::new();
737
738 for name in self.nodes.keys() {
740 new_adjacency.insert(name.clone(), Vec::new());
741 }
742
743 for edge in &self.edges {
744 if let (Some(&i), Some(&j)) = (idx.get(edge.from.as_str()), idx.get(edge.to.as_str())) {
745 if sparse_adj[[i, j]] > 0.0 {
746 let edge_idx = new_edges.len();
747 if let Some(adj_list) = new_adjacency.get_mut(&edge.from) {
748 adj_list.push(edge_idx);
749 }
750 if let Some(adj_list) = new_adjacency.get_mut(&edge.to) {
751 adj_list.push(edge_idx);
752 }
753 new_edges.push(edge.clone());
754 }
755 }
756 }
757
758 self.edges = new_edges;
759 self.adjacency = new_adjacency;
760 }
761}
762
763impl brainwires_core::graph::RelationshipGraphT for RelationshipGraph {
764 fn get_node(&self, name: &str) -> Option<&GraphNode> {
765 self.nodes.get(name)
766 }
767
768 fn get_neighbors(&self, name: &str) -> Vec<&GraphNode> {
769 RelationshipGraph::get_neighbors(self, name)
770 }
771
772 fn get_edges(&self, name: &str) -> Vec<&GraphEdge> {
773 RelationshipGraph::get_edges(self, name)
774 }
775
776 fn search(&self, query: &str, limit: usize) -> Vec<&GraphNode> {
777 RelationshipGraph::search(self, query, limit)
778 }
779
780 fn find_path(&self, from: &str, to: &str) -> Option<Vec<String>> {
781 RelationshipGraph::find_path(self, from, to)
782 }
783}
784
785#[derive(Debug, Clone)]
787pub struct ImpactedEntity {
788 pub name: String,
790 pub entity_type: EntityType,
792 pub distance: usize,
794 pub impact_score: f32,
796 pub impact_path: Vec<String>,
798}
799
800#[derive(Debug)]
802pub struct EntityCluster {
803 pub id: usize,
805 pub nodes: Vec<GraphNode>,
807 pub avg_importance: f32,
809 pub dominant_type: Option<EntityType>,
811}
812
813#[derive(Debug)]
815pub struct SuggestedEntity {
816 pub name: String,
818 pub entity_type: EntityType,
820 pub relevance_score: f32,
822 pub reason: String,
824}
825
826#[derive(Debug)]
828pub struct EntityContext {
829 pub root: String,
831 pub related_entities: Vec<RelatedEntity>,
833 pub message_ids: HashSet<String>,
835}
836
837#[derive(Debug)]
839pub struct RelatedEntity {
840 pub name: String,
842 pub entity_type: EntityType,
844 pub relationship: EdgeType,
846 pub distance: usize,
848 pub relevance: f32,
850}
851
852#[derive(Debug)]
854pub struct GraphStats {
855 pub node_count: usize,
857 pub edge_count: usize,
859 pub nodes_by_type: HashMap<&'static str, usize>,
861 pub edges_by_type: HashMap<String, usize>,
863}
864
865#[cfg(test)]
866mod tests {
867 use super::*;
868
869 fn create_test_graph() -> RelationshipGraph {
870 let mut graph = RelationshipGraph::new();
871
872 graph.add_node(GraphNode {
874 entity_name: "src/main.rs".to_string(),
875 entity_type: EntityType::File,
876 message_ids: vec!["msg1".to_string(), "msg2".to_string()],
877 mention_count: 5,
878 importance: 0.8,
879 });
880
881 graph.add_node(GraphNode {
882 entity_name: "main".to_string(),
883 entity_type: EntityType::Function,
884 message_ids: vec!["msg1".to_string()],
885 mention_count: 2,
886 importance: 0.6,
887 });
888
889 graph.add_node(GraphNode {
890 entity_name: "Config".to_string(),
891 entity_type: EntityType::Type,
892 message_ids: vec!["msg2".to_string()],
893 mention_count: 3,
894 importance: 0.7,
895 });
896
897 graph.add_edge(GraphEdge {
899 from: "src/main.rs".to_string(),
900 to: "main".to_string(),
901 edge_type: EdgeType::Contains,
902 weight: 0.9,
903 message_id: Some("msg1".to_string()),
904 });
905
906 graph.add_edge(GraphEdge {
907 from: "main".to_string(),
908 to: "Config".to_string(),
909 edge_type: EdgeType::References,
910 weight: 0.6,
911 message_id: Some("msg2".to_string()),
912 });
913
914 graph
915 }
916
917 #[test]
918 fn test_add_and_get_node() {
919 let graph = create_test_graph();
920
921 let node = graph.get_node("src/main.rs");
922 assert!(node.is_some());
923 assert_eq!(node.unwrap().mention_count, 5);
924 }
925
926 #[test]
927 fn test_get_neighbors() {
928 let graph = create_test_graph();
929
930 let neighbors = graph.get_neighbors("src/main.rs");
931 assert_eq!(neighbors.len(), 1);
932 assert_eq!(neighbors[0].entity_name, "main");
933 }
934
935 #[test]
936 fn test_find_path() {
937 let graph = create_test_graph();
938
939 let path = graph.find_path("src/main.rs", "Config");
940 assert!(path.is_some());
941 let path = path.unwrap();
942 assert_eq!(path.len(), 3);
943 assert_eq!(path[0], "src/main.rs");
944 assert_eq!(path[2], "Config");
945 }
946
947 #[test]
948 fn test_get_entity_context() {
949 let graph = create_test_graph();
950
951 let context = graph.get_entity_context("src/main.rs", 2);
952 assert_eq!(context.root, "src/main.rs");
953 assert!(!context.related_entities.is_empty());
954 assert!(!context.message_ids.is_empty());
955 }
956
957 #[test]
958 fn test_search() {
959 let graph = create_test_graph();
960
961 let results = graph.search("main", 5);
962 assert!(!results.is_empty());
963 assert!(results.iter().any(|n| n.entity_name == "main"));
965 }
966
967 #[test]
968 fn test_graph_stats() {
969 let graph = create_test_graph();
970
971 let stats = graph.stats();
972 assert_eq!(stats.node_count, 3);
973 assert_eq!(stats.edge_count, 2);
974 }
975
976 #[test]
977 fn test_empty_path() {
978 let graph = create_test_graph();
979
980 let mut graph = graph;
982 graph.add_node(GraphNode {
983 entity_name: "isolated".to_string(),
984 entity_type: EntityType::Concept,
985 message_ids: vec![],
986 mention_count: 1,
987 importance: 0.1,
988 });
989
990 let path = graph.find_path("src/main.rs", "isolated");
991 assert!(path.is_none());
992 }
993
994 #[test]
997 fn test_get_impact_set() {
998 let graph = create_test_graph();
999
1000 let impacts = graph.get_impact_set("src/main.rs", 2);
1001 assert!(!impacts.is_empty());
1002
1003 let names: Vec<_> = impacts.iter().map(|i| i.name.as_str()).collect();
1005 assert!(names.contains(&"main"));
1006 }
1007
1008 #[test]
1009 fn test_get_impact_set_empty() {
1010 let graph = create_test_graph();
1011
1012 let impacts = graph.get_impact_set("nonexistent", 2);
1014 assert!(impacts.is_empty());
1015 }
1016
1017 #[test]
1018 fn test_find_clusters() {
1019 let mut graph = create_test_graph();
1020
1021 graph.add_node(GraphNode {
1023 entity_name: "isolated".to_string(),
1024 entity_type: EntityType::Concept,
1025 message_ids: vec![],
1026 mention_count: 1,
1027 importance: 0.1,
1028 });
1029
1030 let clusters = graph.find_clusters();
1031 assert_eq!(clusters.len(), 2);
1032
1033 assert_eq!(clusters[0].nodes.len(), 3);
1035 assert_eq!(clusters[1].nodes.len(), 1);
1036 }
1037
1038 #[test]
1039 fn test_suggest_related() {
1040 let graph = create_test_graph();
1041
1042 let suggestions = graph.suggest_related(&["src/main.rs"]);
1043
1044 let suggested_names: Vec<_> = suggestions.iter().map(|s| s.name.as_str()).collect();
1046 assert!(suggested_names.contains(&"main"));
1047 }
1048
1049 #[test]
1050 fn test_get_central_nodes() {
1051 let graph = create_test_graph();
1052
1053 let central = graph.get_central_nodes(2);
1054 assert!(!central.is_empty());
1055
1056 let names: Vec<_> = central.iter().map(|n| n.entity_name.as_str()).collect();
1058 assert!(names.contains(&"src/main.rs") || names.contains(&"main"));
1059 }
1060}