Skip to main content

tensor_chain/
validation.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Codebook-based transition validation for the tensor chain.
3//!
4//! Validates state transitions to ensure they stay within the
5//! semantic vocabulary defined by the codebook system:
6//!
7//! - States must be near known codebook entries
8//! - Transitions must have bounded magnitude
9//! - Domain-specific validation via local codebooks
10
11use std::{collections::HashMap, sync::Arc};
12
13use parking_lot::RwLock;
14use tensor_store::SparseVector;
15
16use crate::{
17    codebook::{CodebookConfig, GlobalCodebook, LocalCodebook},
18    error::{ChainError, Result},
19};
20
21/// Validation configuration.
22#[derive(Debug, Clone)]
23pub struct ValidationConfig {
24    /// Minimum similarity for a state to be considered valid.
25    pub state_threshold: f32,
26    /// Maximum allowed transition magnitude.
27    pub max_transition_magnitude: f32,
28    /// Whether to require both states be valid for a valid transition.
29    pub strict_transition: bool,
30    /// Codebook configuration.
31    pub codebook_config: CodebookConfig,
32}
33
34impl Default for ValidationConfig {
35    fn default() -> Self {
36        Self {
37            state_threshold: 0.8,
38            max_transition_magnitude: 1.0,
39            strict_transition: true,
40            codebook_config: CodebookConfig::default(),
41        }
42    }
43}
44
45/// Result of state validation.
46#[derive(Debug, Clone)]
47pub struct StateValidation {
48    /// Whether the state is valid.
49    pub is_valid: bool,
50    /// Nearest global codebook entry ID.
51    pub global_entry: Option<u32>,
52    /// Similarity to global entry.
53    pub global_similarity: f32,
54    /// Nearest local codebook entry ID (if domain-specific).
55    pub local_entry: Option<u32>,
56    /// Similarity to local entry.
57    pub local_similarity: f32,
58    /// Domain that was checked.
59    pub domain: String,
60}
61
62/// Result of transition validation.
63#[derive(Debug, Clone)]
64pub struct TransitionValidation {
65    /// Whether the transition is valid.
66    pub is_valid: bool,
67    /// Validation result for the source state.
68    pub from_validation: StateValidation,
69    /// Validation result for the target state.
70    pub to_validation: StateValidation,
71    /// Euclidean magnitude of the transition.
72    pub magnitude: f32,
73    /// Cosine similarity between from and to states.
74    pub direction_similarity: f32,
75    /// Reason for rejection (if any).
76    pub rejection_reason: Option<String>,
77}
78
79/// Validates state transitions using the hierarchical codebook system.
80pub struct TransitionValidator {
81    /// Global codebook (shared across all nodes).
82    global: Arc<GlobalCodebook>,
83    /// Local codebooks per domain.
84    locals: RwLock<HashMap<String, LocalCodebook>>,
85    /// Validation configuration.
86    config: ValidationConfig,
87}
88
89impl TransitionValidator {
90    #[must_use]
91    pub fn new(global: Arc<GlobalCodebook>, config: ValidationConfig) -> Self {
92        Self {
93            global,
94            locals: RwLock::new(HashMap::new()),
95            config,
96        }
97    }
98
99    #[must_use]
100    pub fn with_global(global: Arc<GlobalCodebook>) -> Self {
101        Self::new(global, ValidationConfig::default())
102    }
103
104    #[must_use]
105    pub fn global(&self) -> &GlobalCodebook {
106        &self.global
107    }
108
109    /// Execute a closure with a mutable reference to the local codebook for a domain.
110    ///
111    /// The local codebook is created if it doesn't exist and persisted across calls.
112    #[allow(clippy::significant_drop_tightening)] // Entry borrows from write guard
113    pub fn with_local<F, R>(&self, domain: &str, f: F) -> R
114    where
115        F: FnOnce(&mut LocalCodebook) -> R,
116    {
117        let mut locals = self.locals.write();
118        let local = locals.entry(domain.to_string()).or_insert_with(|| {
119            LocalCodebook::new(
120                domain,
121                self.global.dimension(),
122                self.config.codebook_config.local_capacity,
123                self.config.codebook_config.ema_alpha,
124            )
125        });
126        f(local)
127    }
128
129    /// Get or create a local codebook for a domain.
130    ///
131    /// Note: Returns a new `LocalCodebook` initialized with the same config for backwards
132    /// compatibility. Prefer `with_local()` for operations that need persistence.
133    #[deprecated(note = "Use with_local() for operations that need persistence")]
134    #[allow(clippy::significant_drop_tightening)] // Entry borrows from write guard
135    pub fn get_or_create_local(&self, domain: &str) -> LocalCodebook {
136        let mut locals = self.locals.write();
137        let local = locals.entry(domain.to_string()).or_insert_with(|| {
138            LocalCodebook::new(
139                domain,
140                self.global.dimension(),
141                self.config.codebook_config.local_capacity,
142                self.config.codebook_config.ema_alpha,
143            )
144        });
145        // Return a new LocalCodebook with the same config (for backwards compatibility)
146        LocalCodebook::new(
147            local.domain(),
148            local.dimension(),
149            self.config.codebook_config.local_capacity,
150            self.config.codebook_config.ema_alpha,
151        )
152    }
153
154    pub fn register_local(&self, domain: &str, local: LocalCodebook) {
155        self.locals.write().insert(domain.to_string(), local);
156    }
157
158    #[must_use]
159    pub fn is_valid_state(&self, domain: &str, state: &[f32]) -> bool {
160        self.validate_state(domain, state).is_valid
161    }
162
163    #[must_use]
164    pub fn validate_state(&self, domain: &str, state: &[f32]) -> StateValidation {
165        // Check global codebook
166        let (global_entry, global_similarity) = self.global.quantize(state).unwrap_or((0, 0.0));
167
168        // Check local codebook if exists
169        let (local_entry, local_similarity) = {
170            let locals = self.locals.read();
171            locals
172                .get(domain)
173                .map_or((0, 0.0), |local| local.quantize(state).unwrap_or((0, 0.0)))
174        };
175
176        // Valid if either global or local similarity meets threshold
177        let is_valid = global_similarity >= self.config.state_threshold
178            || local_similarity >= self.config.state_threshold;
179
180        StateValidation {
181            is_valid,
182            global_entry: Some(global_entry),
183            global_similarity,
184            local_entry: if local_similarity > 0.0 {
185                Some(local_entry)
186            } else {
187                None
188            },
189            local_similarity,
190            domain: domain.to_string(),
191        }
192    }
193
194    #[must_use]
195    pub fn is_valid_transition(&self, domain: &str, from: &[f32], to: &[f32]) -> bool {
196        self.validate_transition(domain, from, to).is_valid
197    }
198
199    #[must_use]
200    pub fn validate_transition(
201        &self,
202        domain: &str,
203        from: &[f32],
204        to: &[f32],
205    ) -> TransitionValidation {
206        let from_validation = self.validate_state(domain, from);
207        let to_validation = self.validate_state(domain, to);
208
209        // Compute transition magnitude
210        let magnitude: f32 = from
211            .iter()
212            .zip(to.iter())
213            .map(|(f, t)| (t - f).powi(2))
214            .sum::<f32>()
215            .sqrt();
216
217        // Compute direction similarity
218        let direction_similarity =
219            SparseVector::from_dense(from).cosine_similarity(&SparseVector::from_dense(to));
220
221        let max_mag = self.config.max_transition_magnitude;
222
223        // Determine validity
224        let (is_valid, rejection_reason) = if self.config.strict_transition {
225            if !from_validation.is_valid {
226                (false, Some("source state invalid".to_string()))
227            } else if !to_validation.is_valid {
228                (false, Some("target state invalid".to_string()))
229            } else if magnitude > max_mag {
230                (
231                    false,
232                    Some(format!(
233                        "transition magnitude {magnitude} exceeds max {max_mag}"
234                    )),
235                )
236            } else {
237                (true, None)
238            }
239        } else if magnitude > max_mag {
240            (
241                false,
242                Some(format!(
243                    "transition magnitude {magnitude} exceeds max {max_mag}"
244                )),
245            )
246        } else {
247            (true, None)
248        };
249
250        TransitionValidation {
251            is_valid,
252            from_validation,
253            to_validation,
254            magnitude,
255            direction_similarity,
256            rejection_reason,
257        }
258    }
259
260    #[must_use]
261    pub fn validate_batch(
262        &self,
263        domain: &str,
264        transitions: &[(Vec<f32>, Vec<f32>)],
265    ) -> Vec<TransitionValidation> {
266        transitions
267            .iter()
268            .map(|(from, to)| self.validate_transition(domain, from, to))
269            .collect()
270    }
271
272    /// # Errors
273    /// Returns an error if any transition in the path is invalid.
274    pub fn validate_path(&self, domain: &str, states: &[Vec<f32>]) -> Result<()> {
275        if states.len() < 2 {
276            return Ok(());
277        }
278
279        for (i, window) in states.windows(2).enumerate() {
280            let validation = self.validate_transition(domain, &window[0], &window[1]);
281            if !validation.is_valid {
282                return Err(ChainError::ValidationFailed(format!(
283                    "transition {i} -> {}: {}",
284                    i + 1,
285                    validation.rejection_reason.unwrap_or_default()
286                )));
287            }
288        }
289
290        Ok(())
291    }
292
293    #[must_use]
294    pub fn compute_path_drift(&self, states: &[Vec<f32>]) -> f32 {
295        if states.len() < 2 {
296            return 0.0;
297        }
298
299        states
300            .windows(2)
301            .map(|window| {
302                window[0]
303                    .iter()
304                    .zip(window[1].iter())
305                    .map(|(a, b)| (b - a).powi(2))
306                    .sum::<f32>()
307                    .sqrt()
308            })
309            .sum()
310    }
311
312    #[must_use]
313    pub fn find_max_deviation(&self, domain: &str, states: &[Vec<f32>]) -> Option<(usize, f32)> {
314        states
315            .iter()
316            .enumerate()
317            .map(|(i, state)| {
318                let validation = self.validate_state(domain, state);
319                let max_sim = validation
320                    .global_similarity
321                    .max(validation.local_similarity);
322                (i, 1.0 - max_sim) // Deviation = 1 - similarity
323            })
324            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
325    }
326
327    #[allow(clippy::significant_drop_tightening)] // Entry borrows from write guard
328    pub fn learn_from_states(&self, domain: &str, states: &[Vec<f32>], threshold: f32) {
329        let mut locals = self.locals.write();
330        let local = locals.entry(domain.to_string()).or_insert_with(|| {
331            LocalCodebook::new(
332                domain,
333                self.global.dimension(),
334                self.config.codebook_config.local_capacity,
335                self.config.codebook_config.ema_alpha,
336            )
337        });
338
339        for state in states {
340            local.quantize_and_update(state, threshold);
341        }
342    }
343}
344
345/// Mode for validation - controls how thorough validation should be.
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
347#[non_exhaustive]
348pub enum ValidationMode {
349    /// Full validation using codebook-based state and transition checks.
350    #[default]
351    Full,
352    /// Fast-path validation using only similarity comparison.
353    /// Contains the similarity score that triggered fast-path.
354    FastPath,
355    /// Skip validation entirely (for trusted sources).
356    Trusted,
357}
358
359/// Result of fast-path validation check.
360#[derive(Debug, Clone)]
361pub struct FastPathResult {
362    /// Whether fast-path can be used.
363    pub can_use_fast_path: bool,
364    /// Similarity score with recent embeddings.
365    pub similarity: f32,
366    /// Number of blocks checked for similarity.
367    pub blocks_checked: usize,
368    /// Reason for rejection (if any).
369    pub rejection_reason: Option<String>,
370}
371
372impl FastPathResult {
373    #[must_use]
374    pub const fn accept(similarity: f32, blocks_checked: usize) -> Self {
375        Self {
376            can_use_fast_path: true,
377            similarity,
378            blocks_checked,
379            rejection_reason: None,
380        }
381    }
382
383    #[must_use]
384    pub fn reject(reason: &str, similarity: f32, blocks_checked: usize) -> Self {
385        Self {
386            can_use_fast_path: false,
387            similarity,
388            blocks_checked,
389            rejection_reason: Some(reason.to_string()),
390        }
391    }
392}
393
394/// Validates whether fast-path replication can be used.
395pub struct FastPathValidator {
396    /// Minimum similarity threshold for fast-path.
397    pub similarity_threshold: f32,
398    /// Minimum number of blocks from leader before allowing fast-path.
399    pub min_leader_history: usize,
400    /// Interval for forcing full validation (every N blocks).
401    pub full_validation_interval: usize,
402    /// Blocks since last full validation.
403    blocks_since_full: std::sync::atomic::AtomicUsize,
404}
405
406impl Default for FastPathValidator {
407    fn default() -> Self {
408        Self {
409            similarity_threshold: 0.95,
410            min_leader_history: 3,
411            full_validation_interval: 10,
412            blocks_since_full: std::sync::atomic::AtomicUsize::new(0),
413        }
414    }
415}
416
417impl FastPathValidator {
418    #[must_use]
419    pub const fn new(similarity_threshold: f32, min_leader_history: usize) -> Self {
420        Self {
421            similarity_threshold,
422            min_leader_history,
423            full_validation_interval: 10,
424            blocks_since_full: std::sync::atomic::AtomicUsize::new(0),
425        }
426    }
427
428    #[must_use]
429    pub fn check_fast_path(
430        &self,
431        block_embedding: &[f32],
432        recent_embeddings: &[Vec<f32>],
433    ) -> FastPathResult {
434        // Need minimum history from leader
435        if recent_embeddings.len() < self.min_leader_history {
436            return FastPathResult::reject(
437                "insufficient leader history",
438                0.0,
439                recent_embeddings.len(),
440            );
441        }
442
443        // Force full validation periodically
444        let blocks_since = self
445            .blocks_since_full
446            .load(std::sync::atomic::Ordering::Relaxed);
447        if blocks_since >= self.full_validation_interval {
448            return FastPathResult::reject(
449                "periodic full validation required",
450                0.0,
451                recent_embeddings.len(),
452            );
453        }
454
455        // Check similarity with recent embeddings
456        let block_vec = SparseVector::from_dense(block_embedding);
457        let max_similarity = recent_embeddings
458            .iter()
459            .map(|emb| block_vec.cosine_similarity(&SparseVector::from_dense(emb)))
460            .fold(0.0f32, f32::max);
461
462        if max_similarity >= self.similarity_threshold {
463            FastPathResult::accept(max_similarity, recent_embeddings.len())
464        } else {
465            FastPathResult::reject(
466                "similarity below threshold",
467                max_similarity,
468                recent_embeddings.len(),
469            )
470        }
471    }
472
473    /// Record that a block was validated.
474    /// Call with `used_fast_path=true` if fast-path was used.
475    pub fn record_validation(&self, used_fast_path: bool) {
476        if used_fast_path {
477            self.blocks_since_full
478                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
479        } else {
480            self.blocks_since_full
481                .store(0, std::sync::atomic::Ordering::Relaxed);
482        }
483    }
484
485    pub fn reset(&self) {
486        self.blocks_since_full
487            .store(0, std::sync::atomic::Ordering::Relaxed);
488    }
489}
490
491#[cfg(test)]
492#[allow(clippy::field_reassign_with_default)]
493mod tests {
494    use super::*;
495
496    fn create_test_validator() -> TransitionValidator {
497        let centroids = vec![
498            vec![1.0, 0.0, 0.0],
499            vec![0.0, 1.0, 0.0],
500            vec![0.0, 0.0, 1.0],
501        ];
502        let global = Arc::new(GlobalCodebook::from_centroids(centroids));
503        TransitionValidator::with_global(global)
504    }
505
506    #[test]
507    fn test_state_validation() {
508        let validator = create_test_validator();
509
510        // Valid state (close to centroid)
511        let validation = validator.validate_state("test", &[1.0, 0.0, 0.0]);
512        assert!(validation.is_valid);
513        assert!(validation.global_similarity > 0.99);
514
515        // Slightly off but still valid
516        let validation = validator.validate_state("test", &[0.95, 0.05, 0.0]);
517        assert!(validation.is_valid);
518
519        // Invalid state (too far from any centroid)
520        let validation = validator.validate_state("test", &[0.5, 0.5, 0.5]);
521        // With default threshold of 0.8, this should be invalid
522        // cos([0.5,0.5,0.5], [1,0,0]) = 0.5/0.866 = 0.577 < 0.8
523        assert!(!validation.is_valid);
524    }
525
526    #[test]
527    fn test_transition_validation() {
528        let config = ValidationConfig {
529            state_threshold: 0.9,
530            max_transition_magnitude: 0.5,
531            strict_transition: true,
532            codebook_config: CodebookConfig::default(),
533        };
534        let centroids = vec![vec![1.0, 0.0, 0.0], vec![0.9, 0.1, 0.0]];
535        let global = Arc::new(GlobalCodebook::from_centroids(centroids));
536        let validator = TransitionValidator::new(global, config);
537
538        // Small valid transition
539        let validation =
540            validator.validate_transition("test", &[1.0, 0.0, 0.0], &[0.95, 0.05, 0.0]);
541        assert!(validation.is_valid);
542        assert!(validation.magnitude < 0.5);
543
544        // Large invalid transition
545        let validation = validator.validate_transition("test", &[1.0, 0.0, 0.0], &[0.0, 1.0, 0.0]);
546        assert!(!validation.is_valid);
547        assert!(validation.magnitude > 0.5);
548    }
549
550    #[test]
551    fn test_path_validation() {
552        let config = ValidationConfig {
553            state_threshold: 0.9,
554            max_transition_magnitude: 0.3,
555            strict_transition: true,
556            codebook_config: CodebookConfig::default(),
557        };
558        let centroids = vec![
559            vec![1.0, 0.0, 0.0],
560            vec![0.9, 0.1, 0.0],
561            vec![0.8, 0.2, 0.0],
562            vec![0.7, 0.3, 0.0],
563        ];
564        let global = Arc::new(GlobalCodebook::from_centroids(centroids));
565        let validator = TransitionValidator::new(global, config);
566
567        // Valid path (small steps)
568        let path = vec![
569            vec![1.0, 0.0, 0.0],
570            vec![0.95, 0.05, 0.0],
571            vec![0.9, 0.1, 0.0],
572        ];
573        assert!(validator.validate_path("test", &path).is_ok());
574    }
575
576    #[test]
577    fn test_path_drift() {
578        let validator = create_test_validator();
579
580        let path = vec![
581            vec![0.0, 0.0, 0.0],
582            vec![1.0, 0.0, 0.0],
583            vec![1.0, 1.0, 0.0],
584        ];
585
586        let drift = validator.compute_path_drift(&path);
587        // First hop: 1.0, Second hop: 1.0, Total: 2.0
588        assert!((drift - 2.0).abs() < 0.001);
589    }
590
591    #[test]
592    fn test_max_deviation() {
593        let validator = create_test_validator();
594
595        let states = vec![
596            vec![1.0, 0.0, 0.0], // Valid (sim = 1.0)
597            vec![0.5, 0.5, 0.5], // Invalid (low similarity)
598            vec![0.0, 1.0, 0.0], // Valid (sim = 1.0)
599        ];
600
601        let (idx, deviation) = validator.find_max_deviation("test", &states).unwrap();
602        assert_eq!(idx, 1); // Middle state has highest deviation
603        assert!(deviation > 0.4); // Deviation should be significant
604    }
605
606    #[test]
607    fn test_learn_from_states() {
608        let validator = create_test_validator();
609
610        // Learn some states
611        let states = vec![vec![0.5, 0.5, 0.0], vec![0.6, 0.4, 0.0]];
612        validator.learn_from_states("custom", &states, 0.9);
613
614        // Now validate against learned domain
615        // Note: The learned states are stored in the local codebook
616        let locals = validator.locals.read();
617        assert!(locals.contains_key("custom"));
618        assert!(!locals.get("custom").unwrap().is_empty());
619    }
620
621    #[test]
622    fn test_batch_validation() {
623        let validator = create_test_validator();
624
625        let transitions = vec![
626            (vec![1.0, 0.0, 0.0], vec![0.9, 0.1, 0.0]),
627            (vec![0.0, 1.0, 0.0], vec![0.1, 0.9, 0.0]),
628        ];
629
630        let results = validator.validate_batch("test", &transitions);
631        assert_eq!(results.len(), 2);
632    }
633
634    #[test]
635    fn test_validation_mode_default() {
636        let mode = ValidationMode::default();
637        assert_eq!(mode, ValidationMode::Full);
638    }
639
640    #[test]
641    fn test_fast_path_result_accept() {
642        let result = FastPathResult::accept(0.98, 5);
643        assert!(result.can_use_fast_path);
644        assert_eq!(result.similarity, 0.98);
645        assert_eq!(result.blocks_checked, 5);
646        assert!(result.rejection_reason.is_none());
647    }
648
649    #[test]
650    fn test_fast_path_result_reject() {
651        let result = FastPathResult::reject("test reason", 0.5, 3);
652        assert!(!result.can_use_fast_path);
653        assert_eq!(result.similarity, 0.5);
654        assert_eq!(result.blocks_checked, 3);
655        assert_eq!(result.rejection_reason.as_deref(), Some("test reason"));
656    }
657
658    #[test]
659    fn test_fast_path_validator_insufficient_history() {
660        let validator = FastPathValidator::new(0.95, 3);
661
662        // Only 2 recent embeddings (need 3)
663        let recent = vec![vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0]];
664
665        let result = validator.check_fast_path(&[1.0, 0.0, 0.0], &recent);
666        assert!(!result.can_use_fast_path);
667        assert!(result
668            .rejection_reason
669            .as_deref()
670            .unwrap()
671            .contains("insufficient"));
672    }
673
674    #[test]
675    fn test_fast_path_validator_high_similarity() {
676        let validator = FastPathValidator::new(0.95, 2);
677
678        // Similar embeddings
679        let recent = vec![vec![1.0, 0.0, 0.0], vec![0.98, 0.02, 0.0]];
680
681        let result = validator.check_fast_path(&[0.99, 0.01, 0.0], &recent);
682        assert!(result.can_use_fast_path);
683        assert!(result.similarity >= 0.95);
684    }
685
686    #[test]
687    fn test_fast_path_validator_low_similarity() {
688        let validator = FastPathValidator::new(0.95, 2);
689
690        // Dissimilar embeddings
691        let recent = vec![vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0]];
692
693        let result = validator.check_fast_path(&[0.0, 0.0, 1.0], &recent);
694        assert!(!result.can_use_fast_path);
695        assert!(result.similarity < 0.95);
696    }
697
698    #[test]
699    fn test_fast_path_validator_periodic_full() {
700        let validator = FastPathValidator::new(0.95, 2);
701
702        // Simulate many fast-path validations
703        for _ in 0..10 {
704            validator.record_validation(true);
705        }
706
707        // Should force full validation after interval
708        let recent = vec![vec![1.0, 0.0, 0.0], vec![0.99, 0.01, 0.0]];
709        let result = validator.check_fast_path(&[1.0, 0.0, 0.0], &recent);
710        assert!(!result.can_use_fast_path);
711        assert!(result
712            .rejection_reason
713            .as_deref()
714            .unwrap()
715            .contains("periodic"));
716    }
717
718    #[test]
719    fn test_fast_path_validator_reset() {
720        let validator = FastPathValidator::new(0.95, 2);
721
722        // Simulate fast-path validations
723        for _ in 0..5 {
724            validator.record_validation(true);
725        }
726
727        // Record full validation
728        validator.record_validation(false);
729
730        // Now fast-path should work again
731        let recent = vec![vec![1.0, 0.0, 0.0], vec![0.99, 0.01, 0.0]];
732        let result = validator.check_fast_path(&[1.0, 0.0, 0.0], &recent);
733        assert!(result.can_use_fast_path);
734    }
735
736    #[test]
737    fn test_validation_config_default() {
738        let config = ValidationConfig::default();
739        assert_eq!(config.state_threshold, 0.8);
740        assert_eq!(config.max_transition_magnitude, 1.0);
741        assert!(config.strict_transition);
742    }
743
744    #[test]
745    fn test_validation_config_clone_debug() {
746        let config = ValidationConfig::default();
747        let cloned = config.clone();
748        assert_eq!(config.state_threshold, cloned.state_threshold);
749
750        let debug = format!("{:?}", config);
751        assert!(debug.contains("ValidationConfig"));
752    }
753
754    #[test]
755    fn test_state_validation_debug_clone() {
756        let validation = StateValidation {
757            is_valid: true,
758            global_entry: Some(42),
759            global_similarity: 0.95,
760            local_entry: Some(10),
761            local_similarity: 0.85,
762            domain: "test".to_string(),
763        };
764
765        let cloned = validation.clone();
766        assert_eq!(validation.is_valid, cloned.is_valid);
767        assert_eq!(validation.global_entry, cloned.global_entry);
768
769        let debug = format!("{:?}", validation);
770        assert!(debug.contains("StateValidation"));
771    }
772
773    #[test]
774    fn test_transition_validation_debug_clone() {
775        let state_val = StateValidation {
776            is_valid: true,
777            global_entry: Some(0),
778            global_similarity: 1.0,
779            local_entry: None,
780            local_similarity: 0.0,
781            domain: "test".to_string(),
782        };
783
784        let validation = TransitionValidation {
785            is_valid: true,
786            from_validation: state_val.clone(),
787            to_validation: state_val,
788            magnitude: 0.5,
789            direction_similarity: 0.9,
790            rejection_reason: None,
791        };
792
793        let cloned = validation.clone();
794        assert_eq!(validation.is_valid, cloned.is_valid);
795        assert_eq!(validation.magnitude, cloned.magnitude);
796
797        let debug = format!("{:?}", validation);
798        assert!(debug.contains("TransitionValidation"));
799    }
800
801    #[test]
802    fn test_transition_validator_global_accessor() {
803        let validator = create_test_validator();
804        let global = validator.global();
805        assert_eq!(global.len(), 3);
806    }
807
808    #[test]
809    #[allow(deprecated)]
810    fn test_transition_validator_get_or_create_local() {
811        let validator = create_test_validator();
812        let local = validator.get_or_create_local("new_domain");
813        assert_eq!(local.dimension(), 3);
814    }
815
816    #[test]
817    fn test_transition_validator_register_local() {
818        let validator = create_test_validator();
819
820        let local = LocalCodebook::new("my_domain", 3, 10, 0.9);
821        validator.register_local("my_domain", local);
822
823        let locals = validator.locals.read();
824        assert!(locals.contains_key("my_domain"));
825    }
826
827    #[test]
828    fn test_is_valid_state() {
829        let validator = create_test_validator();
830
831        assert!(validator.is_valid_state("test", &[1.0, 0.0, 0.0]));
832        assert!(!validator.is_valid_state("test", &[0.5, 0.5, 0.5]));
833    }
834
835    #[test]
836    fn test_is_valid_transition() {
837        let config = ValidationConfig {
838            state_threshold: 0.9,
839            max_transition_magnitude: 0.5,
840            strict_transition: true,
841            codebook_config: CodebookConfig::default(),
842        };
843        let centroids = vec![vec![1.0, 0.0, 0.0], vec![0.9, 0.1, 0.0]];
844        let global = Arc::new(GlobalCodebook::from_centroids(centroids));
845        let validator = TransitionValidator::new(global, config);
846
847        // Small valid transition
848        assert!(validator.is_valid_transition("test", &[1.0, 0.0, 0.0], &[0.95, 0.05, 0.0]));
849
850        // Large invalid transition
851        assert!(!validator.is_valid_transition("test", &[1.0, 0.0, 0.0], &[0.0, 1.0, 0.0]));
852    }
853
854    #[test]
855    fn test_non_strict_transition_validation() {
856        let config = ValidationConfig {
857            state_threshold: 0.9,
858            max_transition_magnitude: 1.5,
859            strict_transition: false, // Non-strict
860            codebook_config: CodebookConfig::default(),
861        };
862        let centroids = vec![vec![1.0, 0.0, 0.0]];
863        let global = Arc::new(GlobalCodebook::from_centroids(centroids));
864        let validator = TransitionValidator::new(global, config);
865
866        // Non-strict allows invalid states if magnitude is within limit
867        let validation = validator.validate_transition("test", &[0.5, 0.5, 0.0], &[0.6, 0.4, 0.0]);
868        assert!(validation.is_valid);
869    }
870
871    #[test]
872    fn test_non_strict_transition_exceeds_magnitude() {
873        let config = ValidationConfig {
874            state_threshold: 0.9,
875            max_transition_magnitude: 0.1, // Very small limit
876            strict_transition: false,
877            codebook_config: CodebookConfig::default(),
878        };
879        let centroids = vec![vec![1.0, 0.0, 0.0]];
880        let global = Arc::new(GlobalCodebook::from_centroids(centroids));
881        let validator = TransitionValidator::new(global, config);
882
883        // Even non-strict fails if magnitude exceeds limit
884        let validation = validator.validate_transition("test", &[0.0, 0.0, 0.0], &[1.0, 0.0, 0.0]);
885        assert!(!validation.is_valid);
886        assert!(validation
887            .rejection_reason
888            .as_deref()
889            .unwrap()
890            .contains("magnitude"));
891    }
892
893    #[test]
894    fn test_transition_invalid_source_state() {
895        let config = ValidationConfig {
896            state_threshold: 0.99,
897            max_transition_magnitude: 10.0,
898            strict_transition: true,
899            codebook_config: CodebookConfig::default(),
900        };
901        let centroids = vec![vec![1.0, 0.0, 0.0]];
902        let global = Arc::new(GlobalCodebook::from_centroids(centroids));
903        let validator = TransitionValidator::new(global, config);
904
905        // Invalid source state
906        let validation = validator.validate_transition("test", &[0.0, 1.0, 0.0], &[1.0, 0.0, 0.0]);
907        assert!(!validation.is_valid);
908        assert!(validation
909            .rejection_reason
910            .as_deref()
911            .unwrap()
912            .contains("source"));
913    }
914
915    #[test]
916    fn test_transition_invalid_target_state() {
917        let config = ValidationConfig {
918            state_threshold: 0.99,
919            max_transition_magnitude: 10.0,
920            strict_transition: true,
921            codebook_config: CodebookConfig::default(),
922        };
923        let centroids = vec![vec![1.0, 0.0, 0.0]];
924        let global = Arc::new(GlobalCodebook::from_centroids(centroids));
925        let validator = TransitionValidator::new(global, config);
926
927        // Invalid target state
928        let validation = validator.validate_transition("test", &[1.0, 0.0, 0.0], &[0.0, 1.0, 0.0]);
929        assert!(!validation.is_valid);
930        assert!(validation
931            .rejection_reason
932            .as_deref()
933            .unwrap()
934            .contains("target"));
935    }
936
937    #[test]
938    fn test_validate_path_short() {
939        let validator = create_test_validator();
940
941        // Empty path
942        assert!(validator.validate_path("test", &[]).is_ok());
943
944        // Single state
945        assert!(validator
946            .validate_path("test", &[vec![1.0, 0.0, 0.0]])
947            .is_ok());
948    }
949
950    #[test]
951    fn test_validate_path_invalid() {
952        let config = ValidationConfig {
953            state_threshold: 0.99,
954            max_transition_magnitude: 0.1,
955            strict_transition: true,
956            codebook_config: CodebookConfig::default(),
957        };
958        let centroids = vec![vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0]];
959        let global = Arc::new(GlobalCodebook::from_centroids(centroids));
960        let validator = TransitionValidator::new(global, config);
961
962        // Invalid path (big jump)
963        let path = vec![vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0]];
964        let result = validator.validate_path("test", &path);
965        assert!(result.is_err());
966    }
967
968    #[test]
969    fn test_compute_path_drift_short() {
970        let validator = create_test_validator();
971
972        // Empty path
973        assert_eq!(validator.compute_path_drift(&[]), 0.0);
974
975        // Single state
976        assert_eq!(validator.compute_path_drift(&[vec![1.0, 0.0, 0.0]]), 0.0);
977    }
978
979    #[test]
980    fn test_find_max_deviation_empty() {
981        let validator = create_test_validator();
982
983        let result = validator.find_max_deviation("test", &[]);
984        assert!(result.is_none());
985    }
986
987    #[test]
988    fn test_cosine_similarity_zero_magnitude() {
989        // Zero magnitude vectors - tests SparseVector behavior
990        let zero = SparseVector::from_dense(&[0.0, 0.0, 0.0]);
991        let unit = SparseVector::from_dense(&[1.0, 0.0, 0.0]);
992
993        let sim = zero.cosine_similarity(&unit);
994        assert_eq!(sim, 0.0);
995
996        let sim = unit.cosine_similarity(&zero);
997        assert_eq!(sim, 0.0);
998
999        let sim = zero.cosine_similarity(&zero);
1000        assert_eq!(sim, 0.0);
1001    }
1002
1003    #[test]
1004    fn test_validation_mode_debug_copy() {
1005        let mode = ValidationMode::FastPath;
1006        let copied = mode;
1007        assert_eq!(mode, copied);
1008
1009        let debug = format!("{:?}", mode);
1010        assert!(debug.contains("FastPath"));
1011
1012        let debug = format!("{:?}", ValidationMode::Full);
1013        assert!(debug.contains("Full"));
1014
1015        let debug = format!("{:?}", ValidationMode::Trusted);
1016        assert!(debug.contains("Trusted"));
1017    }
1018
1019    #[test]
1020    fn test_fast_path_result_debug_clone() {
1021        let result = FastPathResult::accept(0.97, 4);
1022        let cloned = result.clone();
1023        assert_eq!(result.similarity, cloned.similarity);
1024
1025        let debug = format!("{:?}", result);
1026        assert!(debug.contains("FastPathResult"));
1027    }
1028
1029    #[test]
1030    fn test_fast_path_validator_default() {
1031        let validator = FastPathValidator::default();
1032        assert_eq!(validator.similarity_threshold, 0.95);
1033        assert_eq!(validator.min_leader_history, 3);
1034        assert_eq!(validator.full_validation_interval, 10);
1035    }
1036
1037    #[test]
1038    fn test_fast_path_validator_reset_method() {
1039        let validator = FastPathValidator::new(0.95, 2);
1040
1041        // Simulate some validations
1042        for _ in 0..5 {
1043            validator.record_validation(true);
1044        }
1045
1046        // Verify counter incremented
1047        let count = validator
1048            .blocks_since_full
1049            .load(std::sync::atomic::Ordering::Relaxed);
1050        assert_eq!(count, 5);
1051
1052        // Reset
1053        validator.reset();
1054
1055        // Verify counter is zero
1056        let count = validator
1057            .blocks_since_full
1058            .load(std::sync::atomic::Ordering::Relaxed);
1059        assert_eq!(count, 0);
1060    }
1061
1062    #[test]
1063    fn test_state_validation_with_local_codebook() {
1064        let centroids = vec![vec![1.0, 0.0, 0.0]];
1065        let global = Arc::new(GlobalCodebook::from_centroids(centroids));
1066        let config = ValidationConfig {
1067            state_threshold: 0.99, // Very high threshold
1068            ..Default::default()
1069        };
1070        let validator = TransitionValidator::new(global, config);
1071
1072        // Register a local codebook with a different centroid
1073        let local = LocalCodebook::new("domain", 3, 10, 0.9);
1074        local.quantize_and_update(&[0.0, 1.0, 0.0], 0.1);
1075        validator.register_local("domain", local);
1076
1077        // State should be valid via local codebook
1078        let validation = validator.validate_state("domain", &[0.0, 1.0, 0.0]);
1079        assert!(validation.local_entry.is_some());
1080        assert!(validation.local_similarity > 0.0);
1081    }
1082
1083    #[test]
1084    fn test_validate_state_no_local_codebook() {
1085        let validator = create_test_validator();
1086
1087        // State validated against global only (no local codebook for domain)
1088        let validation = validator.validate_state("unknown_domain", &[1.0, 0.0, 0.0]);
1089        assert!(validation.is_valid);
1090        assert!(validation.local_entry.is_none());
1091        assert_eq!(validation.local_similarity, 0.0);
1092    }
1093
1094    #[test]
1095    fn test_with_local_persists_across_calls() {
1096        let validator = create_test_validator();
1097
1098        // Use with_local to add an entry - use very low threshold to force new entry
1099        validator.with_local("my_domain", |local| {
1100            local.quantize_and_update(&[1.0, 0.0, 0.0], 0.01); // Very low threshold
1101        });
1102
1103        // Verify the entry persists across calls
1104        let entry_count = validator.with_local("my_domain", |local| local.len());
1105        assert_eq!(entry_count, 1);
1106
1107        // Add another entry using orthogonal vector
1108        validator.with_local("my_domain", |local| {
1109            local.quantize_and_update(&[0.0, 1.0, 0.0], 0.01); // Orthogonal, low threshold
1110        });
1111
1112        // Verify both entries persist
1113        let entry_count = validator.with_local("my_domain", |local| local.len());
1114        assert_eq!(entry_count, 2);
1115    }
1116
1117    #[test]
1118    fn test_with_local_creates_and_persists() {
1119        let validator = create_test_validator();
1120
1121        // Initially, local codebook doesn't exist
1122        let locals = validator.locals.read();
1123        assert!(!locals.contains_key("new_domain"));
1124        drop(locals);
1125
1126        // Use with_local - should create the local codebook
1127        validator.with_local("new_domain", |local| {
1128            assert_eq!(local.domain(), "new_domain");
1129            assert!(local.is_empty());
1130        });
1131
1132        // Verify it was persisted
1133        let locals = validator.locals.read();
1134        assert!(locals.contains_key("new_domain"));
1135    }
1136}