mnemo_baseline/
anomaly.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub enum Severity {
7 Info,
8 Notice,
9 Warning,
10 High,
11 Critical,
12}
13
14impl Severity {
15 pub fn from_z(z: f32) -> Self {
16 let a = z.abs();
17 match a {
18 x if x >= 4.0 => Severity::Critical,
19 x if x >= 3.0 => Severity::High,
20 x if x >= 2.0 => Severity::Warning,
21 x if x >= 1.0 => Severity::Notice,
22 _ => Severity::Info,
23 }
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub enum BaselineMetric {
29 RecallRate,
30 WriteRate,
31 NamespaceFanout,
32 ToolMix,
33 HmacContinuity,
34 ForgetRate,
35}
36
37impl BaselineMetric {
38 pub fn as_str(&self) -> &'static str {
39 match self {
40 BaselineMetric::RecallRate => "recall_rate_per_min",
41 BaselineMetric::WriteRate => "write_rate_per_min",
42 BaselineMetric::NamespaceFanout => "namespace_fanout",
43 BaselineMetric::ToolMix => "tool_mix_kl_divergence",
44 BaselineMetric::HmacContinuity => "hmac_continuity",
45 BaselineMetric::ForgetRate => "forget_rate_per_min",
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub struct BaselineDelta {
52 pub metric: BaselineMetric,
53 pub z: f32,
54 pub ewma_drift: f32,
55 pub severity: Severity,
56}
57
58impl BaselineDelta {
59 pub fn new(metric: BaselineMetric, z: f32, ewma_drift: f32) -> Self {
60 Self {
61 metric,
62 z,
63 ewma_drift,
64 severity: Severity::from_z(z),
65 }
66 }
67}
68
69pub fn z_score(x: f32, mean: f32, stddev: f32) -> f32 {
73 let s = stddev.max(1e-6);
74 (x - mean) / s
75}
76
77pub fn ewma_drift(prev_ewma: f32, x: f32, alpha: f32) -> f32 {
81 let a = alpha.clamp(0.0, 1.0);
82 a * x + (1.0 - a) * prev_ewma
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn severity_thresholds() {
91 assert_eq!(Severity::from_z(0.5), Severity::Info);
92 assert_eq!(Severity::from_z(1.5), Severity::Notice);
93 assert_eq!(Severity::from_z(2.5), Severity::Warning);
94 assert_eq!(Severity::from_z(3.5), Severity::High);
95 assert_eq!(Severity::from_z(5.0), Severity::Critical);
96 }
97
98 #[test]
99 fn z_score_handles_zero_variance() {
100 let z = z_score(2.0, 1.0, 0.0);
102 assert!(z.is_finite());
103 }
104
105 #[test]
106 fn ewma_clamps_alpha() {
107 let d = ewma_drift(10.0, 100.0, 5.0);
108 assert!((d - 100.0).abs() < 1e-3);
110 }
111
112 #[test]
113 fn burst_flips_severity_to_high() {
114 let z = z_score(50.0, 5.0, 2.0);
116 let sev = Severity::from_z(z);
117 assert!(sev == Severity::Critical || sev == Severity::High);
119 }
120
121 #[test]
122 fn metric_strings_are_stable() {
123 for m in [
124 BaselineMetric::RecallRate,
125 BaselineMetric::WriteRate,
126 BaselineMetric::NamespaceFanout,
127 BaselineMetric::ToolMix,
128 BaselineMetric::HmacContinuity,
129 BaselineMetric::ForgetRate,
130 ] {
131 assert!(!m.as_str().is_empty());
132 }
133 }
134}