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