Skip to main content

fallow_cli/health_types/
mod.rs

1//! Health / complexity analysis report types.
2//!
3//! Separated from the `health` command module so that report formatters
4//! (which are compiled as part of both the lib and bin targets) can
5//! reference these types without pulling in binary-only dependencies.
6
7mod coverage;
8mod scores;
9mod targets;
10mod trends;
11mod vital_signs;
12
13pub use coverage::*;
14pub use scores::*;
15pub use targets::*;
16pub use trends::*;
17pub use vital_signs::*;
18
19/// Detailed timing breakdown for the health pipeline.
20///
21/// Only populated when `--performance` is passed.
22#[derive(Debug, Clone, serde::Serialize)]
23pub struct HealthTimings {
24    pub config_ms: f64,
25    pub discover_ms: f64,
26    pub parse_ms: f64,
27    pub complexity_ms: f64,
28    pub file_scores_ms: f64,
29    pub git_churn_ms: f64,
30    pub git_churn_cache_hit: bool,
31    pub hotspots_ms: f64,
32    pub duplication_ms: f64,
33    pub targets_ms: f64,
34    pub total_ms: f64,
35}
36
37/// Result of complexity analysis for reporting.
38#[derive(Debug, serde::Serialize)]
39pub struct HealthReport {
40    /// Functions exceeding thresholds.
41    pub findings: Vec<HealthFinding>,
42    /// Summary statistics.
43    pub summary: HealthSummary,
44    /// Project-wide vital signs (always computed from available data).
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub vital_signs: Option<VitalSigns>,
47    /// Project-wide health score (only populated with `--score`).
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub health_score: Option<HealthScore>,
50    /// Per-file health scores (only populated with `--file-scores` or `--hotspots`).
51    #[serde(skip_serializing_if = "Vec::is_empty")]
52    pub file_scores: Vec<FileHealthScore>,
53    /// Static coverage gaps (only populated with `--coverage-gaps`).
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub coverage_gaps: Option<CoverageGaps>,
56    /// Hotspot entries (only populated with `--hotspots`).
57    #[serde(skip_serializing_if = "Vec::is_empty")]
58    pub hotspots: Vec<HotspotEntry>,
59    /// Hotspot analysis summary (only set with `--hotspots`).
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub hotspot_summary: Option<HotspotSummary>,
62    /// Functions exceeding 60 LOC (only populated when unit size very-high-risk >= 3%).
63    #[serde(skip_serializing_if = "Vec::is_empty")]
64    pub large_functions: Vec<LargeFunctionEntry>,
65    /// Ranked refactoring recommendations (only populated with `--targets`).
66    #[serde(skip_serializing_if = "Vec::is_empty")]
67    pub targets: Vec<RefactoringTarget>,
68    /// Adaptive thresholds used for target scoring (only set with `--targets`).
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub target_thresholds: Option<TargetThresholds>,
71    /// Health trend comparison against a previous snapshot (only set with `--trend`).
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub health_trend: Option<HealthTrend>,
74}
75
76#[cfg(test)]
77#[expect(
78    clippy::derivable_impls,
79    reason = "test-only Default with custom HealthSummary thresholds (20/15)"
80)]
81impl Default for HealthReport {
82    fn default() -> Self {
83        Self {
84            findings: vec![],
85            summary: HealthSummary::default(),
86            vital_signs: None,
87            health_score: None,
88            file_scores: vec![],
89            coverage_gaps: None,
90            hotspots: vec![],
91            hotspot_summary: None,
92            large_functions: vec![],
93            targets: vec![],
94            target_thresholds: None,
95            health_trend: None,
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn health_report_skips_empty_collections() {
106        let report = HealthReport::default();
107        let json = serde_json::to_string(&report).unwrap();
108        // Empty vecs should be omitted due to skip_serializing_if
109        assert!(!json.contains("file_scores"));
110        assert!(!json.contains("hotspots"));
111        assert!(!json.contains("hotspot_summary"));
112        assert!(!json.contains("large_functions"));
113        assert!(!json.contains("targets"));
114        assert!(!json.contains("vital_signs"));
115        assert!(!json.contains("health_score"));
116    }
117
118    #[test]
119    fn health_score_none_skipped_in_report() {
120        let report = HealthReport::default();
121        let json = serde_json::to_string(&report).unwrap();
122        assert!(!json.contains("health_score"));
123    }
124}