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(skip_serializing_if = "Option::is_none")]
90 pub provenance: Option<String>,
91 #[serde(flatten)]
92 pub extra: HashMap<String, serde_json::Value>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct Hyperedge {
97 pub nodes: Vec<String>,
98 pub relation: String,
99 pub label: String,
100}
101
102#[derive(Debug, Clone, Default, Serialize, Deserialize)]
103pub struct ExtractionResult {
104 pub nodes: Vec<GraphNode>,
105 pub edges: Vec<GraphEdge>,
106 #[serde(default)]
107 pub hyperedges: Vec<Hyperedge>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct CommunityInfo {
112 pub id: usize,
113 pub nodes: Vec<String>,
114 pub cohesion: f64,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub label: Option<String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct GodNode {
121 pub id: String,
122 pub label: String,
123 pub degree: usize,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub community: Option<usize>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct Surprise {
130 pub source: String,
131 pub target: String,
132 pub source_community: usize,
133 pub target_community: usize,
134 pub relation: String,
135}
136
137#[derive(Debug, Clone, Default, Serialize, Deserialize)]
138pub struct AnalysisResult {
139 pub god_nodes: Vec<GodNode>,
140 pub surprises: Vec<Surprise>,
141 pub questions: Vec<String>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct BridgeNode {
147 pub id: String,
148 pub label: String,
149 pub total_edges: usize,
150 pub cross_community_edges: usize,
151 pub bridge_ratio: f64,
153 pub communities_touched: Vec<usize>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct PageRankNode {
159 pub id: String,
160 pub label: String,
161 pub score: f64,
162 pub degree: usize,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct DependencyCycle {
168 pub nodes: Vec<String>,
169 pub edges: Vec<(String, String)>,
170 pub severity: f64,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct TemporalNode {
177 pub id: String,
178 pub label: String,
179 pub last_modified: String,
180 pub change_count: usize,
181 pub age_days: u64,
182 pub churn_rate: f64,
183 pub risk_score: f64,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct SimilarPair {
189 pub node_a: String,
190 pub node_b: String,
191 pub similarity: f64,
192 pub label_a: String,
193 pub label_b: String,
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 fn sample_node() -> GraphNode {
201 GraphNode {
202 id: "my_class".into(),
203 label: "MyClass".into(),
204 source_file: "src/main.rs".into(),
205 source_location: Some("L42".into()),
206 node_type: NodeType::Class,
207 community: None,
208 extra: HashMap::new(),
209 }
210 }
211
212 fn sample_edge() -> GraphEdge {
213 GraphEdge {
214 source: "a".into(),
215 target: "b".into(),
216 relation: "calls".into(),
217 confidence: Confidence::Extracted,
218 confidence_score: 1.0,
219 source_file: "src/main.rs".into(),
220 source_location: None,
221 weight: 1.0,
222 provenance: None,
223 extra: HashMap::new(),
224 }
225 }
226
227 #[test]
228 fn node_type_serializes_lowercase() {
229 assert_eq!(
230 serde_json::to_string(&NodeType::Class).unwrap(),
231 r#""class""#
232 );
233 assert_eq!(
234 serde_json::to_string(&NodeType::Function).unwrap(),
235 r#""function""#
236 );
237 assert_eq!(
238 serde_json::to_string(&NodeType::Namespace).unwrap(),
239 r#""namespace""#
240 );
241 }
242
243 #[test]
244 fn node_roundtrip() {
245 let node = sample_node();
246 let json = serde_json::to_string(&node).unwrap();
247 let back: GraphNode = serde_json::from_str(&json).unwrap();
248 assert_eq!(back.id, "my_class");
249 assert_eq!(back.node_type, NodeType::Class);
250 }
251
252 #[test]
253 fn node_skip_none_fields() {
254 let mut node = sample_node();
255 node.source_location = None;
256 node.community = None;
257 let json = serde_json::to_string(&node).unwrap();
258 assert!(!json.contains("source_location"));
259 assert!(!json.contains("community"));
260 }
261
262 #[test]
263 fn edge_defaults() {
264 let json = r#"{
265 "source": "a",
266 "target": "b",
267 "relation": "calls",
268 "confidence": "EXTRACTED",
269 "source_file": "x.rs"
270 }"#;
271 let edge: GraphEdge = serde_json::from_str(json).unwrap();
272 assert!((edge.confidence_score - 1.0).abs() < f64::EPSILON);
273 assert!((edge.weight - 1.0).abs() < f64::EPSILON);
274 }
275
276 #[test]
277 fn edge_roundtrip() {
278 let edge = sample_edge();
279 let json = serde_json::to_string(&edge).unwrap();
280 let back: GraphEdge = serde_json::from_str(&json).unwrap();
281 assert_eq!(back.relation, "calls");
282 assert_eq!(back.confidence, Confidence::Extracted);
283 }
284
285 #[test]
286 fn extraction_result_default() {
287 let r = ExtractionResult::default();
288 assert!(r.nodes.is_empty());
289 assert!(r.edges.is_empty());
290 assert!(r.hyperedges.is_empty());
291 }
292
293 #[test]
294 fn extra_fields_flatten() {
295 let mut node = sample_node();
296 node.extra
297 .insert("custom".into(), serde_json::Value::Bool(true));
298 let json = serde_json::to_string(&node).unwrap();
299 assert!(json.contains(r#""custom":true"#));
300 }
301
302 #[test]
303 fn community_info_roundtrip() {
304 let ci = CommunityInfo {
305 id: 0,
306 nodes: vec!["a".into(), "b".into()],
307 cohesion: 0.85,
308 label: Some("cluster-0".into()),
309 };
310 let json = serde_json::to_string(&ci).unwrap();
311 let back: CommunityInfo = serde_json::from_str(&json).unwrap();
312 assert_eq!(back.id, 0);
313 assert_eq!(back.nodes.len(), 2);
314 }
315
316 #[test]
317 fn analysis_result_default() {
318 let ar = AnalysisResult::default();
319 assert!(ar.god_nodes.is_empty());
320 assert!(ar.surprises.is_empty());
321 assert!(ar.questions.is_empty());
322 }
323}