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