Skip to main content

weavatrix_memory/
graph_projection.rs

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