Skip to main content

zenkey_fleet/report/
condition.rs

1//! The watchdog plane: a rule's state over a window, every transition of it,
2//! and the summary a long run collapses to.
3//!
4//! [`Transition`] is the shape a watchdog emits per change rather than per
5//! tick, which is what makes an hours-long run readable — and
6//! [`WatchdogSummary`] carries what the run could *not* see, because a
7//! watchdog that dropped samples has not been quiet, it has been blind
8//! (RFC 09 §5.1 O6).
9//!
10//! `CondWindow` — the raw observation a [`CondState`] is judged from — is
11//! deliberately *not* here: it carries no `Serialize`, so it is
12//! [`crate::judge::condition`]'s own working value, not a contract.
13
14use serde::Serialize;
15
16use super::judgement::Judgement;
17
18/// One condition's evaluation state — the watchdog's serde-stable **wire
19/// projection** of the [`Judgement`] core (RFC 13, v1.24; RFC 09 §5.1
20/// pre-v1.24). Three states, not two: `unobservable` is "I could not tell",
21/// which is neither "fine" nor "fire".
22///
23/// The mapping (see [`From<Judgement>`](#impl-From<Judgement>-for-CondState)),
24/// with the **polarity note spelled out**: a [`Condition`](crate::judge::condition::Condition) names what
25/// *firing* means, so `CondState::Ok` means **the condition does not hold**
26/// — it is `Established(no)` / [`Judgement::NotEstablished`], not a bare
27/// "fine". `Firing` is `Established(yes)`; both `NotAsked` and
28/// `Unobservable` project to `unobservable`, because this wire vocabulary
29/// predates the NotAsked pole and the watchdog evaluates every declared rule
30/// every tick — it never leaves one unasked.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
32#[serde(rename_all = "snake_case")]
33pub enum CondState {
34    /// The condition conclusively does not hold ([`Judgement::NotEstablished`]
35    /// — note the polarity: `ok` is the *established-clean* pole).
36    Ok,
37    /// The condition conclusively holds ([`Judgement::Established`]).
38    Firing,
39    /// The observation cannot carry the claim: a drop under a completeness
40    /// claim, a window shorter than the claim's span, or an ask that failed
41    /// ([`Judgement::Unobservable`]; a hypothetical [`Judgement::NotAsked`]
42    /// also lands here — the wire cannot say more).
43    Unobservable,
44}
45
46/// The documented wire projection (RFC 13, v1.24): `Established` → `firing`,
47/// `NotEstablished` → `ok` (the polarity note on [`CondState`]), both
48/// unestablished poles → `unobservable`.
49impl From<&Judgement> for CondState {
50    fn from(j: &Judgement) -> CondState {
51        match j {
52            Judgement::Established => CondState::Firing,
53            Judgement::NotEstablished { .. } => CondState::Ok,
54            Judgement::NotAsked | Judgement::Unobservable { .. } => CondState::Unobservable,
55        }
56    }
57}
58
59impl From<Judgement> for CondState {
60    fn from(j: Judgement) -> CondState {
61        CondState::from(&j)
62    }
63}
64
65/// One genuine state change — the only thing the watchdog ever emits.
66#[derive(Debug, Clone, Serialize)]
67pub struct Transition {
68    /// The rule, in its canonical spelling ([`Condition`](crate::judge::condition::Condition)'s `Display`).
69    pub rule: String,
70    /// `null` on the first evaluation: the baseline stated out loud, because
71    /// inventing a prior state would answer a question nobody asked (O4).
72    pub from: Option<CondState>,
73    pub to: CondState,
74    /// RFC 3339 wall clock.
75    pub at: String,
76    pub evidence: String,
77}
78
79/// What a bounded watchdog run cost and said.
80#[derive(Debug, Clone, Copy, Serialize)]
81pub struct WatchdogSummary {
82    pub ticks: u64,
83    pub transitions: u64,
84    /// Key projections the bounded facts cache retired to stay within its
85    /// bound (RFC 09 §5.1 O6). The watchdog is the run-forever mode, so its
86    /// per-key cache is a [`crate::model::facts::FactsCache`], not a map that grows
87    /// one entry per distinct key ever seen — and a bound must count what it
88    /// cost. An evicted key re-observed is re-projected identically (the
89    /// projection is a pure function of key and slice set), so evictions
90    /// cost recompute, never a changed verdict.
91    pub facts_evicted: u64,
92}