Skip to main content

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}