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 = "checkpoint_saved";
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}