KeiSeiKit-1.0/_primitives/_rust/kei-diff/src/diff.rs
Parfii-bot a4e667de10 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

106 lines
3.4 KiB
Rust

//! Structural JSON diff.
//!
//! Algorithm:
//! * Both objects → recurse per-key across the union (add/remove/recurse).
//! * Both arrays → index-based (recurse on overlap; add-tail or remove-tail
//! for length delta). NOT LCS — simpler, idempotent enough for drift
//! detection, and cheap (O(n)).
//! * Otherwise, if values differ → `replace`.
//! * Equal values → no-op.
//!
//! Rationale for skipping LCS: the consumer (kei-replay drift check) cares
//! about "does anything differ" and "at which logical coordinate", not
//! minimum-edit-distance. Index-based gives stable paths; LCS would produce
//! a smaller patch on shuffled arrays but with ambiguous paths.
use crate::op::{Op, Patch};
use crate::path::PathBuf;
use serde_json::Value;
/// Compute an RFC 6902 subset patch that transforms `old` into `new`.
/// Invariant: `apply(old, diff(old, new)) == new`.
pub fn diff(old: &Value, new: &Value) -> Patch {
let mut patch = Patch::new();
let mut path = PathBuf::new();
diff_recurse(old, new, &mut path, &mut patch);
patch
}
fn diff_recurse(old: &Value, new: &Value, path: &mut PathBuf, patch: &mut Patch) {
if old == new {
return;
}
match (old, new) {
(Value::Object(a), Value::Object(b)) => diff_objects(a, b, path, patch),
(Value::Array(a), Value::Array(b)) => diff_arrays(a, b, path, patch),
_ => patch.push(Op::Replace {
path: path.as_string(),
value: new.clone(),
}),
}
}
fn diff_objects(
a: &serde_json::Map<String, Value>,
b: &serde_json::Map<String, Value>,
path: &mut PathBuf,
patch: &mut Patch,
) {
// Removals: keys in `a` but not `b`. Emit in stable key order for determinism.
for key in a.keys() {
if !b.contains_key(key) {
path.push_key(key);
patch.push(Op::Remove { path: path.as_string() });
path.pop();
}
}
// Additions + recursion: iterate `b` in its key order.
for (key, b_val) in b {
path.push_key(key);
match a.get(key) {
None => patch.push(Op::Add {
path: path.as_string(),
value: b_val.clone(),
}),
Some(a_val) => diff_recurse(a_val, b_val, path, patch),
}
path.pop();
}
}
fn diff_arrays(a: &[Value], b: &[Value], path: &mut PathBuf, patch: &mut Patch) {
let common = a.len().min(b.len());
// Recurse on overlapping prefix.
for i in 0..common {
path.push_index(i);
diff_recurse(&a[i], &b[i], path, patch);
path.pop();
}
if a.len() > b.len() {
emit_array_truncate(a.len(), b.len(), path, patch);
} else if b.len() > a.len() {
emit_array_append(b, a.len(), path, patch);
}
}
// Remove trailing indices highest-first so surviving indices don't shift.
fn emit_array_truncate(old_len: usize, new_len: usize, path: &mut PathBuf, patch: &mut Patch) {
for i in (new_len..old_len).rev() {
path.push_index(i);
patch.push(Op::Remove { path: path.as_string() });
path.pop();
}
}
// Append new tail. Emit in ascending order so each add references the
// just-created length as the next insertion point.
fn emit_array_append(b: &[Value], old_len: usize, path: &mut PathBuf, patch: &mut Patch) {
for i in old_len..b.len() {
path.push_index(i);
patch.push(Op::Add {
path: path.as_string(),
value: b[i].clone(),
});
path.pop();
}
}