1use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::hlc::Hlc;
10use crate::{Chunk, Document, Edge, EdgeKind, Embedding};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct DiffMetadata {
15 pub prev_root: [u8; 32],
17
18 pub new_root: [u8; 32],
20
21 pub hlc: Hlc,
23
24 pub device_id: Uuid,
26
27 pub seq: u64,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct CognitiveDiff {
40 pub added_docs: Vec<Document>,
42
43 pub removed_doc_ids: Vec<Uuid>,
45
46 pub added_chunks: Vec<Chunk>,
48
49 pub removed_chunk_ids: Vec<Uuid>,
51
52 pub added_embeddings: Vec<Embedding>,
54
55 pub removed_embedding_ids: Vec<Uuid>,
57
58 pub added_edges: Vec<Edge>,
60
61 pub removed_edges: Vec<(Uuid, Uuid, EdgeKind)>,
63
64 pub metadata: DiffMetadata,
66}
67
68impl CognitiveDiff {
69 pub fn empty(prev_root: [u8; 32], device_id: Uuid, seq: u64, hlc: Hlc) -> Self {
71 Self {
72 added_docs: Vec::new(),
73 removed_doc_ids: Vec::new(),
74 added_chunks: Vec::new(),
75 removed_chunk_ids: Vec::new(),
76 added_embeddings: Vec::new(),
77 removed_embedding_ids: Vec::new(),
78 added_edges: Vec::new(),
79 removed_edges: Vec::new(),
80 metadata: DiffMetadata {
81 prev_root,
82 new_root: [0u8; 32], hlc,
84 device_id,
85 seq,
86 },
87 }
88 }
89
90 pub fn is_empty(&self) -> bool {
92 self.added_docs.is_empty()
93 && self.removed_doc_ids.is_empty()
94 && self.added_chunks.is_empty()
95 && self.removed_chunk_ids.is_empty()
96 && self.added_embeddings.is_empty()
97 && self.removed_embedding_ids.is_empty()
98 && self.added_edges.is_empty()
99 && self.removed_edges.is_empty()
100 }
101
102 pub fn change_count(&self) -> usize {
104 self.added_docs.len()
105 + self.removed_doc_ids.len()
106 + self.added_chunks.len()
107 + self.removed_chunk_ids.len()
108 + self.added_embeddings.len()
109 + self.removed_embedding_ids.len()
110 + self.added_edges.len()
111 + self.removed_edges.len()
112 }
113
114 pub fn estimated_size(&self) -> usize {
116 const DOC_SIZE: usize = 200;
117 const CHUNK_SIZE: usize = 1000;
118 const EMBEDDING_SIZE: usize = 3072; const EDGE_SIZE: usize = 50;
120 const ID_SIZE: usize = 16;
121
122 self.added_docs.len() * DOC_SIZE
123 + self.removed_doc_ids.len() * ID_SIZE
124 + self.added_chunks.len() * CHUNK_SIZE
125 + self.removed_chunk_ids.len() * ID_SIZE
126 + self.added_embeddings.len() * EMBEDDING_SIZE
127 + self.removed_embedding_ids.len() * ID_SIZE
128 + self.added_edges.len() * EDGE_SIZE
129 + self.removed_edges.len() * (ID_SIZE * 2 + 1)
130 + 200 }
132}
133
134impl PartialEq for CognitiveDiff {
135 fn eq(&self, other: &Self) -> bool {
136 self.metadata == other.metadata
137 }
138}
139
140impl Eq for CognitiveDiff {}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct EmbeddingInput {
145 pub text_hash: [u8; 32],
147 pub model_hash: [u8; 32],
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct ExecutionTrace {
157 pub operation: Operation,
159 pub prev_state_root: [u8; 32],
161 pub new_state_root: [u8; 32],
163
164 pub document_subtree_root: [u8; 32],
166 pub chunk_subtree_root: [u8; 32],
168 pub embedding_subtree_root: [u8; 32],
170 pub edge_subtree_root: [u8; 32],
172
173 pub embedding_inputs: Vec<EmbeddingInput>,
175 pub embedding_outputs: Vec<[u8; 32]>,
177
178 pub timestamp: Hlc,
180 pub device_id: Uuid,
182 pub signature: Vec<u8>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
189pub enum Operation {
190 AddDocument {
192 document: Document,
194 chunks: Vec<Chunk>,
196 embeddings: Vec<Embedding>,
198 timestamp: Hlc,
200 },
201 RemoveDocument {
203 document_id: Uuid,
205 timestamp: Hlc,
207 },
208 UpdateDocument {
210 document_id: Uuid,
212 new_chunks: Vec<Chunk>,
214 new_embeddings: Vec<Embedding>,
216 timestamp: Hlc,
218 },
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn test_hlc() -> Hlc {
226 Hlc::new(1000, [1u8; 16])
227 }
228
229 #[test]
230 fn test_empty_diff() {
231 let diff = CognitiveDiff::empty([0u8; 32], Uuid::from_bytes([1u8; 16]), 0, test_hlc());
232 assert!(diff.is_empty());
233 assert_eq!(diff.change_count(), 0);
234 }
235
236 #[test]
237 fn test_diff_with_changes() {
238 let mut diff = CognitiveDiff::empty([0u8; 32], Uuid::from_bytes([1u8; 16]), 0, test_hlc());
239
240 diff.added_docs.push(Document::new(
241 std::path::PathBuf::from("test.md"),
242 b"content",
243 0,
244 ));
245
246 assert!(!diff.is_empty());
247 assert_eq!(diff.change_count(), 1);
248 }
249
250 #[test]
251 fn test_diff_has_hlc() {
252 let hlc = Hlc::new(12345, [7u8; 16]);
253 let diff = CognitiveDiff::empty([0u8; 32], Uuid::from_bytes([1u8; 16]), 0, hlc.clone());
254 assert_eq!(diff.metadata.hlc, hlc);
255 }
256
257 #[test]
258 fn test_diff_removed_edges_has_kind() {
259 let mut diff = CognitiveDiff::empty([0u8; 32], Uuid::from_bytes([1u8; 16]), 0, test_hlc());
260
261 diff.removed_edges.push((
262 Uuid::from_bytes([2u8; 16]),
263 Uuid::from_bytes([3u8; 16]),
264 EdgeKind::DocToChunk,
265 ));
266
267 assert_eq!(diff.change_count(), 1);
268 assert_eq!(diff.removed_edges[0].2, EdgeKind::DocToChunk);
269 }
270}