1use std::collections::{BTreeMap, BTreeSet};
35
36use thiserror::Error;
37
38use crate::canonical::CanonicalRecord;
39use crate::clock::ClockTime;
40use crate::symbol::SymbolId;
41
42#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
44pub enum EdgeKind {
45 Supersedes,
47 Corrects,
49 StaleParent,
52 Reconfirms,
55}
56
57#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
59pub struct Edge {
60 pub kind: EdgeKind,
62 pub from: SymbolId,
64 pub to: SymbolId,
66 pub at: ClockTime,
68}
69
70impl Edge {
71 #[must_use]
74 pub fn try_from_record(record: &CanonicalRecord) -> Option<Self> {
75 let (kind, r) = match record {
76 CanonicalRecord::Supersedes(r) => (EdgeKind::Supersedes, r),
77 CanonicalRecord::Corrects(r) => (EdgeKind::Corrects, r),
78 CanonicalRecord::StaleParent(r) => (EdgeKind::StaleParent, r),
79 CanonicalRecord::Reconfirms(r) => (EdgeKind::Reconfirms, r),
80 _ => return None,
81 };
82 Some(Self {
83 kind,
84 from: r.from,
85 to: r.to,
86 at: r.at,
87 })
88 }
89}
90
91#[derive(Clone, Debug, Error, PartialEq, Eq)]
93pub enum DagError {
94 #[error("supersession cycle: {kind:?} edge {from:?} -> {to:?} would close a cycle")]
97 SupersessionCycle {
98 from: SymbolId,
100 to: SymbolId,
102 kind: EdgeKind,
104 },
105
106 #[error("self-edge is forbidden: {kind:?} edge on {memory:?}")]
109 SelfEdge {
110 memory: SymbolId,
112 kind: EdgeKind,
114 },
115}
116
117#[derive(Clone, Debug, Default, PartialEq, Eq)]
124pub struct SupersessionDag {
125 edges: Vec<Edge>,
126 outgoing: BTreeMap<SymbolId, Vec<usize>>,
127 incoming: BTreeMap<SymbolId, Vec<usize>>,
128}
129
130impl SupersessionDag {
131 #[must_use]
133 pub fn new() -> Self {
134 Self::default()
135 }
136
137 pub fn add_edge(&mut self, edge: Edge) -> Result<(), DagError> {
146 if edge.from == edge.to {
147 return Err(DagError::SelfEdge {
148 memory: edge.from,
149 kind: edge.kind,
150 });
151 }
152 if self.reaches(edge.to, edge.from) {
153 tracing::warn!(
154 target: "mimir.dag.cycle_rejected",
155 from = %edge.from,
156 to = %edge.to,
157 edge_kind = ?edge.kind,
158 "supersession cycle rejected",
159 );
160 return Err(DagError::SupersessionCycle {
161 from: edge.from,
162 to: edge.to,
163 kind: edge.kind,
164 });
165 }
166 let idx = self.edges.len();
167 self.outgoing.entry(edge.from).or_default().push(idx);
168 self.incoming.entry(edge.to).or_default().push(idx);
169 self.edges.push(edge);
170 Ok(())
171 }
172
173 fn reaches(&self, start: SymbolId, target: SymbolId) -> bool {
176 if start == target {
177 return true;
178 }
179 let mut visited = BTreeSet::new();
180 let mut stack = vec![start];
181 while let Some(node) = stack.pop() {
182 if !visited.insert(node) {
183 continue;
184 }
185 if let Some(indices) = self.outgoing.get(&node) {
186 for &i in indices {
187 let next = self.edges[i].to;
188 if next == target {
189 return true;
190 }
191 stack.push(next);
192 }
193 }
194 }
195 false
196 }
197
198 #[must_use]
200 pub fn edges(&self) -> &[Edge] {
201 &self.edges
202 }
203
204 #[must_use]
206 pub fn len(&self) -> usize {
207 self.edges.len()
208 }
209
210 #[must_use]
212 pub fn is_empty(&self) -> bool {
213 self.edges.is_empty()
214 }
215
216 pub fn edges_from(&self, memory: SymbolId) -> impl Iterator<Item = &Edge> {
219 self.outgoing
220 .get(&memory)
221 .into_iter()
222 .flat_map(move |idxs| idxs.iter().map(move |&i| &self.edges[i]))
223 }
224
225 pub fn edges_to(&self, memory: SymbolId) -> impl Iterator<Item = &Edge> {
228 self.incoming
229 .get(&memory)
230 .into_iter()
231 .flat_map(move |idxs| idxs.iter().map(move |&i| &self.edges[i]))
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 fn m(n: u64) -> SymbolId {
240 SymbolId::new(n)
241 }
242
243 fn at(millis: u64) -> ClockTime {
244 ClockTime::try_from_millis(millis).expect("non-sentinel")
245 }
246
247 fn edge(kind: EdgeKind, from: u64, to: u64) -> Edge {
248 Edge {
249 kind,
250 from: m(from),
251 to: m(to),
252 at: at(1_000_000),
253 }
254 }
255
256 #[test]
257 fn empty_dag_has_zero_edges() {
258 let dag = SupersessionDag::new();
259 assert!(dag.is_empty());
260 assert_eq!(dag.len(), 0);
261 assert_eq!(dag.edges(), &[]);
262 assert!(dag.edges_from(m(0)).next().is_none());
263 assert!(dag.edges_to(m(0)).next().is_none());
264 }
265
266 #[test]
267 fn single_edge_adds_and_indexes() {
268 let mut dag = SupersessionDag::new();
269 let e = edge(EdgeKind::Supersedes, 1, 2);
270 dag.add_edge(e).expect("add");
271 assert_eq!(dag.len(), 1);
272 assert_eq!(dag.edges(), &[e]);
273 let out: Vec<_> = dag.edges_from(m(1)).copied().collect();
274 assert_eq!(out, vec![e]);
275 let inc: Vec<_> = dag.edges_to(m(2)).copied().collect();
276 assert_eq!(inc, vec![e]);
277 assert!(dag.edges_to(m(1)).next().is_none());
279 assert!(dag.edges_from(m(2)).next().is_none());
280 }
281
282 #[test]
283 fn self_edge_is_rejected() {
284 let mut dag = SupersessionDag::new();
285 let err = dag
286 .add_edge(edge(EdgeKind::Supersedes, 7, 7))
287 .expect_err("self edge");
288 assert_eq!(
289 err,
290 DagError::SelfEdge {
291 memory: m(7),
292 kind: EdgeKind::Supersedes
293 }
294 );
295 assert!(dag.is_empty(), "failed add must not mutate");
296 }
297
298 #[test]
299 fn direct_cycle_is_rejected() {
300 let mut dag = SupersessionDag::new();
302 dag.add_edge(edge(EdgeKind::Supersedes, 1, 2))
303 .expect("first");
304 let err = dag
305 .add_edge(edge(EdgeKind::Supersedes, 2, 1))
306 .expect_err("cycle");
307 assert!(matches!(
308 err,
309 DagError::SupersessionCycle {
310 from, to, kind: EdgeKind::Supersedes
311 } if from == m(2) && to == m(1)
312 ));
313 assert_eq!(dag.len(), 1, "failed add must not mutate");
314 }
315
316 #[test]
317 fn indirect_cycle_across_kinds_is_rejected() {
318 let mut dag = SupersessionDag::new();
322 dag.add_edge(edge(EdgeKind::Supersedes, 1, 2)).expect("a");
323 dag.add_edge(edge(EdgeKind::Corrects, 2, 3)).expect("b");
324 let err = dag
325 .add_edge(edge(EdgeKind::StaleParent, 3, 1))
326 .expect_err("3-cycle across kinds");
327 assert!(matches!(err, DagError::SupersessionCycle { .. }));
328 assert_eq!(dag.len(), 2);
329 }
330
331 #[test]
332 fn dag_shaped_additions_all_succeed() {
333 let mut dag = SupersessionDag::new();
335 dag.add_edge(edge(EdgeKind::Supersedes, 1, 2)).expect("12");
336 dag.add_edge(edge(EdgeKind::Supersedes, 1, 3)).expect("13");
337 dag.add_edge(edge(EdgeKind::Supersedes, 2, 4)).expect("24");
338 dag.add_edge(edge(EdgeKind::Supersedes, 3, 4)).expect("34");
339 assert_eq!(dag.len(), 4);
340 assert_eq!(dag.edges_to(m(4)).count(), 2);
342 assert_eq!(dag.edges_from(m(1)).count(), 2);
343 }
344
345 #[test]
346 fn edge_try_from_record_matches_every_edge_variant() {
347 use crate::canonical::{CanonicalRecord, EdgeRecord};
348
349 let r = EdgeRecord {
350 from: m(10),
351 to: m(11),
352 at: at(42),
353 };
354 let cases = [
355 (CanonicalRecord::Supersedes(r), EdgeKind::Supersedes),
356 (CanonicalRecord::Corrects(r), EdgeKind::Corrects),
357 (CanonicalRecord::StaleParent(r), EdgeKind::StaleParent),
358 (CanonicalRecord::Reconfirms(r), EdgeKind::Reconfirms),
359 ];
360 for (record, expected_kind) in cases {
361 let e = Edge::try_from_record(&record).expect("edge variant");
362 assert_eq!(e.kind, expected_kind);
363 assert_eq!(e.from, m(10));
364 assert_eq!(e.to, m(11));
365 assert_eq!(e.at, at(42));
366 }
367 }
368
369 #[test]
370 fn edge_try_from_record_rejects_non_edge_variants() {
371 use crate::canonical::{CanonicalRecord, CheckpointRecord};
372 let cp = CanonicalRecord::Checkpoint(CheckpointRecord {
373 episode_id: m(1),
374 at: at(1),
375 memory_count: 0,
376 });
377 assert!(Edge::try_from_record(&cp).is_none());
378 }
379
380 #[test]
381 fn failed_cycle_add_does_not_corrupt_indices() {
382 let mut dag = SupersessionDag::new();
386 dag.add_edge(edge(EdgeKind::Supersedes, 1, 2)).expect("a");
387 let _ = dag
388 .add_edge(edge(EdgeKind::Supersedes, 2, 1))
389 .expect_err("cycle");
390 assert_eq!(dag.len(), 1);
391 assert_eq!(dag.edges_from(m(1)).count(), 1);
392 assert_eq!(dag.edges_from(m(2)).count(), 0);
393 assert_eq!(dag.edges_to(m(1)).count(), 0);
394 assert_eq!(dag.edges_to(m(2)).count(), 1);
395 }
396}