Skip to main content

fallow_cli/health_types/
trends.rs

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