zero_operator_state/snapshot.rs
1//! A renderer-friendly snapshot of operator state.
2//!
3//! Widgets do not read the [`crate::StateVector`] directly; they read
4//! a `Snapshot`. The snapshot includes the computed label, the
5//! derived friction level, and a monotonically increasing `version`
6//! so widgets can skip rendering when nothing has changed.
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11use crate::friction::{FrictionLevel, RiskContext};
12use crate::label::Label;
13use crate::vector::StateVector;
14
15/// Cheap, clone-safe summary consumed by the TUI.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Snapshot {
18 pub label: Label,
19 pub friction: FrictionLevel,
20 pub vector: StateVector,
21 /// When this snapshot was produced.
22 pub as_of: DateTime<Utc>,
23 /// Monotonic version number; widgets compare to their last seen
24 /// and skip render when equal.
25 pub version: u64,
26}
27
28impl Snapshot {
29 /// Construct a snapshot whose `friction` is derived from
30 /// `label` alone. Caps at L2 — call
31 /// [`Snapshot::new_with_risk`] from a caller with engine
32 /// access to reach L3/L4.
33 #[must_use]
34 pub fn new(label: Label, vector: StateVector, as_of: DateTime<Utc>, version: u64) -> Self {
35 let friction = FrictionLevel::from_label(label);
36 Self {
37 label,
38 friction,
39 vector,
40 as_of,
41 version,
42 }
43 }
44
45 /// Construct a snapshot whose `friction` is derived from both
46 /// `label` **and** the engine-reported `risk`.
47 ///
48 /// This is the M2 entrypoint. It uses
49 /// [`FrictionLevel::from_label_and_risk`] so the snapshot's
50 /// `.friction` field can reach L3 (TILT + guardrail proximity)
51 /// or L4 (TILT + halt). Callers without engine context (tests,
52 /// pure replay, the classifier's default `classify` path) stay
53 /// on [`Self::new`] and retain the L2 cap.
54 #[must_use]
55 pub fn new_with_risk(
56 label: Label,
57 vector: StateVector,
58 as_of: DateTime<Utc>,
59 version: u64,
60 risk: RiskContext,
61 ) -> Self {
62 let friction = FrictionLevel::from_label_and_risk(label, risk);
63 Self {
64 label,
65 friction,
66 vector,
67 as_of,
68 version,
69 }
70 }
71}