Skip to main content

evidential_protocol/
provenance.rs

1//! Root Provenance Tracing for the Evidential Protocol.
2//!
3//! Builds directed acyclic chains of [`ProvenanceNode`]s, each carrying an
4//! [`Evidence`] record. Supports tracing from any leaf back to its root,
5//! computing chain confidence, and identifying the weakest evidential link.
6
7use crate::types::*;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12static NODE_COUNTER: AtomicU64 = AtomicU64::new(1);
13
14// ---------------------------------------------------------------------------
15// ProvenanceNode
16// ---------------------------------------------------------------------------
17
18/// A single node in a provenance chain.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ProvenanceNode {
21    /// Unique identifier for this node (format: `prov-{n}`).
22    pub id: String,
23    /// The claim this node represents.
24    pub claim: String,
25    /// Epistemic evidence backing the claim.
26    pub evidence: Evidence,
27    /// The agent or system that produced this node.
28    pub producer: String,
29    /// ISO-8601 timestamp of when this node was created.
30    pub timestamp: String,
31    /// Parent node ID, or `None` if this is a root.
32    pub parent_id: Option<String>,
33}
34
35// ---------------------------------------------------------------------------
36// ProvenanceChain
37// ---------------------------------------------------------------------------
38
39/// A collection of provenance nodes forming one or more chains.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ProvenanceChain {
42    nodes: HashMap<String, ProvenanceNode>,
43}
44
45impl Default for ProvenanceChain {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl ProvenanceChain {
52    /// Create an empty provenance chain.
53    pub fn new() -> Self {
54        Self {
55            nodes: HashMap::new(),
56        }
57    }
58
59    /// Add a new provenance node and return a reference to it.
60    ///
61    /// Node IDs are auto-generated using a monotonic counter (`prov-1`, `prov-2`, ...).
62    pub fn add_node(
63        &mut self,
64        claim: &str,
65        evidence: Evidence,
66        producer: &str,
67        parent_id: Option<&str>,
68    ) -> &ProvenanceNode {
69        let id = format!("prov-{}", NODE_COUNTER.fetch_add(1, Ordering::Relaxed));
70        let node = ProvenanceNode {
71            id: id.clone(),
72            claim: claim.to_string(),
73            evidence,
74            producer: producer.to_string(),
75            timestamp: chrono::Utc::now().to_rfc3339(),
76            parent_id: parent_id.map(|s| s.to_string()),
77        };
78        self.nodes.insert(id.clone(), node);
79        self.nodes.get(&id).unwrap()
80    }
81
82    /// Trace from a node back to the root, returning nodes in leaf-to-root order.
83    pub fn trace_to_root(&self, node_id: &str) -> Vec<&ProvenanceNode> {
84        let mut chain = Vec::new();
85        let mut current = node_id;
86        while let Some(node) = self.nodes.get(current) {
87            chain.push(node);
88            match &node.parent_id {
89                Some(pid) => current = pid.as_str(),
90                None => break,
91            }
92        }
93        chain
94    }
95
96    /// Return the depth of a node (root = 0).
97    pub fn get_depth(&self, node_id: &str) -> usize {
98        let chain = self.trace_to_root(node_id);
99        if chain.is_empty() {
100            0
101        } else {
102            chain.len() - 1
103        }
104    }
105
106    /// Return all root nodes (nodes with no parent).
107    pub fn get_roots(&self) -> Vec<&ProvenanceNode> {
108        self.nodes
109            .values()
110            .filter(|n| n.parent_id.is_none())
111            .collect()
112    }
113
114    /// Return all leaf nodes (nodes that are not referenced as a parent by any other node).
115    pub fn get_leaves(&self) -> Vec<&ProvenanceNode> {
116        let parent_ids: std::collections::HashSet<&str> = self
117            .nodes
118            .values()
119            .filter_map(|n| n.parent_id.as_deref())
120            .collect();
121        self.nodes
122            .values()
123            .filter(|n| !parent_ids.contains(n.id.as_str()))
124            .collect()
125    }
126
127    /// Return the weakest [`EvidenceClass`] along the chain from a node to root.
128    pub fn weakest_link(&self, node_id: &str) -> EvidenceClass {
129        let chain = self.trace_to_root(node_id);
130        chain
131            .iter()
132            .map(|n| n.evidence.class)
133            .min_by_key(|c| c.strength())
134            .unwrap_or(EvidenceClass::Conjecture)
135    }
136
137    /// Compute the product of confidence values along the chain from a node to root.
138    ///
139    /// Returns 0.0 if the node is not found.
140    pub fn chain_confidence(&self, node_id: &str) -> f64 {
141        let chain = self.trace_to_root(node_id);
142        if chain.is_empty() {
143            return 0.0;
144        }
145        chain.iter().map(|n| n.evidence.confidence).product()
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    fn make_evidence(class: EvidenceClass, confidence: f64) -> Evidence {
154        Evidence {
155            class,
156            confidence,
157            source: "test".to_string(),
158            reasoning: None,
159            timestamp: chrono::Utc::now().to_rfc3339(),
160            ttl: None,
161            sources: None,
162        }
163    }
164
165    #[test]
166    fn chain_operations() {
167        let mut chain = ProvenanceChain::new();
168        let root = chain.add_node(
169            "root claim",
170            make_evidence(EvidenceClass::Direct, 0.95),
171            "agent-a",
172            None,
173        );
174        let root_id = root.id.clone();
175
176        let mid = chain.add_node(
177            "derived claim",
178            make_evidence(EvidenceClass::Inferred, 0.8),
179            "agent-b",
180            Some(&root_id),
181        );
182        let mid_id = mid.id.clone();
183
184        let leaf = chain.add_node(
185            "leaf claim",
186            make_evidence(EvidenceClass::Reported, 0.6),
187            "agent-c",
188            Some(&mid_id),
189        );
190        let leaf_id = leaf.id.clone();
191
192        assert_eq!(chain.get_depth(&root_id), 0);
193        assert_eq!(chain.get_depth(&mid_id), 1);
194        assert_eq!(chain.get_depth(&leaf_id), 2);
195
196        let trace = chain.trace_to_root(&leaf_id);
197        assert_eq!(trace.len(), 3);
198        assert_eq!(trace[0].id, leaf_id);
199        assert_eq!(trace[2].id, root_id);
200
201        assert_eq!(chain.get_roots().len(), 1);
202        assert_eq!(chain.get_leaves().len(), 1);
203
204        assert_eq!(chain.weakest_link(&leaf_id), EvidenceClass::Reported);
205
206        let conf = chain.chain_confidence(&leaf_id);
207        let expected = 0.95 * 0.8 * 0.6;
208        assert!((conf - expected).abs() < 1e-10);
209    }
210}