Skip to main content

fallow_cli/health_types/
trends.rs

1//! Trend types — comparing current run against a saved snapshot.
2
3/// Health trend comparison: current run vs. a previous snapshot.
4#[derive(Debug, Clone, serde::Serialize)]
5pub struct HealthTrend {
6    /// The snapshot being compared against.
7    pub compared_to: TrendPoint,
8    /// Per-metric deltas.
9    pub metrics: Vec<TrendMetric>,
10    /// Number of snapshots found in the snapshot directory.
11    pub snapshots_loaded: usize,
12    /// Overall direction across all metrics.
13    pub overall_direction: TrendDirection,
14}
15
16/// A reference to a snapshot used in trend comparison.
17#[derive(Debug, Clone, serde::Serialize)]
18pub struct TrendPoint {
19    /// ISO 8601 timestamp of the snapshot.
20    pub timestamp: String,
21    /// Git SHA at time of snapshot.
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub git_sha: Option<String>,
24    /// Health score from the snapshot (stored, not re-derived).
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub score: Option<f64>,
27    /// Letter grade from the snapshot.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub grade: Option<String>,
30    /// Coverage model used for CRAP computation in this snapshot.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub coverage_model: Option<super::CoverageModel>,
33    /// Schema version of the compared snapshot.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub snapshot_schema_version: Option<u32>,
36}
37
38/// A single metric's trend between two snapshots.
39#[derive(Debug, Clone, serde::Serialize)]
40pub struct TrendMetric {
41    /// Metric identifier (e.g., `"score"`, `"dead_file_pct"`).
42    pub name: &'static str,
43    /// Human-readable label (e.g., `"Health Score"`, `"Dead Files"`).
44    pub label: &'static str,
45    /// Previous value (from snapshot).
46    pub previous: f64,
47    /// Current value (from this run).
48    pub current: f64,
49    /// Absolute change (current − previous).
50    pub delta: f64,
51    /// Direction of change.
52    pub direction: TrendDirection,
53    /// Unit for display (e.g., `"%"`, `""`, `"pts"`).
54    pub unit: &'static str,
55    /// Raw count from previous snapshot (for JSON consumers).
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub previous_count: Option<TrendCount>,
58    /// Raw count from current run (for JSON consumers).
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub current_count: Option<TrendCount>,
61}
62
63/// Raw numerator/denominator for a percentage metric.
64#[derive(Debug, Clone, serde::Serialize)]
65pub struct TrendCount {
66    /// The numerator (e.g., dead files count).
67    pub value: usize,
68    /// The denominator (e.g., total files).
69    pub total: usize,
70}
71
72/// Direction of a metric's change, semantically (improving/declining/stable).
73#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
74#[serde(rename_all = "snake_case")]
75pub enum TrendDirection {
76    /// The metric moved in a beneficial direction.
77    Improving,
78    /// The metric moved in a detrimental direction.
79    Declining,
80    /// The metric stayed within tolerance.
81    Stable,
82}
83
84impl TrendDirection {
85    /// Arrow symbol for terminal output.
86    #[must_use]
87    pub const fn arrow(self) -> &'static str {
88        match self {
89            Self::Improving => "\u{2191}", // ↑
90            Self::Declining => "\u{2193}", // ↓
91            Self::Stable => "\u{2192}",    // →
92        }
93    }
94
95    /// Human-readable label.
96    #[must_use]
97    pub const fn label(self) -> &'static str {
98        match self {
99            Self::Improving => "improving",
100            Self::Declining => "declining",
101            Self::Stable => "stable",
102        }
103    }
104}