1use std::collections::{HashSet, VecDeque};
21
22use super::graph::EventDag;
23
24#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
26pub enum LcaError {
27 #[error("event not found in DAG: {0}")]
29 EventNotFound(String),
30}
31
32pub fn find_lca(dag: &EventDag, tip_a: &str, tip_b: &str) -> Result<Option<String>, LcaError> {
52 if !dag.contains(tip_a) {
54 return Err(LcaError::EventNotFound(tip_a.to_string()));
55 }
56 if !dag.contains(tip_b) {
57 return Err(LcaError::EventNotFound(tip_b.to_string()));
58 }
59
60 if tip_a == tip_b {
62 return Ok(Some(tip_a.to_string()));
63 }
64
65 let mut visited_a: HashSet<String> = HashSet::new();
68 let mut visited_b: HashSet<String> = HashSet::new();
69 let mut queue_a: VecDeque<String> = VecDeque::new();
70 let mut queue_b: VecDeque<String> = VecDeque::new();
71
72 visited_a.insert(tip_a.to_string());
74 visited_b.insert(tip_b.to_string());
75 queue_a.push_back(tip_a.to_string());
76 queue_b.push_back(tip_b.to_string());
77
78 if visited_b.contains(tip_a) {
80 return Ok(Some(tip_a.to_string()));
81 }
82 if visited_a.contains(tip_b) {
83 return Ok(Some(tip_b.to_string()));
84 }
85
86 loop {
88 let a_done = queue_a.is_empty();
89 let b_done = queue_b.is_empty();
90
91 if a_done && b_done {
92 return Ok(None);
94 }
95
96 if !a_done && let Some(lca) = bfs_step(dag, &mut queue_a, &mut visited_a, &visited_b) {
98 return Ok(Some(lca));
99 }
100
101 if !b_done && let Some(lca) = bfs_step(dag, &mut queue_b, &mut visited_b, &visited_a) {
103 return Ok(Some(lca));
104 }
105 }
106}
107
108fn bfs_step(
111 dag: &EventDag,
112 queue: &mut VecDeque<String>,
113 visited: &mut HashSet<String>,
114 other_visited: &HashSet<String>,
115) -> Option<String> {
116 let current = queue.pop_front()?;
117
118 if let Some(node) = dag.get(¤t) {
119 for parent_hash in &node.parents {
120 if visited.insert(parent_hash.clone()) {
121 if other_visited.contains(parent_hash) {
123 return Some(parent_hash.clone());
125 }
126 queue.push_back(parent_hash.clone());
127 }
128 }
129 }
130
131 None
132}
133
134pub fn find_all_lcas(dag: &EventDag, tip_a: &str, tip_b: &str) -> Result<Vec<String>, LcaError> {
147 if !dag.contains(tip_a) {
148 return Err(LcaError::EventNotFound(tip_a.to_string()));
149 }
150 if !dag.contains(tip_b) {
151 return Err(LcaError::EventNotFound(tip_b.to_string()));
152 }
153
154 if tip_a == tip_b {
155 return Ok(vec![tip_a.to_string()]);
156 }
157
158 let mut ancestors_a = dag.ancestors(tip_a);
160 ancestors_a.insert(tip_a.to_string());
161 let mut ancestors_b = dag.ancestors(tip_b);
162 ancestors_b.insert(tip_b.to_string());
163
164 let common: HashSet<&String> = ancestors_a.intersection(&ancestors_b).collect();
166
167 if common.is_empty() {
168 return Ok(vec![]);
169 }
170
171 let mut lcas: Vec<String> = Vec::new();
174 for &ca in &common {
175 let has_common_child = dag
176 .get(ca)
177 .is_some_and(|node| node.children.iter().any(|c| common.contains(c)));
178
179 if !has_common_child {
180 lcas.push(ca.clone());
181 }
182 }
183
184 lcas.sort(); Ok(lcas)
186}
187
188#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::dag::graph::EventDag;
196 use crate::event::Event;
197 use crate::event::data::{CreateData, EventData, MoveData, UpdateData};
198 use crate::event::types::EventType;
199 use crate::event::writer::write_event;
200 use crate::model::item::{Kind, State, Urgency};
201 use crate::model::item_id::ItemId;
202 use std::collections::BTreeMap;
203
204 fn make_root(ts: i64, agent: &str) -> Event {
209 let mut event = Event {
210 wall_ts_us: ts,
211 agent: agent.into(),
212 itc: "itc:AQ".into(),
213 parents: vec![],
214 event_type: EventType::Create,
215 item_id: ItemId::new_unchecked("bn-test"),
216 data: EventData::Create(CreateData {
217 title: format!("Root by {agent}"),
218 kind: Kind::Task,
219 size: None,
220 urgency: Urgency::Default,
221 labels: vec![],
222 parent: None,
223 causation: None,
224 description: None,
225 extra: BTreeMap::new(),
226 }),
227 event_hash: String::new(),
228 };
229 write_event(&mut event).unwrap();
230 event
231 }
232
233 fn make_child(ts: i64, parents: &[&str], agent: &str) -> Event {
234 let mut event = Event {
235 wall_ts_us: ts,
236 agent: agent.into(),
237 itc: format!("itc:AQ.{ts}"),
238 parents: parents.iter().map(|s| (*s).to_string()).collect(),
239 event_type: EventType::Move,
240 item_id: ItemId::new_unchecked("bn-test"),
241 data: EventData::Move(MoveData {
242 state: State::Doing,
243 reason: None,
244 extra: BTreeMap::new(),
245 }),
246 event_hash: String::new(),
247 };
248 write_event(&mut event).unwrap();
249 event
250 }
251
252 fn make_update(ts: i64, parents: &[&str], field: &str, agent: &str) -> Event {
253 let mut event = Event {
254 wall_ts_us: ts,
255 agent: agent.into(),
256 itc: format!("itc:AQ.{ts}"),
257 parents: parents.iter().map(|s| (*s).to_string()).collect(),
258 event_type: EventType::Update,
259 item_id: ItemId::new_unchecked("bn-test"),
260 data: EventData::Update(UpdateData {
261 field: field.into(),
262 value: serde_json::json!("new-value"),
263 extra: BTreeMap::new(),
264 }),
265 event_hash: String::new(),
266 };
267 write_event(&mut event).unwrap();
268 event
269 }
270
271 #[test]
276 fn lca_same_tip() {
277 let root = make_root(1_000, "agent-a");
278 let dag = EventDag::from_events(&[root.clone()]);
279
280 let lca = find_lca(&dag, &root.event_hash, &root.event_hash).unwrap();
281 assert_eq!(lca, Some(root.event_hash));
282 }
283
284 #[test]
285 fn lca_event_not_found() {
286 let dag = EventDag::new();
287 let err = find_lca(&dag, "blake3:nope", "blake3:also-nope").unwrap_err();
288 assert!(matches!(err, LcaError::EventNotFound(_)));
289 }
290
291 #[test]
292 fn lca_one_is_ancestor_of_other() {
293 let root = make_root(1_000, "agent-a");
295 let child = make_child(2_000, &[&root.event_hash], "agent-a");
296 let dag = EventDag::from_events(&[root.clone(), child.clone()]);
297
298 let lca = find_lca(&dag, &root.event_hash, &child.event_hash).unwrap();
300 assert_eq!(lca, Some(root.event_hash.clone()));
301
302 let lca2 = find_lca(&dag, &child.event_hash, &root.event_hash).unwrap();
304 assert_eq!(lca2, Some(root.event_hash));
305 }
306
307 #[test]
308 fn lca_simple_fork() {
309 let root = make_root(1_000, "agent-a");
313 let left = make_child(2_000, &[&root.event_hash], "agent-a");
314 let right = make_child(2_100, &[&root.event_hash], "agent-b");
315 let dag = EventDag::from_events(&[root.clone(), left.clone(), right.clone()]);
316
317 let lca = find_lca(&dag, &left.event_hash, &right.event_hash).unwrap();
318 assert_eq!(lca, Some(root.event_hash));
319 }
320
321 #[test]
322 fn lca_deep_fork() {
323 let root = make_root(1_000, "agent-a");
326 let a = make_child(2_000, &[&root.event_hash], "agent-a");
327 let b = make_child(3_000, &[&a.event_hash], "agent-a");
328 let left = make_child(4_000, &[&b.event_hash], "agent-a");
329 let right = make_child(4_100, &[&b.event_hash], "agent-b");
330
331 let dag = EventDag::from_events(&[
332 root.clone(),
333 a.clone(),
334 b.clone(),
335 left.clone(),
336 right.clone(),
337 ]);
338
339 let lca = find_lca(&dag, &left.event_hash, &right.event_hash).unwrap();
340 assert_eq!(lca, Some(b.event_hash));
341 }
342
343 #[test]
344 fn lca_asymmetric_depth() {
345 let root = make_root(1_000, "agent-a");
348 let a = make_child(2_000, &[&root.event_hash], "agent-a");
349 let b = make_child(3_000, &[&a.event_hash], "agent-a");
350 let c = make_child(4_000, &[&b.event_hash], "agent-a");
351 let left = make_child(5_000, &[&c.event_hash], "agent-a");
352 let right = make_child(2_100, &[&root.event_hash], "agent-b");
353
354 let dag = EventDag::from_events(&[
355 root.clone(),
356 a.clone(),
357 b.clone(),
358 c.clone(),
359 left.clone(),
360 right.clone(),
361 ]);
362
363 let lca = find_lca(&dag, &left.event_hash, &right.event_hash).unwrap();
364 assert_eq!(lca, Some(root.event_hash));
365 }
366
367 #[test]
368 fn lca_diamond_after_fork() {
369 let root = make_root(1_000, "agent-a");
377 let a1 = make_child(2_000, &[&root.event_hash], "agent-a");
378 let b1 = make_child(2_100, &[&root.event_hash], "agent-b");
379 let merge = make_child(3_000, &[&a1.event_hash, &b1.event_hash], "agent-a");
380 let a2 = make_child(4_000, &[&merge.event_hash], "agent-a");
381 let b2 = make_child(4_100, &[&merge.event_hash], "agent-b");
382
383 let dag = EventDag::from_events(&[
384 root.clone(),
385 a1.clone(),
386 b1.clone(),
387 merge.clone(),
388 a2.clone(),
389 b2.clone(),
390 ]);
391
392 let lca = find_lca(&dag, &a2.event_hash, &b2.event_hash).unwrap();
394 assert_eq!(lca, Some(merge.event_hash));
395 }
396
397 #[test]
398 fn lca_disjoint_roots_returns_none() {
399 let root_a = make_root(1_000, "agent-a");
401 let root_b = make_root(1_100, "agent-b");
402 let dag = EventDag::from_events(&[root_a.clone(), root_b.clone()]);
403
404 let lca = find_lca(&dag, &root_a.event_hash, &root_b.event_hash).unwrap();
405 assert_eq!(lca, None);
406 }
407
408 #[test]
409 fn lca_is_symmetric() {
410 let root = make_root(1_000, "agent-a");
411 let left = make_child(2_000, &[&root.event_hash], "agent-a");
412 let right = make_child(2_100, &[&root.event_hash], "agent-b");
413 let dag = EventDag::from_events(&[root.clone(), left.clone(), right.clone()]);
414
415 let lca_ab = find_lca(&dag, &left.event_hash, &right.event_hash).unwrap();
416 let lca_ba = find_lca(&dag, &right.event_hash, &left.event_hash).unwrap();
417 assert_eq!(lca_ab, lca_ba, "LCA must be symmetric");
418 }
419
420 #[test]
425 fn all_lcas_simple_fork() {
426 let root = make_root(1_000, "agent-a");
427 let left = make_child(2_000, &[&root.event_hash], "agent-a");
428 let right = make_child(2_100, &[&root.event_hash], "agent-b");
429 let dag = EventDag::from_events(&[root.clone(), left.clone(), right.clone()]);
430
431 let lcas = find_all_lcas(&dag, &left.event_hash, &right.event_hash).unwrap();
432 assert_eq!(lcas, vec![root.event_hash]);
433 }
434
435 #[test]
436 fn all_lcas_criss_cross_merge() {
437 let root = make_root(1_000, "agent-a");
477 let a1 = make_child(2_000, &[&root.event_hash], "agent-a");
478 let b1 = make_child(2_100, &[&root.event_hash], "agent-b");
479 let ma = make_child(3_000, &[&a1.event_hash, &b1.event_hash], "agent-a");
481 let mb = make_update(3_100, &[&a1.event_hash, &b1.event_hash], "title", "agent-b");
482 let a2 = make_child(4_000, &[&ma.event_hash], "agent-a");
484 let b2 = make_update(4_100, &[&mb.event_hash], "desc", "agent-b");
485
486 let dag = EventDag::from_events(&[
487 root.clone(),
488 a1.clone(),
489 b1.clone(),
490 ma.clone(),
491 mb.clone(),
492 a2.clone(),
493 b2.clone(),
494 ]);
495
496 let mut lcas = find_all_lcas(&dag, &a2.event_hash, &b2.event_hash).unwrap();
497 lcas.sort();
498 assert_eq!(lcas.len(), 2);
503 let mut expected = vec![a1.event_hash.clone(), b1.event_hash.clone()];
504 expected.sort();
505 assert_eq!(lcas, expected);
506 }
507
508 #[test]
509 fn all_lcas_same_tip() {
510 let root = make_root(1_000, "agent-a");
511 let dag = EventDag::from_events(&[root.clone()]);
512
513 let lcas = find_all_lcas(&dag, &root.event_hash, &root.event_hash).unwrap();
514 assert_eq!(lcas, vec![root.event_hash]);
515 }
516
517 #[test]
518 fn all_lcas_disjoint() {
519 let root_a = make_root(1_000, "agent-a");
520 let root_b = make_root(1_100, "agent-b");
521 let dag = EventDag::from_events(&[root_a.clone(), root_b.clone()]);
522
523 let lcas = find_all_lcas(&dag, &root_a.event_hash, &root_b.event_hash).unwrap();
524 assert!(lcas.is_empty());
525 }
526
527 #[test]
528 fn all_lcas_event_not_found() {
529 let dag = EventDag::new();
530 let err = find_all_lcas(&dag, "blake3:nope", "blake3:also-nope").unwrap_err();
531 assert!(matches!(err, LcaError::EventNotFound(_)));
532 }
533}