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