1#[derive(Debug, Clone)]
16pub struct FactPayload {
17 pub id: String,
18 pub timestamp: String,
19 pub owner: String,
20 pub encrypted_blob_hex: String,
21 pub blind_indices: Vec<String>,
22 pub decay_score: f64,
23 pub source: String,
24 pub content_fp: String,
25 pub agent_id: String,
26 pub encrypted_embedding: Option<String>,
27 pub version: u32,
32}
33
34pub const DEFAULT_PROTOBUF_VERSION: u32 = 3;
36
37pub const PROTOBUF_VERSION_V4: u32 = 4;
40
41pub fn encode_fact_protobuf(fact: &FactPayload) -> Vec<u8> {
43 let mut buf = Vec::with_capacity(512);
44
45 write_string(&mut buf, 1, &fact.id);
47 write_string(&mut buf, 2, &fact.timestamp);
49 write_string(&mut buf, 3, &fact.owner);
51 if let Ok(blob_bytes) = hex::decode(&fact.encrypted_blob_hex) {
53 write_bytes(&mut buf, 4, &blob_bytes);
54 }
55 for index in &fact.blind_indices {
57 write_string(&mut buf, 5, index);
58 }
59 write_double(&mut buf, 6, fact.decay_score);
61 write_varint_field(&mut buf, 7, 1);
63 let version = if fact.version == 0 {
65 DEFAULT_PROTOBUF_VERSION
66 } else {
67 fact.version
68 };
69 write_varint_field(&mut buf, 8, version);
70 write_string(&mut buf, 10, &fact.content_fp);
73 if let Some(ref emb) = fact.encrypted_embedding {
76 write_string(&mut buf, 13, emb);
77 }
78
79 buf
80}
81
82pub fn encode_tombstone_protobuf(fact_id: &str, owner: &str, version: u32) -> Vec<u8> {
87 let mut buf = Vec::with_capacity(128);
88
89 write_string(&mut buf, 1, fact_id);
90 write_string(&mut buf, 2, &chrono::Utc::now().to_rfc3339());
91 write_string(&mut buf, 3, owner);
92 write_bytes(&mut buf, 4, &[]);
94 write_double(&mut buf, 6, 0.0);
96 write_varint_field(&mut buf, 7, 0);
98 let v = if version == 0 {
99 DEFAULT_PROTOBUF_VERSION
100 } else {
101 version
102 };
103 write_varint_field(&mut buf, 8, v);
104 buf
107}
108
109fn write_string(buf: &mut Vec<u8>, field: u32, value: &str) {
114 if value.is_empty() {
115 return;
116 }
117 let data = value.as_bytes();
118 let key = (field << 3) | 2; encode_varint(buf, key);
120 encode_varint(buf, data.len() as u32);
121 buf.extend_from_slice(data);
122}
123
124fn write_bytes(buf: &mut Vec<u8>, field: u32, value: &[u8]) {
125 let key = (field << 3) | 2;
126 encode_varint(buf, key);
127 encode_varint(buf, value.len() as u32);
128 buf.extend_from_slice(value);
129}
130
131fn write_double(buf: &mut Vec<u8>, field: u32, value: f64) {
132 let key = (field << 3) | 1; encode_varint(buf, key);
134 buf.extend_from_slice(&value.to_le_bytes());
135}
136
137fn write_varint_field(buf: &mut Vec<u8>, field: u32, value: u32) {
138 let key = (field << 3) | 0; encode_varint(buf, key);
140 encode_varint(buf, value);
141}
142
143fn encode_varint(buf: &mut Vec<u8>, mut value: u32) {
144 loop {
145 if value <= 0x7f {
146 buf.push(value as u8);
147 break;
148 }
149 buf.push(((value & 0x7f) | 0x80) as u8);
150 value >>= 7;
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_varint_encoding() {
160 let mut buf = Vec::new();
161 encode_varint(&mut buf, 1);
162 assert_eq!(buf, vec![1]);
163
164 buf.clear();
165 encode_varint(&mut buf, 300);
166 assert_eq!(buf, vec![0xAC, 0x02]);
167 }
168
169 #[test]
170 fn test_encode_fact_protobuf() {
171 let payload = FactPayload {
172 id: "test-id".into(),
173 timestamp: "2026-01-01T00:00:00Z".into(),
174 owner: "0xABCD".into(),
175 encrypted_blob_hex: "deadbeef".into(),
176 blind_indices: vec!["hash1".into(), "hash2".into()],
177 decay_score: 0.8,
178 source: "zeroclaw_fact".into(),
179 content_fp: "fp123".into(),
180 agent_id: "zeroclaw".into(),
181 encrypted_embedding: None,
182 version: 0, };
184 let encoded = encode_fact_protobuf(&payload);
185 assert!(!encoded.is_empty());
186 assert!(encoded.windows(7).any(|w| w == b"test-id"));
188 }
189
190 #[test]
191 fn test_encode_fact_protobuf_v3_vs_v4() {
192 let base = FactPayload {
193 id: "test-id".into(),
194 timestamp: "2026-01-01T00:00:00Z".into(),
195 owner: "0xABCD".into(),
196 encrypted_blob_hex: "deadbeef".into(),
197 blind_indices: vec![],
198 decay_score: 0.8,
199 source: "".into(),
200 content_fp: "fp".into(),
201 agent_id: "".into(),
202 encrypted_embedding: None,
203 version: DEFAULT_PROTOBUF_VERSION,
204 };
205 let mut v4 = base.clone();
206 v4.version = PROTOBUF_VERSION_V4;
207 let encoded_v3 = encode_fact_protobuf(&base);
208 let encoded_v4 = encode_fact_protobuf(&v4);
209 assert_ne!(encoded_v3, encoded_v4);
210 assert!(encoded_v3.windows(2).any(|w| w == [0x40, 3]));
212 assert!(encoded_v4.windows(2).any(|w| w == [0x40, 4]));
213 }
214
215 #[test]
216 fn test_encode_tombstone_protobuf_version() {
217 let ts_v3 = encode_tombstone_protobuf("id", "0xABCD", DEFAULT_PROTOBUF_VERSION);
221 let ts_v4 = encode_tombstone_protobuf("id", "0xABCD", PROTOBUF_VERSION_V4);
222 let ts_default = encode_tombstone_protobuf("id", "0xABCD", 0);
223 assert!(ts_v3.windows(2).any(|w| w == [0x40, 3]));
224 assert!(ts_v4.windows(2).any(|w| w == [0x40, 4]));
225 assert!(ts_default.windows(2).any(|w| w == [0x40, 3]));
227 assert!(!ts_v3.windows(2).any(|w| w == [0x40, 4]));
229 }
230}