| use hyperkitty_core::{Glyph, MAX_ENTROPY}; |
| use hyperkitty_routing::QRADispatcher; |
| use crate::validity::{ValidityPredicate, ValidityInput, ValidityDecision, ValidityGate}; |
|
|
| |
| #[derive(Debug, Clone)] |
| pub struct ReconciliationDecision { |
| pub accepted: bool, |
| pub current_state: Glyph, |
| pub previous_state: Glyph, |
| pub next_state: Option<Glyph>, |
| pub entropy: f64, |
| pub validity_decision: ValidityDecision, |
| pub route_valid: bool, |
| pub invariant_preserved: bool, |
| pub failed_gate: Option<ValidityGate>, |
| pub trace_id: u64, |
| } |
|
|
| impl ReconciliationDecision { |
| pub fn accept( |
| current: Glyph, |
| previous: Glyph, |
| next: Glyph, |
| entropy: f64, |
| validity: ValidityDecision, |
| ) -> Self { |
| ReconciliationDecision { |
| accepted: true, |
| current_state: current, |
| previous_state: previous, |
| next_state: Some(next), |
| entropy, |
| validity_decision: validity, |
| route_valid: true, |
| invariant_preserved: validity.invariant_ok, |
| failed_gate: None, |
| trace_id: 0, |
| } |
| } |
|
|
| pub fn reject( |
| current: Glyph, |
| previous: Glyph, |
| entropy: f64, |
| validity: ValidityDecision, |
| gate: ValidityGate, |
| ) -> Self { |
| ReconciliationDecision { |
| accepted: false, |
| current_state: current, |
| previous_state: previous, |
| next_state: None, |
| entropy, |
| validity_decision: validity, |
| route_valid: false, |
| invariant_preserved: false, |
| failed_gate: Some(gate), |
| trace_id: 0, |
| } |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone)] |
| pub struct ReconciliationState { |
| pub committed_state: Glyph, |
| pub committed_at: u64, |
| pub last_entropy: f64, |
| pub rejection_count: u64, |
| pub acceptance_count: u64, |
| } |
|
|
| impl ReconciliationState { |
| pub fn new(initial: Glyph) -> Self { |
| ReconciliationState { |
| committed_state: initial, |
| committed_at: 0, |
| last_entropy: 0.0, |
| rejection_count: 0, |
| acceptance_count: 0, |
| } |
| } |
|
|
| pub fn advance(&mut self, next: Glyph, entropy: f64) { |
| self.committed_state = next; |
| self.committed_at = self.committed_at.wrapping_add(1); |
| self.last_entropy = entropy; |
| self.acceptance_count = self.acceptance_count.wrapping_add(1); |
| } |
|
|
| pub fn reject(&mut self) { |
| self.rejection_count = self.rejection_count.wrapping_add(1); |
| } |
| } |
|
|
| |
| pub struct ReconciliationProtocol { |
| predicate: ValidityPredicate, |
| state: ReconciliationState, |
| } |
|
|
| impl ReconciliationProtocol { |
| pub fn new(initial_state: Glyph) -> Self { |
| ReconciliationProtocol { |
| predicate: ValidityPredicate::new(), |
| state: ReconciliationState::new(initial_state), |
| } |
| } |
|
|
| |
| |
| pub fn reconcile( |
| &mut self, |
| current: Glyph, |
| previous: Glyph, |
| proof_exists: bool, |
| invariant_ok: bool, |
| ) -> hyperkitty_core::Result<ReconciliationDecision> { |
| |
| let qra_result = QRADispatcher::dispatch(current, previous) |
| .map_err(|_| hyperkitty_core::Error::ParseError("qra_dispatch_failed".to_string()))?; |
|
|
| |
| |
| |
| let entropy = 0.0; |
|
|
| |
| let invariant_ok_computed = invariant_ok && qra_result.is_valid; |
|
|
| |
| let validity_input = ValidityInput { |
| current_state: current, |
| previous_state: previous, |
| route_result: Some(qra_result.next), |
| entropy, |
| proof_exists, |
| invariant_ok: invariant_ok_computed, |
| }; |
|
|
| |
| let validity_decision = self.predicate.check(&validity_input); |
|
|
| |
| let decision = if validity_decision.accepted { |
| ReconciliationDecision::accept( |
| current, |
| previous, |
| qra_result.next, |
| entropy, |
| validity_decision, |
| ) |
| } else { |
| ReconciliationDecision::reject( |
| current, |
| previous, |
| entropy, |
| validity_decision, |
| validity_decision.failed_gate.unwrap_or(ValidityGate::Route), |
| ) |
| }; |
|
|
| Ok(decision) |
| } |
|
|
| |
| |
| pub fn commit(&mut self, decision: &ReconciliationDecision) -> hyperkitty_core::Result<()> { |
| if !decision.accepted { |
| self.state.reject(); |
| return Err(hyperkitty_core::Error::ParseError( |
| format!("reconciliation_rejected: {:?}", decision.failed_gate), |
| )); |
| } |
|
|
| if let Some(next_state) = decision.next_state { |
| self.state.advance(next_state, decision.entropy); |
| Ok(()) |
| } else { |
| Err(hyperkitty_core::Error::ParseError( |
| "accepted but no next_state".to_string(), |
| )) |
| } |
| } |
|
|
| |
| pub fn get_state(&self) -> Glyph { |
| self.state.committed_state |
| } |
|
|
| |
| pub fn snapshot(&self) -> ReconciliationState { |
| self.state.clone() |
| } |
|
|
| |
| |
| pub fn is_deterministic() -> bool { |
| true |
| } |
| } |
|
|
| impl Default for ReconciliationProtocol { |
| fn default() -> Self { |
| Self::new(Glyph::Lambda) |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| #[test] |
| fn test_reconciliation_accepts_valid_transition() { |
| let mut recon = ReconciliationProtocol::new(Glyph::Lambda); |
| let decision = recon |
| .reconcile(Glyph::Pi, Glyph::Gamma, true, true) |
| .unwrap(); |
|
|
| assert!(decision.accepted); |
| assert!(decision.route_valid); |
| assert_eq!(decision.current_state, Glyph::Pi); |
| assert_eq!(decision.previous_state, Glyph::Gamma); |
| assert!(decision.next_state.is_some()); |
| } |
|
|
| #[test] |
| fn test_reconciliation_rejects_missing_proof() { |
| let mut recon = ReconciliationProtocol::new(Glyph::Lambda); |
| let decision = recon |
| .reconcile(Glyph::Pi, Glyph::Gamma, false, true) |
| .unwrap(); |
|
|
| assert!(!decision.accepted); |
| assert_eq!(decision.failed_gate, Some(ValidityGate::Proof)); |
| } |
|
|
| #[test] |
| fn test_reconciliation_rejects_broken_invariant() { |
| let mut recon = ReconciliationProtocol::new(Glyph::Lambda); |
| let decision = recon |
| .reconcile(Glyph::Pi, Glyph::Gamma, true, false) |
| .unwrap(); |
|
|
| assert!(!decision.accepted); |
| assert_eq!(decision.failed_gate, Some(ValidityGate::Invariant)); |
| } |
|
|
| #[test] |
| fn test_reconciliation_commit_advances_state() { |
| let mut recon = ReconciliationProtocol::new(Glyph::Lambda); |
| let decision = recon |
| .reconcile(Glyph::Pi, Glyph::Gamma, true, true) |
| .unwrap(); |
|
|
| assert!(decision.accepted); |
| let result = recon.commit(&decision); |
| assert!(result.is_ok()); |
|
|
| let snapshot = recon.snapshot(); |
| assert_eq!(snapshot.acceptance_count, 1); |
| assert!(snapshot.committed_state.index() < 6); |
| } |
|
|
| #[test] |
| fn test_reconciliation_commit_rejects_invalid_decision() { |
| let mut recon = ReconciliationProtocol::new(Glyph::Lambda); |
| let decision = recon |
| .reconcile(Glyph::Pi, Glyph::Gamma, false, true) |
| .unwrap(); |
|
|
| assert!(!decision.accepted); |
| let result = recon.commit(&decision); |
| assert!(result.is_err()); |
|
|
| let snapshot = recon.snapshot(); |
| assert_eq!(snapshot.rejection_count, 1); |
| } |
|
|
| #[test] |
| fn test_reconciliation_idempotent_input_produces_identical_decision() { |
| let mut recon1 = ReconciliationProtocol::new(Glyph::Lambda); |
| let mut recon2 = ReconciliationProtocol::new(Glyph::Lambda); |
|
|
| let d1 = recon1 |
| .reconcile(Glyph::Delta, Glyph::Omega, true, true) |
| .unwrap(); |
| let d2 = recon2 |
| .reconcile(Glyph::Delta, Glyph::Omega, true, true) |
| .unwrap(); |
|
|
| assert_eq!(d1.accepted, d2.accepted); |
| assert_eq!(d1.next_state, d2.next_state); |
| assert_eq!(d1.entropy, d2.entropy); |
| assert_eq!(d1.failed_gate, d2.failed_gate); |
| } |
|
|
| #[test] |
| fn test_reconciliation_state_no_commit_on_rejection() { |
| let mut recon = ReconciliationProtocol::new(Glyph::Lambda); |
| let initial = recon.get_state(); |
|
|
| let decision = recon |
| .reconcile(Glyph::Pi, Glyph::Gamma, false, true) |
| .unwrap(); |
| assert!(!decision.accepted); |
|
|
| let _ = recon.commit(&decision); |
| let after = recon.get_state(); |
|
|
| assert_eq!(initial, after); |
| } |
|
|
| #[test] |
| fn test_reconciliation_deterministic() { |
| assert!(ReconciliationProtocol::is_deterministic()); |
| } |
|
|
| #[test] |
| fn test_reconciliation_entropy_always_zero() { |
| let mut recon = ReconciliationProtocol::new(Glyph::Lambda); |
| for c in Glyph::all() { |
| for p in Glyph::all() { |
| let decision = recon.reconcile(c, p, true, true).unwrap(); |
| assert_eq!(decision.entropy, 0.0); |
| } |
| } |
| } |
|
|
| #[test] |
| fn test_reconciliation_protocol_default() { |
| let recon = ReconciliationProtocol::default(); |
| assert_eq!(recon.get_state(), Glyph::Lambda); |
| } |
| } |
|
|