use sha2::{Digest, Sha256};
use super::{ContextSnapshot, RegionEntrySnapshot, RegionSnapshot};
const DIGEST_HEX: usize = 32;
pub struct EntryFacts<'a> {
pub content: &'a crate::region::EntryContent,
pub tokens: usize,
pub kind: &'a crate::region::EntryKind,
pub metadata: Option<&'a serde_json::Value>,
pub key: Option<&'a str>,
pub taint: crate::taint::TaintLevel,
pub reasoning: Option<&'a str>,
}
impl<'a> From<&'a RegionEntrySnapshot> for EntryFacts<'a> {
fn from(entry: &'a RegionEntrySnapshot) -> Self {
Self {
content: &entry.content,
tokens: entry.tokens,
kind: &entry.kind,
metadata: entry.metadata.as_ref(),
key: entry.key.as_deref(),
taint: entry.taint,
reasoning: entry.reasoning.as_deref(),
}
}
}
pub struct RegionFacts<'a> {
pub name: &'a str,
pub kind: &'a str,
pub current_tokens: usize,
pub max_tokens: usize,
pub digest: &'a str,
}
fn field(hasher: &mut Sha256, bytes: &[u8]) {
hasher.update((bytes.len() as u64).to_be_bytes());
hasher.update(bytes);
}
fn number(hasher: &mut Sha256, value: u64) {
hasher.update(value.to_be_bytes());
}
fn short(hasher: Sha256) -> String {
let full = hasher.finalize();
full.iter()
.flat_map(|byte| [byte >> 4, byte & 0x0f])
.take(DIGEST_HEX)
.map(|nibble| char::from_digit(u32::from(nibble), 16).unwrap_or('0'))
.collect()
}
fn canonical<T: serde::Serialize>(value: &T) -> String {
serde_json::to_string(value).unwrap_or_default()
}
fn fingerprint(hasher: &mut Sha256, entry: &EntryFacts<'_>) {
field(hasher, canonical(entry.content).as_bytes());
number(hasher, entry.tokens as u64);
field(hasher, canonical(entry.kind).as_bytes());
field(hasher, canonical(&entry.metadata).as_bytes());
field(hasher, canonical(&entry.key).as_bytes());
field(hasher, canonical(&entry.taint).as_bytes());
field(hasher, canonical(&entry.reasoning).as_bytes());
}
pub fn region_digest<'a>(entries: impl IntoIterator<Item = EntryFacts<'a>>) -> String {
let mut hasher = Sha256::new();
for entry in entries {
fingerprint(&mut hasher, &entry);
}
format!("rg1-{}", short(hasher))
}
pub fn snapshot_region_digest(region: &RegionSnapshot) -> String {
region_digest(region.entries.iter().map(EntryFacts::from))
}
pub fn window_revision<'a>(
total_tokens: usize,
max_tokens: usize,
regions: impl IntoIterator<Item = RegionFacts<'a>>,
) -> String {
let mut hasher = Sha256::new();
number(&mut hasher, total_tokens as u64);
number(&mut hasher, max_tokens as u64);
for region in regions {
field(&mut hasher, region.name.as_bytes());
field(&mut hasher, region.kind.as_bytes());
number(&mut hasher, region.current_tokens as u64);
number(&mut hasher, region.max_tokens as u64);
field(&mut hasher, region.digest.as_bytes());
}
format!("cw1-{}", short(hasher))
}
pub fn context_revision(snapshot: &ContextSnapshot) -> String {
let digests: Vec<String> = snapshot
.regions
.iter()
.map(snapshot_region_digest)
.collect();
window_revision(
snapshot.total_tokens,
snapshot.max_tokens,
snapshot
.regions
.iter()
.zip(&digests)
.map(|(region, digest)| RegionFacts {
name: ®ion.name,
kind: ®ion.kind,
current_tokens: region.current_tokens,
max_tokens: region.max_tokens,
digest,
}),
)
}
#[cfg(test)]
#[path = "revision_tests.rs"]
mod tests;