Skip to main content

zero_operator_state/
label.rs

1//! State labels, straight from Addendum A §2.3.
2//!
3//! Labels are computed from a [`crate::StateVector`] via
4//! [`crate::Classifier`]. They are rendered by `zero-tui` in the
5//! status bar and the `/state` overlay.
6
7use serde::{Deserialize, Serialize};
8
9/// Operator behavioral state.
10///
11/// Labels are **descriptive, not judgmental** (Addendum A §2.4).
12/// The variants match §2.3 exactly.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum Label {
16    /// <5 decisions in last hour, session <2h.
17    Fresh,
18    /// Normal velocity, deviation rate <20%, no concerning patterns.
19    Steady,
20    /// Velocity 1.5x baseline OR deviation 20-40% OR session 4h+.
21    Elevated,
22    /// Velocity 2x baseline AND (deviation >40% OR loss-reaction <2min).
23    Tilt,
24    /// Session >6h continuous OR sleep proxy >18h.
25    Fatigued,
26    /// Post-tilt cooldown active.
27    Recovery,
28}
29
30impl Label {
31    /// Screen-rendered short form.
32    ///
33    /// Used in the status bar where space is scarce. The `/state`
34    /// overlay uses the full Display impl.
35    #[must_use]
36    pub const fn short(self) -> &'static str {
37        match self {
38            Self::Fresh => "FRESH",
39            Self::Steady => "STEADY",
40            Self::Elevated => "ELEVATED",
41            Self::Tilt => "TILT",
42            Self::Fatigued => "FATIGUED",
43            Self::Recovery => "RECOVERY",
44        }
45    }
46
47    /// Theme hint for the renderer. Maps 1:1 to the colors in
48    /// Addendum A §2.3.
49    #[must_use]
50    pub const fn color_hint(self) -> ColorHint {
51        match self {
52            Self::Fresh | Self::Steady => ColorHint::Phosphor,
53            Self::Elevated | Self::Fatigued => ColorHint::Amber,
54            Self::Tilt => ColorHint::Red,
55            Self::Recovery => ColorHint::MutedOlive,
56        }
57    }
58}
59
60impl std::fmt::Display for Label {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.write_str(self.short())
63    }
64}
65
66/// Abstract color identity the `zero-tui` theme resolves to a
67/// concrete `ratatui::style::Color`. This crate stays renderer-
68/// agnostic so it can be tested without pulling in ratatui.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum ColorHint {
71    Phosphor,
72    Amber,
73    Red,
74    MutedOlive,
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn short_labels_are_uppercase_single_word() {
83        for l in [
84            Label::Fresh,
85            Label::Steady,
86            Label::Elevated,
87            Label::Tilt,
88            Label::Fatigued,
89            Label::Recovery,
90        ] {
91            assert!(l.short().chars().all(|c| c.is_ascii_uppercase()));
92            assert!(!l.short().contains(' '));
93        }
94    }
95
96    #[test]
97    fn color_hints_match_spec() {
98        assert_eq!(Label::Fresh.color_hint(), ColorHint::Phosphor);
99        assert_eq!(Label::Steady.color_hint(), ColorHint::Phosphor);
100        assert_eq!(Label::Elevated.color_hint(), ColorHint::Amber);
101        assert_eq!(Label::Fatigued.color_hint(), ColorHint::Amber);
102        assert_eq!(Label::Tilt.color_hint(), ColorHint::Red);
103        assert_eq!(Label::Recovery.color_hint(), ColorHint::MutedOlive);
104    }
105}