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