pub const TOKENS_PER_CHUNK: usize = 32;
pub fn hash_chunk(model: &str, tokens: &[u32]) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(b"cognitora.v1\0");
h.update(model.as_bytes());
h.update(b"\0");
for &t in tokens {
h.update(&t.to_le_bytes());
}
*h.finalize().as_bytes()
}
pub fn hash_seq_chunk(model: &str, parent: &[u8; 32], tokens: &[u32]) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(b"cognitora.seq.v1\0");
h.update(model.as_bytes());
h.update(b"\0");
h.update(parent);
for &t in tokens {
h.update(&t.to_le_bytes());
}
*h.finalize().as_bytes()
}
pub fn hash_chunks(model: &str, tokens: &[u32]) -> Vec<[u8; 32]> {
if tokens.is_empty() {
return Vec::new();
}
let mut out = Vec::with_capacity(tokens.len().div_ceil(TOKENS_PER_CHUNK));
for chunk in tokens.chunks(TOKENS_PER_CHUNK) {
out.push(hash_chunk(model, chunk));
}
out
}
pub fn hash_seq_chunks(model: &str, tokens: &[u32]) -> Vec<[u8; 32]> {
if tokens.is_empty() {
return Vec::new();
}
let mut out = Vec::with_capacity(tokens.len().div_ceil(TOKENS_PER_CHUNK));
let mut parent = [0u8; 32];
for chunk in tokens.chunks(TOKENS_PER_CHUNK) {
let d = hash_seq_chunk(model, &parent, chunk);
out.push(d);
parent = d;
}
out
}
pub fn short(digest: &[u8; 32]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(16);
for b in &digest[..8] {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0x0f) as usize] as char);
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deterministic() {
let a = hash_chunk("llama3", &[1, 2, 3]);
let b = hash_chunk("llama3", &[1, 2, 3]);
assert_eq!(a, b);
}
#[test]
fn model_salts() {
let a = hash_chunk("llama3", &[1, 2, 3]);
let b = hash_chunk("qwen3", &[1, 2, 3]);
assert_ne!(a, b);
}
#[test]
fn chunks_partial_tail() {
let toks: Vec<u32> = (0..70).collect();
let h = hash_chunks("m", &toks);
assert_eq!(h.len(), 3); }
#[test]
fn short_is_16_chars() {
let d = hash_chunk("m", &[1, 2, 3]);
assert_eq!(short(&d).len(), 16);
}
#[test]
fn seq_chunks_are_position_dependent() {
let toks_ab: Vec<u32> = (0..64u32).collect();
let toks_cb: Vec<u32> = std::iter::repeat_n(99u32, 32).chain(32..64u32).collect();
let h1 = hash_seq_chunks("m", &toks_ab);
let h2 = hash_seq_chunks("m", &toks_cb);
assert_eq!(h1.len(), 2);
assert_eq!(h2.len(), 2);
assert_ne!(h1[0], h2[0]);
assert_ne!(h1[1], h2[1]);
}
#[test]
fn seq_chunks_extend_consistently() {
let toks: Vec<u32> = (0..96u32).collect();
let prefix: Vec<u32> = toks[..64].to_vec();
let h_full = hash_seq_chunks("m", &toks);
let h_prefix = hash_seq_chunks("m", &prefix);
assert_eq!(h_prefix, h_full[..2]);
}
#[test]
fn seq_chunks_diverge_when_independent_chunks_match() {
let mut a: Vec<u32> = (0..64).collect();
let mut b: Vec<u32> = (0..64).collect();
a[0] = 9999; b[0] = 1234;
let ha = hash_seq_chunks("m", &a);
let hb = hash_seq_chunks("m", &b);
assert_ne!(ha[0], hb[0]);
let ind = hash_chunks("m", &a);
let ind_b = hash_chunks("m", &b);
assert_eq!(ind[1], ind_b[1], "independent hashing collides at pos 1");
assert_ne!(ha[1], hb[1], "seq hashing must diverge at pos 1");
}
}