weavatrix_memory/analytics/
belief.rs1use super::{
2 BeliefRevisionReport, BeliefRevisionRequest, CascadeEffect, Contradiction, MemoryAnalytics,
3};
4use crate::{EntityId, MemoryProjection, Result, project_graph};
5use std::collections::{BTreeMap, BTreeSet, VecDeque};
6
7impl MemoryAnalytics {
8 pub fn belief_revision(
17 projection: &MemoryProjection,
18 request: &BeliefRevisionRequest,
19 ) -> Result<BeliefRevisionReport> {
20 request.hypothesis.validate()?;
21 let view = projection.view(request.clock);
22 let graph = project_graph(&view)?;
23 let mut contradictions = view
24 .facts
25 .iter()
26 .filter_map(|fact| contradiction(fact, &request.hypothesis))
27 .collect::<Vec<_>>();
28 contradictions.sort_by(|left, right| left.fact.cmp(&right.fact));
29 let roots = contradictions
30 .iter()
31 .filter_map(|item| {
32 view.facts
33 .iter()
34 .find(|fact| fact.id == item.fact)
35 .map(|fact| fact.target.clone())
36 })
37 .collect::<BTreeSet<_>>();
38 let cascade = cascade(
39 &graph,
40 roots,
41 request.max_depth,
42 request.hypothesis.confidence.basis_points(),
43 );
44 let kinds = view
45 .nodes
46 .iter()
47 .map(|node| (node.id.clone(), node.kind.as_str()))
48 .collect::<BTreeMap<_, _>>();
49 let invalidated_decisions = cascade
50 .iter()
51 .filter(|effect| {
52 kinds
53 .get(&effect.entity)
54 .is_some_and(|kind| kind.eq_ignore_ascii_case("decision"))
55 })
56 .map(|effect| effect.entity.clone())
57 .collect();
58 Ok(BeliefRevisionReport {
59 contradictions,
60 cascade,
61 invalidated_decisions,
62 })
63 }
64}
65
66fn contradiction(
67 fact: &crate::MemoryFact,
68 hypothesis: &crate::MemoryFact,
69) -> Option<Contradiction> {
70 let competing = fact.source == hypothesis.source
71 && fact.relation == hypothesis.relation
72 && fact.target != hypothesis.target;
73 let explicit = fact.relation.eq_ignore_ascii_case("contradicts")
74 && ((fact.source == hypothesis.source && fact.target == hypothesis.target)
75 || (fact.source == hypothesis.target && fact.target == hypothesis.source));
76 let corrected = hypothesis.supersedes.as_ref() == Some(&fact.id);
77 if !(competing || explicit || corrected) {
78 return None;
79 }
80 let reason = if explicit {
81 "explicit contradicts relation"
82 } else if corrected {
83 "hypothesis explicitly supersedes this fact"
84 } else {
85 "same source and relation assert a different target"
86 };
87 Some(Contradiction {
88 fact: fact.id.clone(),
89 strength_bps: fact
90 .confidence
91 .basis_points()
92 .min(hypothesis.confidence.basis_points()),
93 reason: reason.to_owned(),
94 })
95}
96
97fn cascade(
98 graph: &weavatrix_graph::Graph,
99 roots: BTreeSet<EntityId>,
100 max_depth: usize,
101 hypothesis_confidence: u16,
102) -> Vec<CascadeEffect> {
103 let mut distances = BTreeMap::<EntityId, usize>::new();
104 let mut queue = VecDeque::new();
105 for root in roots {
106 distances.insert(root.clone(), 0);
107 queue.push_back(root);
108 }
109 while let Some(entity) = queue.pop_front() {
110 let depth = distances[&entity];
111 if depth >= max_depth {
112 continue;
113 }
114 let Some(index) = graph.node_index(entity.as_str()) else {
115 continue;
116 };
117 for neighbor in graph.outgoing_neighbors_at(index) {
118 let Some(node) = graph.node_at(neighbor) else {
119 continue;
120 };
121 let Ok(next) = EntityId::new(node.id.as_str()) else {
122 continue;
123 };
124 if !distances.contains_key(&next) {
125 distances.insert(next.clone(), depth + 1);
126 queue.push_back(next);
127 }
128 }
129 }
130 distances
131 .into_iter()
132 .map(|(entity, depth)| CascadeEffect {
133 entity,
134 depth,
135 revised_confidence_bps: revised_confidence(hypothesis_confidence, depth),
136 })
137 .collect()
138}
139
140fn revised_confidence(confidence: u16, depth: usize) -> u16 {
141 let divisor = u32::try_from(depth + 2).unwrap_or(u32::MAX);
142 let weakening = u32::from(confidence) / divisor;
143 u16::try_from(10_000_u32.saturating_sub(weakening)).unwrap_or(0)
144}