fallow_cli/health_types.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
7/// Result of complexity analysis for reporting.
8#[derive(Debug, serde::Serialize)]
9pub struct HealthReport {
10 /// Functions exceeding thresholds.
11 pub findings: Vec<HealthFinding>,
12 /// Summary statistics.
13 pub summary: HealthSummary,
14 /// Per-file health scores (only populated with `--file-scores` or `--hotspots`).
15 #[serde(skip_serializing_if = "Vec::is_empty")]
16 pub file_scores: Vec<FileHealthScore>,
17 /// Hotspot entries (only populated with `--hotspots`).
18 #[serde(skip_serializing_if = "Vec::is_empty")]
19 pub hotspots: Vec<HotspotEntry>,
20 /// Hotspot analysis summary (only set with `--hotspots`).
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub hotspot_summary: Option<HotspotSummary>,
23}
24
25/// A single function that exceeds a complexity threshold.
26#[derive(Debug, serde::Serialize)]
27pub struct HealthFinding {
28 /// Absolute file path.
29 pub path: std::path::PathBuf,
30 /// Function name.
31 pub name: String,
32 /// 1-based line number.
33 pub line: u32,
34 /// 0-based column.
35 pub col: u32,
36 /// Cyclomatic complexity.
37 pub cyclomatic: u16,
38 /// Cognitive complexity.
39 pub cognitive: u16,
40 /// Number of lines in the function.
41 pub line_count: u32,
42 /// Which threshold was exceeded.
43 pub exceeded: ExceededThreshold,
44}
45
46/// Which complexity threshold was exceeded.
47#[derive(Debug, serde::Serialize)]
48#[serde(rename_all = "snake_case")]
49pub enum ExceededThreshold {
50 /// Only cyclomatic exceeded.
51 Cyclomatic,
52 /// Only cognitive exceeded.
53 Cognitive,
54 /// Both thresholds exceeded.
55 Both,
56}
57
58/// Summary statistics for the health report.
59#[derive(Debug, serde::Serialize)]
60pub struct HealthSummary {
61 /// Number of files analyzed.
62 pub files_analyzed: usize,
63 /// Total number of functions found.
64 pub functions_analyzed: usize,
65 /// Number of functions above threshold.
66 pub functions_above_threshold: usize,
67 /// Configured cyclomatic threshold.
68 pub max_cyclomatic_threshold: u16,
69 /// Configured cognitive threshold.
70 pub max_cognitive_threshold: u16,
71 /// Number of files scored (only set with `--file-scores`).
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub files_scored: Option<usize>,
74 /// Average maintainability index across all scored files (only set with `--file-scores`).
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub average_maintainability: Option<f64>,
77}
78
79/// Per-file health score combining complexity, coupling, and dead code metrics.
80///
81/// Files with zero functions (barrel files, re-export files) are excluded by default.
82///
83/// ## Maintainability Index Formula
84///
85/// ```text
86/// fan_out_penalty = min(ln(fan_out + 1) × 4, 15)
87/// maintainability = 100
88/// - (complexity_density × 30)
89/// - (dead_code_ratio × 20)
90/// - fan_out_penalty
91/// ```
92///
93/// Clamped to \[0, 100\]. Higher is better.
94///
95/// - **complexity_density**: total cyclomatic complexity / lines of code
96/// - **dead_code_ratio**: fraction of value exports (excluding type-only exports) with zero references (0.0–1.0)
97/// - **fan_out_penalty**: logarithmic scaling with cap at 15 points; reflects diminishing marginal risk of additional imports
98#[derive(Debug, Clone, serde::Serialize)]
99pub struct FileHealthScore {
100 /// File path (absolute; stripped to relative in output).
101 pub path: std::path::PathBuf,
102 /// Number of files that import this file.
103 pub fan_in: usize,
104 /// Number of files this file imports.
105 pub fan_out: usize,
106 /// Fraction of value exports with zero references (0.0–1.0). Files with no value exports get 0.0.
107 /// Type-only exports (interfaces, type aliases) are excluded from both numerator and denominator
108 /// to avoid inflating the ratio for well-typed codebases that export props types alongside components.
109 pub dead_code_ratio: f64,
110 /// Total cyclomatic complexity / lines of code.
111 pub complexity_density: f64,
112 /// Weighted composite score (0–100, higher is better).
113 pub maintainability_index: f64,
114 /// Sum of cyclomatic complexity across all functions.
115 pub total_cyclomatic: u32,
116 /// Sum of cognitive complexity across all functions.
117 pub total_cognitive: u32,
118 /// Number of functions in this file.
119 pub function_count: usize,
120 /// Total lines of code (from line_offsets).
121 pub lines: u32,
122}
123
124/// A hotspot: a file that is both complex and frequently changing.
125///
126/// ## Score Formula
127///
128/// ```text
129/// normalized_churn = weighted_commits / max_weighted_commits (0..1)
130/// normalized_complexity = complexity_density / max_density (0..1)
131/// score = normalized_churn × normalized_complexity × 100 (0..100)
132/// ```
133///
134/// Score uses within-project max normalization. Higher score = higher risk.
135/// Fan-in is shown separately as "blast radius" — not baked into the score.
136#[derive(Debug, Clone, serde::Serialize)]
137pub struct HotspotEntry {
138 /// File path (absolute; stripped to relative in output).
139 pub path: std::path::PathBuf,
140 /// Hotspot score (0–100). Higher means more risk.
141 pub score: f64,
142 /// Number of commits in the analysis window.
143 pub commits: u32,
144 /// Recency-weighted commit count (exponential decay, half-life 90 days).
145 pub weighted_commits: f64,
146 /// Total lines added across all commits.
147 pub lines_added: u32,
148 /// Total lines deleted across all commits.
149 pub lines_deleted: u32,
150 /// Cyclomatic complexity / lines of code.
151 pub complexity_density: f64,
152 /// Number of files that import this file (blast radius).
153 pub fan_in: usize,
154 /// Churn trend: accelerating, stable, or cooling.
155 pub trend: fallow_core::churn::ChurnTrend,
156}
157
158/// Summary statistics for hotspot analysis.
159#[derive(Debug, serde::Serialize)]
160pub struct HotspotSummary {
161 /// Analysis window display string (e.g., "6 months").
162 pub since: String,
163 /// Minimum commits threshold.
164 pub min_commits: u32,
165 /// Number of files with churn data meeting the threshold.
166 pub files_analyzed: usize,
167 /// Number of files excluded (below min_commits).
168 pub files_excluded: usize,
169 /// Whether the repository is a shallow clone.
170 pub shallow_clone: bool,
171}