use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LayerFingerprint {
pub hash: String,
pub bytes: usize,
pub lines: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ContextFingerprint {
pub layers: BTreeMap<String, LayerFingerprint>,
pub order: Vec<String>,
pub full_hash: String,
pub total_bytes: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ContextDiff {
pub identical: bool,
pub changed: Vec<LayerDelta>,
pub added: Vec<String>,
pub removed: Vec<String>,
pub reordered: bool,
pub byte_delta: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LayerDelta {
pub layer: String,
pub before_bytes: usize,
pub after_bytes: usize,
pub before_lines: usize,
pub after_lines: usize,
}
fn short_sha(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
hex_of(&digest[..8])
}
fn hex_of(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn fingerprint_context(context: &str) -> ContextFingerprint {
let mut layers: BTreeMap<String, LayerFingerprint> = BTreeMap::new();
let mut order: Vec<String> = Vec::new();
let mut seen: BTreeMap<String, usize> = BTreeMap::new();
let mut current: Option<String> = None;
let mut body = String::new();
let flush = |name: Option<String>,
body: &mut String,
layers: &mut BTreeMap<String, LayerFingerprint>,
order: &mut Vec<String>,
seen: &mut BTreeMap<String, usize>| {
let raw = match name {
Some(n) => n,
None if body.trim().is_empty() => return,
None => "(preamble)".to_string(),
};
let count = seen.entry(raw.clone()).or_insert(0);
*count += 1;
let key = if *count == 1 {
raw
} else {
format!("{raw}#{count}")
};
layers.insert(
key.clone(),
LayerFingerprint {
hash: short_sha(body.as_bytes()),
bytes: body.len(),
lines: body.lines().count(),
},
);
order.push(key);
body.clear();
};
for line in context.lines() {
if let Some(header) = line.strip_prefix("## ") {
flush(
current.take(),
&mut body,
&mut layers,
&mut order,
&mut seen,
);
current = Some(header.trim().to_string());
} else {
body.push_str(line);
body.push('\n');
}
}
flush(current, &mut body, &mut layers, &mut order, &mut seen);
ContextFingerprint {
layers,
order,
full_hash: short_sha(context.as_bytes()),
total_bytes: context.len(),
}
}
pub fn diff(before: &ContextFingerprint, after: &ContextFingerprint) -> ContextDiff {
let mut changed = Vec::new();
let mut added = Vec::new();
let mut removed = Vec::new();
for (name, a) in &after.layers {
match before.layers.get(name) {
None => added.push(name.clone()),
Some(b) if b.hash != a.hash => changed.push(LayerDelta {
layer: name.clone(),
before_bytes: b.bytes,
after_bytes: a.bytes,
before_lines: b.lines,
after_lines: a.lines,
}),
Some(_) => {}
}
}
for name in before.layers.keys() {
if !after.layers.contains_key(name) {
removed.push(name.clone());
}
}
let reordered = before.order != after.order && added.is_empty() && removed.is_empty();
ContextDiff {
identical: before.full_hash == after.full_hash && before.total_bytes == after.total_bytes,
changed,
added,
removed,
reordered,
byte_delta: after.total_bytes as i64 - before.total_bytes as i64,
}
}
impl ContextDiff {
pub fn summary(&self) -> String {
if self.identical {
return "context identical — any metric delta here is noise".to_string();
}
let mut parts = Vec::new();
if !self.changed.is_empty() {
let names: Vec<&str> = self.changed.iter().map(|d| d.layer.as_str()).collect();
parts.push(format!("changed: {}", names.join(", ")));
}
if !self.added.is_empty() {
parts.push(format!("added: {}", self.added.join(", ")));
}
if !self.removed.is_empty() {
parts.push(format!("removed: {}", self.removed.join(", ")));
}
if self.reordered {
parts.push("layer order changed".to_string());
}
format!("{} ({:+} bytes)", parts.join("; "), self.byte_delta)
}
}
#[cfg(test)]
mod tests {
use super::*;
const CTX: &str = "\
## Identity
You are CAR.
## Current Facts
- deploy target is fly.io
- the db is postgres
## Recent Context
user: what is the target?
";
#[test]
fn identical_contexts_are_reported_identical() {
let d = diff(&fingerprint_context(CTX), &fingerprint_context(CTX));
assert!(d.identical);
assert!(d.changed.is_empty() && d.added.is_empty() && d.removed.is_empty());
assert_eq!(d.byte_delta, 0);
assert!(d.summary().contains("noise"));
}
#[test]
fn a_change_is_localised_to_its_layer() {
let after = CTX.replace("the db is postgres", "the db is sqlite");
let d = diff(&fingerprint_context(CTX), &fingerprint_context(&after));
assert!(!d.identical);
assert_eq!(d.changed.len(), 1, "{:?}", d.changed);
assert_eq!(d.changed[0].layer, "Current Facts");
assert!(d.added.is_empty() && d.removed.is_empty());
}
#[test]
fn added_and_removed_layers_are_distinguished_from_edits() {
let after = format!("{CTX}## Known Unknowns\n- unclear: the region\n");
let d = diff(&fingerprint_context(CTX), &fingerprint_context(&after));
assert_eq!(d.added, vec!["Known Unknowns".to_string()]);
assert!(
d.changed.is_empty(),
"adding a layer must not perturb existing ones: {:?}",
d.changed
);
let back = diff(&fingerprint_context(&after), &fingerprint_context(CTX));
assert_eq!(back.removed, vec!["Known Unknowns".to_string()]);
assert!(back.byte_delta < 0);
}
#[test]
fn reordering_is_detected_even_though_every_layer_hash_matches() {
let reordered = "\
## Current Facts
- deploy target is fly.io
- the db is postgres
## Identity
You are CAR.
## Recent Context
user: what is the target?
";
let a = fingerprint_context(CTX);
let b = fingerprint_context(reordered);
assert_eq!(a.layers, b.layers, "every layer body is unchanged");
let d = diff(&a, &b);
assert!(!d.identical, "a reordered context is not identical");
assert!(d.reordered);
assert!(d.summary().contains("order"));
}
#[test]
fn content_before_the_first_header_is_not_silently_dropped() {
let with_preamble = format!("a caller-prepended note\n{CTX}");
let d = diff(
&fingerprint_context(CTX),
&fingerprint_context(&with_preamble),
);
assert!(!d.identical);
assert_eq!(d.added, vec!["(preamble)".to_string()]);
}
#[test]
fn duplicate_layer_names_do_not_collapse() {
let doubled = format!("{CTX}\n## Current Facts\n- a second block\n");
let fp = fingerprint_context(&doubled);
assert!(fp.layers.contains_key("Current Facts"));
assert!(
fp.layers.contains_key("Current Facts#2"),
"second block must get its own entry: {:?}",
fp.layers.keys().collect::<Vec<_>>()
);
}
#[test]
fn fingerprints_are_stable_across_calls() {
assert_eq!(fingerprint_context(CTX), fingerprint_context(CTX));
}
#[test]
fn empty_context_is_handled() {
let fp = fingerprint_context("");
assert!(fp.layers.is_empty());
assert_eq!(fp.total_bytes, 0);
assert!(diff(&fp, &fingerprint_context("")).identical);
}
}