Skip to main content

zero_operator_state/
vector.rs

1//! The numeric state vector. Fed by [`Classifier`], read by
2//! widgets (`zero-tui`) and the friction path (`zero-commands`).
3//!
4//! Every field is computed; none is hand-edited. The operator can
5//! reset the persisted copy (with confirmation) via `/state reset`,
6//! but they cannot poke at individual numbers. "Never operator-faked"
7//! is the property — see Addendum A §2.2.
8//!
9//! [`Classifier`]: crate::Classifier
10
11use serde::{Deserialize, Serialize};
12
13/// Decision velocity over rolling windows, in decisions per hour.
14/// Addendum A §2.1, §2.3.
15#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
16pub struct Velocity {
17    pub last_1h: u32,
18    pub last_4h: u32,
19    pub last_24h: u32,
20    /// Operator's personal baseline decisions/h, derived from the
21    /// last 30 days of session data. Present when enough history
22    /// exists.
23    pub baseline_1h: Option<f64>,
24}
25
26impl Velocity {
27    #[must_use]
28    pub fn ratio_to_baseline(&self) -> Option<f64> {
29        self.baseline_1h
30            .filter(|b| *b > 0.0)
31            .map(|b| f64::from(self.last_1h) / b)
32    }
33}
34
35/// Strategy-deviation rate over last N verdicts.
36#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
37pub struct Deviation {
38    pub overrides_last_10: u32,
39    pub verdicts_last_10: u32,
40    pub overrides_last_50: u32,
41    pub verdicts_last_50: u32,
42}
43
44impl Deviation {
45    /// Override rate over the last 10 verdicts (0.0-1.0).
46    #[must_use]
47    pub fn rate_last_10(&self) -> f64 {
48        if self.verdicts_last_10 == 0 {
49            0.0
50        } else {
51            f64::from(self.overrides_last_10) / f64::from(self.verdicts_last_10)
52        }
53    }
54}
55
56/// Session duration and focus metrics.
57#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
58pub struct Session {
59    /// Continuous active-interaction time since session start.
60    pub active_duration_ms: u64,
61    /// Longest uninterrupted focus block.
62    pub longest_focus_ms: u64,
63    /// Time since the last break or sleep-proxy rest.
64    pub since_last_break_ms: u64,
65}
66
67/// Loss-reaction profile — time from a losing close to the next
68/// entry. Addendum A §2.1, §10.2.
69#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
70pub struct LossReaction {
71    /// Median time from loss → entry, over last 10 losses, ms.
72    pub median_last_10_ms: u64,
73    /// Fastest loss-reaction in current session, ms.
74    pub fastest_session_ms: u64,
75    /// Operator's personal baseline median, ms.
76    pub baseline_ms: Option<u64>,
77}
78
79/// Same-symbol re-entry counts, by time window. Addendum A §10.2.
80#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
81pub struct ReEntry {
82    pub within_15m: u32,
83    pub within_30m: u32,
84    pub within_2h: u32,
85}
86
87/// Sleep-proxy: how long since the last >6h input-free window ended.
88#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
89pub struct SleepProxy {
90    pub hours_since_rest_ended: Option<u32>,
91}
92
93/// The composite state vector — all the ingredients the classifier
94/// folds into a [`crate::Label`].
95#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
96pub struct StateVector {
97    pub velocity: Velocity,
98    pub deviation: Deviation,
99    pub session: Session,
100    pub loss_reaction: LossReaction,
101    pub re_entry: ReEntry,
102    pub sleep_proxy: SleepProxy,
103    /// True when the most recent `BreakStarted` has not yet been
104    /// followed by `BreakEnded`. While on break, velocity counters
105    /// freeze.
106    pub on_break: bool,
107}