1use crate::envelope::EnvelopePosition;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum GrammarState {
12 Admissible,
15 Boundary,
18 Violation,
21}
22
23impl GrammarState {
24 pub fn severity(&self) -> u8 {
26 match self {
27 Self::Admissible => 0,
28 Self::Boundary => 1,
29 Self::Violation => 2,
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy)]
36pub struct GrammarTransition {
37 pub from: GrammarState,
39 pub to: GrammarState,
41 pub timestamp_ns: u64,
43 pub confirmation_count: u32,
46}
47
48pub struct GrammarMachine {
54 current_state: GrammarState,
55 pending_position: Option<EnvelopePosition>,
56 consecutive_count: u32,
57 hysteresis_count: u32,
58 last_transition_ts: u64,
59}
60
61impl GrammarMachine {
62 pub fn new(hysteresis_count: u32) -> Self {
68 Self {
69 current_state: GrammarState::Admissible,
70 pending_position: None,
71 consecutive_count: 0,
72 hysteresis_count: hysteresis_count.max(1),
73 last_transition_ts: 0,
74 }
75 }
76
77 pub fn step(
80 &mut self,
81 position: EnvelopePosition,
82 timestamp_ns: u64,
83 ) -> (GrammarState, Option<GrammarTransition>) {
84 let target_state = match position {
85 EnvelopePosition::Interior => GrammarState::Admissible,
86 EnvelopePosition::BoundaryZone => GrammarState::Boundary,
87 EnvelopePosition::Exterior => GrammarState::Violation,
88 };
89
90 if target_state == self.current_state {
91 self.pending_position = None;
93 self.consecutive_count = 0;
94 return (self.current_state, None);
95 }
96
97 match self.pending_position {
101 Some(pending) if position_to_state(pending) == target_state => {
102 self.consecutive_count += 1;
103 }
104 None
105 | Some(EnvelopePosition::Interior)
106 | Some(EnvelopePosition::BoundaryZone)
107 | Some(EnvelopePosition::Exterior) => {
108 self.pending_position = Some(position);
109 self.consecutive_count = 1;
110 }
111 }
112
113 if self.consecutive_count >= self.hysteresis_count {
114 let transition = GrammarTransition {
115 from: self.current_state,
116 to: target_state,
117 timestamp_ns,
118 confirmation_count: self.consecutive_count,
119 };
120 self.current_state = target_state;
121 self.pending_position = None;
122 self.consecutive_count = 0;
123 self.last_transition_ts = timestamp_ns;
124 (self.current_state, Some(transition))
125 } else {
126 (self.current_state, None)
127 }
128 }
129
130 pub fn state(&self) -> GrammarState {
132 self.current_state
133 }
134
135 pub fn reset(&mut self) {
138 self.current_state = GrammarState::Admissible;
139 self.pending_position = None;
140 self.consecutive_count = 0;
141 }
142}
143
144fn position_to_state(pos: EnvelopePosition) -> GrammarState {
145 match pos {
146 EnvelopePosition::Interior => GrammarState::Admissible,
147 EnvelopePosition::BoundaryZone => GrammarState::Boundary,
148 EnvelopePosition::Exterior => GrammarState::Violation,
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_hysteresis_prevents_premature_transition() {
158 let mut machine = GrammarMachine::new(3);
159
160 let (state, trans) = machine.step(EnvelopePosition::BoundaryZone, 1);
162 assert_eq!(state, GrammarState::Admissible);
163 assert!(trans.is_none());
164
165 let (state, trans) = machine.step(EnvelopePosition::BoundaryZone, 2);
166 assert_eq!(state, GrammarState::Admissible);
167 assert!(trans.is_none());
168
169 let (state, trans) = machine.step(EnvelopePosition::BoundaryZone, 3);
171 assert_eq!(state, GrammarState::Boundary);
172 assert!(trans.is_some());
173 let t = trans.unwrap();
174 assert_eq!(t.from, GrammarState::Admissible);
175 assert_eq!(t.to, GrammarState::Boundary);
176 }
177
178 #[test]
179 fn test_interrupted_hysteresis_resets() {
180 let mut machine = GrammarMachine::new(3);
181
182 machine.step(EnvelopePosition::BoundaryZone, 1);
183 machine.step(EnvelopePosition::BoundaryZone, 2);
184 machine.step(EnvelopePosition::Interior, 3);
186
187 machine.step(EnvelopePosition::BoundaryZone, 4);
189 machine.step(EnvelopePosition::BoundaryZone, 5);
190 let (state, _) = machine.step(EnvelopePosition::BoundaryZone, 6);
191 assert_eq!(state, GrammarState::Boundary);
192 }
193}