Skip to main content

aivcs_core/diff/
state_diff.rs

1use oxidized_state::RunEvent;
2use serde_json::Value;
3
4/// A single delta at an RFC 6901 JSON pointer path between two states.
5#[derive(Debug, Clone, PartialEq)]
6pub struct StateDelta {
7    /// RFC 6901 JSON pointer, e.g. `"/memory/0/context"`.
8    pub pointer: String,
9    /// Value in A (`Null` if absent).
10    pub before: Value,
11    /// Value in B (`Null` if absent).
12    pub after: Value,
13}
14
15/// The result of diffing two states at scoped JSON pointer paths.
16#[derive(Debug, Clone, PartialEq)]
17pub struct ScopedStateDiff {
18    pub deltas: Vec<StateDelta>,
19}
20
21impl ScopedStateDiff {
22    pub fn is_empty(&self) -> bool {
23        self.deltas.is_empty()
24    }
25}
26
27// ---------------------------------------------------------------------------
28// Extraction
29// ---------------------------------------------------------------------------
30
31/// Event kind emitted by the oxidizedgraph event adapter when a checkpoint is saved.
32pub const CHECKPOINT_SAVED_KIND: &str = "CheckpointSaved";
33
34/// Extract the payload of the last `"CheckpointSaved"` event from a run event stream.
35///
36/// Returns `None` if no checkpoint-saved events exist.
37pub fn extract_last_checkpoint(events: &[RunEvent]) -> Option<Value> {
38    events
39        .iter()
40        .rev()
41        .find(|e| e.kind == CHECKPOINT_SAVED_KIND)
42        .map(|e| e.payload.clone())
43}
44
45// ---------------------------------------------------------------------------
46// Public API
47// ---------------------------------------------------------------------------
48
49/// Diff two JSON values at the given RFC 6901 JSON pointer paths.
50///
51/// For each pointer, resolves the value in both `a` and `b`. If they differ
52/// (including one being absent while the other is present), a `StateDelta` is
53/// emitted. Pointers where both values are identical or both absent are skipped.
54pub fn diff_scoped_state(a: &Value, b: &Value, pointers: &[&str]) -> ScopedStateDiff {
55    let deltas = pointers
56        .iter()
57        .filter_map(|ptr| {
58            let val_a = a.pointer(ptr).unwrap_or(&Value::Null);
59            let val_b = b.pointer(ptr).unwrap_or(&Value::Null);
60
61            if val_a == val_b {
62                None
63            } else {
64                Some(StateDelta {
65                    pointer: (*ptr).to_string(),
66                    before: val_a.clone(),
67                    after: val_b.clone(),
68                })
69            }
70        })
71        .collect();
72
73    ScopedStateDiff { deltas }
74}
75
76/// Convenience: extract last checkpoint state from two event streams and diff
77/// at the given JSON pointer paths.
78///
79/// Returns an empty diff if either stream has no checkpoint events.
80pub fn diff_run_states(a: &[RunEvent], b: &[RunEvent], pointers: &[&str]) -> ScopedStateDiff {
81    let state_a = extract_last_checkpoint(a).unwrap_or(Value::Null);
82    let state_b = extract_last_checkpoint(b).unwrap_or(Value::Null);
83    diff_scoped_state(&state_a, &state_b, pointers)
84}