Skip to main content

weavatrix_memory/
graph_projection.rs

1use crate::{
2    domain::{Confidence as MemoryConfidence, MemoryView},
3    error::{MemoryError, Result},
4    id::EntityId,
5};
6use std::{collections::BTreeMap, str::FromStr};
7use weavatrix_graph::{
8    AttributeValue, Confidence, Edge, EdgeKind, EvidenceKind, Graph, Node, NodeId, NodeKind,
9    Provenance,
10};
11
12/// Converts a temporal memory view into a canonical immutable graph.
13///
14/// # Errors
15///
16/// Returns graph validation errors for invalid custom kinds or endpoints.
17pub fn project_graph(view: &MemoryView) -> Result<Graph> {
18    let mut ids = BTreeMap::new();
19    let mut nodes = Vec::with_capacity(view.nodes.len());
20    for memory_node in &view.nodes {
21        let mut node = Node::new(
22            memory_node.id.as_str(),
23            memory_node.label.clone(),
24            NodeKind::from_str(&memory_node.kind)?,
25        )?;
26        if let Some(repository) = &memory_node.repository {
27            node = node.with_attribute("memory.repository", repository.clone());
28        }
29        if let Some(branch) = &memory_node.branch {
30            node = node.with_attribute("memory.branch", branch.clone());
31        }
32        for (key, value) in &memory_node.attributes {
33            node = node.with_attribute(format!("memory.{key}"), value.clone());
34        }
35        ids.insert(memory_node.id.clone(), node.id.clone());
36        nodes.push(node);
37    }
38
39    let mut edges = Vec::with_capacity(view.facts.len());
40    for fact in &view.facts {
41        let source = endpoint(&ids, &fact.source)?;
42        let target = endpoint(&ids, &fact.target)?;
43        let primary = &fact.evidence[0];
44        let provenance = Provenance::new(
45            primary.source.clone(),
46            EvidenceKind::from_str(&primary.kind)?,
47            graph_confidence(fact.confidence),
48        )?
49        .with_detail(
50            primary
51                .locator
52                .clone()
53                .unwrap_or_else(|| fact.id.to_string()),
54        );
55        let evidence = fact
56            .evidence
57            .iter()
58            .map(|item| {
59                AttributeValue::String(format!(
60                    "{}:{}:{}",
61                    item.kind,
62                    item.source,
63                    item.locator.as_deref().unwrap_or("")
64                ))
65            })
66            .collect::<Vec<_>>();
67        let mut edge = Edge::new(
68            source,
69            target,
70            EdgeKind::from_str(&fact.relation)?,
71            provenance,
72        )
73        .with_attribute("memory.fact_id", fact.id.to_string())
74        .with_attribute("memory.valid_from", fact.valid_from.as_unix_micros())
75        .with_attribute("memory.recorded_at", fact.recorded_at.as_unix_micros())
76        .with_attribute(
77            "memory.confidence_bps",
78            u64::from(fact.confidence.basis_points()),
79        )
80        .with_attribute("memory.evidence", AttributeValue::List(evidence));
81        if let Some(valid_until) = fact.valid_until {
82            edge = edge.with_attribute("memory.valid_until", valid_until.as_unix_micros());
83        }
84        if let Some(supersedes) = &fact.supersedes {
85            edge = edge.with_attribute("memory.supersedes", supersedes.to_string());
86        }
87        edges.push(edge);
88    }
89    Ok(Graph::try_from_sorted_nodes(nodes, edges)?)
90}
91
92fn endpoint(ids: &BTreeMap<EntityId, NodeId>, id: &EntityId) -> Result<NodeId> {
93    ids.get(id)
94        .cloned()
95        .ok_or_else(|| MemoryError::MissingEntity { id: id.to_string() })
96}
97
98const fn graph_confidence(confidence: MemoryConfidence) -> Confidence {
99    match confidence.basis_points() {
100        9_500..=10_000 => Confidence::Exact,
101        8_000..=9_499 => Confidence::High,
102        5_000..=7_999 => Confidence::Medium,
103        _ => Confidence::Low,
104    }
105}