Skip to main content

weavatrix_memory/analytics/
belief.rs

1use 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    /// Evaluates a structured hypothesis without mutating recorded memory.
9    ///
10    /// Exact competing targets and explicit `contradicts` facts start a
11    /// confidence cascade over the canonical Weavatrix graph.
12    ///
13    /// # Errors
14    ///
15    /// Rejects an invalid hypothesis or graph projection.
16    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}