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