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 finding;
9mod grouped;
10mod runtime_coverage;
11mod scores;
12mod targets;
13mod trends;
14mod vital_signs;
15
16pub use coverage::*;
17pub use finding::*;
18pub use grouped::*;
19pub use runtime_coverage::*;
20pub use scores::*;
21pub use targets::*;
22pub use trends::*;
23pub use vital_signs::*;
24
25/// Detailed timing breakdown for the health pipeline.
26///
27/// Only populated when `--performance` is passed.
28#[derive(Debug, Clone, serde::Serialize)]
29pub struct HealthTimings {
30    pub config_ms: f64,
31    pub discover_ms: f64,
32    pub parse_ms: f64,
33    pub complexity_ms: f64,
34    pub file_scores_ms: f64,
35    pub git_churn_ms: f64,
36    pub git_churn_cache_hit: bool,
37    pub hotspots_ms: f64,
38    pub duplication_ms: f64,
39    pub targets_ms: f64,
40    pub total_ms: f64,
41}
42
43/// Auditable breadcrumb recording when health-finding `suppress-line`
44/// action hints were omitted from the report.
45///
46/// Set at construction time on [`HealthReport::actions_meta`] (and on
47/// each [`HealthGroup::actions_meta`](crate::health_types::HealthGroup)
48/// when grouped) by the report builder, derived from the active
49/// [`HealthActionContext`]. Lets consumers see "where did the
50/// suppress-line hints go?" without having to grep the config or CLI
51/// history.
52///
53/// Stable `reason` codes:
54/// - `baseline-active`: a baseline is active and inline ignores would
55///   become dead annotations once the baseline regenerates.
56/// - `config-disabled`: `health.suggestInlineSuppression` is `false`.
57/// - `unspecified`: the caller did not record a reason.
58#[derive(Debug, Clone, serde::Serialize)]
59#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60pub struct HealthActionsMeta {
61    /// Always `true` when the breadcrumb is emitted. Absent from the wire
62    /// when no suppression occurred.
63    pub suppression_hints_omitted: bool,
64    /// Stable code describing why the suppression occurred.
65    pub reason: String,
66    /// Scope of the omission. Always `"health-findings"` today.
67    pub scope: String,
68}
69
70/// Result of complexity analysis for reporting.
71#[derive(Debug, Clone, serde::Serialize)]
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73pub struct HealthReport {
74    /// Functions and synthetic template entries exceeding complexity
75    /// thresholds, sorted by the --sort criteria. Each entry wraps its
76    /// inner [`ComplexityViolation`] payload (flattened on the wire) with
77    /// the typed `actions` list and an optional audit-mode `introduced`
78    /// flag.
79    pub findings: Vec<HealthFinding>,
80    /// Summary statistics.
81    pub summary: HealthSummary,
82    /// Project-wide vital signs (always computed from available data).
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub vital_signs: Option<VitalSigns>,
85    /// Project-wide health score (only populated with `--score`).
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub health_score: Option<HealthScore>,
88    /// Per-file health scores. Only present when --file-scores is used. Sorted
89    /// by maintainability_index ascending (worst first). Zero-function files
90    /// (barrels) are excluded by default.
91    #[serde(default, skip_serializing_if = "Vec::is_empty")]
92    pub file_scores: Vec<FileHealthScore>,
93    /// Static coverage gaps.
94    ///
95    /// Populated when coverage gaps are explicitly requested, or when the
96    /// top-level `health` command allows config severity to surface them in the
97    /// default report.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub coverage_gaps: Option<CoverageGaps>,
100    /// Hotspot entries combining git churn with complexity. Only present when
101    /// --hotspots is used. Sorted by score descending (highest risk first).
102    /// Each entry wraps its inner [`HotspotEntry`] payload (flattened on the
103    /// wire) with a typed `actions` list.
104    #[serde(default, skip_serializing_if = "Vec::is_empty")]
105    pub hotspots: Vec<HotspotFinding>,
106    /// Hotspot analysis summary (only set with `--hotspots`).
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub hotspot_summary: Option<HotspotSummary>,
109    /// Runtime coverage findings from the paid sidecar (only populated with
110    /// `--runtime-coverage`).
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub runtime_coverage: Option<RuntimeCoverageReport>,
113    /// Functions exceeding 60 LOC (very high risk). Only present when unit size
114    /// very-high-risk bin >= 3%. Sorted by line count descending.
115    #[serde(default, skip_serializing_if = "Vec::is_empty")]
116    pub large_functions: Vec<LargeFunctionEntry>,
117    /// Ranked refactoring recommendations. Only present when --targets is used.
118    /// Sorted by efficiency (priority/effort) descending. Each entry wraps
119    /// its inner [`RefactoringTarget`] payload (flattened on the wire) with
120    /// a typed `actions` list.
121    #[serde(default, skip_serializing_if = "Vec::is_empty")]
122    pub targets: Vec<RefactoringTargetFinding>,
123    /// Adaptive thresholds used for target scoring (only set with `--targets`).
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub target_thresholds: Option<TargetThresholds>,
126    /// Health trend comparison against a previous snapshot (only set with `--trend`).
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub health_trend: Option<HealthTrend>,
129    /// Audit breadcrumb explaining systemic action-array adjustments. Present
130    /// only when at least one adjustment was made (e.g., health finding
131    /// suppression hints omitted because a baseline is active). When --group-by
132    /// is active, each entry of `groups` may carry its own `actions_meta`
133    /// describing the same omission so per-group consumers do not need to walk
134    /// back to the report root.
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub actions_meta: Option<HealthActionsMeta>,
137}
138
139#[cfg(test)]
140#[expect(
141    clippy::derivable_impls,
142    reason = "test-only Default with custom HealthSummary thresholds (20/15)"
143)]
144impl Default for HealthReport {
145    fn default() -> Self {
146        Self {
147            findings: vec![],
148            summary: HealthSummary::default(),
149            vital_signs: None,
150            health_score: None,
151            file_scores: vec![],
152            coverage_gaps: None,
153            hotspots: vec![],
154            hotspot_summary: None,
155            runtime_coverage: None,
156            large_functions: vec![],
157            targets: vec![],
158            target_thresholds: None,
159            health_trend: None,
160            actions_meta: None,
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn health_report_skips_empty_collections() {
171        let report = HealthReport::default();
172        let json = serde_json::to_string(&report).unwrap();
173        // Empty vecs should be omitted due to skip_serializing_if
174        assert!(!json.contains("file_scores"));
175        assert!(!json.contains("hotspots"));
176        assert!(!json.contains("hotspot_summary"));
177        assert!(!json.contains("runtime_coverage"));
178        assert!(!json.contains("large_functions"));
179        assert!(!json.contains("targets"));
180        assert!(!json.contains("vital_signs"));
181        assert!(!json.contains("health_score"));
182    }
183
184    #[test]
185    fn health_score_none_skipped_in_report() {
186        let report = HealthReport::default();
187        let json = serde_json::to_string(&report).unwrap();
188        assert!(!json.contains("health_score"));
189    }
190}