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