1use std::collections::{HashMap, HashSet, VecDeque};
63
64#[inline]
69pub fn xorshift64(state: &mut u64) -> u64 {
70 let mut x = *state;
71 x ^= x << 13;
72 x ^= x >> 7;
73 x ^= x << 17;
74 *state = x;
75 x
76}
77
78#[derive(Debug, Clone, PartialEq)]
82pub struct LogicRule {
83 pub id: String,
85 pub head: String,
87 pub body: Vec<String>,
90 pub priority: i32,
92 pub source: String,
94 pub is_defeasible: bool,
96}
97
98impl LogicRule {
99 #[inline]
101 pub fn positive_body(&self) -> impl Iterator<Item = &str> {
102 self.body
103 .iter()
104 .filter(|p| !p.starts_with("NOT:"))
105 .map(String::as_str)
106 }
107
108 #[inline]
110 pub fn negated_body(&self) -> impl Iterator<Item = &str> {
111 self.body
112 .iter()
113 .filter(|p| p.starts_with("NOT:"))
114 .map(|p| &p["NOT:".len()..])
115 }
116}
117
118#[derive(Debug, Clone, PartialEq)]
122pub enum ConflictType {
123 DirectContradiction {
125 rule_a: String,
127 rule_b: String,
129 },
130 PriorityConflict {
132 higher: String,
134 lower: String,
136 },
137 CyclicDependency {
139 cycle: Vec<String>,
141 },
142 UndercutConflict {
144 undercutter: String,
146 undercut: String,
148 },
149 RebuttalConflict {
151 rule_a: String,
153 rule_b: String,
155 },
156}
157
158#[derive(Debug, Clone, PartialEq)]
162pub enum ResolutionStrategy {
163 PriorityOrder,
165 Specificity,
167 LastWriter,
169 Inhibit,
171 Merge(String),
173 AskOracle,
175}
176
177#[derive(Debug, Clone)]
181pub struct ConflictRecord {
182 pub conflict_type: ConflictType,
184 pub detected_at: u64,
187 pub resolved: bool,
189 pub resolution: Option<ResolutionStrategy>,
191 pub winning_rule: Option<String>,
193}
194
195impl ConflictRecord {
196 fn new(conflict_type: ConflictType, timestamp: u64) -> Self {
197 Self {
198 conflict_type,
199 detected_at: timestamp,
200 resolved: false,
201 resolution: None,
202 winning_rule: None,
203 }
204 }
205}
206
207#[derive(Debug, Clone)]
211pub struct ResolverConfig {
212 pub default_strategy: ResolutionStrategy,
215 pub enable_cycle_detection: bool,
217 pub max_rules: usize,
219 pub specificity_weight: f64,
222 pub priority_weight: f64,
224}
225
226impl Default for ResolverConfig {
227 fn default() -> Self {
228 Self {
229 default_strategy: ResolutionStrategy::PriorityOrder,
230 enable_cycle_detection: true,
231 max_rules: 10_000,
232 specificity_weight: 0.5,
233 priority_weight: 0.5,
234 }
235 }
236}
237
238#[derive(Debug, Clone, Default)]
242pub struct ResolverStats {
243 pub rules_loaded: usize,
245 pub conflicts_detected: usize,
247 pub conflicts_resolved: usize,
249 pub cycles_found: usize,
251 pub unresolved_conflicts: usize,
253}
254
255#[derive(Debug, Clone, PartialEq, thiserror::Error)]
259pub enum ResolverError {
260 #[error("rule not found: {0}")]
262 RuleNotFound(String),
263
264 #[error("unresolvable conflict between '{rule_a}' and '{rule_b}'")]
266 UnresolvableConflict {
267 rule_a: String,
269 rule_b: String,
271 },
272
273 #[error("cyclic dependency: {0:?}")]
275 CyclicDependency(Vec<String>),
276
277 #[error("configuration error: {0}")]
279 ConfigurationError(String),
280
281 #[error("max rules exceeded")]
283 MaxRulesExceeded,
284}
285
286#[derive(Debug, Clone)]
290struct RuleEntry {
291 rule: LogicRule,
292 insertion_order: usize,
294}
295
296pub struct RuleConflictResolver {
302 rules: HashMap<String, RuleEntry>,
304 clock: u64,
306 config: ResolverConfig,
308 stats: ResolverStats,
310}
311
312impl RuleConflictResolver {
313 pub fn new(config: ResolverConfig) -> Self {
315 Self {
316 rules: HashMap::new(),
317 clock: 0,
318 config,
319 stats: ResolverStats::default(),
320 }
321 }
322
323 #[inline]
325 fn tick(&mut self) -> u64 {
326 self.clock += 1;
327 self.clock
328 }
329
330 pub fn add_rule(&mut self, rule: LogicRule) -> Result<(), ResolverError> {
342 if self.rules.len() >= self.config.max_rules {
343 return Err(ResolverError::MaxRulesExceeded);
344 }
345
346 let insertion_order = self.rules.len();
347 let ts = self.tick();
348
349 let same_head: Vec<String> = self
351 .rules
352 .values()
353 .filter(|e| e.rule.head == rule.head)
354 .map(|e| e.rule.id.clone())
355 .collect();
356
357 for existing_id in same_head {
358 let existing_body: HashSet<&str> = self.rules[&existing_id]
359 .rule
360 .body
361 .iter()
362 .map(String::as_str)
363 .collect();
364 let new_body: HashSet<&str> = rule.body.iter().map(String::as_str).collect();
365
366 if existing_body.is_disjoint(&new_body) {
367 self.stats.conflicts_detected += 1;
368 let _ = ts; }
370 }
371
372 self.rules.insert(
373 rule.id.clone(),
374 RuleEntry {
375 rule,
376 insertion_order,
377 },
378 );
379
380 self.stats.rules_loaded = self.rules.len();
381 Ok(())
382 }
383
384 pub fn remove_rule(&mut self, id: &str) -> Result<(), ResolverError> {
390 if self.rules.remove(id).is_none() {
391 return Err(ResolverError::RuleNotFound(id.to_string()));
392 }
393 self.stats.rules_loaded = self.rules.len();
394 Ok(())
395 }
396
397 pub fn detect_conflicts(&mut self) -> Vec<ConflictRecord> {
408 let rule_snapshots: Vec<LogicRule> = self.rules.values().map(|e| e.rule.clone()).collect();
411
412 let mut pending_types: Vec<ConflictType> = Vec::new();
413
414 for i in 0..rule_snapshots.len() {
416 for j in (i + 1)..rule_snapshots.len() {
417 let a = &rule_snapshots[i];
418 let b = &rule_snapshots[j];
419
420 if a.head != b.head {
421 continue;
422 }
423
424 let body_a: HashSet<&str> = a.body.iter().map(String::as_str).collect();
425 let body_b: HashSet<&str> = b.body.iter().map(String::as_str).collect();
426
427 if body_a.is_disjoint(&body_b) {
429 pending_types.push(ConflictType::DirectContradiction {
430 rule_a: a.id.clone(),
431 rule_b: b.id.clone(),
432 });
433 }
434
435 if a.priority != b.priority {
437 let (higher, lower) = if a.priority > b.priority {
438 (a.id.clone(), b.id.clone())
439 } else {
440 (b.id.clone(), a.id.clone())
441 };
442 pending_types.push(ConflictType::PriorityConflict { higher, lower });
443 }
444 }
445 }
446
447 if self.config.enable_cycle_detection {
449 let cycles = self.detect_cycles();
450 let cycle_count = cycles.len();
451 for cycle in cycles {
452 pending_types.push(ConflictType::CyclicDependency { cycle });
453 }
454 self.stats.cycles_found += cycle_count;
455 }
456
457 for a in &rule_snapshots {
460 let negated_head = format!("NOT:{}", a.head);
461 for b in &rule_snapshots {
462 if a.id == b.id {
463 continue;
464 }
465 if b.body.contains(&negated_head) {
466 pending_types.push(ConflictType::UndercutConflict {
467 undercutter: a.id.clone(),
468 undercut: b.id.clone(),
469 });
470 }
471 }
472 }
473
474 for i in 0..rule_snapshots.len() {
477 for j in (i + 1)..rule_snapshots.len() {
478 let a = &rule_snapshots[i];
479 let b = &rule_snapshots[j];
480
481 let a_negates_b = a.head == format!("NOT:{}", b.head);
482 let b_negates_a = b.head == format!("NOT:{}", a.head);
483
484 if a_negates_b || b_negates_a {
485 pending_types.push(ConflictType::RebuttalConflict {
486 rule_a: a.id.clone(),
487 rule_b: b.id.clone(),
488 });
489 }
490 }
491 }
492
493 let records: Vec<ConflictRecord> = pending_types
495 .into_iter()
496 .map(|ct| {
497 let ts = self.tick();
498 ConflictRecord::new(ct, ts)
499 })
500 .collect();
501
502 let new_conflicts = records.len();
504 self.stats.conflicts_detected += new_conflicts;
505 self.stats.unresolved_conflicts += new_conflicts;
506
507 records
508 }
509
510 fn detect_cycles(&self) -> Vec<Vec<String>> {
515 let all_heads: HashSet<&str> = self.rules.values().map(|e| e.rule.head.as_str()).collect();
522
523 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
525 for entry in self.rules.values() {
526 for premise in entry.rule.positive_body() {
527 if all_heads.contains(premise) {
528 adj.entry(premise)
529 .or_default()
530 .push(entry.rule.head.as_str());
531 }
532 }
533 }
534
535 let mut found_cycles: Vec<Vec<String>> = Vec::new();
537 let mut globally_visited: HashSet<&str> = HashSet::new();
538
539 for start in all_heads.iter().copied() {
540 if globally_visited.contains(start) {
541 continue;
542 }
543
544 let mut stack: VecDeque<(&str, Vec<&str>, HashSet<&str>)> = VecDeque::new();
546 let mut path_visited: HashSet<&str> = HashSet::new();
547 path_visited.insert(start);
548 stack.push_back((start, vec![start], path_visited));
549
550 while let Some((node, path, mut visited)) = stack.pop_back() {
551 globally_visited.insert(node);
552
553 if let Some(neighbors) = adj.get(node) {
554 for &neighbor in neighbors {
555 if neighbor == start {
556 let mut cycle: Vec<String> =
558 path.iter().map(|s| s.to_string()).collect();
559 cycle.push(start.to_string());
560 found_cycles.push(cycle);
561 } else if !visited.contains(neighbor) {
562 let mut new_path = path.clone();
563 new_path.push(neighbor);
564 let mut new_visited = visited.clone();
565 new_visited.insert(neighbor);
566 stack.push_back((neighbor, new_path, new_visited));
567 }
568 }
569 }
570
571 for n in &path {
573 visited.insert(n);
574 }
575 }
576 }
577
578 found_cycles
579 }
580
581 pub fn resolve(&mut self, conflict: &ConflictRecord) -> Result<String, ResolverError> {
596 let strategy = self.config.default_strategy.clone();
597 let winner = self.apply_strategy(conflict, &strategy)?;
598 self.stats.conflicts_resolved += 1;
599 if self.stats.unresolved_conflicts > 0 {
600 self.stats.unresolved_conflicts -= 1;
601 }
602 Ok(winner)
603 }
604
605 pub fn resolve_all(&mut self) -> Vec<(ConflictRecord, Result<String, ResolverError>)> {
608 let conflicts = self.detect_conflicts();
609 let mut results = Vec::with_capacity(conflicts.len());
610 for conflict in conflicts {
611 let res = {
612 let strategy = self.config.default_strategy.clone();
613 self.apply_strategy(&conflict, &strategy)
614 };
615 if res.is_ok() {
616 self.stats.conflicts_resolved += 1;
617 if self.stats.unresolved_conflicts > 0 {
618 self.stats.unresolved_conflicts -= 1;
619 }
620 }
621 results.push((conflict, res));
622 }
623 results
624 }
625
626 fn apply_strategy(
628 &self,
629 conflict: &ConflictRecord,
630 strategy: &ResolutionStrategy,
631 ) -> Result<String, ResolverError> {
632 match strategy {
633 ResolutionStrategy::AskOracle => {
634 let (a, b) = self.conflict_pair_ids(&conflict.conflict_type)?;
635 Err(ResolverError::UnresolvableConflict {
636 rule_a: a,
637 rule_b: b,
638 })
639 }
640
641 ResolutionStrategy::Merge(_op) => {
642 let (a, b) = self.conflict_pair_ids(&conflict.conflict_type)?;
644 Err(ResolverError::UnresolvableConflict {
645 rule_a: a,
646 rule_b: b,
647 })
648 }
649
650 ResolutionStrategy::PriorityOrder => {
651 let (id_a, id_b) = self.conflict_pair_ids(&conflict.conflict_type)?;
652 let entry_a = self
653 .rules
654 .get(&id_a)
655 .ok_or_else(|| ResolverError::RuleNotFound(id_a.clone()))?;
656 let entry_b = self
657 .rules
658 .get(&id_b)
659 .ok_or_else(|| ResolverError::RuleNotFound(id_b.clone()))?;
660 Ok(if entry_a.rule.priority >= entry_b.rule.priority {
661 id_a
662 } else {
663 id_b
664 })
665 }
666
667 ResolutionStrategy::Specificity => {
668 let (id_a, id_b) = self.conflict_pair_ids(&conflict.conflict_type)?;
669 let entry_a = self
670 .rules
671 .get(&id_a)
672 .ok_or_else(|| ResolverError::RuleNotFound(id_a.clone()))?;
673 let entry_b = self
674 .rules
675 .get(&id_b)
676 .ok_or_else(|| ResolverError::RuleNotFound(id_b.clone()))?;
677 Ok(if entry_a.rule.body.len() >= entry_b.rule.body.len() {
678 id_a
679 } else {
680 id_b
681 })
682 }
683
684 ResolutionStrategy::LastWriter => {
685 let (id_a, id_b) = self.conflict_pair_ids(&conflict.conflict_type)?;
686 let entry_a = self
687 .rules
688 .get(&id_a)
689 .ok_or_else(|| ResolverError::RuleNotFound(id_a.clone()))?;
690 let entry_b = self
691 .rules
692 .get(&id_b)
693 .ok_or_else(|| ResolverError::RuleNotFound(id_b.clone()))?;
694 Ok(if entry_a.insertion_order >= entry_b.insertion_order {
695 id_a
696 } else {
697 id_b
698 })
699 }
700
701 ResolutionStrategy::Inhibit => {
702 let (id_a, id_b) = self.conflict_pair_ids(&conflict.conflict_type)?;
703 let entry_a = self
704 .rules
705 .get(&id_a)
706 .ok_or_else(|| ResolverError::RuleNotFound(id_a.clone()))?;
707 let entry_b = self
708 .rules
709 .get(&id_b)
710 .ok_or_else(|| ResolverError::RuleNotFound(id_b.clone()))?;
711
712 let a_defeasible = entry_a.rule.is_defeasible;
713 let b_defeasible = entry_b.rule.is_defeasible;
714
715 let winner = match (a_defeasible, b_defeasible) {
716 (true, false) => id_b.clone(),
717 (false, true) => id_a.clone(),
718 _ => {
720 if entry_a.rule.priority >= entry_b.rule.priority {
721 id_a.clone()
722 } else {
723 id_b.clone()
724 }
725 }
726 };
727 Ok(winner)
728 }
729 }
730 }
731
732 fn conflict_pair_ids(&self, ct: &ConflictType) -> Result<(String, String), ResolverError> {
737 match ct {
738 ConflictType::DirectContradiction { rule_a, rule_b } => {
739 Ok((rule_a.clone(), rule_b.clone()))
740 }
741 ConflictType::PriorityConflict { higher, lower } => Ok((higher.clone(), lower.clone())),
742 ConflictType::CyclicDependency { cycle } => {
743 if cycle.len() < 2 {
744 return Err(ResolverError::CyclicDependency(cycle.clone()));
745 }
746 Err(ResolverError::CyclicDependency(cycle.clone()))
747 }
748 ConflictType::UndercutConflict {
749 undercutter,
750 undercut,
751 } => Ok((undercutter.clone(), undercut.clone())),
752 ConflictType::RebuttalConflict { rule_a, rule_b } => {
753 Ok((rule_a.clone(), rule_b.clone()))
754 }
755 }
756 }
757
758 pub fn applicable_rules<'a>(&'a self, facts: &[String]) -> Vec<&'a LogicRule> {
765 let fact_set: HashSet<&str> = facts.iter().map(String::as_str).collect();
766 let mut result: Vec<&'a LogicRule> = self
767 .rules
768 .values()
769 .filter(|entry| entry.rule.positive_body().all(|p| fact_set.contains(p)))
770 .map(|entry| &entry.rule)
771 .collect();
772 result.sort_by_key(|r| self.rules.get(&r.id).map_or(0, |e| e.insertion_order));
774 result
775 }
776
777 pub fn winning_rule<'a>(&'a self, head: &str, facts: &[String]) -> Option<&'a LogicRule> {
782 let candidates: Vec<&'a LogicRule> = self
783 .applicable_rules(facts)
784 .into_iter()
785 .filter(|r| r.head == head)
786 .collect();
787
788 if candidates.is_empty() {
789 return None;
790 }
791
792 let winner = match &self.config.default_strategy {
793 ResolutionStrategy::PriorityOrder => candidates.into_iter().max_by_key(|r| r.priority),
794
795 ResolutionStrategy::Specificity => candidates.into_iter().max_by_key(|r| r.body.len()),
796
797 ResolutionStrategy::LastWriter => candidates
798 .into_iter()
799 .max_by_key(|r| self.rules.get(&r.id).map_or(0, |e| e.insertion_order)),
800
801 ResolutionStrategy::Inhibit => {
802 let non_def: Vec<&'a LogicRule> = candidates
804 .iter()
805 .copied()
806 .filter(|r| !r.is_defeasible)
807 .collect();
808 if !non_def.is_empty() {
809 non_def.into_iter().max_by_key(|r| r.priority)
810 } else {
811 candidates.into_iter().max_by_key(|r| r.priority)
812 }
813 }
814
815 ResolutionStrategy::AskOracle | ResolutionStrategy::Merge(_) => {
817 candidates.into_iter().max_by_key(|r| r.priority)
818 }
819 };
820
821 winner
822 }
823
824 pub fn stats(&self) -> ResolverStats {
828 self.stats.clone()
829 }
830}
831
832#[cfg(test)]
835mod tests {
836 use super::*;
837
838 fn make_rule(id: &str, head: &str, body: &[&str], priority: i32) -> LogicRule {
841 LogicRule {
842 id: id.to_string(),
843 head: head.to_string(),
844 body: body.iter().map(|s| s.to_string()).collect(),
845 priority,
846 source: "test".to_string(),
847 is_defeasible: false,
848 }
849 }
850
851 fn make_defeasible(id: &str, head: &str, body: &[&str], priority: i32) -> LogicRule {
852 LogicRule {
853 is_defeasible: true,
854 ..make_rule(id, head, body, priority)
855 }
856 }
857
858 fn default_resolver() -> RuleConflictResolver {
859 RuleConflictResolver::new(ResolverConfig::default())
860 }
861
862 #[test]
865 fn xorshift_produces_nonzero() {
866 let mut state = 12345u64;
867 let v = xorshift64(&mut state);
868 assert_ne!(v, 0);
869 }
870
871 #[test]
872 fn xorshift_sequence_differs() {
873 let mut state = 99u64;
874 let a = xorshift64(&mut state);
875 let b = xorshift64(&mut state);
876 assert_ne!(a, b);
877 }
878
879 #[test]
882 fn add_rule_success() {
883 let mut r = default_resolver();
884 let rule = make_rule("r1", "bird", &["has_wings", "feathers"], 10);
885 assert!(r.add_rule(rule).is_ok());
886 assert_eq!(r.stats().rules_loaded, 1);
887 }
888
889 #[test]
890 fn add_multiple_rules() {
891 let mut r = default_resolver();
892 for i in 0..5u32 {
893 let rule = make_rule(&format!("r{i}"), "head", &[&format!("body{i}")], i as i32);
894 assert!(r.add_rule(rule).is_ok());
895 }
896 assert_eq!(r.stats().rules_loaded, 5);
897 }
898
899 #[test]
900 fn add_rule_max_exceeded() {
901 let cfg = ResolverConfig {
902 max_rules: 2,
903 ..Default::default()
904 };
905 let mut r = RuleConflictResolver::new(cfg);
906 r.add_rule(make_rule("r1", "a", &["x"], 1))
907 .expect("test setup: add_rule should not fail");
908 r.add_rule(make_rule("r2", "b", &["y"], 1))
909 .expect("test setup: add_rule should not fail");
910 let err = r
911 .add_rule(make_rule("r3", "c", &["z"], 1))
912 .expect_err("test setup: expected MaxRulesExceeded error");
913 assert_eq!(err, ResolverError::MaxRulesExceeded);
914 }
915
916 #[test]
919 fn remove_existing_rule() {
920 let mut r = default_resolver();
921 r.add_rule(make_rule("r1", "bird", &["wings"], 10))
922 .expect("test setup: add_rule should not fail");
923 assert!(r.remove_rule("r1").is_ok());
924 assert_eq!(r.stats().rules_loaded, 0);
925 }
926
927 #[test]
928 fn remove_nonexistent_returns_error() {
929 let mut r = default_resolver();
930 let err = r
931 .remove_rule("ghost")
932 .expect_err("test setup: expected RuleNotFound error");
933 assert_eq!(err, ResolverError::RuleNotFound("ghost".to_string()));
934 }
935
936 #[test]
937 fn remove_reduces_count() {
938 let mut r = default_resolver();
939 r.add_rule(make_rule("r1", "a", &["x"], 1))
940 .expect("test setup: add_rule should not fail");
941 r.add_rule(make_rule("r2", "b", &["y"], 1))
942 .expect("test setup: add_rule should not fail");
943 r.remove_rule("r1")
944 .expect("test setup: remove_rule should not fail");
945 assert_eq!(r.stats().rules_loaded, 1);
946 }
947
948 #[test]
951 fn detect_direct_contradiction() {
952 let mut r = default_resolver();
953 r.add_rule(make_rule("r1", "bird", &["wings", "feathers"], 10))
954 .expect("test setup: add_rule should not fail");
955 r.add_rule(make_rule("r2", "bird", &["lays_eggs"], 5))
956 .expect("test setup: add_rule should not fail");
957 let conflicts = r.detect_conflicts();
958 let has_contradiction = conflicts
959 .iter()
960 .any(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }));
961 assert!(has_contradiction);
962 }
963
964 #[test]
965 fn no_contradiction_when_bodies_overlap() {
966 let mut r = default_resolver();
967 r.add_rule(make_rule("r1", "bird", &["vertebrate", "wings"], 10))
969 .expect("test setup: add_rule should not fail");
970 r.add_rule(make_rule("r2", "bird", &["vertebrate", "feathers"], 5))
971 .expect("test setup: add_rule should not fail");
972 let conflicts = r.detect_conflicts();
973 let has_contradiction = conflicts
974 .iter()
975 .any(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }));
976 assert!(!has_contradiction);
977 }
978
979 #[test]
980 fn contradiction_ids_are_correct() {
981 let mut r = default_resolver();
982 r.add_rule(make_rule("alpha", "P", &["A"], 1))
983 .expect("test setup: add_rule should not fail");
984 r.add_rule(make_rule("beta", "P", &["B"], 1))
985 .expect("test setup: add_rule should not fail");
986 let conflicts = r.detect_conflicts();
987 let ct = conflicts
988 .iter()
989 .find_map(|c| {
990 if let ConflictType::DirectContradiction { rule_a, rule_b } = &c.conflict_type {
991 Some((rule_a.clone(), rule_b.clone()))
992 } else {
993 None
994 }
995 })
996 .expect("should have contradiction");
997 let ids: HashSet<String> = [ct.0, ct.1].into_iter().collect();
998 assert!(ids.contains("alpha"));
999 assert!(ids.contains("beta"));
1000 }
1001
1002 #[test]
1005 fn detect_priority_conflict() {
1006 let mut r = default_resolver();
1007 r.add_rule(make_rule("r1", "q", &["a"], 10))
1008 .expect("test setup: add_rule should not fail");
1009 r.add_rule(make_rule("r2", "q", &["b"], 5))
1010 .expect("test setup: add_rule should not fail");
1011 let conflicts = r.detect_conflicts();
1012 let has_prio = conflicts
1013 .iter()
1014 .any(|c| matches!(&c.conflict_type, ConflictType::PriorityConflict { .. }));
1015 assert!(has_prio);
1016 }
1017
1018 #[test]
1019 fn no_priority_conflict_when_same_priority() {
1020 let mut r = default_resolver();
1021 r.add_rule(make_rule("r1", "q", &["a"], 7))
1023 .expect("test setup: add_rule should not fail");
1024 r.add_rule(make_rule("r2", "q", &["b"], 7))
1025 .expect("test setup: add_rule should not fail");
1026 let conflicts = r.detect_conflicts();
1027 let has_prio = conflicts
1028 .iter()
1029 .any(|c| matches!(&c.conflict_type, ConflictType::PriorityConflict { .. }));
1030 assert!(!has_prio);
1031 }
1032
1033 #[test]
1034 fn priority_conflict_higher_lower_correct() {
1035 let mut r = default_resolver();
1036 r.add_rule(make_rule("lo", "q", &["a"], 2))
1037 .expect("test setup: add_rule should not fail");
1038 r.add_rule(make_rule("hi", "q", &["b"], 9))
1039 .expect("test setup: add_rule should not fail");
1040 let conflicts = r.detect_conflicts();
1041 let prio = conflicts
1042 .iter()
1043 .find_map(|c| {
1044 if let ConflictType::PriorityConflict { higher, lower } = &c.conflict_type {
1045 Some((higher.clone(), lower.clone()))
1046 } else {
1047 None
1048 }
1049 })
1050 .expect("should have priority conflict");
1051 assert_eq!(prio.0, "hi");
1052 assert_eq!(prio.1, "lo");
1053 }
1054
1055 #[test]
1058 fn detect_simple_cycle() {
1059 let mut r = default_resolver();
1060 r.add_rule(make_rule("r_ab", "B", &["A"], 1))
1062 .expect("test setup: add_rule should not fail");
1063 r.add_rule(make_rule("r_ba", "A", &["B"], 1))
1064 .expect("test setup: add_rule should not fail");
1065 let conflicts = r.detect_conflicts();
1066 let has_cycle = conflicts
1067 .iter()
1068 .any(|c| matches!(&c.conflict_type, ConflictType::CyclicDependency { .. }));
1069 assert!(has_cycle);
1070 }
1071
1072 #[test]
1073 fn no_cycle_in_dag() {
1074 let mut r = default_resolver();
1075 r.add_rule(make_rule("r1", "B", &["A"], 1))
1077 .expect("test setup: add_rule should not fail");
1078 r.add_rule(make_rule("r2", "C", &["B"], 1))
1079 .expect("test setup: add_rule should not fail");
1080 let conflicts = r.detect_conflicts();
1081 let has_cycle = conflicts
1082 .iter()
1083 .any(|c| matches!(&c.conflict_type, ConflictType::CyclicDependency { .. }));
1084 assert!(!has_cycle);
1085 }
1086
1087 #[test]
1088 fn cycle_detection_disabled() {
1089 let cfg = ResolverConfig {
1090 enable_cycle_detection: false,
1091 ..Default::default()
1092 };
1093 let mut r = RuleConflictResolver::new(cfg);
1094 r.add_rule(make_rule("r_ab", "B", &["A"], 1))
1095 .expect("test setup: add_rule should not fail");
1096 r.add_rule(make_rule("r_ba", "A", &["B"], 1))
1097 .expect("test setup: add_rule should not fail");
1098 let conflicts = r.detect_conflicts();
1099 let has_cycle = conflicts
1100 .iter()
1101 .any(|c| matches!(&c.conflict_type, ConflictType::CyclicDependency { .. }));
1102 assert!(!has_cycle);
1103 }
1104
1105 #[test]
1106 fn three_node_cycle() {
1107 let mut r = default_resolver();
1108 r.add_rule(make_rule("ab", "B", &["A"], 1))
1109 .expect("test setup: add_rule should not fail");
1110 r.add_rule(make_rule("bc", "C", &["B"], 1))
1111 .expect("test setup: add_rule should not fail");
1112 r.add_rule(make_rule("ca", "A", &["C"], 1))
1113 .expect("test setup: add_rule should not fail");
1114 let conflicts = r.detect_conflicts();
1115 let has_cycle = conflicts
1116 .iter()
1117 .any(|c| matches!(&c.conflict_type, ConflictType::CyclicDependency { .. }));
1118 assert!(has_cycle);
1119 }
1120
1121 #[test]
1124 fn detect_undercut_conflict() {
1125 let mut r = default_resolver();
1126 r.add_rule(make_rule("r1", "flies", &["has_wings"], 10))
1128 .expect("test setup: add_rule should not fail");
1129 r.add_rule(make_rule("r2", "penguin", &["NOT:flies", "lays_eggs"], 5))
1130 .expect("test setup: add_rule should not fail");
1131 let conflicts = r.detect_conflicts();
1132 let has_undercut = conflicts
1133 .iter()
1134 .any(|c| matches!(&c.conflict_type, ConflictType::UndercutConflict { .. }));
1135 assert!(has_undercut);
1136 }
1137
1138 #[test]
1139 fn undercut_ids_correct() {
1140 let mut r = default_resolver();
1141 r.add_rule(make_rule("cutter", "X", &["p"], 5))
1142 .expect("test setup: add_rule should not fail");
1143 r.add_rule(make_rule("victim", "Y", &["NOT:X", "q"], 5))
1144 .expect("test setup: add_rule should not fail");
1145 let conflicts = r.detect_conflicts();
1146 let uc = conflicts
1147 .iter()
1148 .find_map(|c| {
1149 if let ConflictType::UndercutConflict {
1150 undercutter,
1151 undercut,
1152 } = &c.conflict_type
1153 {
1154 Some((undercutter.clone(), undercut.clone()))
1155 } else {
1156 None
1157 }
1158 })
1159 .expect("should have undercut conflict");
1160 assert_eq!(uc.0, "cutter");
1161 assert_eq!(uc.1, "victim");
1162 }
1163
1164 #[test]
1165 fn no_undercut_without_not_prefix() {
1166 let mut r = default_resolver();
1167 r.add_rule(make_rule("r1", "flies", &["wings"], 1))
1168 .expect("test setup: add_rule should not fail");
1169 r.add_rule(make_rule("r2", "bird", &["flies", "feathers"], 1))
1171 .expect("test setup: add_rule should not fail");
1172 let conflicts = r.detect_conflicts();
1173 let has_undercut = conflicts
1174 .iter()
1175 .any(|c| matches!(&c.conflict_type, ConflictType::UndercutConflict { .. }));
1176 assert!(!has_undercut);
1177 }
1178
1179 #[test]
1182 fn detect_rebuttal_conflict() {
1183 let mut r = default_resolver();
1184 r.add_rule(make_rule("r1", "bird", &["wings"], 5))
1185 .expect("test setup: add_rule should not fail");
1186 r.add_rule(make_rule("r2", "NOT:bird", &["penguin"], 5))
1187 .expect("test setup: add_rule should not fail");
1188 let conflicts = r.detect_conflicts();
1189 let has_rebuttal = conflicts
1190 .iter()
1191 .any(|c| matches!(&c.conflict_type, ConflictType::RebuttalConflict { .. }));
1192 assert!(has_rebuttal);
1193 }
1194
1195 #[test]
1196 fn rebuttal_ids_correct() {
1197 let mut r = default_resolver();
1198 r.add_rule(make_rule("pos", "alive", &["moving"], 1))
1199 .expect("test setup: add_rule should not fail");
1200 r.add_rule(make_rule("neg", "NOT:alive", &["still"], 1))
1201 .expect("test setup: add_rule should not fail");
1202 let conflicts = r.detect_conflicts();
1203 let rb = conflicts
1204 .iter()
1205 .find_map(|c| {
1206 if let ConflictType::RebuttalConflict { rule_a, rule_b } = &c.conflict_type {
1207 Some((rule_a.clone(), rule_b.clone()))
1208 } else {
1209 None
1210 }
1211 })
1212 .expect("should have rebuttal conflict");
1213 let ids: HashSet<String> = [rb.0, rb.1].into_iter().collect();
1214 assert!(ids.contains("pos"));
1215 assert!(ids.contains("neg"));
1216 }
1217
1218 #[test]
1219 fn no_rebuttal_without_not_head() {
1220 let mut r = default_resolver();
1221 r.add_rule(make_rule("r1", "bird", &["wings"], 1))
1222 .expect("test setup: add_rule should not fail");
1223 r.add_rule(make_rule("r2", "reptile", &["scales"], 1))
1224 .expect("test setup: add_rule should not fail");
1225 let conflicts = r.detect_conflicts();
1226 let has_rebuttal = conflicts
1227 .iter()
1228 .any(|c| matches!(&c.conflict_type, ConflictType::RebuttalConflict { .. }));
1229 assert!(!has_rebuttal);
1230 }
1231
1232 #[test]
1235 fn resolve_priority_order_higher_wins() {
1236 let cfg = ResolverConfig {
1237 default_strategy: ResolutionStrategy::PriorityOrder,
1238 ..Default::default()
1239 };
1240 let mut r = RuleConflictResolver::new(cfg);
1241 r.add_rule(make_rule("lo", "q", &["a"], 3))
1242 .expect("test setup: add_rule should not fail");
1243 r.add_rule(make_rule("hi", "q", &["b"], 9))
1244 .expect("test setup: add_rule should not fail");
1245 let mut conflicts = r.detect_conflicts();
1246 let prio_conflict = conflicts
1247 .iter_mut()
1248 .find(|c| matches!(&c.conflict_type, ConflictType::PriorityConflict { .. }))
1249 .cloned()
1250 .expect("must find priority conflict");
1251 let winner = r
1252 .resolve(&prio_conflict)
1253 .expect("test setup: resolve should succeed");
1254 assert_eq!(winner, "hi");
1255 }
1256
1257 #[test]
1258 fn resolve_priority_order_equal_returns_first_id() {
1259 let cfg = ResolverConfig {
1260 default_strategy: ResolutionStrategy::PriorityOrder,
1261 ..Default::default()
1262 };
1263 let mut r = RuleConflictResolver::new(cfg);
1264 r.add_rule(make_rule("r1", "q", &["a"], 5))
1265 .expect("test setup: add_rule should not fail");
1266 r.add_rule(make_rule("r2", "q", &["b"], 5))
1267 .expect("test setup: add_rule should not fail");
1268 let mut conflicts = r.detect_conflicts();
1269 let dc = conflicts
1271 .iter_mut()
1272 .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1273 .cloned()
1274 .expect("must find contradiction");
1275 let winner = r.resolve(&dc).expect("test setup: resolve should succeed");
1276 assert!(winner == "r1" || winner == "r2");
1278 }
1279
1280 #[test]
1283 fn resolve_specificity_more_conditions_wins() {
1284 let cfg = ResolverConfig {
1285 default_strategy: ResolutionStrategy::Specificity,
1286 ..Default::default()
1287 };
1288 let mut r = RuleConflictResolver::new(cfg);
1289 r.add_rule(make_rule("general", "bird", &["flies"], 1))
1292 .expect("test setup: add_rule should not fail");
1293 r.add_rule(make_rule(
1294 "specific",
1295 "bird",
1296 &["lays_eggs", "warm_blooded", "beak"],
1297 1,
1298 ))
1299 .expect("test setup: add_rule should not fail");
1300 let mut conflicts = r.detect_conflicts();
1301 let dc = conflicts
1302 .iter_mut()
1303 .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1304 .cloned()
1305 .expect("test setup: should find DirectContradiction conflict");
1306 let winner = r.resolve(&dc).expect("test setup: resolve should succeed");
1307 assert_eq!(winner, "specific");
1308 }
1309
1310 #[test]
1313 fn resolve_last_writer_newer_wins() {
1314 let cfg = ResolverConfig {
1315 default_strategy: ResolutionStrategy::LastWriter,
1316 ..Default::default()
1317 };
1318 let mut r = RuleConflictResolver::new(cfg);
1319 r.add_rule(make_rule("old", "q", &["a"], 5))
1320 .expect("test setup: add_rule should not fail");
1321 r.add_rule(make_rule("new", "q", &["b"], 5))
1322 .expect("test setup: add_rule should not fail");
1323 let mut conflicts = r.detect_conflicts();
1324 let dc = conflicts
1325 .iter_mut()
1326 .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1327 .cloned()
1328 .expect("test setup: should find DirectContradiction conflict");
1329 let winner = r.resolve(&dc).expect("test setup: resolve should succeed");
1330 assert_eq!(winner, "new");
1331 }
1332
1333 #[test]
1336 fn resolve_inhibit_non_defeasible_wins() {
1337 let cfg = ResolverConfig {
1338 default_strategy: ResolutionStrategy::Inhibit,
1339 ..Default::default()
1340 };
1341 let mut r = RuleConflictResolver::new(cfg);
1342 r.add_rule(make_defeasible("def", "q", &["a"], 10))
1343 .expect("test setup: add_rule should not fail");
1344 r.add_rule(make_rule("firm", "q", &["b"], 1))
1345 .expect("test setup: add_rule should not fail");
1346 let mut conflicts = r.detect_conflicts();
1347 let dc = conflicts
1348 .iter_mut()
1349 .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1350 .cloned()
1351 .expect("test setup: should find DirectContradiction conflict");
1352 let winner = r.resolve(&dc).expect("test setup: resolve should succeed");
1353 assert_eq!(winner, "firm");
1354 }
1355
1356 #[test]
1357 fn resolve_inhibit_both_defeasible_uses_priority() {
1358 let cfg = ResolverConfig {
1359 default_strategy: ResolutionStrategy::Inhibit,
1360 ..Default::default()
1361 };
1362 let mut r = RuleConflictResolver::new(cfg);
1363 r.add_rule(make_defeasible("lo", "q", &["a"], 2))
1364 .expect("test setup: add_rule should not fail");
1365 r.add_rule(make_defeasible("hi", "q", &["b"], 9))
1366 .expect("test setup: add_rule should not fail");
1367 let mut conflicts = r.detect_conflicts();
1368 let prio = conflicts
1369 .iter_mut()
1370 .find(|c| matches!(&c.conflict_type, ConflictType::PriorityConflict { .. }))
1371 .cloned()
1372 .expect("test setup: should find PriorityConflict conflict");
1373 let winner = r
1374 .resolve(&prio)
1375 .expect("test setup: resolve should succeed");
1376 assert_eq!(winner, "hi");
1377 }
1378
1379 #[test]
1380 fn resolve_inhibit_neither_defeasible_uses_priority() {
1381 let cfg = ResolverConfig {
1382 default_strategy: ResolutionStrategy::Inhibit,
1383 ..Default::default()
1384 };
1385 let mut r = RuleConflictResolver::new(cfg);
1386 r.add_rule(make_rule("lo", "q", &["a"], 2))
1387 .expect("test setup: add_rule should not fail");
1388 r.add_rule(make_rule("hi", "q", &["b"], 9))
1389 .expect("test setup: add_rule should not fail");
1390 let mut conflicts = r.detect_conflicts();
1391 let prio = conflicts
1392 .iter_mut()
1393 .find(|c| matches!(&c.conflict_type, ConflictType::PriorityConflict { .. }))
1394 .cloned()
1395 .expect("test setup: should find PriorityConflict conflict");
1396 let winner = r
1397 .resolve(&prio)
1398 .expect("test setup: resolve should succeed");
1399 assert_eq!(winner, "hi");
1400 }
1401
1402 #[test]
1405 fn resolve_ask_oracle_returns_error() {
1406 let cfg = ResolverConfig {
1407 default_strategy: ResolutionStrategy::AskOracle,
1408 ..Default::default()
1409 };
1410 let mut r = RuleConflictResolver::new(cfg);
1411 r.add_rule(make_rule("r1", "q", &["a"], 1))
1412 .expect("test setup: add_rule should not fail");
1413 r.add_rule(make_rule("r2", "q", &["b"], 1))
1414 .expect("test setup: add_rule should not fail");
1415 let mut conflicts = r.detect_conflicts();
1416 let dc = conflicts
1417 .iter_mut()
1418 .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1419 .cloned()
1420 .expect("test setup: should find DirectContradiction conflict");
1421 let err = r
1422 .resolve(&dc)
1423 .expect_err("test setup: expected UnresolvableConflict error");
1424 assert!(matches!(err, ResolverError::UnresolvableConflict { .. }));
1425 }
1426
1427 #[test]
1430 fn resolve_merge_returns_error() {
1431 let cfg = ResolverConfig {
1432 default_strategy: ResolutionStrategy::Merge("union".to_string()),
1433 ..Default::default()
1434 };
1435 let mut r = RuleConflictResolver::new(cfg);
1436 r.add_rule(make_rule("r1", "q", &["a"], 1))
1437 .expect("test setup: add_rule should not fail");
1438 r.add_rule(make_rule("r2", "q", &["b"], 1))
1439 .expect("test setup: add_rule should not fail");
1440 let mut conflicts = r.detect_conflicts();
1441 let dc = conflicts
1442 .iter_mut()
1443 .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1444 .cloned()
1445 .expect("test setup: should find DirectContradiction conflict");
1446 let err = r
1447 .resolve(&dc)
1448 .expect_err("test setup: expected UnresolvableConflict error");
1449 assert!(matches!(err, ResolverError::UnresolvableConflict { .. }));
1450 }
1451
1452 #[test]
1455 fn resolve_all_returns_results_for_all_conflicts() {
1456 let mut r = default_resolver();
1457 r.add_rule(make_rule("r1", "q", &["a"], 10))
1458 .expect("test setup: add_rule should not fail");
1459 r.add_rule(make_rule("r2", "q", &["b"], 5))
1460 .expect("test setup: add_rule should not fail");
1461 let results = r.resolve_all();
1462 assert!(!results.is_empty());
1463 }
1464
1465 #[test]
1466 fn resolve_all_increments_resolved_stats() {
1467 let mut r = default_resolver();
1468 r.add_rule(make_rule("r1", "q", &["a"], 10))
1469 .expect("test setup: add_rule should not fail");
1470 r.add_rule(make_rule("r2", "q", &["b"], 5))
1471 .expect("test setup: add_rule should not fail");
1472 r.resolve_all();
1473 assert!(r.stats().conflicts_resolved > 0);
1474 }
1475
1476 #[test]
1479 fn applicable_rules_all_facts_present() {
1480 let mut r = default_resolver();
1481 r.add_rule(make_rule("r1", "bird", &["wings", "feathers"], 1))
1482 .expect("test setup: add_rule should not fail");
1483 let facts: Vec<String> = vec!["wings".to_string(), "feathers".to_string()];
1484 let applicable = r.applicable_rules(&facts);
1485 assert_eq!(applicable.len(), 1);
1486 assert_eq!(applicable[0].id, "r1");
1487 }
1488
1489 #[test]
1490 fn applicable_rules_missing_fact() {
1491 let mut r = default_resolver();
1492 r.add_rule(make_rule("r1", "bird", &["wings", "feathers"], 1))
1493 .expect("test setup: add_rule should not fail");
1494 let facts: Vec<String> = vec!["wings".to_string()]; let applicable = r.applicable_rules(&facts);
1496 assert!(applicable.is_empty());
1497 }
1498
1499 #[test]
1500 fn applicable_rules_ignores_negated_conditions() {
1501 let mut r = default_resolver();
1502 r.add_rule(make_rule(
1504 "r1",
1505 "mammal",
1506 &["warm_blooded", "NOT:has_gills"],
1507 1,
1508 ))
1509 .expect("test setup: add_rule should not fail");
1510 let facts: Vec<String> = vec!["warm_blooded".to_string()];
1511 let applicable = r.applicable_rules(&facts);
1513 assert_eq!(applicable.len(), 1);
1514 }
1515
1516 #[test]
1517 fn applicable_rules_empty_body_always_applicable() {
1518 let mut r = default_resolver();
1519 r.add_rule(make_rule("r_always", "axiom", &[], 1))
1520 .expect("test setup: add_rule should not fail");
1521 let facts: Vec<String> = vec![];
1522 let applicable = r.applicable_rules(&facts);
1523 assert_eq!(applicable.len(), 1);
1524 }
1525
1526 #[test]
1527 fn applicable_rules_multiple() {
1528 let mut r = default_resolver();
1529 r.add_rule(make_rule("r1", "a", &["x"], 1))
1530 .expect("test setup: add_rule should not fail");
1531 r.add_rule(make_rule("r2", "b", &["y"], 1))
1532 .expect("test setup: add_rule should not fail");
1533 r.add_rule(make_rule("r3", "c", &["x", "y"], 1))
1534 .expect("test setup: add_rule should not fail");
1535 let facts = vec!["x".to_string(), "y".to_string()];
1536 let applicable = r.applicable_rules(&facts);
1537 assert_eq!(applicable.len(), 3);
1538 }
1539
1540 #[test]
1543 fn winning_rule_priority_strategy() {
1544 let cfg = ResolverConfig {
1545 default_strategy: ResolutionStrategy::PriorityOrder,
1546 ..Default::default()
1547 };
1548 let mut r = RuleConflictResolver::new(cfg);
1549 r.add_rule(make_rule("lo", "q", &["a"], 2))
1550 .expect("test setup: add_rule should not fail");
1551 r.add_rule(make_rule("hi", "q", &["a"], 9))
1552 .expect("test setup: add_rule should not fail");
1553 let facts = vec!["a".to_string()];
1554 let winner = r.winning_rule("q", &facts).expect("must have winner");
1555 assert_eq!(winner.id, "hi");
1556 }
1557
1558 #[test]
1559 fn winning_rule_specificity_strategy() {
1560 let cfg = ResolverConfig {
1561 default_strategy: ResolutionStrategy::Specificity,
1562 ..Default::default()
1563 };
1564 let mut r = RuleConflictResolver::new(cfg);
1565 r.add_rule(make_rule("gen", "q", &["a"], 1))
1566 .expect("test setup: add_rule should not fail");
1567 r.add_rule(make_rule("spec", "q", &["a", "b", "c"], 1))
1568 .expect("test setup: add_rule should not fail");
1569 let facts = vec!["a".to_string(), "b".to_string(), "c".to_string()];
1570 let winner = r.winning_rule("q", &facts).expect("must have winner");
1571 assert_eq!(winner.id, "spec");
1572 }
1573
1574 #[test]
1575 fn winning_rule_last_writer_strategy() {
1576 let cfg = ResolverConfig {
1577 default_strategy: ResolutionStrategy::LastWriter,
1578 ..Default::default()
1579 };
1580 let mut r = RuleConflictResolver::new(cfg);
1581 r.add_rule(make_rule("old", "q", &["a"], 1))
1582 .expect("test setup: add_rule should not fail");
1583 r.add_rule(make_rule("new", "q", &["a"], 1))
1584 .expect("test setup: add_rule should not fail");
1585 let facts = vec!["a".to_string()];
1586 let winner = r.winning_rule("q", &facts).expect("must have winner");
1587 assert_eq!(winner.id, "new");
1588 }
1589
1590 #[test]
1591 fn winning_rule_none_when_no_applicable() {
1592 let mut r = default_resolver();
1593 r.add_rule(make_rule("r1", "q", &["missing_fact"], 1))
1594 .expect("test setup: add_rule should not fail");
1595 let facts: Vec<String> = vec![];
1596 assert!(r.winning_rule("q", &facts).is_none());
1597 }
1598
1599 #[test]
1600 fn winning_rule_none_when_wrong_head() {
1601 let mut r = default_resolver();
1602 r.add_rule(make_rule("r1", "q", &["a"], 1))
1603 .expect("test setup: add_rule should not fail");
1604 let facts = vec!["a".to_string()];
1605 assert!(r.winning_rule("unrelated_head", &facts).is_none());
1606 }
1607
1608 #[test]
1609 fn winning_rule_inhibit_non_defeasible_preferred() {
1610 let cfg = ResolverConfig {
1611 default_strategy: ResolutionStrategy::Inhibit,
1612 ..Default::default()
1613 };
1614 let mut r = RuleConflictResolver::new(cfg);
1615 r.add_rule(make_defeasible("soft", "q", &["a"], 99))
1616 .expect("test setup: add_rule should not fail");
1617 r.add_rule(make_rule("hard", "q", &["a"], 1))
1618 .expect("test setup: add_rule should not fail");
1619 let facts = vec!["a".to_string()];
1620 let winner = r.winning_rule("q", &facts).expect("must have winner");
1621 assert_eq!(winner.id, "hard");
1622 }
1623
1624 #[test]
1627 fn stats_rules_loaded_accurate() {
1628 let mut r = default_resolver();
1629 assert_eq!(r.stats().rules_loaded, 0);
1630 r.add_rule(make_rule("r1", "a", &["x"], 1))
1631 .expect("test setup: add_rule should not fail");
1632 assert_eq!(r.stats().rules_loaded, 1);
1633 r.remove_rule("r1")
1634 .expect("test setup: remove_rule should not fail");
1635 assert_eq!(r.stats().rules_loaded, 0);
1636 }
1637
1638 #[test]
1639 fn stats_conflicts_detected_increments() {
1640 let mut r = default_resolver();
1641 r.add_rule(make_rule("r1", "q", &["a"], 1))
1642 .expect("test setup: add_rule should not fail");
1643 r.add_rule(make_rule("r2", "q", &["b"], 1))
1644 .expect("test setup: add_rule should not fail");
1645 let before = r.stats().conflicts_detected;
1646 r.detect_conflicts();
1647 let after = r.stats().conflicts_detected;
1648 assert!(after > before);
1649 }
1650
1651 #[test]
1652 fn stats_cycles_found() {
1653 let mut r = default_resolver();
1654 r.add_rule(make_rule("ab", "B", &["A"], 1))
1655 .expect("test setup: add_rule should not fail");
1656 r.add_rule(make_rule("ba", "A", &["B"], 1))
1657 .expect("test setup: add_rule should not fail");
1658 r.detect_conflicts();
1659 assert!(r.stats().cycles_found > 0);
1660 }
1661
1662 #[test]
1663 fn stats_conflicts_resolved() {
1664 let mut r = default_resolver();
1665 r.add_rule(make_rule("r1", "q", &["a"], 5))
1666 .expect("test setup: add_rule should not fail");
1667 r.add_rule(make_rule("r2", "q", &["b"], 10))
1668 .expect("test setup: add_rule should not fail");
1669 r.resolve_all();
1670 assert!(r.stats().conflicts_resolved > 0);
1671 }
1672
1673 #[test]
1676 fn resolve_rule_not_found_after_removal() {
1677 let mut r = default_resolver();
1678 r.add_rule(make_rule("r1", "q", &["a"], 5))
1679 .expect("test setup: add_rule should not fail");
1680 r.add_rule(make_rule("r2", "q", &["b"], 10))
1681 .expect("test setup: add_rule should not fail");
1682 let mut conflicts = r.detect_conflicts();
1683 let dc = conflicts
1684 .iter_mut()
1685 .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1686 .cloned()
1687 .expect("test setup: should find DirectContradiction conflict");
1688 r.remove_rule("r1")
1690 .expect("test setup: remove_rule should not fail");
1691 let err = r
1692 .resolve(&dc)
1693 .expect_err("test setup: expected RuleNotFound error after removal");
1694 assert!(matches!(err, ResolverError::RuleNotFound(_)));
1695 }
1696
1697 #[test]
1698 fn cyclic_conflict_pair_error() {
1699 let mut r = default_resolver();
1700 r.add_rule(make_rule("ab", "B", &["A"], 1))
1701 .expect("test setup: add_rule should not fail");
1702 r.add_rule(make_rule("ba", "A", &["B"], 1))
1703 .expect("test setup: add_rule should not fail");
1704 let mut conflicts = r.detect_conflicts();
1705 let cycle_conflict = conflicts
1706 .iter_mut()
1707 .find(|c| matches!(&c.conflict_type, ConflictType::CyclicDependency { .. }))
1708 .cloned()
1709 .expect("test setup: should find CyclicDependency conflict");
1710 let err = r
1711 .resolve(&cycle_conflict)
1712 .expect_err("test setup: expected CyclicDependency error");
1713 assert!(matches!(err, ResolverError::CyclicDependency(_)));
1714 }
1715
1716 #[test]
1717 fn config_error_variant_display() {
1718 let err = ResolverError::ConfigurationError("bad value".to_string());
1719 assert!(err.to_string().contains("bad value"));
1720 }
1721
1722 #[test]
1723 fn max_rules_exceeded_display() {
1724 let err = ResolverError::MaxRulesExceeded;
1725 assert!(!err.to_string().is_empty());
1726 }
1727
1728 #[test]
1731 fn positive_body_excludes_negations() {
1732 let rule = make_rule("r", "head", &["a", "NOT:b", "c"], 1);
1733 let pos: Vec<&str> = rule.positive_body().collect();
1734 assert_eq!(pos, vec!["a", "c"]);
1735 }
1736
1737 #[test]
1738 fn negated_body_strips_prefix() {
1739 let rule = make_rule("r", "head", &["a", "NOT:b", "NOT:c"], 1);
1740 let neg: Vec<&str> = rule.negated_body().collect();
1741 assert_eq!(neg, vec!["b", "c"]);
1742 }
1743
1744 #[test]
1747 fn empty_resolver_no_conflicts() {
1748 let mut r = default_resolver();
1749 assert!(r.detect_conflicts().is_empty());
1750 }
1751
1752 #[test]
1753 fn single_rule_no_conflicts() {
1754 let mut r = default_resolver();
1755 r.add_rule(make_rule("only", "q", &["a"], 1))
1756 .expect("test setup: add_rule should not fail");
1757 assert!(r.detect_conflicts().is_empty());
1758 }
1759
1760 #[test]
1761 fn different_heads_no_direct_contradiction() {
1762 let mut r = default_resolver();
1763 r.add_rule(make_rule("r1", "bird", &["wings"], 1))
1764 .expect("test setup: add_rule should not fail");
1765 r.add_rule(make_rule("r2", "fish", &["fins"], 1))
1766 .expect("test setup: add_rule should not fail");
1767 let conflicts = r.detect_conflicts();
1768 let has_dc = conflicts
1769 .iter()
1770 .any(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }));
1771 assert!(!has_dc);
1772 }
1773
1774 #[test]
1775 fn resolve_all_empty_resolver() {
1776 let mut r = default_resolver();
1777 let results = r.resolve_all();
1778 assert!(results.is_empty());
1779 }
1780
1781 #[test]
1782 fn applicable_rules_superset_of_body() {
1783 let mut r = default_resolver();
1784 r.add_rule(make_rule("r1", "q", &["a"], 1))
1785 .expect("test setup: add_rule should not fail");
1786 let facts = vec!["a".to_string(), "b".to_string(), "c".to_string()];
1788 let applicable = r.applicable_rules(&facts);
1789 assert_eq!(applicable.len(), 1);
1790 }
1791}