Skip to main content

dsfb_gray/
grammar.rs

1//! Grammar state machine: structural classification of residual trajectories.
2//!
3//! The grammar layer maps envelope positions to interpretable states
4//! (`Admissible`, `Boundary`, `Violation`) with hysteresis to prevent
5//! chattering at envelope boundaries.
6
7use crate::envelope::EnvelopePosition;
8
9/// Grammar state: the structural classification output.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum GrammarState {
12    /// Residual trajectory is within the envelope interior.
13    /// No action required.
14    Admissible,
15    /// Residual trajectory is in the boundary zone.
16    /// Structural shift detected; monitoring escalated.
17    Boundary,
18    /// Residual trajectory has exited the envelope.
19    /// Structural violation confirmed; intervention warranted.
20    Violation,
21}
22
23impl GrammarState {
24    /// Returns the severity level (0=Admissible, 1=Boundary, 2=Violation).
25    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/// A grammar state transition event.
35#[derive(Debug, Clone, Copy)]
36pub struct GrammarTransition {
37    /// Previous grammar state.
38    pub from: GrammarState,
39    /// New grammar state.
40    pub to: GrammarState,
41    /// Timestamp of the transition (nanoseconds).
42    pub timestamp_ns: u64,
43    /// Number of consecutive observations in the new position before
44    /// the transition was confirmed (hysteresis count).
45    pub confirmation_count: u32,
46}
47
48/// Grammar state machine with hysteresis.
49///
50/// Transitions require `hysteresis_count` consecutive observations in
51/// the new position before the grammar state changes. This prevents
52/// chattering at envelope boundaries (Failure Mode FM-02).
53pub 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    /// Create a new grammar machine starting in `Admissible` state.
63    ///
64    /// `hysteresis_count` is the number of consecutive observations required
65    /// to confirm a state transition. Recommended: 3–10 depending on
66    /// sampling rate and noise characteristics.
67    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    /// Process an envelope position and return the current grammar state
78    /// and any transition that occurred.
79    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            // Already in the correct state; reset pending
92            self.pending_position = None;
93            self.consecutive_count = 0;
94            return (self.current_state, None);
95        }
96
97        // Escalation (toward Violation) requires hysteresis
98        // De-escalation (toward Admissible) also requires hysteresis
99        // to prevent flickering during recovery
100        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    /// Current grammar state.
131    pub fn state(&self) -> GrammarState {
132        self.current_state
133    }
134
135    /// Reset the machine to Admissible. Used on system restart or
136    /// phase transition.
137    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        // Two boundary observations — not enough for transition
161        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        // Third boundary observation — transition confirmed
170        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        // Interior interrupts the pending transition
185        machine.step(EnvelopePosition::Interior, 3);
186
187        // Restart boundary sequence
188        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}