Skip to main content

fallow_output/
health_report.rs

1//! Top-level health report contract.
2
3use crate::{
4    CoverageGaps, CoverageIntelligenceReport, CssAnalyticsReport, FileHealthScore,
5    FrameworkHealthDiagnostics, HealthActionsMeta, HealthFinding, HealthScore, HealthSummary,
6    HealthTrend, HotspotFinding, HotspotSummary, LargeFunctionEntry, RefactoringTargetFinding,
7    RuntimeCoverageReport, StylingFinding, StylingHealth, TargetThresholds, ThresholdOverrideState,
8    VitalSigns,
9};
10use fallow_types::output_dead_code::PropDrillingChainFinding;
11
12/// Result of complexity analysis for reporting.
13#[derive(Debug, Clone, Default, serde::Serialize)]
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15pub struct HealthReport {
16    /// Functions and synthetic template entries exceeding complexity
17    /// thresholds, sorted by the --sort criteria. Each entry wraps its
18    /// inner `ComplexityViolation` payload (flattened on the wire) with
19    /// the typed `actions` list and an optional audit-mode `introduced`
20    /// flag.
21    pub findings: Vec<HealthFinding>,
22    /// Summary statistics.
23    pub summary: HealthSummary,
24    /// Configured threshold override states. Entries are emitted for active
25    /// exceptions, stale exceptions, and full-run no-match cleanup hints.
26    #[serde(default, skip_serializing_if = "Vec::is_empty")]
27    pub threshold_overrides: Vec<ThresholdOverrideState>,
28    /// Project-wide vital signs (always computed from available data).
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub vital_signs: Option<VitalSigns>,
31    /// Project-wide health score (only populated with `--score`).
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub health_score: Option<HealthScore>,
34    /// Per-file health scores. Only present when --file-scores is used. Sorted
35    /// by risk-aware triage concern, combining low maintainability and high
36    /// CRAP risk. Zero-function files (barrels) are excluded by default.
37    #[serde(default, skip_serializing_if = "Vec::is_empty")]
38    pub file_scores: Vec<FileHealthScore>,
39    /// Static coverage gaps.
40    ///
41    /// Populated when coverage gaps are explicitly requested, or when the
42    /// top-level `health` command allows config severity to surface them in the
43    /// default report.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub coverage_gaps: Option<CoverageGaps>,
46    /// Located prop-drilling chains (React/Preact props forwarded unchanged
47    /// through 3+ pass-through components). Only present when the opt-in
48    /// `prop-drilling` rule is enabled (it defaults to off). Each entry carries
49    /// the source, every pass-through hop, and the consumer with file + line +
50    /// component, so CI / an agent can act. Surfaced alongside hotspots as a
51    /// graph-derived health signal.
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub prop_drilling_chains: Vec<PropDrillingChainFinding>,
54    /// Hotspot entries combining git churn with complexity. Only present when
55    /// --hotspots is used. Sorted by score descending (highest risk first).
56    /// Each entry wraps its inner `HotspotEntry` payload (flattened on the
57    /// wire) with a typed `actions` list.
58    #[serde(default, skip_serializing_if = "Vec::is_empty")]
59    pub hotspots: Vec<HotspotFinding>,
60    /// Hotspot analysis summary.
61    ///
62    /// Set whenever the run measured churn, which needs readable git history;
63    /// `--hotspots` adds the per-file [`hotspots`](Self::hotspots) listing
64    /// beside it rather than gating this summary.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub hotspot_summary: Option<HotspotSummary>,
67    /// Runtime coverage findings from the paid sidecar (only populated with
68    /// `--runtime-coverage`).
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub runtime_coverage: Option<RuntimeCoverageReport>,
71    /// Combined coverage, runtime, complexity, and change-scope verdicts.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub coverage_intelligence: Option<CoverageIntelligenceReport>,
74    /// Functions exceeding 60 LOC (very high risk). Only present when unit size
75    /// very-high-risk bin >= 3%. Sorted by line count descending.
76    #[serde(default, skip_serializing_if = "Vec::is_empty")]
77    pub large_functions: Vec<LargeFunctionEntry>,
78    /// Ranked refactoring recommendations. Only present when --targets is used.
79    /// Sorted by efficiency (priority/effort) descending. Each entry wraps
80    /// its inner `RefactoringTarget` payload (flattened on the wire) with
81    /// a typed `actions` list.
82    #[serde(default, skip_serializing_if = "Vec::is_empty")]
83    pub targets: Vec<RefactoringTargetFinding>,
84    /// Adaptive thresholds used for target scoring (only set with `--targets`).
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub target_thresholds: Option<TargetThresholds>,
87    /// Health trend comparison against a previous snapshot (only set with `--trend`).
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub health_trend: Option<HealthTrend>,
90    /// Audit breadcrumb explaining systemic action-array adjustments. Present
91    /// only when at least one adjustment was made (e.g., health finding
92    /// suppression hints omitted because a baseline is active). When --group-by
93    /// is active, each entry of `groups` may carry its own `actions_meta`
94    /// describing the same omission so per-group consumers do not need to walk
95    /// back to the report root.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub actions_meta: Option<HealthActionsMeta>,
98    /// Optional framework-specific detector coverage. Present only when the
99    /// health run already needed the dead-code analysis output.
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub framework_health: Option<FrameworkHealthDiagnostics>,
102    /// Structural CSS analytics (specificity hotspots, `!important` density,
103    /// over-complex selectors, deep nesting). Present only with `--css`.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub css_analytics: Option<CssAnalyticsReport>,
106    /// Styling-health score and letter grade: a SECOND health axis derived from
107    /// the CSS analytics (the design-system axis), orthogonal to the JS/TS code
108    /// `health_score`. Present only with `--css` (the same condition as
109    /// `css_analytics`), so a plain `fallow health` run is byte-unchanged. The
110    /// code score is never affected by this field.
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub styling_health: Option<StylingHealth>,
113    /// Advisory STYLING FINDINGS: the graduation of the descriptive css
114    /// candidates into first-class, severity-aware, suppressible findings
115    /// surfaced in `fallow audit`. Verdict-neutral by default (the rule defaults
116    /// to `warn`); the styling domain's OWN findings collection, not the dead-code
117    /// `AnalysisResults`. Present only with `--css`; empty is skipped so a plain
118    /// run is byte-unchanged.
119    #[serde(default, skip_serializing_if = "Vec::is_empty")]
120    pub styling_findings: Vec<StylingFinding>,
121    /// Per-file top render fan-in for the descriptive human drill-down only.
122    #[serde(skip)]
123    pub render_fan_in_top: rustc_hash::FxHashMap<std::path::PathBuf, (String, u32)>,
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn health_report_skips_empty_collections() {
132        let report = HealthReport::default();
133        let json = serde_json::to_string(&report).expect("health report should serialize");
134        assert!(!json.contains("file_scores"));
135        assert!(!json.contains("hotspots"));
136        assert!(!json.contains("hotspot_summary"));
137        assert!(!json.contains("runtime_coverage"));
138        assert!(!json.contains("coverage_intelligence"));
139        assert!(!json.contains("large_functions"));
140        assert!(!json.contains("targets"));
141        assert!(!json.contains("threshold_overrides"));
142        assert!(!json.contains("vital_signs"));
143        assert!(!json.contains("health_score"));
144        assert!(!json.contains("framework_health"));
145        assert!(!json.contains("css_analytics"));
146        assert!(!json.contains("styling_health"));
147        assert!(!json.contains("styling_findings"));
148    }
149
150    #[test]
151    fn health_score_none_skipped_in_report() {
152        let report = HealthReport::default();
153        let json = serde_json::to_string(&report).expect("health report should serialize");
154        assert!(!json.contains("health_score"));
155    }
156}