1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//! Top-level health report contract.
use crate::{
CoverageGaps, CoverageIntelligenceReport, CssAnalyticsReport, FileHealthScore,
FrameworkHealthDiagnostics, HealthActionsMeta, HealthFinding, HealthScore, HealthSummary,
HealthTrend, HotspotFinding, HotspotSummary, LargeFunctionEntry, RefactoringTargetFinding,
RuntimeCoverageReport, StylingFinding, StylingHealth, TargetThresholds, ThresholdOverrideState,
VitalSigns,
};
use fallow_types::output_dead_code::PropDrillingChainFinding;
/// One health section that a run produced.
///
/// A section is in [`HealthReport::sections`] when the run computed it and
/// the report carries its result, also when that result is empty. The value
/// set is OPEN: a later release can add a section, so a consumer must accept
/// a token that it does not know.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum HealthSection {
/// The run lists the complexity findings in `findings`. An empty list then
/// means that no function above a threshold is left after the baseline.
/// Without this token, `findings` is empty because the run did not list
/// them, and `summary.functions_above_threshold` is the only count.
Complexity,
/// `vital_signs`.
VitalSigns,
/// `health_score`.
Score,
/// `file_scores`.
FileScores,
/// `coverage_gaps`.
CoverageGaps,
/// `hotspots`. The list can be empty when the run could not read the git
/// history; `workspace_diagnostics` then tells why.
Hotspots,
/// `targets`.
Targets,
/// `health_trend`.
Trend,
/// `runtime_coverage`.
RuntimeCoverage,
/// `css_analytics`, `styling_health` and `styling_findings`.
Css,
}
/// Result of complexity analysis for reporting.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct HealthReport {
/// Functions and synthetic template entries exceeding complexity
/// thresholds, sorted by the --sort criteria. Each entry wraps its
/// inner `ComplexityViolation` payload (flattened on the wire) with
/// the typed `actions` list and an optional audit-mode `introduced`
/// flag.
pub findings: Vec<HealthFinding>,
/// Summary statistics.
pub summary: HealthSummary,
/// The sections that this run produced, in a fixed order. A renderer
/// reads it to tell an empty section from a section that the run did not
/// produce: `findings` is the complexity list only when `complexity` is
/// in this array. The value set is OPEN (see [`HealthSection`]). Absent
/// in an envelope from a fallow version before this member, and on a
/// report that no health run built.
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_open_sections"
)]
pub sections: Option<Vec<HealthSection>>,
/// Configured threshold override states. Entries are emitted for active
/// exceptions, stale exceptions, and full-run no-match cleanup hints.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub threshold_overrides: Vec<ThresholdOverrideState>,
/// Project-wide vital signs (always computed from available data).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vital_signs: Option<VitalSigns>,
/// Project-wide health score (only populated with `--score`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub health_score: Option<HealthScore>,
/// Per-file health scores. Only present when --file-scores is used. Sorted
/// by risk-aware triage concern, combining low maintainability and high
/// CRAP risk. Zero-function files (barrels) are excluded by default.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub file_scores: Vec<FileHealthScore>,
/// Static coverage gaps.
///
/// Populated when coverage gaps are explicitly requested, or when the
/// top-level `health` command allows config severity to surface them in the
/// default report.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coverage_gaps: Option<CoverageGaps>,
/// Located prop-drilling chains (React/Preact props forwarded unchanged
/// through 3+ pass-through components). Only present when the opt-in
/// `prop-drilling` rule is enabled (it defaults to off). Each entry carries
/// the source, every pass-through hop, and the consumer with file + line +
/// component, so CI / an agent can act. Surfaced alongside hotspots as a
/// graph-derived health signal.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub prop_drilling_chains: Vec<PropDrillingChainFinding>,
/// Hotspot entries combining git churn with complexity. Only present when
/// --hotspots is used. Sorted by score descending (highest risk first).
/// Each entry wraps its inner `HotspotEntry` payload (flattened on the
/// wire) with a typed `actions` list.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hotspots: Vec<HotspotFinding>,
/// Hotspot analysis summary.
///
/// Set whenever the run measured churn, which needs readable git history;
/// `--hotspots` adds the per-file [`hotspots`](Self::hotspots) listing
/// beside it rather than gating this summary.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hotspot_summary: Option<HotspotSummary>,
/// Runtime coverage findings from the paid sidecar (only populated with
/// `--runtime-coverage`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime_coverage: Option<RuntimeCoverageReport>,
/// Combined coverage, runtime, complexity, and change-scope verdicts.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coverage_intelligence: Option<CoverageIntelligenceReport>,
/// Functions exceeding 60 LOC (very high risk). Only present when unit size
/// very-high-risk bin >= 3%. Sorted by line count descending.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub large_functions: Vec<LargeFunctionEntry>,
/// Ranked refactoring recommendations. Only present when --targets is used.
/// Sorted by efficiency (priority/effort) descending. Each entry wraps
/// its inner `RefactoringTarget` payload (flattened on the wire) with
/// a typed `actions` list.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub targets: Vec<RefactoringTargetFinding>,
/// Adaptive thresholds used for target scoring (only set with `--targets`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_thresholds: Option<TargetThresholds>,
/// Health trend comparison against a previous snapshot (only set with `--trend`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub health_trend: Option<HealthTrend>,
/// Audit breadcrumb explaining systemic action-array adjustments. Present
/// only when at least one adjustment was made (e.g., health finding
/// suppression hints omitted because a baseline is active). When --group-by
/// is active, each entry of `groups` may carry its own `actions_meta`
/// describing the same omission so per-group consumers do not need to walk
/// back to the report root.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actions_meta: Option<HealthActionsMeta>,
/// Optional framework-specific detector coverage. Present only when the
/// health run already needed the dead-code analysis output.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub framework_health: Option<FrameworkHealthDiagnostics>,
/// Structural CSS analytics (specificity hotspots, `!important` density,
/// over-complex selectors, deep nesting). Present only with `--css`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub css_analytics: Option<CssAnalyticsReport>,
/// Styling-health score and letter grade: a SECOND health axis derived from
/// the CSS analytics (the design-system axis), orthogonal to the JS/TS code
/// `health_score`. Present only with `--css` (the same condition as
/// `css_analytics`), so a plain `fallow health` run is byte-unchanged. The
/// code score is never affected by this field.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub styling_health: Option<StylingHealth>,
/// Advisory STYLING FINDINGS: the graduation of the descriptive css
/// candidates into first-class, severity-aware, suppressible findings
/// surfaced in `fallow audit`. Verdict-neutral by default (the rule defaults
/// to `warn`); the styling domain's OWN findings collection, not the dead-code
/// `AnalysisResults`. Present only with `--css`; empty is skipped so a plain
/// run is byte-unchanged.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub styling_findings: Vec<StylingFinding>,
/// Per-file top render fan-in for the descriptive human drill-down only.
#[serde(skip)]
pub render_fan_in_top: rustc_hash::FxHashMap<std::path::PathBuf, (String, u32)>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn health_report_skips_empty_collections() {
let report = HealthReport::default();
let json = serde_json::to_string(&report).expect("health report should serialize");
assert!(!json.contains("file_scores"));
assert!(!json.contains("hotspots"));
assert!(!json.contains("hotspot_summary"));
assert!(!json.contains("runtime_coverage"));
assert!(!json.contains("coverage_intelligence"));
assert!(!json.contains("large_functions"));
assert!(!json.contains("targets"));
assert!(!json.contains("threshold_overrides"));
assert!(!json.contains("vital_signs"));
assert!(!json.contains("health_score"));
assert!(!json.contains("framework_health"));
assert!(!json.contains("css_analytics"));
assert!(!json.contains("styling_health"));
assert!(!json.contains("styling_findings"));
}
#[test]
fn health_score_none_skipped_in_report() {
let report = HealthReport::default();
let json = serde_json::to_string(&report).expect("health report should serialize");
assert!(!json.contains("health_score"));
}
}
/// Read `sections` from a saved envelope. The value set is open, so a token
/// from a later fallow version is skipped instead of failing the whole read.
fn deserialize_open_sections<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Vec<HealthSection>>, D::Error> {
let tokens: Option<Vec<serde_json::Value>> = serde::Deserialize::deserialize(deserializer)?;
Ok(tokens.map(|tokens| {
tokens
.into_iter()
.filter_map(|token| serde_json::from_value(token).ok())
.collect()
}))
}
#[cfg(test)]
mod open_sections_tests {
use super::*;
#[derive(serde::Deserialize)]
struct Envelope {
#[serde(default, deserialize_with = "deserialize_open_sections")]
sections: Option<Vec<HealthSection>>,
}
#[test]
fn an_unknown_section_token_is_skipped() {
let envelope: Envelope =
serde_json::from_str(r#"{"sections":["complexity","later-section","hotspots"]}"#)
.expect("an unknown token must not fail the read");
assert_eq!(
envelope.sections,
Some(vec![HealthSection::Complexity, HealthSection::Hotspots])
);
}
#[test]
fn an_absent_member_stays_absent() {
let envelope: Envelope = serde_json::from_str("{}").expect("valid envelope");
assert_eq!(envelope.sections, None);
}
}