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