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}