1use anyhow::Result;
45use std::collections::{HashMap, HashSet, VecDeque};
46use std::time::{Duration, Instant};
47
48use crate::{DomainHierarchy, SymbolTable, ValidationReport};
49
50#[derive(Clone, Debug, PartialEq, Eq, Hash)]
52pub enum ChangeType {
53 DomainAdded(String),
55 DomainModified(String),
57 DomainRemoved(String),
59 PredicateAdded(String),
61 PredicateModified(String),
63 PredicateRemoved(String),
65 VariableAdded(String),
67 VariableModified(String),
69 VariableRemoved(String),
71 HierarchyAdded(String, String),
73 HierarchyRemoved(String, String),
75}
76
77#[derive(Clone, Debug)]
79pub struct Change {
80 pub change_type: ChangeType,
81 pub timestamp: Instant,
82 pub batch_id: Option<usize>,
83}
84
85impl Change {
86 pub fn new(change_type: ChangeType) -> Self {
87 Self {
88 change_type,
89 timestamp: Instant::now(),
90 batch_id: None,
91 }
92 }
93
94 pub fn with_batch(mut self, batch_id: usize) -> Self {
95 self.batch_id = Some(batch_id);
96 self
97 }
98}
99
100#[derive(Clone, Debug, Default)]
102pub struct ChangeTracker {
103 changes: Vec<Change>,
104 domains_affected: HashSet<String>,
105 predicates_affected: HashSet<String>,
106 variables_affected: HashSet<String>,
107 current_batch: Option<usize>,
108 next_batch_id: usize,
109}
110
111impl ChangeTracker {
112 pub fn new() -> Self {
113 Self::default()
114 }
115
116 pub fn begin_batch(&mut self) -> usize {
118 let batch_id = self.next_batch_id;
119 self.next_batch_id += 1;
120 self.current_batch = Some(batch_id);
121 batch_id
122 }
123
124 pub fn end_batch(&mut self) {
126 self.current_batch = None;
127 }
128
129 pub fn record_domain_addition(&mut self, domain: impl Into<String>) {
131 let domain = domain.into();
132 self.domains_affected.insert(domain.clone());
133 let mut change = Change::new(ChangeType::DomainAdded(domain));
134 if let Some(batch) = self.current_batch {
135 change = change.with_batch(batch);
136 }
137 self.changes.push(change);
138 }
139
140 pub fn record_domain_modification(&mut self, domain: impl Into<String>) {
142 let domain = domain.into();
143 self.domains_affected.insert(domain.clone());
144 let mut change = Change::new(ChangeType::DomainModified(domain));
145 if let Some(batch) = self.current_batch {
146 change = change.with_batch(batch);
147 }
148 self.changes.push(change);
149 }
150
151 pub fn record_domain_removal(&mut self, domain: impl Into<String>) {
153 let domain = domain.into();
154 self.domains_affected.insert(domain.clone());
155 let mut change = Change::new(ChangeType::DomainRemoved(domain));
156 if let Some(batch) = self.current_batch {
157 change = change.with_batch(batch);
158 }
159 self.changes.push(change);
160 }
161
162 pub fn record_predicate_addition(&mut self, predicate: impl Into<String>) {
164 let predicate = predicate.into();
165 self.predicates_affected.insert(predicate.clone());
166 let mut change = Change::new(ChangeType::PredicateAdded(predicate));
167 if let Some(batch) = self.current_batch {
168 change = change.with_batch(batch);
169 }
170 self.changes.push(change);
171 }
172
173 pub fn record_predicate_modification(&mut self, predicate: impl Into<String>) {
175 let predicate = predicate.into();
176 self.predicates_affected.insert(predicate.clone());
177 let mut change = Change::new(ChangeType::PredicateModified(predicate));
178 if let Some(batch) = self.current_batch {
179 change = change.with_batch(batch);
180 }
181 self.changes.push(change);
182 }
183
184 pub fn record_predicate_removal(&mut self, predicate: impl Into<String>) {
186 let predicate = predicate.into();
187 self.predicates_affected.insert(predicate.clone());
188 let mut change = Change::new(ChangeType::PredicateRemoved(predicate));
189 if let Some(batch) = self.current_batch {
190 change = change.with_batch(batch);
191 }
192 self.changes.push(change);
193 }
194
195 pub fn record_variable_addition(&mut self, variable: impl Into<String>) {
197 let variable = variable.into();
198 self.variables_affected.insert(variable.clone());
199 let mut change = Change::new(ChangeType::VariableAdded(variable));
200 if let Some(batch) = self.current_batch {
201 change = change.with_batch(batch);
202 }
203 self.changes.push(change);
204 }
205
206 pub fn record_variable_modification(&mut self, variable: impl Into<String>) {
208 let variable = variable.into();
209 self.variables_affected.insert(variable.clone());
210 let mut change = Change::new(ChangeType::VariableModified(variable));
211 if let Some(batch) = self.current_batch {
212 change = change.with_batch(batch);
213 }
214 self.changes.push(change);
215 }
216
217 pub fn record_variable_removal(&mut self, variable: impl Into<String>) {
219 let variable = variable.into();
220 self.variables_affected.insert(variable.clone());
221 let mut change = Change::new(ChangeType::VariableRemoved(variable));
222 if let Some(batch) = self.current_batch {
223 change = change.with_batch(batch);
224 }
225 self.changes.push(change);
226 }
227
228 pub fn record_hierarchy_addition(
230 &mut self,
231 subtype: impl Into<String>,
232 supertype: impl Into<String>,
233 ) {
234 let subtype = subtype.into();
235 let supertype = supertype.into();
236 self.domains_affected.insert(subtype.clone());
237 self.domains_affected.insert(supertype.clone());
238 let mut change = Change::new(ChangeType::HierarchyAdded(subtype, supertype));
239 if let Some(batch) = self.current_batch {
240 change = change.with_batch(batch);
241 }
242 self.changes.push(change);
243 }
244
245 pub fn record_hierarchy_removal(
247 &mut self,
248 subtype: impl Into<String>,
249 supertype: impl Into<String>,
250 ) {
251 let subtype = subtype.into();
252 let supertype = supertype.into();
253 self.domains_affected.insert(subtype.clone());
254 self.domains_affected.insert(supertype.clone());
255 let mut change = Change::new(ChangeType::HierarchyRemoved(subtype, supertype));
256 if let Some(batch) = self.current_batch {
257 change = change.with_batch(batch);
258 }
259 self.changes.push(change);
260 }
261
262 pub fn changes(&self) -> &[Change] {
264 &self.changes
265 }
266
267 pub fn domains_affected(&self) -> &HashSet<String> {
269 &self.domains_affected
270 }
271
272 pub fn predicates_affected(&self) -> &HashSet<String> {
274 &self.predicates_affected
275 }
276
277 pub fn variables_affected(&self) -> &HashSet<String> {
279 &self.variables_affected
280 }
281
282 pub fn has_changes(&self) -> bool {
284 !self.changes.is_empty()
285 }
286
287 pub fn clear(&mut self) {
289 self.changes.clear();
290 self.domains_affected.clear();
291 self.predicates_affected.clear();
292 self.variables_affected.clear();
293 }
294
295 pub fn stats(&self) -> ChangeStats {
297 let mut by_type = HashMap::new();
298 for change in &self.changes {
299 let type_name = match &change.change_type {
300 ChangeType::DomainAdded(_) => "DomainAdded",
301 ChangeType::DomainModified(_) => "DomainModified",
302 ChangeType::DomainRemoved(_) => "DomainRemoved",
303 ChangeType::PredicateAdded(_) => "PredicateAdded",
304 ChangeType::PredicateModified(_) => "PredicateModified",
305 ChangeType::PredicateRemoved(_) => "PredicateRemoved",
306 ChangeType::VariableAdded(_) => "VariableAdded",
307 ChangeType::VariableModified(_) => "VariableModified",
308 ChangeType::VariableRemoved(_) => "VariableRemoved",
309 ChangeType::HierarchyAdded(_, _) => "HierarchyAdded",
310 ChangeType::HierarchyRemoved(_, _) => "HierarchyRemoved",
311 };
312 *by_type.entry(type_name.to_string()).or_insert(0) += 1;
313 }
314
315 ChangeStats {
316 total_changes: self.changes.len(),
317 domains_affected: self.domains_affected.len(),
318 predicates_affected: self.predicates_affected.len(),
319 variables_affected: self.variables_affected.len(),
320 changes_by_type: by_type,
321 }
322 }
323}
324
325#[derive(Clone, Debug)]
327pub struct ChangeStats {
328 pub total_changes: usize,
329 pub domains_affected: usize,
330 pub predicates_affected: usize,
331 pub variables_affected: usize,
332 pub changes_by_type: HashMap<String, usize>,
333}
334
335#[derive(Clone, Debug, Default)]
337pub struct DependencyGraph {
338 domain_dependents: HashMap<String, HashSet<String>>,
340 predicate_dependencies: HashMap<String, HashSet<String>>,
342 variable_dependents: HashMap<String, HashSet<String>>,
344}
345
346impl DependencyGraph {
347 pub fn new() -> Self {
348 Self::default()
349 }
350
351 pub fn from_symbol_table(table: &SymbolTable) -> Self {
353 let mut graph = Self::new();
354
355 for (pred_name, pred) in &table.predicates {
357 for domain in &pred.arg_domains {
358 graph.add_predicate_dependency(pred_name, domain);
359 }
360 }
361
362 for (var, domain) in &table.variables {
364 graph.add_variable_dependency(var, domain);
365 }
366
367 graph
368 }
369
370 pub fn add_predicate_dependency(
372 &mut self,
373 predicate: impl Into<String>,
374 domain: impl Into<String>,
375 ) {
376 let predicate = predicate.into();
377 let domain = domain.into();
378
379 self.domain_dependents
380 .entry(domain.clone())
381 .or_default()
382 .insert(predicate.clone());
383
384 self.predicate_dependencies
385 .entry(predicate)
386 .or_default()
387 .insert(domain);
388 }
389
390 pub fn add_variable_dependency(
392 &mut self,
393 variable: impl Into<String>,
394 domain: impl Into<String>,
395 ) {
396 let variable = variable.into();
397 let domain = domain.into();
398
399 self.variable_dependents
400 .entry(domain)
401 .or_default()
402 .insert(variable);
403 }
404
405 pub fn get_dependent_predicates(&self, domain: &str) -> HashSet<String> {
407 self.domain_dependents
408 .get(domain)
409 .cloned()
410 .unwrap_or_default()
411 }
412
413 pub fn get_dependent_variables(&self, domain: &str) -> HashSet<String> {
415 self.variable_dependents
416 .get(domain)
417 .cloned()
418 .unwrap_or_default()
419 }
420
421 pub fn get_predicate_dependencies(&self, predicate: &str) -> HashSet<String> {
423 self.predicate_dependencies
424 .get(predicate)
425 .cloned()
426 .unwrap_or_default()
427 }
428
429 pub fn compute_affected_components(
431 &self,
432 initial_domains: &HashSet<String>,
433 ) -> AffectedComponents {
434 let mut affected = AffectedComponents::default();
435 let mut to_process: VecDeque<String> = initial_domains.iter().cloned().collect();
436
437 affected.domains.extend(initial_domains.clone());
438
439 while let Some(domain) = to_process.pop_front() {
440 if let Some(predicates) = self.domain_dependents.get(&domain) {
442 for pred in predicates {
443 if affected.predicates.insert(pred.clone()) {
444 if let Some(deps) = self.predicate_dependencies.get(pred) {
446 for dep_domain in deps {
447 if dep_domain != &domain && !affected.domains.contains(dep_domain) {
448 affected.domains.insert(dep_domain.clone());
450 }
451 }
452 }
453 }
454 }
455 }
456
457 if let Some(variables) = self.variable_dependents.get(&domain) {
459 affected.variables.extend(variables.clone());
460 }
461 }
462
463 affected
464 }
465}
466
467#[derive(Clone, Debug, Default)]
469pub struct AffectedComponents {
470 pub domains: HashSet<String>,
471 pub predicates: HashSet<String>,
472 pub variables: HashSet<String>,
473}
474
475impl AffectedComponents {
476 pub fn is_empty(&self) -> bool {
477 self.domains.is_empty() && self.predicates.is_empty() && self.variables.is_empty()
478 }
479
480 pub fn total_count(&self) -> usize {
481 self.domains.len() + self.predicates.len() + self.variables.len()
482 }
483}
484
485#[derive(Clone, Debug, Default)]
487pub struct ValidationCache {
488 domain_results: HashMap<String, Vec<String>>,
489 predicate_results: HashMap<String, Vec<String>>,
490 variable_results: HashMap<String, Vec<String>>,
491}
492
493impl ValidationCache {
494 pub fn new() -> Self {
495 Self::default()
496 }
497
498 pub fn cache_domain(&mut self, domain: impl Into<String>, errors: Vec<String>) {
500 self.domain_results.insert(domain.into(), errors);
501 }
502
503 pub fn cache_predicate(&mut self, predicate: impl Into<String>, errors: Vec<String>) {
505 self.predicate_results.insert(predicate.into(), errors);
506 }
507
508 pub fn cache_variable(&mut self, variable: impl Into<String>, errors: Vec<String>) {
510 self.variable_results.insert(variable.into(), errors);
511 }
512
513 pub fn get_domain(&self, domain: &str) -> Option<&Vec<String>> {
515 self.domain_results.get(domain)
516 }
517
518 pub fn get_predicate(&self, predicate: &str) -> Option<&Vec<String>> {
520 self.predicate_results.get(predicate)
521 }
522
523 pub fn get_variable(&self, variable: &str) -> Option<&Vec<String>> {
525 self.variable_results.get(variable)
526 }
527
528 pub fn invalidate(&mut self, affected: &AffectedComponents) {
530 for domain in &affected.domains {
531 self.domain_results.remove(domain);
532 }
533 for predicate in &affected.predicates {
534 self.predicate_results.remove(predicate);
535 }
536 for variable in &affected.variables {
537 self.variable_results.remove(variable);
538 }
539 }
540
541 pub fn clear(&mut self) {
543 self.domain_results.clear();
544 self.predicate_results.clear();
545 self.variable_results.clear();
546 }
547
548 pub fn stats(&self) -> CacheStats {
550 CacheStats {
551 domains_cached: self.domain_results.len(),
552 predicates_cached: self.predicate_results.len(),
553 variables_cached: self.variable_results.len(),
554 }
555 }
556}
557
558#[derive(Clone, Debug)]
560pub struct CacheStats {
561 pub domains_cached: usize,
562 pub predicates_cached: usize,
563 pub variables_cached: usize,
564}
565
566pub struct IncrementalValidator<'a> {
568 table: &'a SymbolTable,
569 tracker: &'a ChangeTracker,
570 hierarchy: Option<&'a DomainHierarchy>,
571 cache: ValidationCache,
572 dependency_graph: DependencyGraph,
573}
574
575impl<'a> IncrementalValidator<'a> {
576 pub fn new(table: &'a SymbolTable, tracker: &'a ChangeTracker) -> Self {
577 Self {
578 table,
579 tracker,
580 hierarchy: None,
581 cache: ValidationCache::new(),
582 dependency_graph: DependencyGraph::from_symbol_table(table),
583 }
584 }
585
586 pub fn with_hierarchy(mut self, hierarchy: &'a DomainHierarchy) -> Self {
587 self.hierarchy = Some(hierarchy);
588 self
589 }
590
591 pub fn with_cache(mut self, cache: ValidationCache) -> Self {
592 self.cache = cache;
593 self
594 }
595
596 pub fn cache(&self) -> &ValidationCache {
598 &self.cache
599 }
600
601 pub fn into_cache(self) -> ValidationCache {
603 self.cache
604 }
605
606 pub fn validate_incremental(&mut self) -> Result<IncrementalValidationReport> {
608 let start = Instant::now();
609
610 if !self.tracker.has_changes() {
611 return Ok(IncrementalValidationReport {
613 report: ValidationReport::new(),
614 components_validated: 0,
615 components_cached: self.cache.stats().domains_cached
616 + self.cache.stats().predicates_cached
617 + self.cache.stats().variables_cached,
618 duration: start.elapsed(),
619 });
620 }
621
622 let affected = self
624 .dependency_graph
625 .compute_affected_components(self.tracker.domains_affected());
626
627 self.cache.invalidate(&affected);
629
630 let mut report = ValidationReport::new();
632 let mut components_validated = 0;
633
634 for domain in &affected.domains {
636 if let Some(cached) = self.cache.get_domain(domain) {
637 report.errors.extend(cached.clone());
638 } else {
639 let errors = self.validate_domain(domain)?;
640 report.errors.extend(errors.clone());
641 self.cache.cache_domain(domain, errors);
642 components_validated += 1;
643 }
644 }
645
646 for predicate in &affected.predicates {
648 if let Some(cached) = self.cache.get_predicate(predicate) {
649 report.errors.extend(cached.clone());
650 } else {
651 let errors = self.validate_predicate(predicate)?;
652 report.errors.extend(errors.clone());
653 self.cache.cache_predicate(predicate, errors);
654 components_validated += 1;
655 }
656 }
657
658 for variable in &affected.variables {
660 if let Some(cached) = self.cache.get_variable(variable) {
661 report.errors.extend(cached.clone());
662 } else {
663 let errors = self.validate_variable(variable)?;
664 report.errors.extend(errors.clone());
665 self.cache.cache_variable(variable, errors);
666 components_validated += 1;
667 }
668 }
669
670 let cache_stats = self.cache.stats();
671 let components_cached = cache_stats.domains_cached
672 + cache_stats.predicates_cached
673 + cache_stats.variables_cached;
674
675 Ok(IncrementalValidationReport {
676 report,
677 components_validated,
678 components_cached,
679 duration: start.elapsed(),
680 })
681 }
682
683 fn validate_domain(&self, domain: &str) -> Result<Vec<String>> {
684 let mut errors = Vec::new();
685
686 if !self.table.domains.contains_key(domain) {
687 errors.push(format!("Domain '{}' not found in symbol table", domain));
688 }
689
690 Ok(errors)
691 }
692
693 fn validate_predicate(&self, predicate: &str) -> Result<Vec<String>> {
694 let mut errors = Vec::new();
695
696 if let Some(pred) = self.table.predicates.get(predicate) {
697 for domain in &pred.arg_domains {
698 if domain != "Unknown" && !self.table.domains.contains_key(domain) {
699 errors.push(format!(
700 "Predicate '{}' references undefined domain '{}'",
701 predicate, domain
702 ));
703 }
704 }
705 } else {
706 errors.push(format!(
707 "Predicate '{}' not found in symbol table",
708 predicate
709 ));
710 }
711
712 Ok(errors)
713 }
714
715 fn validate_variable(&self, variable: &str) -> Result<Vec<String>> {
716 let mut errors = Vec::new();
717
718 if let Some(domain) = self.table.variables.get(variable) {
719 if !self.table.domains.contains_key(domain) {
720 errors.push(format!(
721 "Variable '{}' is bound to undefined domain '{}'",
722 variable, domain
723 ));
724 }
725 } else {
726 errors.push(format!("Variable '{}' not found in symbol table", variable));
727 }
728
729 Ok(errors)
730 }
731}
732
733#[derive(Clone, Debug)]
735pub struct IncrementalValidationReport {
736 pub report: ValidationReport,
737 pub components_validated: usize,
738 pub components_cached: usize,
739 pub duration: Duration,
740}
741
742impl IncrementalValidationReport {
743 pub fn is_valid(&self) -> bool {
744 self.report.is_valid()
745 }
746
747 pub fn cache_hit_rate(&self) -> f64 {
748 let total = self.components_validated + self.components_cached;
749 if total == 0 {
750 0.0
751 } else {
752 self.components_cached as f64 / total as f64
753 }
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760 use crate::{DomainInfo, PredicateInfo};
761
762 #[test]
763 fn test_change_tracker_basic() {
764 let mut tracker = ChangeTracker::new();
765
766 tracker.record_domain_addition("Person");
767 tracker.record_predicate_addition("knows");
768 tracker.record_variable_addition("x");
769
770 assert_eq!(tracker.changes().len(), 3);
771 assert_eq!(tracker.domains_affected().len(), 1);
772 assert_eq!(tracker.predicates_affected().len(), 1);
773 assert_eq!(tracker.variables_affected().len(), 1);
774 }
775
776 #[test]
777 fn test_change_tracker_batching() {
778 let mut tracker = ChangeTracker::new();
779
780 let batch1 = tracker.begin_batch();
781 tracker.record_domain_addition("Person");
782 tracker.record_domain_addition("Location");
783 tracker.end_batch();
784
785 let batch2 = tracker.begin_batch();
786 tracker.record_predicate_addition("at");
787 tracker.end_batch();
788
789 assert_eq!(tracker.changes().len(), 3);
790 assert!(tracker.changes()[0].batch_id == Some(batch1));
791 assert!(tracker.changes()[1].batch_id == Some(batch1));
792 assert!(tracker.changes()[2].batch_id == Some(batch2));
793 }
794
795 #[test]
796 fn test_dependency_graph() {
797 let mut table = SymbolTable::new();
798 table.add_domain(DomainInfo::new("Person", 100)).unwrap();
799 table.add_domain(DomainInfo::new("Location", 50)).unwrap();
800
801 let knows = PredicateInfo::new("knows", vec!["Person".to_string(), "Person".to_string()]);
802 table.add_predicate(knows).unwrap();
803
804 let at = PredicateInfo::new("at", vec!["Person".to_string(), "Location".to_string()]);
805 table.add_predicate(at).unwrap();
806
807 let graph = DependencyGraph::from_symbol_table(&table);
808
809 let person_deps = graph.get_dependent_predicates("Person");
810 assert_eq!(person_deps.len(), 2);
811 assert!(person_deps.contains("knows"));
812 assert!(person_deps.contains("at"));
813
814 let location_deps = graph.get_dependent_predicates("Location");
815 assert_eq!(location_deps.len(), 1);
816 assert!(location_deps.contains("at"));
817 }
818
819 #[test]
820 fn test_affected_components() {
821 let mut table = SymbolTable::new();
822 table.add_domain(DomainInfo::new("Person", 100)).unwrap();
823
824 let knows = PredicateInfo::new("knows", vec!["Person".to_string(), "Person".to_string()]);
825 table.add_predicate(knows).unwrap();
826
827 table.bind_variable("x", "Person").unwrap();
828
829 let graph = DependencyGraph::from_symbol_table(&table);
830
831 let mut initial = HashSet::new();
832 initial.insert("Person".to_string());
833
834 let affected = graph.compute_affected_components(&initial);
835
836 assert_eq!(affected.domains.len(), 1);
837 assert_eq!(affected.predicates.len(), 1);
838 assert_eq!(affected.variables.len(), 1);
839 assert!(affected.predicates.contains("knows"));
840 assert!(affected.variables.contains("x"));
841 }
842
843 #[test]
844 fn test_validation_cache() {
845 let mut cache = ValidationCache::new();
846
847 cache.cache_domain("Person", vec![]);
848 cache.cache_predicate("knows", vec!["Error".to_string()]);
849
850 assert_eq!(cache.get_domain("Person"), Some(&vec![]));
851 assert_eq!(
852 cache.get_predicate("knows"),
853 Some(&vec!["Error".to_string()])
854 );
855 assert_eq!(cache.get_variable("x"), None);
856
857 let stats = cache.stats();
858 assert_eq!(stats.domains_cached, 1);
859 assert_eq!(stats.predicates_cached, 1);
860 assert_eq!(stats.variables_cached, 0);
861 }
862
863 #[test]
864 fn test_incremental_validation_basic() {
865 let mut table = SymbolTable::new();
866 let mut tracker = ChangeTracker::new();
867
868 table.add_domain(DomainInfo::new("Person", 100)).unwrap();
869 tracker.record_domain_addition("Person");
870
871 let mut validator = IncrementalValidator::new(&table, &tracker);
872 let report = validator.validate_incremental().unwrap();
873
874 assert!(report.is_valid());
875 assert_eq!(report.components_validated, 1);
876 }
877
878 #[test]
879 fn test_incremental_validation_with_cache() {
880 let mut table = SymbolTable::new();
881 let mut tracker = ChangeTracker::new();
882
883 table.add_domain(DomainInfo::new("Person", 100)).unwrap();
885 tracker.record_domain_addition("Person");
886
887 let mut validator = IncrementalValidator::new(&table, &tracker);
888 let report1 = validator.validate_incremental().unwrap();
889 assert_eq!(report1.components_validated, 1);
890
891 let cache = validator.cache.clone();
893
894 let mut tracker2 = ChangeTracker::new();
896 table.add_domain(DomainInfo::new("Location", 50)).unwrap();
897 tracker2.record_domain_addition("Location");
898
899 let mut validator2 = IncrementalValidator::new(&table, &tracker2).with_cache(cache);
900 let report2 = validator2.validate_incremental().unwrap();
901
902 assert_eq!(report2.components_validated, 1);
904 assert!(report2.components_cached > 0);
905 }
906
907 #[test]
908 fn test_change_stats() {
909 let mut tracker = ChangeTracker::new();
910
911 tracker.record_domain_addition("Person");
912 tracker.record_domain_modification("Person");
913 tracker.record_predicate_addition("knows");
914
915 let stats = tracker.stats();
916 assert_eq!(stats.total_changes, 3);
917 assert_eq!(stats.domains_affected, 1);
918 assert_eq!(stats.predicates_affected, 1);
919 assert_eq!(stats.changes_by_type.get("DomainAdded"), Some(&1));
920 assert_eq!(stats.changes_by_type.get("DomainModified"), Some(&1));
921 assert_eq!(stats.changes_by_type.get("PredicateAdded"), Some(&1));
922 }
923
924 #[test]
925 fn test_incremental_validation_no_changes() {
926 let table = SymbolTable::new();
927 let tracker = ChangeTracker::new();
928
929 let mut validator = IncrementalValidator::new(&table, &tracker);
930 let report = validator.validate_incremental().unwrap();
931
932 assert!(report.is_valid());
933 assert_eq!(report.components_validated, 0);
934 }
935}