debtmap/organization/
god_object_metrics.rs

1use super::{GodObjectAnalysis, GodObjectConfidence};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7/// Metrics tracking for god object detection over time
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct GodObjectMetrics {
10    /// Historical snapshots of god object detections
11    pub snapshots: Vec<GodObjectSnapshot>,
12    /// Summary statistics across all snapshots
13    pub summary: MetricsSummary,
14}
15
16impl Default for GodObjectMetrics {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl GodObjectMetrics {
23    pub fn new() -> Self {
24        Self {
25            snapshots: Vec::new(),
26            summary: MetricsSummary::default(),
27        }
28    }
29
30    /// Record a new snapshot of god object analysis
31    pub fn record_snapshot(&mut self, file_path: PathBuf, analysis: GodObjectAnalysis) {
32        let snapshot = GodObjectSnapshot {
33            timestamp: Utc::now(),
34            file_path,
35            is_god_object: analysis.is_god_object,
36            method_count: analysis.method_count,
37            field_count: analysis.field_count,
38            responsibility_count: analysis.responsibility_count,
39            lines_of_code: analysis.lines_of_code,
40            god_object_score: analysis.god_object_score.value(),
41            confidence: analysis.confidence,
42        };
43
44        self.snapshots.push(snapshot);
45        self.update_summary();
46    }
47
48    /// Update summary statistics based on current snapshots
49    fn update_summary(&mut self) {
50        if self.snapshots.is_empty() {
51            self.summary = MetricsSummary::default();
52            return;
53        }
54
55        let mut total_god_objects = 0;
56        let mut total_methods = 0;
57        let mut total_score = 0.0;
58        let mut file_metrics: HashMap<PathBuf, FileMetricHistory> = HashMap::new();
59
60        for snapshot in &self.snapshots {
61            if snapshot.is_god_object {
62                total_god_objects += 1;
63            }
64            total_methods += snapshot.method_count;
65            total_score += snapshot.god_object_score;
66
67            // Track per-file metrics
68            let file_entry = file_metrics
69                .entry(snapshot.file_path.clone())
70                .or_insert_with(|| FileMetricHistory {
71                    file_path: snapshot.file_path.clone(),
72                    first_seen: snapshot.timestamp,
73                    last_seen: snapshot.timestamp,
74                    max_methods: 0,
75                    max_score: 0.0,
76                    current_is_god_object: false,
77                });
78
79            file_entry.last_seen = snapshot.timestamp;
80            file_entry.max_methods = file_entry.max_methods.max(snapshot.method_count);
81            file_entry.max_score = file_entry.max_score.max(snapshot.god_object_score);
82            file_entry.current_is_god_object = snapshot.is_god_object;
83        }
84
85        let avg_methods = total_methods as f64 / self.snapshots.len() as f64;
86        let avg_score = total_score / self.snapshots.len() as f64;
87
88        self.summary = MetricsSummary {
89            total_snapshots: self.snapshots.len(),
90            total_god_objects_detected: total_god_objects,
91            average_method_count: avg_methods,
92            average_god_object_score: avg_score,
93            files_tracked: file_metrics.len(),
94            file_histories: file_metrics.into_values().collect(),
95        };
96    }
97
98    /// Get trend for a specific file
99    pub fn get_file_trend(&self, file_path: &PathBuf) -> Option<FileTrend> {
100        let file_snapshots: Vec<&GodObjectSnapshot> = self
101            .snapshots
102            .iter()
103            .filter(|s| &s.file_path == file_path)
104            .collect();
105
106        if file_snapshots.len() < 2 {
107            return None;
108        }
109
110        let first = file_snapshots.first()?;
111        let last = file_snapshots.last()?;
112
113        let method_change = last.method_count as i32 - first.method_count as i32;
114        let score_change = last.god_object_score - first.god_object_score;
115
116        Some(FileTrend {
117            file_path: file_path.clone(),
118            method_count_change: method_change,
119            score_change,
120            trend_direction: determine_trend(score_change),
121            improved: score_change < 0.0,
122        })
123    }
124
125    /// Get all files that became god objects
126    pub fn get_new_god_objects(&self) -> Vec<PathBuf> {
127        let mut new_god_objects = Vec::new();
128        let mut file_status: HashMap<PathBuf, bool> = HashMap::new();
129
130        // Process snapshots chronologically
131        for snapshot in &self.snapshots {
132            let was_god_object = file_status.get(&snapshot.file_path).copied();
133            let is_god_object = snapshot.is_god_object;
134
135            if !was_god_object.unwrap_or(false) && is_god_object {
136                new_god_objects.push(snapshot.file_path.clone());
137            }
138
139            file_status.insert(snapshot.file_path.clone(), is_god_object);
140        }
141
142        new_god_objects
143    }
144
145    /// Get all files that stopped being god objects
146    pub fn get_resolved_god_objects(&self) -> Vec<PathBuf> {
147        let mut resolved = Vec::new();
148        let mut file_status: HashMap<PathBuf, bool> = HashMap::new();
149
150        for snapshot in &self.snapshots {
151            let was_god_object = file_status.get(&snapshot.file_path).copied();
152            let is_god_object = snapshot.is_god_object;
153
154            if was_god_object.unwrap_or(false) && !is_god_object {
155                resolved.push(snapshot.file_path.clone());
156            }
157
158            file_status.insert(snapshot.file_path.clone(), is_god_object);
159        }
160
161        resolved
162    }
163}
164
165/// A single snapshot of god object analysis at a point in time
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct GodObjectSnapshot {
168    pub timestamp: DateTime<Utc>,
169    pub file_path: PathBuf,
170    pub is_god_object: bool,
171    pub method_count: usize,
172    pub field_count: usize,
173    pub responsibility_count: usize,
174    pub lines_of_code: usize,
175    pub god_object_score: f64,
176    pub confidence: GodObjectConfidence,
177}
178
179/// Summary statistics across all snapshots
180#[derive(Debug, Clone, Serialize, Deserialize, Default)]
181pub struct MetricsSummary {
182    pub total_snapshots: usize,
183    pub total_god_objects_detected: usize,
184    pub average_method_count: f64,
185    pub average_god_object_score: f64,
186    pub files_tracked: usize,
187    pub file_histories: Vec<FileMetricHistory>,
188}
189
190/// Historical metrics for a single file
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct FileMetricHistory {
193    pub file_path: PathBuf,
194    pub first_seen: DateTime<Utc>,
195    pub last_seen: DateTime<Utc>,
196    pub max_methods: usize,
197    pub max_score: f64,
198    pub current_is_god_object: bool,
199}
200
201/// Trend analysis for a specific file
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct FileTrend {
204    pub file_path: PathBuf,
205    pub method_count_change: i32,
206    pub score_change: f64,
207    pub trend_direction: TrendDirection,
208    pub improved: bool,
209}
210
211/// Direction of trend
212#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
213pub enum TrendDirection {
214    Improving,
215    Stable,
216    Worsening,
217}
218
219fn determine_trend(score_change: f64) -> TrendDirection {
220    if score_change < -10.0 {
221        TrendDirection::Improving
222    } else if score_change > 10.0 {
223        TrendDirection::Worsening
224    } else {
225        TrendDirection::Stable
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use crate::priority::score_types::Score0To100;
233
234    fn create_test_analysis(
235        is_god_object: bool,
236        method_count: usize,
237        score: f64,
238    ) -> GodObjectAnalysis {
239        GodObjectAnalysis {
240            is_god_object,
241            method_count,
242            field_count: 5,
243            responsibility_count: 3,
244            lines_of_code: 500,
245            complexity_sum: 100,
246            god_object_score: Score0To100::new(score),
247            recommended_splits: Vec::new(),
248            confidence: if is_god_object {
249                GodObjectConfidence::Probable
250            } else {
251                GodObjectConfidence::NotGodObject
252            },
253            responsibilities: Vec::new(),
254            responsibility_method_counts: Default::default(),
255            purity_distribution: None,
256            module_structure: None,
257            detection_type: crate::organization::DetectionType::GodClass,
258            struct_name: None,
259            struct_line: None,
260            struct_location: None, // Spec 201: Added for per-struct analysis
261            visibility_breakdown: None, // Spec 134: Added for test compatibility
262            domain_count: 0,
263            domain_diversity: 0.0,
264            struct_ratio: 0.0,
265            analysis_method: crate::organization::SplitAnalysisMethod::None,
266            cross_domain_severity: None,
267            domain_diversity_metrics: None, // Spec 152: Added for test compatibility
268            aggregated_entropy: None,
269            aggregated_error_swallowing_count: None,
270            aggregated_error_swallowing_patterns: None,
271        }
272    }
273
274    #[test]
275    fn test_record_snapshot() {
276        let mut metrics = GodObjectMetrics::new();
277        let analysis = create_test_analysis(true, 30, 150.0);
278
279        metrics.record_snapshot(PathBuf::from("test.rs"), analysis);
280
281        assert_eq!(metrics.snapshots.len(), 1);
282        assert_eq!(metrics.summary.total_snapshots, 1);
283        assert_eq!(metrics.summary.total_god_objects_detected, 1);
284    }
285
286    #[test]
287    fn test_multiple_snapshots() {
288        let mut metrics = GodObjectMetrics::new();
289
290        metrics.record_snapshot(
291            PathBuf::from("file1.rs"),
292            create_test_analysis(true, 30, 150.0),
293        );
294        metrics.record_snapshot(
295            PathBuf::from("file2.rs"),
296            create_test_analysis(false, 10, 50.0),
297        );
298        metrics.record_snapshot(
299            PathBuf::from("file3.rs"),
300            create_test_analysis(true, 50, 250.0),
301        );
302
303        assert_eq!(metrics.snapshots.len(), 3);
304        assert_eq!(metrics.summary.total_god_objects_detected, 2);
305        assert_eq!(metrics.summary.files_tracked, 3);
306        assert_eq!(metrics.summary.average_method_count, 30.0);
307    }
308
309    #[test]
310    fn test_file_trend() {
311        let mut metrics = GodObjectMetrics::new();
312        let file_path = PathBuf::from("evolving.rs");
313
314        // First snapshot - not a god object
315        metrics.record_snapshot(file_path.clone(), create_test_analysis(false, 15, 75.0));
316
317        // Second snapshot - became a god object
318        metrics.record_snapshot(file_path.clone(), create_test_analysis(true, 35, 175.0));
319
320        let trend = metrics.get_file_trend(&file_path).unwrap();
321        assert_eq!(trend.method_count_change, 20);
322        // Score goes from 75.0 to 100.0 (175.0 clamped), so change is 25.0
323        assert_eq!(trend.score_change, 25.0);
324        assert_eq!(trend.trend_direction, TrendDirection::Worsening);
325        assert!(!trend.improved);
326    }
327
328    #[test]
329    fn test_new_god_objects() {
330        let mut metrics = GodObjectMetrics::new();
331
332        metrics.record_snapshot(
333            PathBuf::from("file1.rs"),
334            create_test_analysis(false, 10, 50.0),
335        );
336        metrics.record_snapshot(
337            PathBuf::from("file1.rs"),
338            create_test_analysis(true, 30, 150.0),
339        );
340        metrics.record_snapshot(
341            PathBuf::from("file2.rs"),
342            create_test_analysis(true, 25, 125.0),
343        );
344
345        let new_god_objects = metrics.get_new_god_objects();
346        assert_eq!(new_god_objects.len(), 2);
347        assert!(new_god_objects.contains(&PathBuf::from("file1.rs")));
348        assert!(new_god_objects.contains(&PathBuf::from("file2.rs")));
349    }
350
351    #[test]
352    fn test_resolved_god_objects() {
353        let mut metrics = GodObjectMetrics::new();
354
355        metrics.record_snapshot(
356            PathBuf::from("file1.rs"),
357            create_test_analysis(true, 30, 150.0),
358        );
359        metrics.record_snapshot(
360            PathBuf::from("file1.rs"),
361            create_test_analysis(false, 15, 75.0),
362        );
363
364        let resolved = metrics.get_resolved_god_objects();
365        assert_eq!(resolved.len(), 1);
366        assert!(resolved.contains(&PathBuf::from("file1.rs")));
367    }
368
369    #[test]
370    fn test_trend_direction() {
371        assert_eq!(determine_trend(-20.0), TrendDirection::Improving);
372        assert_eq!(determine_trend(0.0), TrendDirection::Stable);
373        assert_eq!(determine_trend(5.0), TrendDirection::Stable);
374        assert_eq!(determine_trend(20.0), TrendDirection::Worsening);
375    }
376}