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}
31
32/// A single metric's trend between two snapshots.
33#[derive(Debug, Clone, serde::Serialize)]
34pub struct TrendMetric {
35    /// Metric identifier (e.g., `"score"`, `"dead_file_pct"`).
36    pub name: &'static str,
37    /// Human-readable label (e.g., `"Health Score"`, `"Dead Files"`).
38    pub label: &'static str,
39    /// Previous value (from snapshot).
40    pub previous: f64,
41    /// Current value (from this run).
42    pub current: f64,
43    /// Absolute change (current − previous).
44    pub delta: f64,
45    /// Direction of change.
46    pub direction: TrendDirection,
47    /// Unit for display (e.g., `"%"`, `""`, `"pts"`).
48    pub unit: &'static str,
49    /// Raw count from previous snapshot (for JSON consumers).
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub previous_count: Option<TrendCount>,
52    /// Raw count from current run (for JSON consumers).
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub current_count: Option<TrendCount>,
55}
56
57/// Raw numerator/denominator for a percentage metric.
58#[derive(Debug, Clone, serde::Serialize)]
59pub struct TrendCount {
60    /// The numerator (e.g., dead files count).
61    pub value: usize,
62    /// The denominator (e.g., total files).
63    pub total: usize,
64}
65
66/// Direction of a metric's change, semantically (improving/declining/stable).
67#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
68#[serde(rename_all = "snake_case")]
69pub enum TrendDirection {
70    /// The metric moved in a beneficial direction.
71    Improving,
72    /// The metric moved in a detrimental direction.
73    Declining,
74    /// The metric stayed within tolerance.
75    Stable,
76}
77
78impl TrendDirection {
79    /// Arrow symbol for terminal output.
80    #[must_use]
81    pub const fn arrow(self) -> &'static str {
82        match self {
83            Self::Improving => "\u{2191}", // ↑
84            Self::Declining => "\u{2193}", // ↓
85            Self::Stable => "\u{2192}",    // →
86        }
87    }
88
89    /// Human-readable label.
90    #[must_use]
91    pub const fn label(self) -> &'static str {
92        match self {
93            Self::Improving => "improving",
94            Self::Declining => "declining",
95            Self::Stable => "stable",
96        }
97    }
98}