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}