use crate::base::serde_helpers::serialize_f32;
use crate::base::types::RecordInput;
pub const PAYLOAD_DIGEST_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PayloadVariant {
Record,
RecordText,
}
impl PayloadVariant {
fn tag(self) -> u8 {
match self {
PayloadVariant::Record => 1,
PayloadVariant::RecordText => 2,
}
}
}
#[derive(Debug)]
pub struct PayloadView<'a> {
pub variant: PayloadVariant,
pub namespace: &'a str,
pub text: &'a str,
pub memory_type: &'a str,
pub importance: f64,
pub valence: f64,
pub half_life: f64,
pub certainty: f64,
pub domain: &'a str,
pub source: &'a str,
pub emotional_state: Option<&'a str>,
pub metadata: &'a serde_json::Value,
pub embedding: Option<&'a [f32]>,
pub created_at: Option<f64>,
}
impl<'a> PayloadView<'a> {
pub fn from_record_input(input: &'a RecordInput, variant: PayloadVariant) -> Self {
PayloadView {
variant,
namespace: &input.namespace,
text: &input.text,
memory_type: &input.memory_type,
importance: input.importance,
valence: input.valence,
half_life: input.half_life,
certainty: input.certainty,
domain: &input.domain,
source: &input.source,
emotional_state: input.emotional_state.as_deref(),
metadata: &input.metadata,
embedding: match variant {
PayloadVariant::Record => Some(&input.embedding),
PayloadVariant::RecordText => None,
},
created_at: input.created_at,
}
}
}
const T_NONE: u8 = 0;
const T_SOME: u8 = 1;
const T_NUM: u8 = 10;
const T_NAN: u8 = 11;
const T_CREATED_AT: u8 = 30;
const J_NULL: u8 = 20;
const J_BOOL: u8 = 21;
const J_NUM_I64: u8 = 22;
const J_NUM_U64: u8 = 23;
const J_NUM_F64: u8 = 24;
const J_NUM_OTHER: u8 = 25;
const J_STR: u8 = 26;
const J_ARR: u8 = 27;
const J_OBJ: u8 = 28;
fn feed_str(h: &mut blake3::Hasher, s: &str) {
h.update(&(s.len() as u64).to_le_bytes());
h.update(s.as_bytes());
}
fn feed_opt_str(h: &mut blake3::Hasher, s: Option<&str>) {
match s {
None => {
h.update(&[T_NONE]);
}
Some(v) => {
h.update(&[T_SOME]);
feed_str(h, v);
}
}
}
fn feed_f64(h: &mut blake3::Hasher, x: f64) {
if x.is_nan() {
h.update(&[T_NAN]);
return;
}
h.update(&[T_NUM]);
let normalized = if x == 0.0 { 0.0 } else { x };
h.update(&normalized.to_bits().to_le_bytes());
}
fn feed_embedding(h: &mut blake3::Hasher, emb: Option<&[f32]>) {
match emb {
None => {
h.update(&[T_NONE]);
}
Some(v) => {
h.update(&[T_SOME]);
let bytes = serialize_f32(v);
h.update(&(bytes.len() as u64).to_le_bytes());
h.update(&bytes);
}
}
}
fn feed_json(h: &mut blake3::Hasher, v: &serde_json::Value) {
match v {
serde_json::Value::Null => {
h.update(&[J_NULL]);
}
serde_json::Value::Bool(b) => {
h.update(&[J_BOOL]);
h.update(&[*b as u8]);
}
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
h.update(&[J_NUM_I64]);
h.update(&i.to_le_bytes());
} else if let Some(u) = n.as_u64() {
h.update(&[J_NUM_U64]);
h.update(&u.to_le_bytes());
} else if let Some(f) = n.as_f64() {
h.update(&[J_NUM_F64]);
feed_f64(h, f);
} else {
h.update(&[J_NUM_OTHER]);
feed_str(h, &n.to_string());
}
}
serde_json::Value::String(s) => {
h.update(&[J_STR]);
feed_str(h, s);
}
serde_json::Value::Array(a) => {
h.update(&[J_ARR]);
h.update(&(a.len() as u64).to_le_bytes());
for e in a {
feed_json(h, e);
}
}
serde_json::Value::Object(m) => {
h.update(&[J_OBJ]);
h.update(&(m.len() as u64).to_le_bytes());
let mut keys: Vec<&String> = m.keys().collect();
keys.sort();
for k in keys {
feed_str(h, k);
if let Some(val) = m.get(k) {
feed_json(h, val);
}
}
}
}
}
pub fn payload_digest(v: &PayloadView<'_>) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(b"yantrikdb.payload.v");
h.update(&PAYLOAD_DIGEST_VERSION.to_le_bytes());
h.update(&[v.variant.tag()]);
feed_str(&mut h, v.namespace);
feed_str(&mut h, v.text);
feed_str(&mut h, v.memory_type);
feed_f64(&mut h, v.importance);
feed_f64(&mut h, v.valence);
feed_f64(&mut h, v.half_life);
feed_f64(&mut h, v.certainty);
feed_str(&mut h, v.domain);
feed_str(&mut h, v.source);
feed_opt_str(&mut h, v.emotional_state);
feed_json(&mut h, v.metadata);
feed_embedding(&mut h, v.embedding);
if let Some(ts) = v.created_at {
h.update(&[T_CREATED_AT]);
feed_f64(&mut h, ts);
}
*h.finalize().as_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn view<'a>(text: &'a str, metadata: &'a serde_json::Value) -> PayloadView<'a> {
PayloadView {
variant: PayloadVariant::Record,
namespace: "default",
text,
memory_type: "semantic",
importance: 0.5,
valence: 0.0,
half_life: 604800.0,
certainty: 0.8,
domain: "general",
source: "user",
emotional_state: None,
metadata,
embedding: None,
created_at: None,
}
}
#[test]
fn absent_created_at_digest_is_byte_identical_to_v1() {
let m = json!({"a": 1});
let d = payload_digest(&view("golden pin text", &m));
let hex: String = d.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(
hex, "b66864879e42df48f1622f57d7c8b6bcef643357fb0deb318252e176fee1500c",
"absent created_at no longer digests as v1 — stored claims from \
pre-created_at engines would conflict on honest retries"
);
}
#[test]
fn created_at_is_digested_when_present() {
let m = json!({});
let base = payload_digest(&view("t", &m));
let mut with_ts = view("t", &m);
with_ts.created_at = Some(1_700_000_000.0);
let d1 = payload_digest(&with_ts);
assert_ne!(base, d1, "created_at not covered");
let mut other_ts = view("t", &m);
other_ts.created_at = Some(1_700_000_001.0);
assert_ne!(
d1,
payload_digest(&other_ts),
"two event times digested equal — a re-dated write would be \
swallowed as a retry"
);
}
#[test]
fn digest_is_deterministic_and_content_sensitive() {
let m = json!({"a": 1});
assert_eq!(
payload_digest(&view("hello", &m)),
payload_digest(&view("hello", &m))
);
assert_ne!(
payload_digest(&view("hello", &m)),
payload_digest(&view("hello!", &m))
);
}
#[test]
fn digest_is_stable_under_object_key_insertion_order() {
let a: serde_json::Value = serde_json::from_str(r#"{"z":1,"a":2,"m":3}"#).unwrap();
let b: serde_json::Value = serde_json::from_str(r#"{"a":2,"m":3,"z":1}"#).unwrap();
assert_eq!(
payload_digest(&view("t", &a)),
payload_digest(&view("t", &b))
);
}
#[test]
fn digest_framing_is_unambiguous_for_free_form_fields() {
let m = json!({});
let mut x = view("a|b", &m);
x.domain = "c";
let mut y = view("a", &m);
y.domain = "b|c";
assert_ne!(payload_digest(&x), payload_digest(&y));
}
#[test]
fn digest_distinguishes_variants() {
let m = json!({});
let mut rt = view("same text", &m);
rt.variant = PayloadVariant::RecordText;
assert_ne!(payload_digest(&view("same text", &m)), payload_digest(&rt));
}
#[test]
fn floats_are_never_quantized() {
let m = json!({});
let mut a = view("t", &m);
a.importance = 0.5;
let mut b = view("t", &m);
b.importance = 0.5004;
assert_ne!(
payload_digest(&a),
payload_digest(&b),
"engine scalar was quantized"
);
let m1 = json!({"price": 1.00001});
let m2 = json!({"price": 1.00002});
assert_ne!(
payload_digest(&view("t", &m1)),
payload_digest(&view("t", &m2)),
"metadata float was quantized"
);
}
#[test]
fn negative_zero_digests_as_zero() {
let m = json!({});
let mut neg = view("t", &m);
neg.valence = -0.0;
let mut pos = view("t", &m);
pos.valence = 0.0;
assert_eq!(payload_digest(&neg), payload_digest(&pos));
assert_eq!(
payload_digest(&view("t", &json!({"k": -0.0}))),
payload_digest(&view("t", &json!({"k": 0.0})))
);
}
#[test]
fn non_finite_scalars_do_not_alias() {
let m = json!({});
let mut nan = view("t", &m);
nan.importance = f64::NAN;
let mut zero = view("t", &m);
zero.importance = 0.0;
let mut inf = view("t", &m);
inf.importance = f64::INFINITY;
let mut neg_inf = view("t", &m);
neg_inf.importance = f64::NEG_INFINITY;
let d_nan = payload_digest(&nan);
assert_ne!(d_nan, payload_digest(&zero));
assert_ne!(d_nan, payload_digest(&inf));
assert_ne!(payload_digest(&inf), payload_digest(&neg_inf));
let mut nan2 = view("t", &m);
nan2.importance = f64::from_bits(f64::NAN.to_bits() | 1);
assert!(nan2.importance.is_nan());
assert_eq!(d_nan, payload_digest(&nan2));
}
#[test]
fn json_types_do_not_collide() {
let vals = [
json!({"k": 1}),
json!({"k": 1.0}),
json!({"k": "1"}),
json!({"k": true}),
json!({"k": null}),
];
let digests: Vec<_> = vals.iter().map(|m| payload_digest(&view("t", m))).collect();
for i in 0..digests.len() {
for j in (i + 1)..digests.len() {
assert_ne!(digests[i], digests[j], "json value {i} and {j} collided");
}
}
assert_ne!(
payload_digest(&view("t", &json!({"a": {"b": 1}}))),
payload_digest(&view("t", &json!({"a.b": 1})))
);
assert_ne!(
payload_digest(&view("t", &json!({"k": [1, 2]}))),
payload_digest(&view("t", &json!({"k": [2, 1]})))
);
}
#[test]
fn every_field_is_covered_by_the_digest() {
let m = json!({});
let base = payload_digest(&view("t", &m));
let mut v = view("t", &m);
v.namespace = "other";
assert_ne!(base, payload_digest(&v), "namespace not covered");
let mut v = view("t", &m);
v.memory_type = "episodic";
assert_ne!(base, payload_digest(&v), "memory_type not covered");
let mut v = view("t", &m);
v.importance = 0.9;
assert_ne!(base, payload_digest(&v), "importance not covered");
let mut v = view("t", &m);
v.valence = 0.9;
assert_ne!(base, payload_digest(&v), "valence not covered");
let mut v = view("t", &m);
v.half_life = 1.0;
assert_ne!(base, payload_digest(&v), "half_life not covered");
let mut v = view("t", &m);
v.certainty = 0.1;
assert_ne!(base, payload_digest(&v), "certainty not covered");
let mut v = view("t", &m);
v.domain = "other";
assert_ne!(base, payload_digest(&v), "domain not covered");
let mut v = view("t", &m);
v.source = "inference";
assert_ne!(base, payload_digest(&v), "source not covered");
let mut v = view("t", &m);
v.emotional_state = Some("calm");
assert_ne!(base, payload_digest(&v), "emotional_state not covered");
let emb = [0.5f32, 0.25];
let mut v = view("t", &m);
v.embedding = Some(&emb);
assert_ne!(base, payload_digest(&v), "embedding not covered");
assert_ne!(
base,
payload_digest(&view("t", &json!({"x": 1}))),
"metadata not covered"
);
assert_ne!(
base,
payload_digest(&view("other text", &m)),
"text not covered"
);
}
#[test]
fn optional_field_none_does_not_alias_onto_empty_string() {
let m = json!({});
let mut none = view("t", &m);
none.emotional_state = None;
let mut empty = view("t", &m);
empty.emotional_state = Some("");
assert_ne!(payload_digest(&none), payload_digest(&empty));
}
#[test]
fn caller_supplied_embedding_is_digested_but_generated_one_is_not() {
let mk = |emb: Vec<f32>| RecordInput {
idempotency_key: None,
text: "t".to_string(),
memory_type: "semantic".to_string(),
importance: 0.5,
valence: 0.0,
half_life: 604800.0,
metadata: json!({}),
embedding: emb,
namespace: "default".to_string(),
certainty: 0.8,
domain: "general".to_string(),
source: "user".to_string(),
emotional_state: None,
created_at: None,
};
let a = mk(vec![1.0, 0.0]);
let b = mk(vec![0.0, 1.0]);
assert_ne!(
payload_digest(&PayloadView::from_record_input(&a, PayloadVariant::Record)),
payload_digest(&PayloadView::from_record_input(&b, PayloadVariant::Record)),
"caller-supplied embedding must be digested"
);
assert_eq!(
payload_digest(&PayloadView::from_record_input(
&a,
PayloadVariant::RecordText
)),
payload_digest(&PayloadView::from_record_input(
&b,
PayloadVariant::RecordText
)),
"generated embedding must not be digested"
);
let empty = mk(vec![]);
assert_ne!(
payload_digest(&PayloadView::from_record_input(
&empty,
PayloadVariant::Record
)),
payload_digest(&PayloadView::from_record_input(
&empty,
PayloadVariant::RecordText
))
);
}
#[test]
fn unicode_is_hashed_by_exact_bytes() {
let m = json!({});
assert_ne!(
payload_digest(&view("caf\u{00e9}", &m)),
payload_digest(&view("cafe\u{0301}", &m))
);
let s = "日本語 🎉 emoji";
assert_eq!(payload_digest(&view(s, &m)), payload_digest(&view(s, &m)));
}
}