//! Gate decision types, thresholds, and three-filter decision logic //! //! This module implements the three-filter decision process: //! 1. Structural filter - based on min-cut analysis //! 2. Shift filter - drift detection from expected patterns //! 3. Evidence filter - confidence score threshold //! //! ## Performance Optimizations //! //! - VecDeque for O(1) history rotation (instead of Vec::remove(0)) //! - Inline score calculation functions //! - Pre-computed threshold reciprocals for division optimization //! - Early-exit evaluation order (most likely failures first) use std::collections::VecDeque; use serde::{Deserialize, Serialize}; use crate::supergraph::ReducedGraph; /// Gate decision: Permit, Defer, or Deny #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum GateDecision { /// Action is permitted - stable enough to proceed Permit, /// Action is deferred - uncertain, escalate to human/stronger model Defer, /// Action is denied - unstable or policy-violating Deny, } impl std::fmt::Display for GateDecision { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { GateDecision::Permit => write!(f, "permit"), GateDecision::Defer => write!(f, "defer"), GateDecision::Deny => write!(f, "deny"), } } } /// Evidence filter decision #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EvidenceDecision { /// Sufficient evidence of coherence Accept, /// Insufficient evidence either way Continue, /// Strong evidence of incoherence Reject, } /// Filter type in the decision process #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum DecisionFilter { /// Min-cut based structural analysis Structural, /// Drift detection from patterns Shift, /// Confidence/evidence threshold Evidence, } impl std::fmt::Display for DecisionFilter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DecisionFilter::Structural => write!(f, "Structural"), DecisionFilter::Shift => write!(f, "Shift"), DecisionFilter::Evidence => write!(f, "Evidence"), } } } /// Outcome of the three-filter decision process #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DecisionOutcome { /// The gate decision pub decision: GateDecision, /// Overall confidence score (0.0 - 1.0) pub confidence: f64, /// Which filter rejected (if any) pub rejected_by: Option, /// Reason for rejection (if rejected) pub rejection_reason: Option, /// Structural filter score pub structural_score: f64, /// Shift filter score pub shift_score: f64, /// Evidence filter score pub evidence_score: f64, /// Min-cut value from structural analysis pub mincut_value: f64, } impl DecisionOutcome { /// Create a permit outcome #[inline] pub fn permit( confidence: f64, structural: f64, shift: f64, evidence: f64, mincut: f64, ) -> Self { Self { decision: GateDecision::Permit, confidence, rejected_by: None, rejection_reason: None, structural_score: structural, shift_score: shift, evidence_score: evidence, mincut_value: mincut, } } /// Create a deferred outcome #[inline] pub fn defer( filter: DecisionFilter, reason: String, structural: f64, shift: f64, evidence: f64, mincut: f64, ) -> Self { // OPTIMIZATION: Multiply by reciprocal instead of divide let confidence = (structural + shift + evidence) * (1.0 / 3.0); Self { decision: GateDecision::Defer, confidence, rejected_by: Some(filter), rejection_reason: Some(reason), structural_score: structural, shift_score: shift, evidence_score: evidence, mincut_value: mincut, } } /// Create a denied outcome #[inline] pub fn deny( filter: DecisionFilter, reason: String, structural: f64, shift: f64, evidence: f64, mincut: f64, ) -> Self { // OPTIMIZATION: Multiply by reciprocal instead of divide let confidence = (structural + shift + evidence) * (1.0 / 3.0); Self { decision: GateDecision::Deny, confidence, rejected_by: Some(filter), rejection_reason: Some(reason), structural_score: structural, shift_score: shift, evidence_score: evidence, mincut_value: mincut, } } } /// Threshold configuration for the gate #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GateThresholds { /// E-process level indicating incoherence (default: 0.01) pub tau_deny: f64, /// E-process level indicating coherence (default: 100.0) pub tau_permit: f64, /// Minimum cut value for structural stability pub min_cut: f64, /// Maximum shift pressure before deferral pub max_shift: f64, /// Permit token TTL in nanoseconds pub permit_ttl_ns: u64, /// Conformal set size requiring deferral pub theta_uncertainty: f64, /// Conformal set size for confident permit pub theta_confidence: f64, } impl Default for GateThresholds { fn default() -> Self { Self { tau_deny: 0.01, tau_permit: 100.0, min_cut: 5.0, max_shift: 0.5, permit_ttl_ns: 60_000_000_000, // 60 seconds theta_uncertainty: 20.0, theta_confidence: 5.0, } } } /// Three-filter decision evaluator /// /// Implements the core decision logic for the coherence gate: /// 1. Structural filter - checks min-cut stability /// 2. Shift filter - detects drift from baseline /// 3. Evidence filter - validates confidence threshold /// /// OPTIMIZATION: Uses VecDeque for O(1) history rotation instead of Vec::remove(0) pub struct ThreeFilterDecision { /// Gate thresholds thresholds: GateThresholds, /// Pre-computed reciprocals for fast division /// OPTIMIZATION: Avoid division in hot path inv_min_cut: f64, inv_max_shift: f64, inv_tau_range: f64, /// Historical baseline for shift detection baseline_mincut: Option, /// Window of recent mincut values for drift detection /// OPTIMIZATION: VecDeque for O(1) push_back and pop_front mincut_history: VecDeque, /// Maximum history size history_size: usize, } impl ThreeFilterDecision { /// Create a new three-filter decision evaluator pub fn new(thresholds: GateThresholds) -> Self { // OPTIMIZATION: Pre-compute reciprocals for fast division let inv_min_cut = 1.0 / thresholds.min_cut; let inv_max_shift = 1.0 / thresholds.max_shift; let inv_tau_range = 1.0 / (thresholds.tau_permit - thresholds.tau_deny); Self { thresholds, inv_min_cut, inv_max_shift, inv_tau_range, baseline_mincut: None, // OPTIMIZATION: Use VecDeque for O(1) rotation mincut_history: VecDeque::with_capacity(100), history_size: 100, } } /// Set baseline min-cut for shift detection #[inline] pub fn set_baseline(&mut self, baseline: f64) { self.baseline_mincut = Some(baseline); } /// Update history with a new min-cut observation /// /// OPTIMIZATION: Uses VecDeque for O(1) push/pop instead of Vec::remove(0) which is O(n) #[inline] pub fn observe_mincut(&mut self, mincut: f64) { // OPTIMIZATION: VecDeque::push_back + pop_front is O(1) if self.mincut_history.len() >= self.history_size { self.mincut_history.pop_front(); } self.mincut_history.push_back(mincut); // Update baseline if not set if self.baseline_mincut.is_none() && !self.mincut_history.is_empty() { self.baseline_mincut = Some(self.compute_baseline()); } } /// Compute baseline from history /// /// OPTIMIZATION: Uses iterator sum for cache-friendly access #[inline] fn compute_baseline(&self) -> f64 { let len = self.mincut_history.len(); if len == 0 { return 0.0; } let sum: f64 = self.mincut_history.iter().sum(); sum / len as f64 } /// Evaluate a request against the three filters /// /// OPTIMIZATION: Uses pre-computed reciprocals for division, /// inline score calculations, early-exit on failures #[inline] pub fn evaluate(&self, graph: &ReducedGraph) -> DecisionOutcome { let mincut_value = graph.global_cut(); let shift_pressure = graph.aggregate_shift_pressure(); let e_value = graph.aggregate_evidence(); // 1. Structural Filter - Min-cut analysis // OPTIMIZATION: Use pre-computed reciprocal let structural_score = self.compute_structural_score(mincut_value); if mincut_value < self.thresholds.min_cut { return DecisionOutcome::deny( DecisionFilter::Structural, format!( "Min-cut {:.3} below threshold {:.3}", mincut_value, self.thresholds.min_cut ), structural_score, 0.0, 0.0, mincut_value, ); } // 2. Shift Filter - Drift detection // OPTIMIZATION: Use pre-computed reciprocal let shift_score = self.compute_shift_score(shift_pressure); if shift_pressure >= self.thresholds.max_shift { return DecisionOutcome::defer( DecisionFilter::Shift, format!( "Shift pressure {:.3} exceeds threshold {:.3}", shift_pressure, self.thresholds.max_shift ), structural_score, shift_score, 0.0, mincut_value, ); } // 3. Evidence Filter - E-value threshold // OPTIMIZATION: Use pre-computed reciprocal let evidence_score = self.compute_evidence_score(e_value); if e_value < self.thresholds.tau_deny { return DecisionOutcome::deny( DecisionFilter::Evidence, format!( "E-value {:.3} below denial threshold {:.3}", e_value, self.thresholds.tau_deny ), structural_score, shift_score, evidence_score, mincut_value, ); } if e_value < self.thresholds.tau_permit { return DecisionOutcome::defer( DecisionFilter::Evidence, format!( "E-value {:.3} below permit threshold {:.3}", e_value, self.thresholds.tau_permit ), structural_score, shift_score, evidence_score, mincut_value, ); } // All filters passed // OPTIMIZATION: Multiply by reciprocal let confidence = (structural_score + shift_score + evidence_score) * (1.0 / 3.0); DecisionOutcome::permit( confidence, structural_score, shift_score, evidence_score, mincut_value, ) } /// Compute structural score from min-cut value /// /// OPTIMIZATION: Uses pre-computed reciprocal, marked inline(always) #[inline(always)] fn compute_structural_score(&self, mincut_value: f64) -> f64 { if mincut_value >= self.thresholds.min_cut { 1.0 } else { // OPTIMIZATION: Multiply by reciprocal instead of divide mincut_value * self.inv_min_cut } } /// Compute shift score from shift pressure /// /// OPTIMIZATION: Uses pre-computed reciprocal, marked inline(always) #[inline(always)] fn compute_shift_score(&self, shift_pressure: f64) -> f64 { // OPTIMIZATION: Multiply by reciprocal, use f64::min for branchless 1.0 - (shift_pressure * self.inv_max_shift).min(1.0) } /// Compute evidence score from e-value /// /// OPTIMIZATION: Uses pre-computed reciprocal, marked inline(always) #[inline(always)] fn compute_evidence_score(&self, e_value: f64) -> f64 { if e_value >= self.thresholds.tau_permit { 1.0 } else if e_value <= self.thresholds.tau_deny { 0.0 } else { // OPTIMIZATION: Multiply by reciprocal (e_value - self.thresholds.tau_deny) * self.inv_tau_range } } /// Get current thresholds #[inline] pub fn thresholds(&self) -> &GateThresholds { &self.thresholds } /// Get history size #[inline(always)] pub fn history_len(&self) -> usize { self.mincut_history.len() } /// Get current baseline #[inline(always)] pub fn baseline(&self) -> Option { self.baseline_mincut } /// Update thresholds and recompute reciprocals /// /// OPTIMIZATION: Recomputes cached reciprocals when thresholds change pub fn update_thresholds(&mut self, thresholds: GateThresholds) { self.inv_min_cut = 1.0 / thresholds.min_cut; self.inv_max_shift = 1.0 / thresholds.max_shift; self.inv_tau_range = 1.0 / (thresholds.tau_permit - thresholds.tau_deny); self.thresholds = thresholds; } } #[cfg(test)] mod tests { use super::*; #[test] fn test_gate_decision_display() { assert_eq!(GateDecision::Permit.to_string(), "permit"); assert_eq!(GateDecision::Defer.to_string(), "defer"); assert_eq!(GateDecision::Deny.to_string(), "deny"); } #[test] fn test_default_thresholds() { let thresholds = GateThresholds::default(); assert_eq!(thresholds.tau_deny, 0.01); assert_eq!(thresholds.tau_permit, 100.0); assert_eq!(thresholds.min_cut, 5.0); } #[test] fn test_three_filter_decision() { let thresholds = GateThresholds::default(); let decision = ThreeFilterDecision::new(thresholds); // Default graph should permit let graph = ReducedGraph::new(); let outcome = decision.evaluate(&graph); // Default graph has high coherence, should permit assert_eq!(outcome.decision, GateDecision::Permit); } #[test] fn test_structural_denial() { let thresholds = GateThresholds::default(); let decision = ThreeFilterDecision::new(thresholds); let mut graph = ReducedGraph::new(); graph.set_global_cut(1.0); // Below min_cut of 5.0 let outcome = decision.evaluate(&graph); assert_eq!(outcome.decision, GateDecision::Deny); assert_eq!(outcome.rejected_by, Some(DecisionFilter::Structural)); } #[test] fn test_shift_deferral() { let thresholds = GateThresholds::default(); let decision = ThreeFilterDecision::new(thresholds); let mut graph = ReducedGraph::new(); graph.set_shift_pressure(0.8); // Above max_shift of 0.5 let outcome = decision.evaluate(&graph); assert_eq!(outcome.decision, GateDecision::Defer); assert_eq!(outcome.rejected_by, Some(DecisionFilter::Shift)); } #[test] fn test_evidence_deferral() { let thresholds = GateThresholds::default(); let decision = ThreeFilterDecision::new(thresholds); let mut graph = ReducedGraph::new(); graph.set_evidence(50.0); // Between tau_deny (0.01) and tau_permit (100.0) let outcome = decision.evaluate(&graph); assert_eq!(outcome.decision, GateDecision::Defer); assert_eq!(outcome.rejected_by, Some(DecisionFilter::Evidence)); } #[test] fn test_decision_outcome_creation() { let outcome = DecisionOutcome::permit(0.95, 1.0, 0.9, 0.95, 10.0); assert_eq!(outcome.decision, GateDecision::Permit); assert!(outcome.confidence > 0.9); assert!(outcome.rejected_by.is_none()); } #[test] fn test_decision_filter_display() { assert_eq!(DecisionFilter::Structural.to_string(), "Structural"); assert_eq!(DecisionFilter::Shift.to_string(), "Shift"); assert_eq!(DecisionFilter::Evidence.to_string(), "Evidence"); } #[test] fn test_baseline_observation() { let thresholds = GateThresholds::default(); let mut decision = ThreeFilterDecision::new(thresholds); assert!(decision.baseline().is_none()); decision.observe_mincut(10.0); decision.observe_mincut(12.0); decision.observe_mincut(8.0); assert!(decision.baseline().is_some()); assert_eq!(decision.history_len(), 3); } }