1use crate::envelope::AdmissibilityEnvelope;
22use crate::sign::SignTuple;
23use crate::platform::WaveformState;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub enum ReasonCode {
32 SustainedOutwardDrift,
35 AbruptSlewViolation,
38 RecurrentBoundaryGrazing,
41 EnvelopeViolation,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub enum GrammarState {
53 Admissible,
55 Boundary(ReasonCode),
58 Violation,
60}
61
62impl GrammarState {
63 #[inline]
65 pub fn requires_attention(&self) -> bool {
66 !matches!(self, GrammarState::Admissible)
67 }
68
69 #[inline]
71 pub fn is_violation(&self) -> bool {
72 matches!(self, GrammarState::Violation)
73 }
74
75 #[inline]
77 pub fn is_boundary(&self) -> bool {
78 matches!(self, GrammarState::Boundary(_))
79 }
80
81 #[inline]
83 pub fn severity(&self) -> u8 {
84 match self {
85 GrammarState::Admissible => 0,
86 GrammarState::Boundary(_) => 1,
87 GrammarState::Violation => 2,
88 }
89 }
90
91 #[inline]
104 pub fn severity_trust(&self) -> f32 {
105 match self {
106 GrammarState::Admissible => 1.0,
107 GrammarState::Boundary(_) => 0.5,
108 GrammarState::Violation => 0.0,
109 }
110 }
111
112 #[inline]
135 pub fn geometry_trust(norm: f32, rho: f32, band_frac: f32) -> f32 {
136 if rho <= 1e-30 { return 0.0; }
137 let margin = (rho - norm) / rho;
138 if band_frac < 1e-12 {
139 return if margin >= 0.0 { 1.0 } else { 0.0 };
140 }
141 let t = margin / band_frac;
142 t.max(0.0).min(1.0)
143 }
144
145 #[inline]
151 pub fn combined_trust(&self, norm: f32, rho: f32, band_frac: f32) -> f32 {
152 let st = self.severity_trust();
153 let gt = GrammarState::geometry_trust(norm, rho, band_frac);
154 st.min(gt)
155 }
156}
157
158pub struct GrammarEvaluator<const K: usize> {
163 pending: GrammarState,
165 confirmations: u8,
167 committed: GrammarState,
169 boundary_hits: [bool; K],
171 hit_head: usize,
173 hit_count: usize,
175}
176
177impl<const K: usize> GrammarEvaluator<K> {
178 pub const fn new() -> Self {
180 Self {
181 pending: GrammarState::Admissible,
182 confirmations: 0,
183 committed: GrammarState::Admissible,
184 boundary_hits: [false; K],
185 hit_head: 0,
186 hit_count: 0,
187 }
188 }
189
190 #[inline]
192 pub fn state(&self) -> GrammarState {
193 self.committed
194 }
195
196 pub fn evaluate(
201 &mut self,
202 sign: &SignTuple,
203 envelope: &AdmissibilityEnvelope,
204 waveform_state: WaveformState,
205 ) -> GrammarState {
206 if waveform_state.is_suppressed() {
208 self.committed = GrammarState::Admissible;
209 self.pending = GrammarState::Admissible;
210 self.confirmations = 0;
211 return GrammarState::Admissible;
212 }
213
214 let multiplier = waveform_state.admissibility_multiplier();
215 let raw_state = self.compute_raw_state(sign, envelope, multiplier);
216
217 let is_boundary_approach = envelope.is_boundary_approach(sign.norm, multiplier)
219 && !envelope.is_violation(sign.norm, multiplier);
220 self.boundary_hits[self.hit_head] = is_boundary_approach;
221 self.hit_head = (self.hit_head + 1) % K;
222 if self.hit_count < K { self.hit_count += 1; }
223
224 if raw_state == self.pending {
226 if self.confirmations < 2 {
227 self.confirmations += 1;
228 }
229 if self.confirmations >= 2 {
230 self.committed = raw_state;
231 }
232 } else {
233 self.pending = raw_state;
234 self.confirmations = 1;
235 }
236
237 self.committed
238 }
239
240 fn compute_raw_state(
242 &self,
243 sign: &SignTuple,
244 envelope: &AdmissibilityEnvelope,
245 multiplier: f32,
246 ) -> GrammarState {
247 if envelope.is_violation(sign.norm, multiplier) {
249 return GrammarState::Violation;
250 }
251
252 if envelope.is_boundary_approach(sign.norm, multiplier) {
254 if sign.is_outward_drift() {
255 return GrammarState::Boundary(ReasonCode::SustainedOutwardDrift);
256 }
257 if sign.is_abrupt_slew(envelope.delta_s) {
258 return GrammarState::Boundary(ReasonCode::AbruptSlewViolation);
259 }
260 }
261
262 let grazing_hits = self.boundary_hits.iter().filter(|&&h| h).count();
264 if self.hit_count >= K && grazing_hits >= K {
265 return GrammarState::Boundary(ReasonCode::RecurrentBoundaryGrazing);
266 }
267
268 GrammarState::Admissible
269 }
270
271 pub fn reset(&mut self) {
273 *self = Self::new();
274 }
275}
276
277impl<const K: usize> Default for GrammarEvaluator<K> {
278 fn default() -> Self {
279 Self::new()
280 }
281}
282
283#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::envelope::AdmissibilityEnvelope;
290 use crate::sign::SignTuple;
291 use crate::platform::WaveformState;
292
293 fn make_envelope() -> AdmissibilityEnvelope {
294 AdmissibilityEnvelope::new(0.1)
295 }
296
297 #[test]
298 fn clean_signal_is_admissible() {
299 let mut eval = GrammarEvaluator::<4>::new();
300 let env = make_envelope();
301 for _ in 0..5 {
302 let sig = SignTuple::new(0.02, 0.0, 0.0);
303 let state = eval.evaluate(&sig, &env, WaveformState::Operational);
304 assert_eq!(state, GrammarState::Admissible);
305 }
306 }
307
308 #[test]
309 fn violation_detected_after_hysteresis() {
310 let mut eval = GrammarEvaluator::<4>::new();
311 let env = make_envelope();
312 let sig = SignTuple::new(0.15, 0.02, 0.001);
314 let s1 = eval.evaluate(&sig, &env, WaveformState::Operational);
315 let s2 = eval.evaluate(&sig, &env, WaveformState::Operational);
317 assert_eq!(s2, GrammarState::Violation, "s1={:?} s2={:?}", s1, s2);
318 }
319
320 #[test]
321 fn transient_spike_dismissed_by_hysteresis() {
322 let mut eval = GrammarEvaluator::<4>::new();
323 let env = make_envelope();
324 let above = SignTuple::new(0.15, 0.02, 0.0);
326 let below = SignTuple::new(0.02, 0.0, 0.0);
327 eval.evaluate(&above, &env, WaveformState::Operational);
328 let state = eval.evaluate(&below, &env, WaveformState::Operational);
330 assert_eq!(state, GrammarState::Admissible,
331 "single transient should be dismissed by hysteresis");
332 }
333
334 #[test]
335 fn transition_suppresses_violation() {
336 let mut eval = GrammarEvaluator::<4>::new();
337 let env = make_envelope();
338 let huge = SignTuple::new(1000.0, 100.0, 10.0);
339 for _ in 0..5 {
341 let state = eval.evaluate(&huge, &env, WaveformState::Transition);
342 assert_eq!(state, GrammarState::Admissible);
343 }
344 }
345
346 #[test]
347 fn sustained_outward_drift_detected() {
348 let mut eval = GrammarEvaluator::<4>::new();
349 let env = make_envelope();
350 let sig = SignTuple::new(0.07, 0.005, 0.0001);
352 eval.evaluate(&sig, &env, WaveformState::Operational);
353 let state = eval.evaluate(&sig, &env, WaveformState::Operational);
354 assert_eq!(state,
355 GrammarState::Boundary(ReasonCode::SustainedOutwardDrift));
356 }
357
358 #[test]
359 fn grammar_state_severity_ordering() {
360 assert!(GrammarState::Violation.severity() >
361 GrammarState::Boundary(ReasonCode::SustainedOutwardDrift).severity());
362 assert!(GrammarState::Boundary(ReasonCode::EnvelopeViolation).severity() >
363 GrammarState::Admissible.severity());
364 }
365
366 #[test]
367 fn severity_trust_bounded_and_ordered() {
368 let t_adm = GrammarState::Admissible.severity_trust();
369 let t_bnd = GrammarState::Boundary(ReasonCode::SustainedOutwardDrift).severity_trust();
370 let t_vio = GrammarState::Violation.severity_trust();
371 assert!((t_adm - 1.0).abs() < 1e-6);
372 assert!((t_bnd - 0.5).abs() < 1e-6);
373 assert!((t_vio - 0.0).abs() < 1e-6);
374 assert!(t_adm > t_bnd);
375 assert!(t_bnd > t_vio);
376 }
377
378 #[test]
379 fn geometry_trust_deep_inside() {
380 let t = GrammarState::geometry_trust(0.0, 0.10, 0.04);
382 assert!((t - 1.0).abs() < 1e-6, "deep inside → T=1.0, got {}", t);
383 }
384
385 #[test]
386 fn geometry_trust_at_boundary() {
387 let t = GrammarState::geometry_trust(0.10, 0.10, 0.04);
389 assert!((t - 0.0).abs() < 1e-6, "at boundary → T=0.0, got {}", t);
390 }
391
392 #[test]
393 fn geometry_trust_interpolates() {
394 let t = GrammarState::geometry_trust(0.098, 0.10, 0.04);
398 assert!((t - 0.5).abs() < 1e-5, "midpoint of band → T=0.5, got {}", t);
399 }
400
401 #[test]
402 fn combined_trust_takes_minimum() {
403 let t = GrammarState::Violation.combined_trust(0.15, 0.10, 0.04);
405 assert!((t - 0.0).abs() < 1e-6);
406 let t2 = GrammarState::Admissible.combined_trust(0.01, 0.10, 0.04);
408 assert!((t2 - 1.0).abs() < 1e-6);
409 }
410}