use serde::{Deserialize, Serialize};
use crate::model::RecordId;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ContentHash(pub String);
impl ContentHash {
pub fn of(text: &str) -> Self {
Self(blake3::hash(text.as_bytes()).to_hex().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ContentHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Chunk {
pub record_id: RecordId,
pub seq: u32,
pub content: String,
pub content_hash: ContentHash,
pub token_estimate: u32,
}
impl Chunk {
pub fn new(record_id: RecordId, seq: u32, content: String, token_estimate: u32) -> Self {
let content_hash = ContentHash::of(&content);
Self {
record_id,
seq,
content,
content_hash,
token_estimate,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn content_hash_is_deterministic_and_hex() {
let a = ContentHash::of("hello");
let b = ContentHash::of("hello");
let c = ContentHash::of("world");
assert_eq!(a, b);
assert_ne!(a, c);
assert_eq!(a.as_str().len(), 64);
assert!(a.as_str().chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn chunk_new_computes_hash() {
let rid = RecordId::from_parts("claude-code", None, "x");
let c = Chunk::new(rid.clone(), 0, "abc".into(), 1);
assert_eq!(c.record_id, rid);
assert_eq!(c.seq, 0);
assert_eq!(c.content, "abc");
assert_eq!(c.content_hash, ContentHash::of("abc"));
assert_eq!(c.token_estimate, 1);
}
#[test]
fn chunk_roundtrips_through_json() {
let rid = RecordId::from_parts("claude-code", None, "x");
let c = Chunk::new(rid, 2, "payload".into(), 3);
let s = serde_json::to_string(&c).unwrap();
let back: Chunk = serde_json::from_str(&s).unwrap();
assert_eq!(c, back);
}
}