Skip to main content

weavatrix_memory/analytics/
belief.rs

1use super::{
2    BeliefRevisionReport, BeliefRevisionRequest, CascadeEffect, Contradiction, MemoryAnalytics,
3};
4use crate::{
5    domain::MemoryFact, error::Result, graph_projection::project_graph, id::EntityId,
6    projection::MemoryProjection,
7};
8use std::collections::{BTreeMap, BTreeSet, VecDeque};
9
10impl MemoryAnalytics {
11    /// Evaluates a structured hypothesis without mutating recorded memory.
12    ///
13    /// Exact competing targets and explicit `contradicts` facts start a
14    /// confidence cascade over the canonical Weavatrix graph.
15    ///
16    /// # Errors
17    ///
18    /// Rejects an invalid hypothesis or graph projection.
19    pub fn belief_revision(
20        projection: &MemoryProjection,
21        request: &BeliefRevisionRequest,
22    ) -> Result<BeliefRevisionReport> {
23        request.hypothesis.validate()?;
24        let view = projection.view(request.clock);
25        let graph = project_graph(&view)?;
26        let mut contradictions = view
27            .facts
28            .iter()
29            .filter_map(|fact| contradiction(fact, &request.hypothesis))
30            .collect::<Vec<_>>();
31        contradictions.sort_by(|left, right| left.fact.cmp(&right.fact));
32        let roots = contradictions
33            .iter()
34            .filter_map(|item| {
35                view.facts
36                    .iter()
37                    .find(|fact| fact.id == item.fact)
38                    .map(|fact| fact.target.clone())
39            })
40            .collect::<BTreeSet<_>>();
41        let cascade = cascade(
42            &graph,
43            roots,
44            request.max_depth,
45            request.hypothesis.confidence.basis_points(),
46        );
47        let kinds = view
48            .nodes
49            .iter()
50            .map(|node| (node.id.clone(), node.kind.as_str()))
51            .collect::<BTreeMap<_, _>>();
52        let invalidated_decisions = cascade
53            .iter()
54            .filter(|effect| {
55                kinds
56                    .get(&effect.entity)
57                    .is_some_and(|kind| kind.eq_ignore_ascii_case("decision"))
58            })
59            .map(|effect| effect.entity.clone())
60            .collect();
61        Ok(BeliefRevisionReport {
62            contradictions,
63            cascade,
64            invalidated_decisions,
65        })
66    }
67}
68
69fn contradiction(fact: &MemoryFact, hypothesis: &MemoryFact) -> 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}