1use std::collections::HashMap;
14
15#[inline]
21pub fn xorshift64(state: &mut u64) -> u64 {
22 let mut x = *state;
23 x ^= x << 13;
24 x ^= x >> 7;
25 x ^= x << 17;
26 *state = x;
27 x
28}
29
30#[derive(Debug, Clone, PartialEq)]
36pub struct Belief {
37 pub id: String,
39 pub formula: String,
41 pub confidence: f64,
43 pub source: String,
45 pub timestamp: u64,
47 pub is_derived: bool,
49}
50
51impl Belief {
52 pub fn new(
54 id: impl Into<String>,
55 formula: impl Into<String>,
56 confidence: f64,
57 source: impl Into<String>,
58 timestamp: u64,
59 ) -> Self {
60 Self {
61 id: id.into(),
62 formula: formula.into(),
63 confidence: confidence.clamp(0.0, 1.0),
64 source: source.into(),
65 timestamp,
66 is_derived: false,
67 }
68 }
69
70 pub fn derived(mut self) -> Self {
72 self.is_derived = true;
73 self
74 }
75}
76
77#[derive(Debug, Clone)]
79pub struct BeliefSet {
80 pub beliefs: Vec<Belief>,
82 pub entailment_cache: HashMap<String, bool>,
84}
85
86impl BeliefSet {
87 pub fn new() -> Self {
89 Self {
90 beliefs: Vec::new(),
91 entailment_cache: HashMap::new(),
92 }
93 }
94
95 pub fn len(&self) -> usize {
97 self.beliefs.len()
98 }
99
100 pub fn is_empty(&self) -> bool {
102 self.beliefs.is_empty()
103 }
104}
105
106impl Default for BeliefSet {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112#[derive(Debug, Clone)]
118pub enum RevisionOp {
119 Expansion(Belief),
121 Contraction(String),
123 Revision(Belief),
125 Consolidation,
127}
128
129#[derive(Debug, Clone, PartialEq)]
135pub enum ConsistencyCheck {
136 Consistent,
138 Inconsistent(Vec<String>),
140 Unknown,
142}
143
144#[derive(Debug, Clone)]
150pub enum RetentionFunction {
151 EpistemicEntrenchment,
153 RecencyBias,
155 SourcePriority(HashMap<String, u32>),
157 MinimalChange,
159}
160
161impl RetentionFunction {
162 pub fn prefers(&self, a: &Belief, b: &Belief) -> bool {
164 match self {
165 RetentionFunction::EpistemicEntrenchment => a.confidence >= b.confidence,
166 RetentionFunction::RecencyBias => a.timestamp >= b.timestamp,
167 RetentionFunction::SourcePriority(priorities) => {
168 let pa = priorities.get(&a.source).copied().unwrap_or(0);
169 let pb = priorities.get(&b.source).copied().unwrap_or(0);
170 pa >= pb
171 }
172 RetentionFunction::MinimalChange => !a.is_derived,
173 }
174 }
175}
176
177#[derive(Debug, Clone)]
183pub struct RevisionConfig {
184 pub max_beliefs: usize,
186 pub consistency_check_enabled: bool,
188 pub retention_function: RetentionFunction,
190 pub auto_consolidate: bool,
192}
193
194impl Default for RevisionConfig {
195 fn default() -> Self {
196 Self {
197 max_beliefs: 0,
198 consistency_check_enabled: true,
199 retention_function: RetentionFunction::EpistemicEntrenchment,
200 auto_consolidate: true,
201 }
202 }
203}
204
205#[derive(Debug, Clone, Default)]
211pub struct RevisionStats {
212 pub expansions: u64,
214 pub contractions: u64,
216 pub revisions: u64,
218 pub consolidations: u64,
220 pub beliefs_retracted: u64,
222 pub current_belief_count: usize,
224}
225
226#[derive(Debug, Clone, PartialEq)]
232pub enum RevisionError {
233 BeliefNotFound(String),
235 MaxBeliefsExceeded,
237 ContradictionDetected {
239 new: String,
241 existing: String,
243 },
244 RevisionFailed(String),
246}
247
248impl std::fmt::Display for RevisionError {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250 match self {
251 RevisionError::BeliefNotFound(id) => write!(f, "belief not found: {id}"),
252 RevisionError::MaxBeliefsExceeded => write!(f, "max belief limit exceeded"),
253 RevisionError::ContradictionDetected { new, existing } => {
254 write!(f, "contradiction: '{new}' vs '{existing}'")
255 }
256 RevisionError::RevisionFailed(msg) => write!(f, "revision failed: {msg}"),
257 }
258 }
259}
260
261impl std::error::Error for RevisionError {}
262
263fn negate(formula: &str) -> String {
272 if let Some(base) = formula.strip_prefix("NOT:") {
273 base.to_owned()
274 } else {
275 format!("NOT:{formula}")
276 }
277}
278
279fn are_contradictory(a: &str, b: &str) -> bool {
283 negate(a) == b
284}
285
286fn parse_conjunction(formula: &str) -> Option<(&str, &str)> {
288 let parts: Vec<&str> = formula.splitn(2, " AND ").collect();
289 if parts.len() == 2 {
290 Some((parts[0].trim(), parts[1].trim()))
291 } else {
292 None
293 }
294}
295
296fn set_entails_formula(formulae: &[&str], target: &str) -> bool {
302 if formulae.contains(&target) {
304 return true;
305 }
306 if let Some((a, b)) = parse_conjunction(target) {
308 return formulae.contains(&a) && formulae.contains(&b);
309 }
310 false
311}
312
313pub struct BeliefRevisionEngine {
337 belief_set: BeliefSet,
339 config: RevisionConfig,
341 stats: RevisionStats,
343 id_seed: u64,
345}
346
347impl BeliefRevisionEngine {
348 pub fn new(config: RevisionConfig) -> Self {
354 Self {
355 belief_set: BeliefSet::new(),
356 config,
357 stats: RevisionStats::default(),
358 id_seed: 0xdeadbeef_cafebabe,
359 }
360 }
361
362 pub fn default_config() -> Self {
364 Self::new(RevisionConfig::default())
365 }
366
367 fn next_id(&mut self) -> String {
373 let v = xorshift64(&mut self.id_seed);
374 format!("bel-{v:016x}")
375 }
376
377 fn current_formulae(&self) -> Vec<&str> {
379 self.belief_set
380 .beliefs
381 .iter()
382 .map(|b| b.formula.as_str())
383 .collect()
384 }
385
386 fn derive_entailed(&mut self, new_formula: &str) -> Vec<Belief> {
391 let existing_formulae: Vec<String> = self
392 .belief_set
393 .beliefs
394 .iter()
395 .map(|b| b.formula.clone())
396 .collect();
397
398 let mut derived = Vec::new();
399
400 if !new_formula.contains(" AND ") {
404 for ef in &existing_formulae {
405 if ef.contains(" AND ") || ef == new_formula {
406 continue; }
408 let conj = format!("{new_formula} AND {ef}");
409 let already_present = existing_formulae.iter().any(|f| f == &conj)
412 || derived.iter().any(|d: &Belief| d.formula == conj);
413 if !already_present {
414 let id = self.next_id();
415 let conf_new = self
417 .belief_set
418 .beliefs
419 .iter()
420 .find(|b| b.formula == new_formula)
421 .map(|b| b.confidence)
422 .unwrap_or(0.5_f64);
423 let conf_ex = self
424 .belief_set
425 .beliefs
426 .iter()
427 .find(|b| b.formula == *ef)
428 .map(|b| b.confidence)
429 .unwrap_or(0.5_f64);
430 let confidence = conf_new.min(conf_ex);
431 derived.push(Belief {
432 id,
433 formula: conj,
434 confidence,
435 source: "derived".to_owned(),
436 timestamp: 0,
437 is_derived: true,
438 });
439 }
440 }
441 }
442
443 derived
444 }
445
446 fn beliefs_derived_from(&self, formula: &str) -> Vec<String> {
449 self.belief_set
450 .beliefs
451 .iter()
452 .filter(|b| {
453 b.formula == formula || b.formula.split(" AND ").any(|part| part.trim() == formula)
454 })
455 .map(|b| b.id.clone())
456 .collect()
457 }
458
459 fn remove_beliefs(&mut self, ids: &[String]) -> Vec<String> {
461 let mut removed = Vec::new();
462 self.belief_set.beliefs.retain(|b| {
463 if ids.contains(&b.id) {
464 removed.push(b.id.clone());
465 false
466 } else {
467 true
468 }
469 });
470 self.belief_set.entailment_cache.clear();
472 self.stats.beliefs_retracted += removed.len() as u64;
473 removed
474 }
475
476 pub fn expand(&mut self, belief: Belief) -> Result<Vec<String>, RevisionError> {
488 if self.config.max_beliefs > 0 && self.belief_set.beliefs.len() >= self.config.max_beliefs {
490 return Err(RevisionError::MaxBeliefsExceeded);
491 }
492
493 let mut belief = belief;
495 if belief.id.is_empty() {
496 belief.id = self.next_id();
497 }
498
499 if self.config.consistency_check_enabled {
501 let neg = negate(&belief.formula);
502 if let Some(existing) = self.belief_set.beliefs.iter().find(|b| b.formula == neg) {
503 let existing_formula = existing.formula.clone();
504 if self.config.auto_consolidate {
505 } else {
507 return Err(RevisionError::ContradictionDetected {
508 new: belief.formula.clone(),
509 existing: existing_formula,
510 });
511 }
512 }
513 }
514
515 let main_id = belief.id.clone();
517 self.belief_set.beliefs.push(belief.clone());
518
519 let derived = self.derive_entailed(&belief.formula);
521 let derived_ids: Vec<String> = derived.iter().map(|d| d.id.clone()).collect();
522 for d in derived {
523 if self.config.max_beliefs > 0
525 && self.belief_set.beliefs.len() >= self.config.max_beliefs
526 {
527 break;
528 }
529 self.belief_set.beliefs.push(d);
530 }
531
532 self.belief_set.entailment_cache.clear();
534
535 if self.config.auto_consolidate
537 && self.config.consistency_check_enabled
538 && matches!(self.check_consistency(), ConsistencyCheck::Inconsistent(_))
539 {
540 self.consolidate();
541 }
542
543 self.stats.expansions += 1;
544 self.stats.current_belief_count = self.belief_set.beliefs.len();
545
546 let mut added = vec![main_id];
547 added.extend(derived_ids);
548 Ok(added)
549 }
550
551 pub fn contract(&mut self, formula: &str) -> Result<Vec<String>, RevisionError> {
558 let to_remove = self.beliefs_derived_from(formula);
559 if to_remove.is_empty() {
560 self.stats.contractions += 1;
561 self.stats.current_belief_count = self.belief_set.beliefs.len();
562 return Ok(Vec::new());
563 }
564
565 let removed = self.remove_beliefs(&to_remove);
566 self.stats.contractions += 1;
567 self.stats.current_belief_count = self.belief_set.beliefs.len();
568 Ok(removed)
569 }
570
571 pub fn revise(&mut self, belief: Belief) -> Result<(Vec<String>, Vec<String>), RevisionError> {
575 let neg = negate(&belief.formula);
576
577 let removed = self
579 .contract(&neg)
580 .map_err(|e| RevisionError::RevisionFailed(format!("contraction step failed: {e}")))?;
581
582 let added = self
584 .expand(belief)
585 .map_err(|e| RevisionError::RevisionFailed(format!("expansion step failed: {e}")))?;
586
587 self.stats.revisions += 1;
589 Ok((removed, added))
590 }
591
592 pub fn consolidate(&mut self) -> Vec<String> {
601 let mut drop_ids: Vec<String> = Vec::new();
602 let mut drop_formulae: Vec<String> = Vec::new();
603
604 let n = self.belief_set.beliefs.len();
606 for i in 0..n {
607 for j in (i + 1)..n {
608 let a = &self.belief_set.beliefs[i];
609 let b = &self.belief_set.beliefs[j];
610 if are_contradictory(&a.formula, &b.formula) {
611 let keep_a = self.config.retention_function.prefers(a, b);
613 let (drop_id, drop_formula) = if keep_a {
614 (b.id.clone(), b.formula.clone())
615 } else {
616 (a.id.clone(), a.formula.clone())
617 };
618 if !drop_ids.contains(&drop_id) {
619 drop_ids.push(drop_id);
620 drop_formulae.push(drop_formula);
621 }
622 }
623 }
624 }
625
626 for formula in &drop_formulae {
628 for derived_id in self.beliefs_derived_from(formula) {
629 if !drop_ids.contains(&derived_id) {
630 drop_ids.push(derived_id);
631 }
632 }
633 }
634
635 let removed = self.remove_beliefs(&drop_ids);
636 self.stats.consolidations += 1;
637 self.stats.current_belief_count = self.belief_set.beliefs.len();
638 removed
639 }
640
641 pub fn check_consistency(&self) -> ConsistencyCheck {
643 let mut conflicting = Vec::new();
644
645 let beliefs = &self.belief_set.beliefs;
646 let n = beliefs.len();
647 for i in 0..n {
648 for j in (i + 1)..n {
649 if are_contradictory(&beliefs[i].formula, &beliefs[j].formula) {
650 if !conflicting.contains(&beliefs[i].id) {
651 conflicting.push(beliefs[i].id.clone());
652 }
653 if !conflicting.contains(&beliefs[j].id) {
654 conflicting.push(beliefs[j].id.clone());
655 }
656 }
657 }
658 }
659
660 if conflicting.is_empty() {
661 ConsistencyCheck::Consistent
662 } else {
663 ConsistencyCheck::Inconsistent(conflicting)
664 }
665 }
666
667 pub fn entails(&mut self, formula: &str) -> bool {
671 if let Some(&cached) = self.belief_set.entailment_cache.get(formula) {
673 return cached;
674 }
675
676 let formulae: Vec<&str> = self.current_formulae();
677 let result = set_entails_formula(&formulae, formula);
678
679 self.belief_set
680 .entailment_cache
681 .insert(formula.to_owned(), result);
682 result
683 }
684
685 pub fn get_belief(&self, id: &str) -> Option<&Belief> {
687 self.belief_set.beliefs.iter().find(|b| b.id == id)
688 }
689
690 pub fn beliefs_about(&self, formula: &str) -> Vec<&Belief> {
692 self.belief_set
693 .beliefs
694 .iter()
695 .filter(|b| b.formula == formula)
696 .collect()
697 }
698
699 pub fn apply_op(&mut self, op: RevisionOp) -> Result<(), RevisionError> {
701 match op {
702 RevisionOp::Expansion(belief) => {
703 self.expand(belief)?;
704 }
705 RevisionOp::Contraction(formula) => {
706 self.contract(&formula)?;
707 }
708 RevisionOp::Revision(belief) => {
709 self.revise(belief)?;
710 }
711 RevisionOp::Consolidation => {
712 self.consolidate();
713 }
714 }
715 Ok(())
716 }
717
718 pub fn stats(&self) -> RevisionStats {
720 let mut s = self.stats.clone();
721 s.current_belief_count = self.belief_set.beliefs.len();
722 s
723 }
724
725 pub fn snapshot(&self) -> BeliefSet {
727 self.belief_set.clone()
728 }
729
730 pub fn belief_count(&self) -> usize {
732 self.belief_set.beliefs.len()
733 }
734
735 pub fn belief_set(&self) -> &BeliefSet {
737 &self.belief_set
738 }
739
740 pub fn belief_set_mut(&mut self) -> &mut BeliefSet {
742 &mut self.belief_set
743 }
744
745 pub fn set_config(&mut self, config: RevisionConfig) {
747 self.config = config;
748 }
749
750 pub fn config(&self) -> &RevisionConfig {
752 &self.config
753 }
754}
755
756#[cfg(test)]
761mod tests {
762 use super::*;
763 use std::collections::HashMap;
764
765 fn engine() -> BeliefRevisionEngine {
768 BeliefRevisionEngine::default_config()
769 }
770
771 fn belief(id: &str, formula: &str, confidence: f64) -> Belief {
772 Belief::new(id, formula, confidence, "test", 100)
773 }
774
775 fn belief_ts(id: &str, formula: &str, confidence: f64, ts: u64) -> Belief {
776 Belief::new(id, formula, confidence, "test", ts)
777 }
778
779 #[test]
782 fn test_xorshift64_non_zero() {
783 let mut state = 1u64;
784 let v = xorshift64(&mut state);
785 assert_ne!(v, 0);
786 }
787
788 #[test]
789 fn test_xorshift64_changes_state() {
790 let mut state = 42u64;
791 let v1 = xorshift64(&mut state);
792 let v2 = xorshift64(&mut state);
793 assert_ne!(v1, v2);
794 }
795
796 #[test]
797 fn test_xorshift64_sequence_deterministic() {
798 let mut s1 = 12345u64;
799 let mut s2 = 12345u64;
800 assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
801 }
802
803 #[test]
806 fn test_belief_confidence_clamped() {
807 let b = Belief::new("x", "p", 1.5, "s", 0);
808 assert_eq!(b.confidence, 1.0);
809 let b2 = Belief::new("y", "q", -0.1, "s", 0);
810 assert_eq!(b2.confidence, 0.0);
811 }
812
813 #[test]
814 fn test_belief_derived_flag() {
815 let b = belief("x", "p", 0.9).derived();
816 assert!(b.is_derived);
817 }
818
819 #[test]
822 fn test_belief_set_empty() {
823 let bs = BeliefSet::new();
824 assert!(bs.is_empty());
825 assert_eq!(bs.len(), 0);
826 }
827
828 #[test]
831 fn test_negate_positive() {
832 assert_eq!(negate("rain"), "NOT:rain");
833 }
834
835 #[test]
836 fn test_negate_negative() {
837 assert_eq!(negate("NOT:rain"), "rain");
838 }
839
840 #[test]
841 fn test_are_contradictory_true() {
842 assert!(are_contradictory("rain", "NOT:rain"));
843 assert!(are_contradictory("NOT:rain", "rain"));
844 }
845
846 #[test]
847 fn test_are_contradictory_false() {
848 assert!(!are_contradictory("rain", "wet"));
849 }
850
851 #[test]
854 fn test_expand_adds_belief() {
855 let mut e = engine();
856 let added = e
857 .expand(belief("b1", "rain", 0.9))
858 .expect("test: should succeed");
859 assert!(added.contains(&"b1".to_string()));
860 assert_eq!(e.belief_count(), 1);
861 }
862
863 #[test]
864 fn test_expand_assigns_id_when_blank() {
865 let mut e = engine();
866 let b = Belief::new("", "rain", 0.9, "s", 0);
867 let added = e.expand(b).expect("test: should succeed");
868 assert!(!added[0].is_empty());
869 }
870
871 #[test]
872 fn test_expand_returns_stats() {
873 let mut e = engine();
874 e.expand(belief("b1", "rain", 0.9))
875 .expect("test: should succeed");
876 assert_eq!(e.stats().expansions, 1);
877 }
878
879 #[test]
880 fn test_expand_max_beliefs_exceeded() {
881 let cfg = RevisionConfig {
882 max_beliefs: 1,
883 ..RevisionConfig::default()
884 };
885 let mut e = BeliefRevisionEngine::new(cfg);
886 e.expand(belief("b1", "a", 0.9))
887 .expect("test: should succeed");
888 let err = e.expand(belief("b2", "b", 0.8)).unwrap_err();
889 assert_eq!(err, RevisionError::MaxBeliefsExceeded);
890 }
891
892 #[test]
893 fn test_expand_auto_consolidate_on_contradiction() {
894 let mut e = engine(); e.expand(belief("b1", "rain", 0.9))
896 .expect("test: should succeed");
897 e.expand(belief("b2", "NOT:rain", 0.3))
899 .expect("test: should succeed");
900 assert_eq!(e.belief_count(), 1);
902 assert!(e.beliefs_about("rain").len() == 1);
903 }
904
905 #[test]
906 fn test_expand_contradiction_error_when_no_auto_consolidate() {
907 let cfg = RevisionConfig {
908 auto_consolidate: false,
909 consistency_check_enabled: true,
910 ..RevisionConfig::default()
911 };
912 let mut e = BeliefRevisionEngine::new(cfg);
913 e.expand(belief("b1", "rain", 0.9))
914 .expect("test: should succeed");
915 let err = e.expand(belief("b2", "NOT:rain", 0.3)).unwrap_err();
916 assert!(matches!(err, RevisionError::ContradictionDetected { .. }));
917 }
918
919 #[test]
920 fn test_expand_derives_conjunction() {
921 let mut e = engine();
922 e.expand(belief("b1", "rain", 0.8))
923 .expect("test: should succeed");
924 e.expand(belief("b2", "wet", 0.7))
925 .expect("test: should succeed");
926 let has_conj = e
928 .belief_set()
929 .beliefs
930 .iter()
931 .any(|b| b.formula.contains(" AND ") && b.is_derived);
932 assert!(has_conj);
933 }
934
935 #[test]
938 fn test_contract_removes_belief() {
939 let mut e = engine();
940 e.expand(belief("b1", "rain", 0.9))
941 .expect("test: should succeed");
942 let removed = e.contract("rain").expect("test: should succeed");
943 assert!(removed.contains(&"b1".to_string()));
944 assert_eq!(e.belief_count(), 0);
945 }
946
947 #[test]
948 fn test_contract_nonexistent_ok() {
949 let mut e = engine();
950 let removed = e.contract("unicorn").expect("test: should succeed");
951 assert!(removed.is_empty());
952 }
953
954 #[test]
955 fn test_contract_removes_derived_conjunction() {
956 let mut e = engine();
957 e.expand(belief("b1", "rain", 0.8))
958 .expect("test: should succeed");
959 e.expand(belief("b2", "wet", 0.7))
960 .expect("test: should succeed");
961 e.contract("rain").expect("test: should succeed");
963 for b in &e.belief_set().beliefs {
964 assert!(
965 !b.formula.contains("rain"),
966 "found residual formula: {}",
967 b.formula
968 );
969 }
970 }
971
972 #[test]
973 fn test_contract_stats() {
974 let mut e = engine();
975 e.expand(belief("b1", "rain", 0.9))
976 .expect("test: should succeed");
977 e.contract("rain").expect("test: should succeed");
978 assert_eq!(e.stats().contractions, 1);
979 }
980
981 #[test]
984 fn test_revise_adds_new_formula() {
985 let mut e = engine();
986 e.expand(belief("b1", "rain", 0.9))
987 .expect("test: should succeed");
988 let (_, added) = e
989 .revise(belief("b2", "NOT:rain", 0.8))
990 .expect("test: should succeed");
991 assert!(!added.is_empty());
992 assert!(e.entails("NOT:rain"));
994 }
995
996 #[test]
997 fn test_revise_removes_negation() {
998 let mut e = engine();
999 e.expand(belief("b1", "rain", 0.9))
1000 .expect("test: should succeed");
1001 let (removed, _) = e
1002 .revise(belief("b2", "NOT:rain", 0.8))
1003 .expect("test: should succeed");
1004 assert!(removed.contains(&"b1".to_string()));
1006 assert!(!e.entails("rain"));
1007 }
1008
1009 #[test]
1010 fn test_revise_levi_identity_no_duplicate() {
1011 let mut e = engine();
1013 e.expand(belief("b1", "rain", 0.5))
1014 .expect("test: should succeed");
1015 e.revise(belief("b2", "NOT:rain", 0.9))
1016 .expect("test: should succeed");
1017 assert_eq!(
1018 e.check_consistency(),
1019 ConsistencyCheck::Consistent,
1020 "set should be consistent after revision"
1021 );
1022 }
1023
1024 #[test]
1025 fn test_revise_stats() {
1026 let mut e = engine();
1027 e.expand(belief("b1", "rain", 0.9))
1028 .expect("test: should succeed");
1029 e.revise(belief("b2", "NOT:rain", 0.6))
1030 .expect("test: should succeed");
1031 assert_eq!(e.stats().revisions, 1);
1032 }
1033
1034 #[test]
1035 fn test_revise_fresh_formula() {
1036 let mut e = engine();
1038 let (removed, added) = e
1039 .revise(belief("b1", "sun", 0.9))
1040 .expect("test: should succeed");
1041 assert!(removed.is_empty());
1042 assert!(!added.is_empty());
1043 }
1044
1045 #[test]
1048 fn test_consolidate_removes_lower_confidence() {
1049 let cfg = RevisionConfig {
1050 auto_consolidate: false,
1051 consistency_check_enabled: false,
1052 retention_function: RetentionFunction::EpistemicEntrenchment,
1053 ..RevisionConfig::default()
1054 };
1055 let mut e = BeliefRevisionEngine::new(cfg);
1056 e.expand(belief("b1", "rain", 0.9))
1057 .expect("test: should succeed");
1058 e.expand(belief("b2", "NOT:rain", 0.3))
1059 .expect("test: should succeed");
1060 let removed = e.consolidate();
1061 assert!(removed.contains(&"b2".to_string()));
1062 assert_eq!(e.belief_count(), 1);
1063 }
1064
1065 #[test]
1066 fn test_consolidate_no_contradictions() {
1067 let mut e = engine();
1068 e.expand(belief("b1", "rain", 0.9))
1069 .expect("test: should succeed");
1070 e.expand(belief("b2", "wet", 0.7))
1071 .expect("test: should succeed");
1072 let removed = e.consolidate();
1074 for id in &removed {
1077 let _ = id;
1079 }
1080 assert_eq!(e.check_consistency(), ConsistencyCheck::Consistent);
1081 }
1082
1083 #[test]
1084 fn test_consolidate_recency_bias() {
1085 let cfg = RevisionConfig {
1086 auto_consolidate: false,
1087 consistency_check_enabled: false,
1088 retention_function: RetentionFunction::RecencyBias,
1089 ..RevisionConfig::default()
1090 };
1091 let mut e = BeliefRevisionEngine::new(cfg);
1092 e.expand(belief_ts("b1", "rain", 0.9, 100))
1093 .expect("test: should succeed");
1094 e.expand(belief_ts("b2", "NOT:rain", 0.3, 200))
1095 .expect("test: should succeed");
1096 let removed = e.consolidate();
1097 assert!(removed.contains(&"b1".to_string()));
1099 }
1100
1101 #[test]
1102 fn test_consolidate_source_priority() {
1103 let mut priorities = HashMap::new();
1104 priorities.insert("sensor".to_string(), 5u32);
1105 priorities.insert("user".to_string(), 10u32);
1106 let cfg = RevisionConfig {
1107 auto_consolidate: false,
1108 consistency_check_enabled: false,
1109 retention_function: RetentionFunction::SourcePriority(priorities),
1110 ..RevisionConfig::default()
1111 };
1112 let mut e = BeliefRevisionEngine::new(cfg);
1113 let b1 = Belief::new("b1", "rain", 0.9, "sensor", 100);
1114 let b2 = Belief::new("b2", "NOT:rain", 0.3, "user", 100);
1115 e.expand(b1).expect("test: should succeed");
1116 e.expand(b2).expect("test: should succeed");
1117 let removed = e.consolidate();
1118 assert!(removed.contains(&"b1".to_string()));
1120 }
1121
1122 #[test]
1123 fn test_consolidate_minimal_change() {
1124 let cfg = RevisionConfig {
1125 auto_consolidate: false,
1126 consistency_check_enabled: false,
1127 retention_function: RetentionFunction::MinimalChange,
1128 ..RevisionConfig::default()
1129 };
1130 let mut e = BeliefRevisionEngine::new(cfg);
1131 let b1 = Belief::new("b1", "rain", 0.5, "test", 100);
1133 let b2 = Belief {
1134 id: "b2".to_string(),
1135 formula: "NOT:rain".to_string(),
1136 confidence: 0.5,
1137 source: "test".to_string(),
1138 timestamp: 100,
1139 is_derived: true,
1140 };
1141 e.expand(b1).expect("test: should succeed");
1142 e.expand(b2).expect("test: should succeed");
1143 let removed = e.consolidate();
1144 assert!(removed.contains(&"b2".to_string()));
1146 }
1147
1148 #[test]
1149 fn test_consolidate_stats() {
1150 let mut e = engine();
1151 e.consolidate();
1152 assert_eq!(e.stats().consolidations, 1);
1153 }
1154
1155 #[test]
1158 fn test_check_consistency_consistent() {
1159 let mut e = engine();
1160 e.expand(belief("b1", "rain", 0.9))
1161 .expect("test: should succeed");
1162 assert_eq!(e.check_consistency(), ConsistencyCheck::Consistent);
1163 }
1164
1165 #[test]
1166 fn test_check_consistency_inconsistent() {
1167 let cfg = RevisionConfig {
1168 auto_consolidate: false,
1169 consistency_check_enabled: false,
1170 ..RevisionConfig::default()
1171 };
1172 let mut e = BeliefRevisionEngine::new(cfg);
1173 e.expand(belief("b1", "rain", 0.9))
1174 .expect("test: should succeed");
1175 e.expand(belief("b2", "NOT:rain", 0.3))
1176 .expect("test: should succeed");
1177 let cc = e.check_consistency();
1178 assert!(matches!(cc, ConsistencyCheck::Inconsistent(_)));
1179 if let ConsistencyCheck::Inconsistent(ids) = cc {
1180 assert!(ids.contains(&"b1".to_string()));
1181 assert!(ids.contains(&"b2".to_string()));
1182 }
1183 }
1184
1185 #[test]
1186 fn test_check_consistency_empty_set() {
1187 let e = engine();
1188 assert_eq!(e.check_consistency(), ConsistencyCheck::Consistent);
1189 }
1190
1191 #[test]
1194 fn test_entails_direct() {
1195 let mut e = engine();
1196 e.expand(belief("b1", "rain", 0.9))
1197 .expect("test: should succeed");
1198 assert!(e.entails("rain"));
1199 assert!(!e.entails("sun"));
1200 }
1201
1202 #[test]
1203 fn test_entails_conjunction() {
1204 let mut e = engine();
1205 e.expand(belief("b1", "rain", 0.8))
1206 .expect("test: should succeed");
1207 e.expand(belief("b2", "wet", 0.7))
1208 .expect("test: should succeed");
1209 assert!(e.entails("rain AND wet") || e.entails("wet AND rain"));
1210 }
1211
1212 #[test]
1213 fn test_entails_cache_hit() {
1214 let mut e = engine();
1215 e.expand(belief("b1", "rain", 0.9))
1216 .expect("test: should succeed");
1217 assert!(e.entails("rain")); assert!(e.entails("rain")); }
1220
1221 #[test]
1222 fn test_entails_not_present() {
1223 let mut e = engine();
1224 assert!(!e.entails("unicorn"));
1225 }
1226
1227 #[test]
1230 fn test_get_belief_found() {
1231 let mut e = engine();
1232 e.expand(belief("b1", "rain", 0.9))
1233 .expect("test: should succeed");
1234 assert!(e.get_belief("b1").is_some());
1235 }
1236
1237 #[test]
1238 fn test_get_belief_not_found() {
1239 let e = engine();
1240 assert!(e.get_belief("missing").is_none());
1241 }
1242
1243 #[test]
1244 fn test_beliefs_about_empty() {
1245 let e = engine();
1246 assert!(e.beliefs_about("rain").is_empty());
1247 }
1248
1249 #[test]
1250 fn test_beliefs_about_found() {
1251 let mut e = engine();
1252 e.expand(belief("b1", "rain", 0.9))
1253 .expect("test: should succeed");
1254 assert_eq!(e.beliefs_about("rain").len(), 1);
1255 }
1256
1257 #[test]
1260 fn test_apply_op_expansion() {
1261 let mut e = engine();
1262 let op = RevisionOp::Expansion(belief("b1", "rain", 0.9));
1263 e.apply_op(op).expect("test: should succeed");
1264 assert!(e.belief_count() >= 1);
1266 assert!(e.entails("rain"));
1267 }
1268
1269 #[test]
1270 fn test_apply_op_contraction() {
1271 let mut e = engine();
1272 e.expand(belief("b1", "rain", 0.9))
1273 .expect("test: should succeed");
1274 let op = RevisionOp::Contraction("rain".to_string());
1275 e.apply_op(op).expect("test: should succeed");
1276 assert_eq!(e.belief_count(), 0);
1277 }
1278
1279 #[test]
1280 fn test_apply_op_revision() {
1281 let mut e = engine();
1282 e.expand(belief("b1", "rain", 0.5))
1283 .expect("test: should succeed");
1284 let op = RevisionOp::Revision(belief("b2", "NOT:rain", 0.9));
1285 e.apply_op(op).expect("test: should succeed");
1286 assert!(e.entails("NOT:rain"));
1287 }
1288
1289 #[test]
1290 fn test_apply_op_consolidation() {
1291 let cfg = RevisionConfig {
1292 auto_consolidate: false,
1293 consistency_check_enabled: false,
1294 ..RevisionConfig::default()
1295 };
1296 let mut e = BeliefRevisionEngine::new(cfg);
1297 e.expand(belief("b1", "rain", 0.9))
1298 .expect("test: should succeed");
1299 e.expand(belief("b2", "NOT:rain", 0.3))
1300 .expect("test: should succeed");
1301 e.apply_op(RevisionOp::Consolidation)
1302 .expect("test: should succeed");
1303 assert_eq!(e.check_consistency(), ConsistencyCheck::Consistent);
1305 }
1306
1307 #[test]
1310 fn test_stats_initial() {
1311 let e = engine();
1312 let s = e.stats();
1313 assert_eq!(s.expansions, 0);
1314 assert_eq!(s.contractions, 0);
1315 assert_eq!(s.revisions, 0);
1316 assert_eq!(s.consolidations, 0);
1317 assert_eq!(s.beliefs_retracted, 0);
1318 assert_eq!(s.current_belief_count, 0);
1319 }
1320
1321 #[test]
1322 fn test_stats_after_operations() {
1323 let mut e = engine();
1324 e.expand(belief("b1", "rain", 0.9))
1325 .expect("test: should succeed");
1326 e.contract("rain").expect("test: should succeed");
1327 let s = e.stats();
1328 assert_eq!(s.expansions, 1);
1329 assert_eq!(s.contractions, 1);
1330 assert!(s.beliefs_retracted >= 1);
1331 assert_eq!(s.current_belief_count, 0);
1332 }
1333
1334 #[test]
1337 fn test_snapshot_clone() {
1338 let mut e = engine();
1339 e.expand(belief("b1", "rain", 0.9))
1340 .expect("test: should succeed");
1341 let snap = e.snapshot();
1342 e.contract("rain").expect("test: should succeed");
1343 assert_eq!(snap.beliefs.len(), 1);
1345 assert_eq!(e.belief_count(), 0);
1346 }
1347
1348 #[test]
1351 fn test_error_display_belief_not_found() {
1352 let e = RevisionError::BeliefNotFound("x".to_string());
1353 assert!(e.to_string().contains("x"));
1354 }
1355
1356 #[test]
1357 fn test_error_display_max_beliefs_exceeded() {
1358 let e = RevisionError::MaxBeliefsExceeded;
1359 assert!(!e.to_string().is_empty());
1360 }
1361
1362 #[test]
1363 fn test_error_display_contradiction_detected() {
1364 let e = RevisionError::ContradictionDetected {
1365 new: "rain".to_string(),
1366 existing: "NOT:rain".to_string(),
1367 };
1368 let s = e.to_string();
1369 assert!(s.contains("rain"));
1370 assert!(s.contains("NOT:rain"));
1371 }
1372
1373 #[test]
1374 fn test_error_display_revision_failed() {
1375 let e = RevisionError::RevisionFailed("oops".to_string());
1376 assert!(e.to_string().contains("oops"));
1377 }
1378
1379 #[test]
1382 fn test_multiple_expansions_and_count() {
1383 let mut e = engine();
1384 for i in 0..10u32 {
1385 e.expand(belief(&format!("b{i}"), &format!("p{i}"), 0.5))
1386 .expect("test: should succeed");
1387 }
1388 assert!(e.belief_count() >= 10);
1389 }
1390
1391 #[test]
1392 fn test_revision_sequence() {
1393 let mut e = engine();
1394 e.expand(belief("b1", "A", 0.8))
1395 .expect("test: should succeed");
1396 e.revise(belief("b2", "NOT:A", 0.9))
1397 .expect("test: should succeed");
1398 e.revise(belief("b3", "A", 0.95))
1399 .expect("test: should succeed");
1400 assert!(e.entails("A"));
1402 assert!(!e.entails("NOT:A"));
1403 assert_eq!(e.check_consistency(), ConsistencyCheck::Consistent);
1404 }
1405
1406 #[test]
1407 fn test_agm_success_postulate_expansion() {
1408 let mut e = engine();
1410 e.expand(belief("b1", "rain", 0.9))
1411 .expect("test: should succeed");
1412 assert!(e.entails("rain"));
1413 }
1414
1415 #[test]
1416 fn test_agm_success_postulate_revision() {
1417 let mut e = engine();
1419 e.expand(belief("b1", "sun", 0.8))
1420 .expect("test: should succeed");
1421 e.revise(belief("b2", "rain", 0.9))
1422 .expect("test: should succeed");
1423 assert!(e.entails("rain"));
1424 }
1425
1426 #[test]
1427 fn test_agm_consistency_postulate_revision() {
1428 let mut e = engine();
1430 e.expand(belief("b1", "X", 0.5))
1431 .expect("test: should succeed");
1432 e.expand(belief("b2", "Y", 0.5))
1433 .expect("test: should succeed");
1434 e.revise(belief("b3", "NOT:X", 0.9))
1435 .expect("test: should succeed");
1436 assert_eq!(e.check_consistency(), ConsistencyCheck::Consistent);
1437 }
1438
1439 #[test]
1440 fn test_contract_idempotent() {
1441 let mut e = engine();
1442 e.expand(belief("b1", "rain", 0.9))
1443 .expect("test: should succeed");
1444 e.contract("rain").expect("test: should succeed");
1445 let removed2 = e.contract("rain").expect("test: should succeed");
1446 assert!(removed2.is_empty());
1447 }
1448
1449 #[test]
1450 fn test_set_config() {
1451 let mut e = engine();
1452 let new_cfg = RevisionConfig {
1453 max_beliefs: 5,
1454 ..RevisionConfig::default()
1455 };
1456 e.set_config(new_cfg);
1457 assert_eq!(e.config().max_beliefs, 5);
1458 }
1459
1460 #[test]
1461 fn test_belief_set_ref() {
1462 let mut e = engine();
1463 e.expand(belief("b1", "rain", 0.9))
1464 .expect("test: should succeed");
1465 let bs = e.belief_set();
1466 assert!(!bs.is_empty());
1468 assert!(bs.beliefs.iter().any(|b| b.id == "b1"));
1469 }
1470
1471 #[test]
1474 fn test_retention_epistemic_equal_confidence() {
1475 let rf = RetentionFunction::EpistemicEntrenchment;
1476 let a = belief("a", "p", 0.7);
1477 let b = belief("b", "q", 0.7);
1478 assert!(rf.prefers(&a, &b));
1480 }
1481
1482 #[test]
1483 fn test_retention_source_priority_missing_key() {
1484 let rf = RetentionFunction::SourcePriority(HashMap::new());
1485 let a = belief("a", "p", 0.5);
1486 let b = belief("b", "q", 0.5);
1487 assert!(rf.prefers(&a, &b));
1489 }
1490
1491 #[test]
1492 fn test_revision_error_implements_std_error() {
1493 let e: Box<dyn std::error::Error> = Box::new(RevisionError::MaxBeliefsExceeded);
1494 assert!(!e.to_string().is_empty());
1495 }
1496}