Skip to main content

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}