1#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use crate::error::FoldError;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct AnchorRef {
13 pub id: Uuid,
15 pub kind: String,
17 pub stable_id: Option<String>,
19}
20
21#[derive(Debug, Clone, Default)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26pub struct AnchorGraph {
27 pub nodes: Vec<AnchorRef>,
29 pub edges: Vec<(Uuid, Uuid, String)>,
31}
32
33impl AnchorGraph {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn add_node(&mut self, anchor: AnchorRef) {
41 self.nodes.push(anchor);
42 }
43
44 pub fn add_edge(&mut self, from: Uuid, to: Uuid, relation: impl Into<String>) {
46 self.edges.push((from, to, relation.into()));
47 }
48
49 pub fn find_node(&self, id: Uuid) -> Option<&AnchorRef> {
51 self.nodes.iter().find(|n| n.id == id)
52 }
53
54 pub fn outgoing(&self, from: Uuid) -> impl Iterator<Item = (Uuid, &str)> {
56 self.edges
57 .iter()
58 .filter(move |(f, _, _)| *f == from)
59 .map(|(_, to, rel)| (*to, rel.as_str()))
60 }
61
62 pub fn incoming(&self, to: Uuid) -> impl Iterator<Item = (Uuid, &str)> {
64 self.edges
65 .iter()
66 .filter(move |(_, t, _)| *t == to)
67 .map(|(from, _, rel)| (*from, rel.as_str()))
68 }
69}
70
71pub trait Anchor: Send + Sync {
73 fn trace(
75 &self,
76 graph: &AnchorGraph,
77 start: &AnchorRef,
78 max_depth: usize,
79 ) -> Result<Vec<AnchorRef>, FoldError>;
80
81 fn credit(
83 &self,
84 graph: &AnchorGraph,
85 outcome: &AnchorRef,
86 max_depth: usize,
87 ) -> Result<Vec<(AnchorRef, f64)>, FoldError>;
88}
89
90#[derive(Debug, Clone, Copy, Default)]
95pub struct BfsAnchor;
96
97impl Anchor for BfsAnchor {
98 fn trace(
99 &self,
100 graph: &AnchorGraph,
101 start: &AnchorRef,
102 max_depth: usize,
103 ) -> Result<Vec<AnchorRef>, FoldError> {
104 if graph.find_node(start.id).is_none() {
105 return Err(FoldError::AnchorNotFound(start.id.to_string()));
106 }
107
108 let mut visited = std::collections::HashSet::new();
109 let mut result = Vec::new();
110 let mut queue = std::collections::VecDeque::new();
111
112 visited.insert(start.id);
113 queue.push_back((start.id, 0usize));
114
115 while let Some((current_id, depth)) = queue.pop_front() {
116 if let Some(node) = graph.find_node(current_id) {
117 if current_id != start.id {
118 result.push(node.clone());
119 }
120
121 if depth < max_depth {
122 for (next_id, _rel) in graph.outgoing(current_id) {
123 if visited.insert(next_id) {
124 queue.push_back((next_id, depth + 1));
125 }
126 }
127 }
128 }
129 }
130
131 Ok(result)
132 }
133
134 fn credit(
135 &self,
136 graph: &AnchorGraph,
137 outcome: &AnchorRef,
138 max_depth: usize,
139 ) -> Result<Vec<(AnchorRef, f64)>, FoldError> {
140 if graph.find_node(outcome.id).is_none() {
141 return Err(FoldError::AnchorNotFound(outcome.id.to_string()));
142 }
143
144 let mut visited = std::collections::HashSet::new();
145 let mut result = Vec::new();
146 let mut queue = std::collections::VecDeque::new();
147
148 visited.insert(outcome.id);
149 queue.push_back((outcome.id, 0usize, 1.0f64));
150
151 while let Some((current_id, depth, weight)) = queue.pop_front() {
152 if current_id != outcome.id {
153 if let Some(node) = graph.find_node(current_id) {
154 result.push((node.clone(), weight));
155 }
156 }
157
158 if depth < max_depth {
159 let predecessors: Vec<(Uuid, f64)> = graph
160 .incoming(current_id)
161 .filter(|(id, _)| visited.insert(*id))
162 .map(|(id, _)| (id, weight * 0.5))
163 .collect();
164
165 for (pred_id, pred_weight) in predecessors {
166 queue.push_back((pred_id, depth + 1, pred_weight));
167 }
168 }
169 }
170
171 Ok(result)
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn make_ref(id: u128, kind: &str) -> AnchorRef {
180 AnchorRef {
181 id: Uuid::from_u128(id),
182 kind: kind.to_string(),
183 stable_id: None,
184 }
185 }
186
187 #[test]
188 fn test_anchor_ref_fields() {
189 let r = AnchorRef {
190 id: Uuid::new_v4(),
191 kind: "paper".into(),
192 stable_id: Some("doi:10.1234/x".into()),
193 };
194 assert_eq!(r.kind, "paper");
195 assert!(r.stable_id.is_some());
196 }
197
198 #[test]
199 fn test_anchor_graph_add_and_find() {
200 let mut graph = AnchorGraph::new();
201 let a = make_ref(1, "record");
202 let b = make_ref(2, "source");
203 graph.add_node(a.clone());
204 graph.add_node(b.clone());
205 graph.add_edge(a.id, b.id, "derives_from");
206
207 assert!(graph.find_node(a.id).is_some());
208 assert!(graph.find_node(Uuid::nil()).is_none());
209 }
210
211 #[test]
212 fn test_bfs_anchor_trace_not_found() {
213 let graph = AnchorGraph::new();
214 let unknown = make_ref(99, "unknown");
215 let err = BfsAnchor.trace(&graph, &unknown, 5).unwrap_err();
216 assert!(matches!(err, FoldError::AnchorNotFound(_)));
217 }
218
219 #[test]
220 fn test_bfs_anchor_trace_chain() {
221 let mut graph = AnchorGraph::new();
222 let a = make_ref(1, "record");
223 let b = make_ref(2, "source");
224 let c = make_ref(3, "paper");
225 graph.add_node(a.clone());
226 graph.add_node(b.clone());
227 graph.add_node(c.clone());
228 graph.add_edge(a.id, b.id, "derives_from");
229 graph.add_edge(b.id, c.id, "uses");
230
231 let chain = BfsAnchor.trace(&graph, &a, 5).unwrap();
232 assert_eq!(chain.len(), 2);
233 assert!(chain.iter().any(|r| r.id == b.id));
234 assert!(chain.iter().any(|r| r.id == c.id));
235 }
236
237 #[test]
238 fn test_bfs_anchor_trace_max_depth() {
239 let mut graph = AnchorGraph::new();
240 let nodes: Vec<AnchorRef> = (1..=5).map(|i| make_ref(i, "node")).collect();
241 for n in &nodes {
242 graph.add_node(n.clone());
243 }
244 for i in 0..4 {
245 graph.add_edge(nodes[i].id, nodes[i + 1].id, "next");
246 }
247
248 let chain = BfsAnchor.trace(&graph, &nodes[0], 1).unwrap();
250 assert_eq!(chain.len(), 1);
251 assert_eq!(chain[0].id, nodes[1].id);
252 }
253
254 #[test]
255 fn test_bfs_anchor_credit_not_found() {
256 let graph = AnchorGraph::new();
257 let unknown = make_ref(99, "unknown");
258 let err = BfsAnchor.credit(&graph, &unknown, 5).unwrap_err();
259 assert!(matches!(err, FoldError::AnchorNotFound(_)));
260 }
261
262 #[test]
263 fn test_bfs_anchor_credit_basic() {
264 let mut graph = AnchorGraph::new();
265 let source = make_ref(1, "paper");
266 let intermediate = make_ref(2, "record");
267 let outcome = make_ref(3, "composition");
268 graph.add_node(source.clone());
269 graph.add_node(intermediate.clone());
270 graph.add_node(outcome.clone());
271 graph.add_edge(source.id, intermediate.id, "uses");
273 graph.add_edge(intermediate.id, outcome.id, "derives_from");
274
275 let credits = BfsAnchor.credit(&graph, &outcome, 5).unwrap();
276 assert!(!credits.is_empty());
277 let inter_credit = credits.iter().find(|(r, _)| r.id == intermediate.id);
279 assert!(inter_credit.is_some());
280 assert!(inter_credit.unwrap().1 > 0.0f64);
281 }
282
283 #[test]
284 fn credit_weights_are_f64() {
285 let mut graph = AnchorGraph::new();
286 let source = make_ref(10, "source");
287 let outcome = make_ref(11, "outcome");
288 graph.add_node(source.clone());
289 graph.add_node(outcome.clone());
290 graph.add_edge(source.id, outcome.id, "causes");
291
292 let credits: Vec<(AnchorRef, f64)> = BfsAnchor.credit(&graph, &outcome, 2).unwrap();
293 assert!(!credits.is_empty());
294 let w: f64 = credits[0].1;
295 assert!(w > 0.0f64 && w <= 1.0f64);
296 }
297}