Skip to main content

dsfb_debug/
grammar.rs

1//! DSFB-Debug: grammar evaluation — paper §5.5.
2//!
3//! Maps the per-(window, signal) `SignTuple` σ(k) = (‖r‖, ṙ, r̈)
4//! to one of three `GrammarState` symbols:
5//!
6//! - **Admissible** — within envelope; drift inward or bounded
7//! - **Boundary** — approaching envelope with sustained outward
8//!   drift (early-warning state)
9//! - **Violation** — exited envelope (structural fault / actionable
10//!   incident)
11//!
12//! The decision is a pure function of σ(k) and the operator-supplied
13//! grammar thresholds (`drift_threshold`, `slew_threshold`,
14//! `envelope_radius`). The downstream `policy.rs` applies hysteresis
15//! (n_confirm windows) before producing the operator-facing
16//! `confirmed_grammar_state`.
17//!
18//! The grammar is the load-bearing structural-detectability mechanism
19//! (paper Law 1): a trajectory that drifts persistently outward is
20//! more detectable — and more interpretively significant — than one
21//! that oscillates at larger amplitude within admissible bounds.
22
23use crate::types::{GrammarState, ReasonCode, SignTuple};
24use crate::config::EngineConfig;
25use crate::envelope;
26
27/// Evaluate raw grammar state from sign tuple and envelope parameters.
28///
29/// - Violation: norm > ρ
30/// - Boundary: norm > boundary_fraction * ρ AND (sustained outward drift OR high slew)
31/// - Admissible: otherwise
32#[inline]
33pub fn evaluate_raw_grammar(
34    sign: &SignTuple,
35    rho: f64,
36    config: &EngineConfig,
37    drift_persistence: f64,
38) -> (GrammarState, ReasonCode) {
39    // Violation check — hard exit, no hysteresis required
40    if envelope::is_violation(sign.norm, rho) {
41        if sign.slew > config.slew_delta || sign.slew < -config.slew_delta {
42            return (GrammarState::Violation, ReasonCode::AbruptSlewViolation);
43        }
44        return (GrammarState::Violation, ReasonCode::EnvelopeViolation);
45    }
46
47    // Boundary check — in boundary zone with structural evidence
48    if envelope::is_boundary_zone(sign.norm, rho, config.boundary_fraction) {
49        // Sustained outward drift
50        if sign.drift > 0.0 && drift_persistence > 0.5 {
51            return (GrammarState::Boundary, ReasonCode::SustainedOutwardDrift);
52        }
53        // Abrupt slew while in boundary zone
54        if sign.slew > config.slew_delta || sign.slew < -config.slew_delta {
55            return (GrammarState::Boundary, ReasonCode::AbruptSlewViolation);
56        }
57        // Recurrent grazing (norm repeatedly in boundary zone)
58        return (GrammarState::Boundary, ReasonCode::BoundaryApproach);
59    }
60
61    // Admissible
62    (GrammarState::Admissible, ReasonCode::Admissible)
63}
64
65/// Apply hysteresis confirmation: raw state must persist for n_confirm
66/// consecutive windows before being committed.
67///
68/// `recent_raw_states` should contain the last n_confirm raw states
69/// (most recent last).
70#[inline]
71pub fn hysteresis_confirm(
72    recent_raw_states: &[GrammarState],
73    n_confirm: usize,
74) -> GrammarState {
75    if recent_raw_states.len() < n_confirm {
76        return GrammarState::Admissible;
77    }
78
79    let latest = recent_raw_states[recent_raw_states.len() - 1];
80
81    // Violation is immediate — no hysteresis (hard exit)
82    if latest == GrammarState::Violation {
83        return GrammarState::Violation;
84    }
85
86    // Boundary requires n_confirm consecutive Boundary-or-higher
87    if latest == GrammarState::Boundary {
88        let start = recent_raw_states.len() - n_confirm;
89        let mut all_boundary_or_higher = true;
90        let mut i = start;
91        while i < recent_raw_states.len() {
92            if recent_raw_states[i] == GrammarState::Admissible {
93                all_boundary_or_higher = false;
94            }
95            i += 1;
96        }
97        if all_boundary_or_higher {
98            return GrammarState::Boundary;
99        }
100    }
101
102    GrammarState::Admissible
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_violation_detection() {
111        let sign = SignTuple { norm: 4.0, drift: 0.1, slew: 0.0 };
112        let config = EngineConfig::default();
113        let (state, _) = evaluate_raw_grammar(&sign, 3.0, &config, 0.5);
114        assert_eq!(state, GrammarState::Violation);
115    }
116
117    #[test]
118    fn test_boundary_with_drift() {
119        let sign = SignTuple { norm: 2.0, drift: 0.5, slew: 0.0 };
120        let config = EngineConfig::default();
121        let (state, reason) = evaluate_raw_grammar(&sign, 3.0, &config, 0.8);
122        assert_eq!(state, GrammarState::Boundary);
123        assert_eq!(reason, ReasonCode::SustainedOutwardDrift);
124    }
125
126    #[test]
127    fn test_admissible() {
128        let sign = SignTuple { norm: 0.5, drift: 0.0, slew: 0.0 };
129        let config = EngineConfig::default();
130        let (state, _) = evaluate_raw_grammar(&sign, 3.0, &config, 0.0);
131        assert_eq!(state, GrammarState::Admissible);
132    }
133
134    #[test]
135    fn test_hysteresis_blocks_transient() {
136        let states = [GrammarState::Admissible, GrammarState::Boundary];
137        assert_eq!(hysteresis_confirm(&states, 2), GrammarState::Admissible);
138    }
139
140    #[test]
141    fn test_hysteresis_confirms_sustained() {
142        let states = [GrammarState::Boundary, GrammarState::Boundary];
143        assert_eq!(hysteresis_confirm(&states, 2), GrammarState::Boundary);
144    }
145
146    #[test]
147    fn test_violation_bypasses_hysteresis() {
148        let states = [GrammarState::Admissible, GrammarState::Violation];
149        assert_eq!(hysteresis_confirm(&states, 2), GrammarState::Violation);
150    }
151}