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}