1mod codegraph_merge;
7
8pub use codegraph_merge::merge_codegraph_edges;
9
10use std::collections::HashSet;
11
12use tracing::debug;
13
14use graphify_core::error::Result;
15use graphify_core::graph::KnowledgeGraph;
16use graphify_core::model::ExtractionResult;
17
18pub fn build_from_extraction(extraction: &ExtractionResult) -> Result<KnowledgeGraph> {
23 let mut graph = KnowledgeGraph::new();
24
25 for node in &extraction.nodes {
26 let _ = graph.add_node(node.clone());
27 }
28
29 let node_ids: HashSet<&str> = extraction.nodes.iter().map(|n| n.id.as_str()).collect();
30
31 let mut skipped = 0usize;
32 for edge in &extraction.edges {
33 if node_ids.contains(edge.source.as_str()) && node_ids.contains(edge.target.as_str()) {
34 let _ = graph.add_edge(edge.clone());
35 } else {
36 skipped += 1;
37 }
38 }
39 if skipped > 0 {
40 debug!("skipped {skipped} dangling edge(s)");
41 }
42
43 graph.set_hyperedges(extraction.hyperedges.clone());
44
45 Ok(graph)
46}
47
48pub fn build(extractions: &[ExtractionResult]) -> Result<KnowledgeGraph> {
53 let mut combined = ExtractionResult::default();
54 for ext in extractions {
55 combined.nodes.extend(ext.nodes.clone());
56 combined.edges.extend(ext.edges.clone());
57 combined.hyperedges.extend(ext.hyperedges.clone());
58 }
59 build_from_extraction(&combined)
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use graphify_core::confidence::Confidence;
66 use graphify_core::model::{GraphEdge, GraphNode, Hyperedge, NodeType};
67 use std::collections::HashMap;
68
69 fn make_node(id: &str) -> GraphNode {
70 GraphNode {
71 id: id.into(),
72 label: id.into(),
73 source_file: "test.rs".into(),
74 source_location: None,
75 node_type: NodeType::Class,
76 community: None,
77 extra: HashMap::new(),
78 }
79 }
80
81 fn make_edge(src: &str, tgt: &str) -> GraphEdge {
82 GraphEdge {
83 source: src.into(),
84 target: tgt.into(),
85 relation: "calls".into(),
86 confidence: Confidence::Extracted,
87 confidence_score: 1.0,
88 source_file: "test.rs".into(),
89 source_location: None,
90 weight: 1.0,
91 provenance: None,
92 extra: HashMap::new(),
93 }
94 }
95
96 #[test]
97 fn build_from_empty() {
98 let ext = ExtractionResult::default();
99 let graph = build_from_extraction(&ext).unwrap();
100 assert_eq!(graph.node_count(), 0);
101 assert_eq!(graph.edge_count(), 0);
102 }
103
104 #[test]
105 fn build_with_nodes_and_edges() {
106 let ext = ExtractionResult {
107 nodes: vec![make_node("a"), make_node("b"), make_node("c")],
108 edges: vec![make_edge("a", "b"), make_edge("b", "c")],
109 hyperedges: vec![],
110 };
111 let graph = build_from_extraction(&ext).unwrap();
112 assert_eq!(graph.node_count(), 3);
113 assert_eq!(graph.edge_count(), 2);
114 assert!(graph.get_node("a").is_some());
115 assert!(graph.get_node("b").is_some());
116 assert!(graph.get_node("c").is_some());
117 }
118
119 #[test]
120 fn dangling_edges_skipped() {
121 let ext = ExtractionResult {
122 nodes: vec![make_node("a"), make_node("b")],
123 edges: vec![
124 make_edge("a", "b"), make_edge("a", "missing"), make_edge("gone", "b"), ],
128 hyperedges: vec![],
129 };
130 let graph = build_from_extraction(&ext).unwrap();
131 assert_eq!(graph.node_count(), 2);
132 assert_eq!(graph.edge_count(), 1); }
134
135 #[test]
136 fn build_merges_multiple_extractions() {
137 let ext1 = ExtractionResult {
138 nodes: vec![make_node("a"), make_node("b")],
139 edges: vec![make_edge("a", "b")],
140 hyperedges: vec![],
141 };
142 let ext2 = ExtractionResult {
143 nodes: vec![make_node("c")],
144 edges: vec![make_edge("b", "c")],
145 hyperedges: vec![],
146 };
147 let graph = build(&[ext1, ext2]).unwrap();
148 assert_eq!(graph.node_count(), 3);
149 assert_eq!(graph.edge_count(), 2);
150 }
151
152 #[test]
153 fn duplicate_nodes_first_wins() {
154 let ext = ExtractionResult {
155 nodes: vec![make_node("a"), make_node("a")],
156 edges: vec![],
157 hyperedges: vec![],
158 };
159 let graph = build_from_extraction(&ext).unwrap();
160 assert_eq!(graph.node_count(), 1);
161 }
162
163 #[test]
164 fn hyperedges_stored() {
165 let ext = ExtractionResult {
166 nodes: vec![make_node("a"), make_node("b")],
167 edges: vec![],
168 hyperedges: vec![Hyperedge {
169 nodes: vec!["a".into(), "b".into()],
170 relation: "coexist".into(),
171 label: "together".into(),
172 }],
173 };
174 let graph = build_from_extraction(&ext).unwrap();
175 assert_eq!(graph.hyperedges.len(), 1);
176 assert_eq!(graph.hyperedges[0].relation, "coexist");
177 }
178}