aivcs_core/diff/node_paths.rs
1use oxidized_state::RunEvent;
2
3/// A single node step extracted from a `RunEvent` stream.
4#[derive(Debug, Clone, PartialEq)]
5pub struct NodeStep {
6 pub seq: u64,
7 pub node_id: String,
8}
9
10/// The divergence point between two node traversal paths.
11#[derive(Debug, Clone, PartialEq)]
12pub struct NodeDivergence {
13 /// Node IDs that both paths share before the first mismatch.
14 pub common_prefix: Vec<String>,
15 /// Remaining steps only in path A after the divergence point.
16 pub tail_a: Vec<NodeStep>,
17 /// Remaining steps only in path B after the divergence point.
18 pub tail_b: Vec<NodeStep>,
19}
20
21/// The result of diffing two node traversal paths.
22#[derive(Debug, Clone, PartialEq)]
23pub struct NodePathDiff {
24 pub divergence: Option<NodeDivergence>,
25}
26
27impl NodePathDiff {
28 pub fn is_empty(&self) -> bool {
29 self.divergence.is_none()
30 }
31}
32
33// ---------------------------------------------------------------------------
34// Extraction
35// ---------------------------------------------------------------------------
36
37/// Extract the ordered node traversal path from a run event stream.
38///
39/// Only `"node_entered"` events are considered. Events without a valid
40/// `payload["node_id"]` string are silently skipped.
41pub fn extract_node_path(events: &[RunEvent]) -> Vec<NodeStep> {
42 events
43 .iter()
44 .filter(|e| e.kind == "node_entered")
45 .filter_map(|e| {
46 let node_id = e
47 .payload
48 .get("node_id")
49 .and_then(|v| v.as_str())
50 .map(|s| s.to_string())?;
51 Some(NodeStep {
52 seq: e.seq,
53 node_id,
54 })
55 })
56 .collect()
57}
58
59// ---------------------------------------------------------------------------
60// Public API
61// ---------------------------------------------------------------------------
62
63/// Diff two ordered `RunEvent` sequences by their node traversal paths.
64///
65/// Extracts `"node_entered"` events from each sequence, then walks both
66/// paths in lockstep to find the first divergence point. Returns
67/// `NodePathDiff { divergence: None }` when the paths are identical.
68pub fn diff_node_paths(a: &[RunEvent], b: &[RunEvent]) -> NodePathDiff {
69 let path_a = extract_node_path(a);
70 let path_b = extract_node_path(b);
71
72 let mut common_prefix = Vec::new();
73 let mut i = 0;
74
75 while i < path_a.len() && i < path_b.len() {
76 if path_a[i].node_id == path_b[i].node_id {
77 common_prefix.push(path_a[i].node_id.clone());
78 i += 1;
79 } else {
80 break;
81 }
82 }
83
84 if i == path_a.len() && i == path_b.len() {
85 NodePathDiff { divergence: None }
86 } else {
87 NodePathDiff {
88 divergence: Some(NodeDivergence {
89 common_prefix,
90 tail_a: path_a[i..].to_vec(),
91 tail_b: path_b[i..].to_vec(),
92 }),
93 }
94 }
95}