Skip to main content

zero_engine_client/
stat.rs

1//! `Stat<T>` — the honesty primitive.
2//!
3//! Every numeric value the TUI renders passes through this type. The
4//! renderer refuses to display a `Stat` whose freshness violates the
5//! configured threshold, and it always shows `n` and `source` when
6//! relevant.
7//!
8//! See spec §3.1 ("honesty is render-native") and ADR-003.
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13/// Where a value came from. Used in rendering for source attribution
14/// and in debugging to trace drift.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum Source {
18    /// HTTP REST response from the engine.
19    Http,
20    /// WebSocket push from the engine bus poller.
21    Ws,
22    /// MCP tool call response.
23    Mcp,
24    /// Derived on CLI side from other `Stat`s (presentation only).
25    Derived,
26    /// Fixture or mock — never rendered in production.
27    Mock,
28}
29
30/// A value with the metadata required to render honestly.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct Stat<T> {
33    /// The actual value.
34    pub value: T,
35    /// When the engine produced this reading.
36    pub as_of: DateTime<Utc>,
37    /// Sample size, when the value is a summary statistic. `None` when
38    /// the value is a live reading (price, position size, etc.).
39    pub n: Option<u64>,
40    /// Where it came from.
41    pub source: Source,
42}
43
44impl<T> Stat<T> {
45    pub fn new(value: T, source: Source) -> Self {
46        Self {
47            value,
48            as_of: Utc::now(),
49            n: None,
50            source,
51        }
52    }
53
54    #[must_use]
55    pub fn with_n(mut self, n: u64) -> Self {
56        self.n = Some(n);
57        self
58    }
59
60    #[must_use]
61    pub fn with_as_of(mut self, as_of: DateTime<Utc>) -> Self {
62        self.as_of = as_of;
63        self
64    }
65
66    /// Age of the reading at the given instant.
67    #[must_use]
68    pub fn age(&self, now: DateTime<Utc>) -> chrono::Duration {
69        now.signed_duration_since(self.as_of)
70    }
71
72    #[must_use]
73    pub fn is_stale(&self, now: DateTime<Utc>, threshold: chrono::Duration) -> bool {
74        self.age(now) > threshold
75    }
76}