templar_proxy_oracle_kernel/proxy/circuit_breaker/
rule.rs

1mod cumulative_change;
2mod monotonic_run;
3mod stepwise_change;
4mod windowed_change_delta;
5
6#[cfg(feature = "schemars")]
7use alloc::borrow::ToOwned;
8#[cfg(any(feature = "borsh", feature = "schemars"))]
9use alloc::string::ToString;
10#[cfg(feature = "schemars")]
11use alloc::{boxed::Box, vec};
12
13pub use cumulative_change::CumulativeChange;
14pub use monotonic_run::MonotonicRun;
15pub use stepwise_change::StepwiseChange;
16pub use windowed_change_delta::WindowedChangeDelta;
17
18use super::{Observation, RingBuffer};
19
20serialize! {
21    #[derive(Debug, Clone, PartialEq, Eq)]
22    pub enum CircuitBreaker {
23        StepwiseChange(StepwiseChange),
24        MonotonicRun(MonotonicRun),
25        WindowedChangeDelta(WindowedChangeDelta),
26        CumulativeChange(CumulativeChange),
27    }
28}
29
30impl CircuitBreaker {
31    fn rule(&self) -> &dyn CircuitBreakerRule {
32        match self {
33            Self::StepwiseChange(inner) => inner,
34            Self::MonotonicRun(inner) => inner,
35            Self::WindowedChangeDelta(inner) => inner,
36            Self::CumulativeChange(inner) => inner,
37        }
38    }
39}
40
41impl CircuitBreakerRule for CircuitBreaker {
42    fn should_trip(&self, history: &RingBuffer<Observation>) -> bool {
43        self.rule().should_trip(history)
44    }
45
46    fn is_valid_for(
47        &self,
48        sample_interval_ns: templar_primitives::Nanoseconds,
49        history_len: u32,
50    ) -> bool {
51        let valid_threshold = |threshold: templar_primitives::Decimal| {
52            threshold != templar_primitives::Decimal::ZERO
53                && threshold <= templar_primitives::Decimal::ONE
54        };
55        match self {
56            Self::StepwiseChange(rule) => {
57                history_len >= 2 && valid_threshold(rule.max_relative_change)
58            }
59            Self::MonotonicRun(rule) => {
60                rule.max_streak > 0
61                    && rule.max_streak < history_len
62                    && sample_interval_ns == templar_primitives::Nanoseconds::zero()
63                    && valid_threshold(rule.min_relative_step_change)
64            }
65            Self::WindowedChangeDelta(rule) => {
66                rule.window_len >= 2
67                    && rule.lookback_windows > 0
68                    && rule
69                        .lookback_windows
70                        .checked_add(1)
71                        .and_then(|windows| rule.window_len.checked_mul(windows))
72                        .is_some_and(|required| required <= history_len)
73                    && valid_threshold(rule.max_relative_mean_change)
74            }
75            Self::CumulativeChange(rule) => {
76                history_len >= 1
77                    && rule.baseline.has_strictly_positive_confidence_interval()
78                    && valid_threshold(rule.max_relative_change)
79            }
80        }
81    }
82}
83
84/// Runtime rule interface used by [`CircuitBreakerSet`](super::CircuitBreakerSet).
85///
86/// The kernel set is generic over this trait for off-chain/library consumers. The NEAR contract
87/// intentionally stores and governs only the closed [`CircuitBreaker`] enum so on-chain rule
88/// schemas remain explicit and auditable.
89pub trait CircuitBreakerRule {
90    fn should_trip(&self, history: &RingBuffer<Observation>) -> bool;
91
92    fn is_valid_for(
93        &self,
94        _sample_interval_ns: templar_primitives::Nanoseconds,
95        _history_len: u32,
96    ) -> bool {
97        true
98    }
99}