evidential_protocol/
provenance.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ProvenanceNode {
21 pub id: String,
23 pub claim: String,
25 pub evidence: Evidence,
27 pub producer: String,
29 pub timestamp: String,
31 pub parent_id: Option<String>,
33}
34
35#[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 pub fn new() -> Self {
54 Self {
55 nodes: HashMap::new(),
56 }
57 }
58
59 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 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 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 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 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 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 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}