1use 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#[derive(Debug, Clone)]
23pub struct ValidationConfig {
24 pub state_threshold: f32,
26 pub max_transition_magnitude: f32,
28 pub strict_transition: bool,
30 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#[derive(Debug, Clone)]
47pub struct StateValidation {
48 pub is_valid: bool,
50 pub global_entry: Option<u32>,
52 pub global_similarity: f32,
54 pub local_entry: Option<u32>,
56 pub local_similarity: f32,
58 pub domain: String,
60}
61
62#[derive(Debug, Clone)]
64pub struct TransitionValidation {
65 pub is_valid: bool,
67 pub from_validation: StateValidation,
69 pub to_validation: StateValidation,
71 pub magnitude: f32,
73 pub direction_similarity: f32,
75 pub rejection_reason: Option<String>,
77}
78
79pub struct TransitionValidator {
81 global: Arc<GlobalCodebook>,
83 locals: RwLock<HashMap<String, LocalCodebook>>,
85 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 #[allow(clippy::significant_drop_tightening)] 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 #[deprecated(note = "Use with_local() for operations that need persistence")]
134 #[allow(clippy::significant_drop_tightening)] 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 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 let (global_entry, global_similarity) = self.global.quantize(state).unwrap_or((0, 0.0));
167
168 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 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 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 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 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 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) })
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)] 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
347#[non_exhaustive]
348pub enum ValidationMode {
349 #[default]
351 Full,
352 FastPath,
355 Trusted,
357}
358
359#[derive(Debug, Clone)]
361pub struct FastPathResult {
362 pub can_use_fast_path: bool,
364 pub similarity: f32,
366 pub blocks_checked: usize,
368 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
394pub struct FastPathValidator {
396 pub similarity_threshold: f32,
398 pub min_leader_history: usize,
400 pub full_validation_interval: usize,
402 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 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 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 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 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 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 let validation = validator.validate_state("test", &[0.95, 0.05, 0.0]);
517 assert!(validation.is_valid);
518
519 let validation = validator.validate_state("test", &[0.5, 0.5, 0.5]);
521 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 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 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 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 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], vec![0.5, 0.5, 0.5], vec![0.0, 1.0, 0.0], ];
600
601 let (idx, deviation) = validator.find_max_deviation("test", &states).unwrap();
602 assert_eq!(idx, 1); assert!(deviation > 0.4); }
605
606 #[test]
607 fn test_learn_from_states() {
608 let validator = create_test_validator();
609
610 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 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 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 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 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 for _ in 0..10 {
704 validator.record_validation(true);
705 }
706
707 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 for _ in 0..5 {
724 validator.record_validation(true);
725 }
726
727 validator.record_validation(false);
729
730 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 assert!(validator.is_valid_transition("test", &[1.0, 0.0, 0.0], &[0.95, 0.05, 0.0]));
849
850 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, 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 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, 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 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 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 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 assert!(validator.validate_path("test", &[]).is_ok());
943
944 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 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 assert_eq!(validator.compute_path_drift(&[]), 0.0);
974
975 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 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 for _ in 0..5 {
1043 validator.record_validation(true);
1044 }
1045
1046 let count = validator
1048 .blocks_since_full
1049 .load(std::sync::atomic::Ordering::Relaxed);
1050 assert_eq!(count, 5);
1051
1052 validator.reset();
1054
1055 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, ..Default::default()
1069 };
1070 let validator = TransitionValidator::new(global, config);
1071
1072 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 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 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 validator.with_local("my_domain", |local| {
1100 local.quantize_and_update(&[1.0, 0.0, 0.0], 0.01); });
1102
1103 let entry_count = validator.with_local("my_domain", |local| local.len());
1105 assert_eq!(entry_count, 1);
1106
1107 validator.with_local("my_domain", |local| {
1109 local.quantize_and_update(&[0.0, 1.0, 0.0], 0.01); });
1111
1112 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 let locals = validator.locals.read();
1123 assert!(!locals.contains_key("new_domain"));
1124 drop(locals);
1125
1126 validator.with_local("new_domain", |local| {
1128 assert_eq!(local.domain(), "new_domain");
1129 assert!(local.is_empty());
1130 });
1131
1132 let locals = validator.locals.read();
1134 assert!(locals.contains_key("new_domain"));
1135 }
1136}