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 (only set with `--hotspots`).
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub hotspot_summary: Option<HotspotSummary>,
63 /// Runtime coverage findings from the paid sidecar (only populated with
64 /// `--runtime-coverage`).
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub runtime_coverage: Option<RuntimeCoverageReport>,
67 /// Combined coverage, runtime, complexity, and change-scope verdicts.
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub coverage_intelligence: Option<CoverageIntelligenceReport>,
70 /// Functions exceeding 60 LOC (very high risk). Only present when unit size
71 /// very-high-risk bin >= 3%. Sorted by line count descending.
72 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub large_functions: Vec<LargeFunctionEntry>,
74 /// Ranked refactoring recommendations. Only present when --targets is used.
75 /// Sorted by efficiency (priority/effort) descending. Each entry wraps
76 /// its inner `RefactoringTarget` payload (flattened on the wire) with
77 /// a typed `actions` list.
78 #[serde(default, skip_serializing_if = "Vec::is_empty")]
79 pub targets: Vec<RefactoringTargetFinding>,
80 /// Adaptive thresholds used for target scoring (only set with `--targets`).
81 #[serde(default, 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(default, skip_serializing_if = "Option::is_none")]
85 pub health_trend: Option<HealthTrend>,
86 /// Audit breadcrumb explaining systemic action-array adjustments. Present
87 /// only when at least one adjustment was made (e.g., health finding
88 /// suppression hints omitted because a baseline is active). When --group-by
89 /// is active, each entry of `groups` may carry its own `actions_meta`
90 /// describing the same omission so per-group consumers do not need to walk
91 /// back to the report root.
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub actions_meta: Option<HealthActionsMeta>,
94 /// Optional framework-specific detector coverage. Present only when the
95 /// health run already needed the dead-code analysis output.
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub framework_health: Option<FrameworkHealthDiagnostics>,
98 /// Structural CSS analytics (specificity hotspots, `!important` density,
99 /// over-complex selectors, deep nesting). Present only with `--css`.
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub css_analytics: Option<CssAnalyticsReport>,
102 /// Styling-health score and letter grade: a SECOND health axis derived from
103 /// the CSS analytics (the design-system axis), orthogonal to the JS/TS code
104 /// `health_score`. Present only with `--css` (the same condition as
105 /// `css_analytics`), so a plain `fallow health` run is byte-unchanged. The
106 /// code score is never affected by this field.
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub styling_health: Option<StylingHealth>,
109 /// Advisory STYLING FINDINGS: the graduation of the descriptive css
110 /// candidates into first-class, severity-aware, suppressible findings
111 /// surfaced in `fallow audit`. Verdict-neutral by default (the rule defaults
112 /// to `warn`); the styling domain's OWN findings collection, not the dead-code
113 /// `AnalysisResults`. Present only with `--css`; empty is skipped so a plain
114 /// run is byte-unchanged.
115 #[serde(default, skip_serializing_if = "Vec::is_empty")]
116 pub styling_findings: Vec<StylingFinding>,
117 /// Per-file top render fan-in for the descriptive human drill-down only.
118 #[serde(skip)]
119 pub render_fan_in_top: rustc_hash::FxHashMap<std::path::PathBuf, (String, u32)>,
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn health_report_skips_empty_collections() {
128 let report = HealthReport::default();
129 let json = serde_json::to_string(&report).expect("health report should serialize");
130 assert!(!json.contains("file_scores"));
131 assert!(!json.contains("hotspots"));
132 assert!(!json.contains("hotspot_summary"));
133 assert!(!json.contains("runtime_coverage"));
134 assert!(!json.contains("coverage_intelligence"));
135 assert!(!json.contains("large_functions"));
136 assert!(!json.contains("targets"));
137 assert!(!json.contains("threshold_overrides"));
138 assert!(!json.contains("vital_signs"));
139 assert!(!json.contains("health_score"));
140 assert!(!json.contains("framework_health"));
141 assert!(!json.contains("css_analytics"));
142 assert!(!json.contains("styling_health"));
143 assert!(!json.contains("styling_findings"));
144 }
145
146 #[test]
147 fn health_score_none_skipped_in_report() {
148 let report = HealthReport::default();
149 let json = serde_json::to_string(&report).expect("health report should serialize");
150 assert!(!json.contains("health_score"));
151 }
152}