#[derive(Debug, Clone)]
pub struct FactPayload {
pub id: String,
pub timestamp: String,
pub owner: String,
pub encrypted_blob_hex: String,
pub blind_indices: Vec<String>,
pub decay_score: f64,
pub source: String,
pub content_fp: String,
pub agent_id: String,
pub encrypted_embedding: Option<String>,
pub version: u32,
}
pub const DEFAULT_PROTOBUF_VERSION: u32 = 3;
pub const PROTOBUF_VERSION_V4: u32 = 4;
pub fn encode_fact_protobuf(fact: &FactPayload) -> Vec<u8> {
let mut buf = Vec::with_capacity(512);
write_string(&mut buf, 1, &fact.id);
write_string(&mut buf, 2, &fact.timestamp);
write_string(&mut buf, 3, &fact.owner);
if let Ok(blob_bytes) = hex::decode(&fact.encrypted_blob_hex) {
write_bytes(&mut buf, 4, &blob_bytes);
}
for index in &fact.blind_indices {
write_string(&mut buf, 5, index);
}
write_double(&mut buf, 6, fact.decay_score);
write_varint_field(&mut buf, 7, 1);
let version = if fact.version == 0 {
DEFAULT_PROTOBUF_VERSION
} else {
fact.version
};
write_varint_field(&mut buf, 8, version);
write_string(&mut buf, 10, &fact.content_fp);
if let Some(ref emb) = fact.encrypted_embedding {
write_string(&mut buf, 13, emb);
}
buf
}
pub fn encode_tombstone_protobuf(fact_id: &str, owner: &str, version: u32) -> Vec<u8> {
let mut buf = Vec::with_capacity(128);
write_string(&mut buf, 1, fact_id);
write_string(&mut buf, 2, &chrono::Utc::now().to_rfc3339());
write_string(&mut buf, 3, owner);
write_bytes(&mut buf, 4, &[]);
write_double(&mut buf, 6, 0.0);
write_varint_field(&mut buf, 7, 0);
let v = if version == 0 {
DEFAULT_PROTOBUF_VERSION
} else {
version
};
write_varint_field(&mut buf, 8, v);
buf
}
fn write_string(buf: &mut Vec<u8>, field: u32, value: &str) {
if value.is_empty() {
return;
}
let data = value.as_bytes();
let key = (field << 3) | 2; encode_varint(buf, key);
encode_varint(buf, data.len() as u32);
buf.extend_from_slice(data);
}
fn write_bytes(buf: &mut Vec<u8>, field: u32, value: &[u8]) {
let key = (field << 3) | 2;
encode_varint(buf, key);
encode_varint(buf, value.len() as u32);
buf.extend_from_slice(value);
}
fn write_double(buf: &mut Vec<u8>, field: u32, value: f64) {
let key = (field << 3) | 1; encode_varint(buf, key);
buf.extend_from_slice(&value.to_le_bytes());
}
fn write_varint_field(buf: &mut Vec<u8>, field: u32, value: u32) {
let key = (field << 3) | 0; encode_varint(buf, key);
encode_varint(buf, value);
}
fn encode_varint(buf: &mut Vec<u8>, mut value: u32) {
loop {
if value <= 0x7f {
buf.push(value as u8);
break;
}
buf.push(((value & 0x7f) | 0x80) as u8);
value >>= 7;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_varint_encoding() {
let mut buf = Vec::new();
encode_varint(&mut buf, 1);
assert_eq!(buf, vec![1]);
buf.clear();
encode_varint(&mut buf, 300);
assert_eq!(buf, vec![0xAC, 0x02]);
}
#[test]
fn test_encode_fact_protobuf() {
let payload = FactPayload {
id: "test-id".into(),
timestamp: "2026-01-01T00:00:00Z".into(),
owner: "0xABCD".into(),
encrypted_blob_hex: "deadbeef".into(),
blind_indices: vec!["hash1".into(), "hash2".into()],
decay_score: 0.8,
source: "zeroclaw_fact".into(),
content_fp: "fp123".into(),
agent_id: "zeroclaw".into(),
encrypted_embedding: None,
version: 0, };
let encoded = encode_fact_protobuf(&payload);
assert!(!encoded.is_empty());
assert!(encoded.windows(7).any(|w| w == b"test-id"));
}
#[test]
fn test_encode_fact_protobuf_v3_vs_v4() {
let base = FactPayload {
id: "test-id".into(),
timestamp: "2026-01-01T00:00:00Z".into(),
owner: "0xABCD".into(),
encrypted_blob_hex: "deadbeef".into(),
blind_indices: vec![],
decay_score: 0.8,
source: "".into(),
content_fp: "fp".into(),
agent_id: "".into(),
encrypted_embedding: None,
version: DEFAULT_PROTOBUF_VERSION,
};
let mut v4 = base.clone();
v4.version = PROTOBUF_VERSION_V4;
let encoded_v3 = encode_fact_protobuf(&base);
let encoded_v4 = encode_fact_protobuf(&v4);
assert_ne!(encoded_v3, encoded_v4);
assert!(encoded_v3.windows(2).any(|w| w == [0x40, 3]));
assert!(encoded_v4.windows(2).any(|w| w == [0x40, 4]));
}
#[test]
fn test_encode_tombstone_protobuf_version() {
let ts_v3 = encode_tombstone_protobuf("id", "0xABCD", DEFAULT_PROTOBUF_VERSION);
let ts_v4 = encode_tombstone_protobuf("id", "0xABCD", PROTOBUF_VERSION_V4);
let ts_default = encode_tombstone_protobuf("id", "0xABCD", 0);
assert!(ts_v3.windows(2).any(|w| w == [0x40, 3]));
assert!(ts_v4.windows(2).any(|w| w == [0x40, 4]));
assert!(ts_default.windows(2).any(|w| w == [0x40, 3]));
assert!(!ts_v3.windows(2).any(|w| w == [0x40, 4]));
}
}