1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::confidence::Confidence;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum NodeType {
13 Class,
14 Function,
15 Module,
16 Concept,
17 Paper,
18 Image,
19 File,
20 Method,
21 Interface,
22 Enum,
23 Struct,
24 Trait,
25 Constant,
26 Variable,
27 Package,
28 Namespace,
29}
30
31impl std::fmt::Display for NodeType {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 NodeType::Class => write!(f, "Class"),
35 NodeType::Function => write!(f, "Function"),
36 NodeType::Module => write!(f, "Module"),
37 NodeType::Concept => write!(f, "Concept"),
38 NodeType::Paper => write!(f, "Paper"),
39 NodeType::Image => write!(f, "Image"),
40 NodeType::File => write!(f, "File"),
41 NodeType::Method => write!(f, "Method"),
42 NodeType::Interface => write!(f, "Interface"),
43 NodeType::Enum => write!(f, "Enum"),
44 NodeType::Struct => write!(f, "Struct"),
45 NodeType::Trait => write!(f, "Trait"),
46 NodeType::Constant => write!(f, "Constant"),
47 NodeType::Variable => write!(f, "Variable"),
48 NodeType::Package => write!(f, "Package"),
49 NodeType::Namespace => write!(f, "Namespace"),
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct GraphNode {
56 pub id: String,
57 pub label: String,
58 pub source_file: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub source_location: Option<String>,
61 pub node_type: NodeType,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub community: Option<usize>,
64 #[serde(flatten)]
65 pub extra: HashMap<String, serde_json::Value>,
66}
67
68fn default_confidence_score() -> f64 {
69 1.0
70}
71
72fn default_weight() -> f64 {
73 1.0
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct GraphEdge {
78 pub source: String,
79 pub target: String,
80 pub relation: String,
81 pub confidence: Confidence,
82 #[serde(default = "default_confidence_score")]
83 pub confidence_score: f64,
84 pub source_file: String,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub source_location: Option<String>,
87 #[serde(default = "default_weight")]
88 pub weight: f64,
89 #[serde(flatten)]
90 pub extra: HashMap<String, serde_json::Value>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct Hyperedge {
95 pub nodes: Vec<String>,
96 pub relation: String,
97 pub label: String,
98}
99
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
101pub struct ExtractionResult {
102 pub nodes: Vec<GraphNode>,
103 pub edges: Vec<GraphEdge>,
104 #[serde(default)]
105 pub hyperedges: Vec<Hyperedge>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct CommunityInfo {
110 pub id: usize,
111 pub nodes: Vec<String>,
112 pub cohesion: f64,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub label: Option<String>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct GodNode {
119 pub id: String,
120 pub label: String,
121 pub degree: usize,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub community: Option<usize>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct Surprise {
128 pub source: String,
129 pub target: String,
130 pub source_community: usize,
131 pub target_community: usize,
132 pub relation: String,
133}
134
135#[derive(Debug, Clone, Default, Serialize, Deserialize)]
136pub struct AnalysisResult {
137 pub god_nodes: Vec<GodNode>,
138 pub surprises: Vec<Surprise>,
139 pub questions: Vec<String>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct BridgeNode {
145 pub id: String,
146 pub label: String,
147 pub total_edges: usize,
148 pub cross_community_edges: usize,
149 pub bridge_ratio: f64,
151 pub communities_touched: Vec<usize>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct PageRankNode {
157 pub id: String,
158 pub label: String,
159 pub score: f64,
160 pub degree: usize,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct DependencyCycle {
166 pub nodes: Vec<String>,
167 pub edges: Vec<(String, String)>,
168 pub severity: f64,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct TemporalNode {
175 pub id: String,
176 pub label: String,
177 pub last_modified: String,
178 pub change_count: usize,
179 pub age_days: u64,
180 pub churn_rate: f64,
181 pub risk_score: f64,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct SimilarPair {
187 pub node_a: String,
188 pub node_b: String,
189 pub similarity: f64,
190 pub label_a: String,
191 pub label_b: String,
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 fn sample_node() -> GraphNode {
199 GraphNode {
200 id: "my_class".into(),
201 label: "MyClass".into(),
202 source_file: "src/main.rs".into(),
203 source_location: Some("L42".into()),
204 node_type: NodeType::Class,
205 community: None,
206 extra: HashMap::new(),
207 }
208 }
209
210 fn sample_edge() -> GraphEdge {
211 GraphEdge {
212 source: "a".into(),
213 target: "b".into(),
214 relation: "calls".into(),
215 confidence: Confidence::Extracted,
216 confidence_score: 1.0,
217 source_file: "src/main.rs".into(),
218 source_location: None,
219 weight: 1.0,
220 extra: HashMap::new(),
221 }
222 }
223
224 #[test]
225 fn node_type_serializes_lowercase() {
226 assert_eq!(
227 serde_json::to_string(&NodeType::Class).unwrap(),
228 r#""class""#
229 );
230 assert_eq!(
231 serde_json::to_string(&NodeType::Function).unwrap(),
232 r#""function""#
233 );
234 assert_eq!(
235 serde_json::to_string(&NodeType::Namespace).unwrap(),
236 r#""namespace""#
237 );
238 }
239
240 #[test]
241 fn node_roundtrip() {
242 let node = sample_node();
243 let json = serde_json::to_string(&node).unwrap();
244 let back: GraphNode = serde_json::from_str(&json).unwrap();
245 assert_eq!(back.id, "my_class");
246 assert_eq!(back.node_type, NodeType::Class);
247 }
248
249 #[test]
250 fn node_skip_none_fields() {
251 let mut node = sample_node();
252 node.source_location = None;
253 node.community = None;
254 let json = serde_json::to_string(&node).unwrap();
255 assert!(!json.contains("source_location"));
256 assert!(!json.contains("community"));
257 }
258
259 #[test]
260 fn edge_defaults() {
261 let json = r#"{
262 "source": "a",
263 "target": "b",
264 "relation": "calls",
265 "confidence": "EXTRACTED",
266 "source_file": "x.rs"
267 }"#;
268 let edge: GraphEdge = serde_json::from_str(json).unwrap();
269 assert!((edge.confidence_score - 1.0).abs() < f64::EPSILON);
270 assert!((edge.weight - 1.0).abs() < f64::EPSILON);
271 }
272
273 #[test]
274 fn edge_roundtrip() {
275 let edge = sample_edge();
276 let json = serde_json::to_string(&edge).unwrap();
277 let back: GraphEdge = serde_json::from_str(&json).unwrap();
278 assert_eq!(back.relation, "calls");
279 assert_eq!(back.confidence, Confidence::Extracted);
280 }
281
282 #[test]
283 fn extraction_result_default() {
284 let r = ExtractionResult::default();
285 assert!(r.nodes.is_empty());
286 assert!(r.edges.is_empty());
287 assert!(r.hyperedges.is_empty());
288 }
289
290 #[test]
291 fn extra_fields_flatten() {
292 let mut node = sample_node();
293 node.extra
294 .insert("custom".into(), serde_json::Value::Bool(true));
295 let json = serde_json::to_string(&node).unwrap();
296 assert!(json.contains(r#""custom":true"#));
297 }
298
299 #[test]
300 fn community_info_roundtrip() {
301 let ci = CommunityInfo {
302 id: 0,
303 nodes: vec!["a".into(), "b".into()],
304 cohesion: 0.85,
305 label: Some("cluster-0".into()),
306 };
307 let json = serde_json::to_string(&ci).unwrap();
308 let back: CommunityInfo = serde_json::from_str(&json).unwrap();
309 assert_eq!(back.id, 0);
310 assert_eq!(back.nodes.len(), 2);
311 }
312
313 #[test]
314 fn analysis_result_default() {
315 let ar = AnalysisResult::default();
316 assert!(ar.god_nodes.is_empty());
317 assert!(ar.surprises.is_empty());
318 assert!(ar.questions.is_empty());
319 }
320}