1use std::collections::HashMap;
5
6pub fn fnv1a_u64(bytes: &[u8]) -> u64 {
12 const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037_u64;
13 const PRIME: u64 = 1_099_511_628_211_u64;
14 let mut hash = OFFSET_BASIS;
15 for &b in bytes {
16 hash ^= b as u64;
17 hash = hash.wrapping_mul(PRIME);
18 }
19 hash
20}
21
22#[derive(Clone, Debug, PartialEq, Eq, Hash)]
28pub enum SnapshotField {
29 Rules,
31 Facts,
33 TensorValues,
35 Metadata,
37 Sessions,
39}
40
41#[derive(Clone, Debug, PartialEq)]
47pub struct FieldData {
48 pub field: SnapshotField,
50 pub size_bytes: u64,
52 pub checksum: u64,
54 pub record_count: usize,
56}
57
58impl FieldData {
59 pub fn new(field: SnapshotField, size_bytes: u64, record_count: usize) -> Self {
62 let checksum = fnv1a_u64(field_name_bytes(&field));
63 Self {
64 field,
65 size_bytes,
66 checksum,
67 record_count,
68 }
69 }
70}
71
72fn field_name_bytes(field: &SnapshotField) -> &'static [u8] {
74 match field {
75 SnapshotField::Rules => b"Rules",
76 SnapshotField::Facts => b"Facts",
77 SnapshotField::TensorValues => b"TensorValues",
78 SnapshotField::Metadata => b"Metadata",
79 SnapshotField::Sessions => b"Sessions",
80 }
81}
82
83#[derive(Clone, Debug)]
89pub struct StateSnapshot {
90 pub snapshot_id: u64,
92 pub created_at_secs: u64,
94 pub node_id: String,
96 pub fields: Vec<FieldData>,
98 pub format_version: u32,
100}
101
102impl StateSnapshot {
103 pub fn total_size(&self) -> u64 {
105 self.fields.iter().map(|f| f.size_bytes).sum()
106 }
107
108 pub fn has_field(&self, field: &SnapshotField) -> bool {
110 self.fields.iter().any(|f| &f.field == field)
111 }
112
113 pub fn field_count(&self) -> usize {
115 self.fields.len()
116 }
117}
118
119#[derive(Clone, Debug)]
125pub struct SnapshotDelta {
126 pub base_snapshot_id: u64,
128 pub new_snapshot_id: u64,
130 pub added_fields: Vec<SnapshotField>,
132 pub removed_fields: Vec<SnapshotField>,
134 pub size_delta_bytes: i64,
136}
137
138impl SnapshotDelta {
139 pub fn is_empty(&self) -> bool {
141 self.added_fields.is_empty() && self.removed_fields.is_empty()
142 }
143}
144
145#[derive(Clone, Debug)]
154pub struct StateSnapshotStats {
155 pub total_snapshots: usize,
157 pub total_size_bytes: u64,
159 pub oldest_snapshot_id: Option<u64>,
161 pub newest_snapshot_id: Option<u64>,
163}
164
165pub struct TensorStateSnapshot {
171 pub snapshots: HashMap<u64, StateSnapshot>,
173 pub next_id: u64,
175}
176
177impl TensorStateSnapshot {
178 pub fn new() -> Self {
180 Self {
181 snapshots: HashMap::new(),
182 next_id: 1,
183 }
184 }
185
186 pub fn capture(&mut self, node_id: &str, fields: Vec<FieldData>, now_secs: u64) -> u64 {
190 let id = self.next_id;
191 self.next_id += 1;
192 let snapshot = StateSnapshot {
193 snapshot_id: id,
194 created_at_secs: now_secs,
195 node_id: node_id.to_owned(),
196 fields,
197 format_version: 1,
198 };
199 self.snapshots.insert(id, snapshot);
200 id
201 }
202
203 pub fn get(&self, id: u64) -> Option<&StateSnapshot> {
205 self.snapshots.get(&id)
206 }
207
208 pub fn delete(&mut self, id: u64) -> bool {
210 self.snapshots.remove(&id).is_some()
211 }
212
213 pub fn diff(&self, old_id: u64, new_id: u64) -> Option<SnapshotDelta> {
217 let old = self.snapshots.get(&old_id)?;
218 let new = self.snapshots.get(&new_id)?;
219
220 let old_fields: std::collections::HashSet<&SnapshotField> =
221 old.fields.iter().map(|f| &f.field).collect();
222 let new_fields: std::collections::HashSet<&SnapshotField> =
223 new.fields.iter().map(|f| &f.field).collect();
224
225 let added_fields: Vec<SnapshotField> = new_fields
226 .difference(&old_fields)
227 .map(|f| (*f).clone())
228 .collect();
229 let removed_fields: Vec<SnapshotField> = old_fields
230 .difference(&new_fields)
231 .map(|f| (*f).clone())
232 .collect();
233
234 let size_delta_bytes = new.total_size() as i64 - old.total_size() as i64;
235
236 Some(SnapshotDelta {
237 base_snapshot_id: old_id,
238 new_snapshot_id: new_id,
239 added_fields,
240 removed_fields,
241 size_delta_bytes,
242 })
243 }
244
245 pub fn latest(&self) -> Option<&StateSnapshot> {
247 self.snapshots.values().max_by_key(|s| s.snapshot_id)
248 }
249
250 pub fn stats(&self) -> StateSnapshotStats {
252 let total_snapshots = self.snapshots.len();
253 let total_size_bytes = self.snapshots.values().map(|s| s.total_size()).sum();
254 let oldest_snapshot_id = self.snapshots.keys().copied().min();
255 let newest_snapshot_id = self.snapshots.keys().copied().max();
256 StateSnapshotStats {
257 total_snapshots,
258 total_size_bytes,
259 oldest_snapshot_id,
260 newest_snapshot_id,
261 }
262 }
263}
264
265impl Default for TensorStateSnapshot {
266 fn default() -> Self {
267 Self::new()
268 }
269}
270
271#[cfg(test)]
276mod tests {
277 use super::*;
278
279 fn rules_field(size: u64) -> FieldData {
284 FieldData::new(SnapshotField::Rules, size, 10)
285 }
286
287 fn facts_field(size: u64) -> FieldData {
288 FieldData::new(SnapshotField::Facts, size, 20)
289 }
290
291 fn tensor_field(size: u64) -> FieldData {
292 FieldData::new(SnapshotField::TensorValues, size, 5)
293 }
294
295 fn metadata_field(size: u64) -> FieldData {
296 FieldData::new(SnapshotField::Metadata, size, 3)
297 }
298
299 fn sessions_field(size: u64) -> FieldData {
300 FieldData::new(SnapshotField::Sessions, size, 2)
301 }
302
303 #[test]
308 fn test_new_starts_empty() {
309 let mgr = TensorStateSnapshot::new();
310 assert!(mgr.snapshots.is_empty());
311 assert_eq!(mgr.next_id, 1);
312 }
313
314 #[test]
319 fn test_capture_stores_snapshot() {
320 let mut mgr = TensorStateSnapshot::new();
321 let id = mgr.capture("node-a", vec![rules_field(512)], 1_000);
322 let snap = mgr.get(id).expect("snapshot must exist after capture");
323 assert_eq!(snap.snapshot_id, id);
324 assert_eq!(snap.node_id, "node-a");
325 assert_eq!(snap.created_at_secs, 1_000);
326 }
327
328 #[test]
329 fn test_capture_returns_incrementing_ids() {
330 let mut mgr = TensorStateSnapshot::new();
331 let id1 = mgr.capture("node-a", vec![rules_field(100)], 1);
332 let id2 = mgr.capture("node-a", vec![rules_field(200)], 2);
333 let id3 = mgr.capture("node-a", vec![rules_field(300)], 3);
334 assert!(id1 < id2);
335 assert!(id2 < id3);
336 }
337
338 #[test]
339 fn test_get_some() {
340 let mut mgr = TensorStateSnapshot::new();
341 let id = mgr.capture("node-b", vec![facts_field(256)], 42);
342 assert!(mgr.get(id).is_some());
343 }
344
345 #[test]
346 fn test_get_none_for_unknown_id() {
347 let mgr = TensorStateSnapshot::new();
348 assert!(mgr.get(999).is_none());
349 }
350
351 #[test]
356 fn test_delete_returns_true_when_exists() {
357 let mut mgr = TensorStateSnapshot::new();
358 let id = mgr.capture("node-c", vec![rules_field(64)], 10);
359 assert!(mgr.delete(id));
360 }
361
362 #[test]
363 fn test_delete_returns_false_when_missing() {
364 let mut mgr = TensorStateSnapshot::new();
365 assert!(!mgr.delete(777));
366 }
367
368 #[test]
369 fn test_delete_removes_snapshot() {
370 let mut mgr = TensorStateSnapshot::new();
371 let id = mgr.capture("node-d", vec![rules_field(128)], 20);
372 mgr.delete(id);
373 assert!(mgr.get(id).is_none());
374 }
375
376 #[test]
381 fn test_has_field_true() {
382 let mut mgr = TensorStateSnapshot::new();
383 let id = mgr.capture("node-e", vec![facts_field(100)], 5);
384 let snap = mgr.get(id).expect("test: should succeed");
385 assert!(snap.has_field(&SnapshotField::Facts));
386 }
387
388 #[test]
389 fn test_has_field_false() {
390 let mut mgr = TensorStateSnapshot::new();
391 let id = mgr.capture("node-e", vec![facts_field(100)], 5);
392 let snap = mgr.get(id).expect("test: should succeed");
393 assert!(!snap.has_field(&SnapshotField::Rules));
394 }
395
396 #[test]
401 fn test_total_size_sum_of_fields() {
402 let mut mgr = TensorStateSnapshot::new();
403 let fields = vec![rules_field(100), facts_field(200), tensor_field(50)];
404 let id = mgr.capture("node-f", fields, 0);
405 let snap = mgr.get(id).expect("test: should succeed");
406 assert_eq!(snap.total_size(), 350);
407 }
408
409 #[test]
410 fn test_field_count_correct() {
411 let mut mgr = TensorStateSnapshot::new();
412 let fields = vec![rules_field(10), facts_field(20), metadata_field(5)];
413 let id = mgr.capture("node-g", fields, 0);
414 let snap = mgr.get(id).expect("test: should succeed");
415 assert_eq!(snap.field_count(), 3);
416 }
417
418 #[test]
423 fn test_diff_added_fields_detected() {
424 let mut mgr = TensorStateSnapshot::new();
425 let id1 = mgr.capture("n", vec![rules_field(100)], 1);
426 let id2 = mgr.capture("n", vec![rules_field(100), facts_field(50)], 2);
427 let delta = mgr.diff(id1, id2).expect("diff must return Some");
428 assert!(delta.added_fields.contains(&SnapshotField::Facts));
429 assert!(delta.removed_fields.is_empty());
430 }
431
432 #[test]
433 fn test_diff_removed_fields_detected() {
434 let mut mgr = TensorStateSnapshot::new();
435 let id1 = mgr.capture("n", vec![rules_field(100), facts_field(50)], 1);
436 let id2 = mgr.capture("n", vec![rules_field(100)], 2);
437 let delta = mgr.diff(id1, id2).expect("diff must return Some");
438 assert!(delta.removed_fields.contains(&SnapshotField::Facts));
439 assert!(delta.added_fields.is_empty());
440 }
441
442 #[test]
443 fn test_diff_size_delta_positive() {
444 let mut mgr = TensorStateSnapshot::new();
445 let id1 = mgr.capture("n", vec![rules_field(100)], 1);
446 let id2 = mgr.capture("n", vec![rules_field(100), facts_field(200)], 2);
447 let delta = mgr.diff(id1, id2).expect("test: should succeed");
448 assert_eq!(delta.size_delta_bytes, 200);
449 }
450
451 #[test]
452 fn test_diff_size_delta_negative() {
453 let mut mgr = TensorStateSnapshot::new();
454 let id1 = mgr.capture("n", vec![rules_field(300)], 1);
455 let id2 = mgr.capture("n", vec![rules_field(100)], 2);
456 let delta = mgr.diff(id1, id2).expect("test: should succeed");
457 assert_eq!(delta.size_delta_bytes, -200);
458 }
459
460 #[test]
461 fn test_diff_returns_none_for_unknown_old_id() {
462 let mut mgr = TensorStateSnapshot::new();
463 let id = mgr.capture("n", vec![rules_field(10)], 1);
464 assert!(mgr.diff(999, id).is_none());
465 }
466
467 #[test]
468 fn test_diff_returns_none_for_unknown_new_id() {
469 let mut mgr = TensorStateSnapshot::new();
470 let id = mgr.capture("n", vec![rules_field(10)], 1);
471 assert!(mgr.diff(id, 999).is_none());
472 }
473
474 #[test]
475 fn test_diff_is_empty_when_no_changes() {
476 let mut mgr = TensorStateSnapshot::new();
477 let id1 = mgr.capture("n", vec![rules_field(100)], 1);
478 let id2 = mgr.capture("n", vec![rules_field(100)], 2);
479 let delta = mgr.diff(id1, id2).expect("test: should succeed");
480 assert!(delta.is_empty());
481 }
482
483 #[test]
484 fn test_diff_is_empty_false_when_changes() {
485 let mut mgr = TensorStateSnapshot::new();
486 let id1 = mgr.capture("n", vec![rules_field(100)], 1);
487 let id2 = mgr.capture("n", vec![facts_field(100)], 2);
488 let delta = mgr.diff(id1, id2).expect("test: should succeed");
489 assert!(!delta.is_empty());
490 }
491
492 #[test]
497 fn test_latest_returns_highest_id() {
498 let mut mgr = TensorStateSnapshot::new();
499 mgr.capture("n", vec![rules_field(10)], 1);
500 mgr.capture("n", vec![rules_field(20)], 2);
501 let id3 = mgr.capture("n", vec![rules_field(30)], 3);
502 let latest = mgr.latest().expect("latest must be Some");
503 assert_eq!(latest.snapshot_id, id3);
504 }
505
506 #[test]
507 fn test_latest_none_when_empty() {
508 let mgr = TensorStateSnapshot::new();
509 assert!(mgr.latest().is_none());
510 }
511
512 #[test]
517 fn test_stats_total_snapshots() {
518 let mut mgr = TensorStateSnapshot::new();
519 mgr.capture("n", vec![rules_field(10)], 1);
520 mgr.capture("n", vec![rules_field(10)], 2);
521 assert_eq!(mgr.stats().total_snapshots, 2);
522 }
523
524 #[test]
525 fn test_stats_total_size_bytes() {
526 let mut mgr = TensorStateSnapshot::new();
527 mgr.capture("n", vec![rules_field(100)], 1);
528 mgr.capture("n", vec![facts_field(200)], 2);
529 assert_eq!(mgr.stats().total_size_bytes, 300);
530 }
531
532 #[test]
533 fn test_stats_oldest_newest_snapshot_id() {
534 let mut mgr = TensorStateSnapshot::new();
535 let id1 = mgr.capture("n", vec![rules_field(10)], 1);
536 mgr.capture("n", vec![rules_field(10)], 2);
537 let id3 = mgr.capture("n", vec![rules_field(10)], 3);
538 let stats = mgr.stats();
539 assert_eq!(stats.oldest_snapshot_id, Some(id1));
540 assert_eq!(stats.newest_snapshot_id, Some(id3));
541 }
542
543 #[test]
548 fn test_fnv1a_u64_deterministic() {
549 let a = fnv1a_u64(b"Rules");
550 let b = fnv1a_u64(b"Rules");
551 assert_eq!(a, b);
552 }
553
554 #[test]
555 fn test_fnv1a_u64_different_inputs_differ() {
556 let a = fnv1a_u64(b"Rules");
557 let b = fnv1a_u64(b"Facts");
558 assert_ne!(a, b);
559 }
560
561 #[test]
566 fn test_field_data_checksum_matches_fnv1a_of_name() {
567 let fd = FieldData::new(SnapshotField::Rules, 512, 10);
568 assert_eq!(fd.checksum, fnv1a_u64(b"Rules"));
569 }
570
571 #[test]
572 fn test_all_fields_have_distinct_checksums() {
573 let fields = [
574 SnapshotField::Rules,
575 SnapshotField::Facts,
576 SnapshotField::TensorValues,
577 SnapshotField::Metadata,
578 SnapshotField::Sessions,
579 ];
580 let checksums: Vec<u64> = fields
581 .iter()
582 .map(|f| FieldData::new(f.clone(), 0, 0).checksum)
583 .collect();
584 let unique: std::collections::HashSet<u64> = checksums.iter().copied().collect();
585 assert_eq!(
586 unique.len(),
587 fields.len(),
588 "all field checksums must be distinct"
589 );
590 }
591
592 #[test]
597 fn test_snapshot_format_version_default_one() {
598 let mut mgr = TensorStateSnapshot::new();
599 let id = mgr.capture("n", vec![sessions_field(64)], 0);
600 assert_eq!(mgr.get(id).expect("test: should succeed").format_version, 1);
601 }
602}