1use std::collections::HashMap;
6
7use serde::{Deserialize, Serialize};
8
9use super::manifest::ConfigProvenance;
10
11pub const MAGIC_BYTES: &[u8; 13] = b"SQRY_GRAPH_V7";
27
28pub const VERSION: u32 = 7;
32
33pub const MAGIC_BYTES_V7: &[u8; 13] = b"SQRY_GRAPH_V7";
38
39pub const MAGIC_BYTES_V8: &[u8; 13] = b"SQRY_GRAPH_V8";
45
46pub const MAGIC_BYTES_V9: &[u8; 13] = b"SQRY_GRAPH_V9";
53
54pub const MAGIC_BYTES_V10: &[u8; 14] = b"SQRY_GRAPH_V10";
61
62pub const MAGIC_BYTES_V11: &[u8; 14] = b"SQRY_GRAPH_V11";
77
78pub const MAGIC_BYTES_V12: &[u8; 14] = b"SQRY_GRAPH_V12";
100
101pub const MAGIC_BYTES_V13: &[u8; 14] = b"SQRY_GRAPH_V13";
110
111pub const LEGACY_VERSION_V7: u32 = 7;
114
115#[repr(u32)]
122#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
123pub enum FormatVersion {
124 V7 = 7,
126 V8 = 8,
128 V9 = 9,
132 V10 = 10,
136 V11 = 11,
142 V12 = 12,
146 V13 = 13,
151}
152
153impl FormatVersion {
154 #[must_use]
156 pub const fn magic(self) -> &'static [u8] {
157 match self {
158 Self::V7 => MAGIC_BYTES_V7.as_slice(),
159 Self::V8 => MAGIC_BYTES_V8.as_slice(),
160 Self::V9 => MAGIC_BYTES_V9.as_slice(),
161 Self::V10 => MAGIC_BYTES_V10.as_slice(),
162 Self::V11 => MAGIC_BYTES_V11.as_slice(),
163 Self::V12 => MAGIC_BYTES_V12.as_slice(),
164 Self::V13 => MAGIC_BYTES_V13.as_slice(),
165 }
166 }
167
168 #[must_use]
170 pub const fn as_u32(self) -> u32 {
171 self as u32
172 }
173
174 #[must_use]
178 pub fn from_magic(bytes: &[u8]) -> Option<Self> {
179 if bytes.len() >= MAGIC_BYTES_V13.len()
183 && bytes[..MAGIC_BYTES_V13.len()] == *MAGIC_BYTES_V13
184 {
185 return Some(Self::V13);
186 }
187 if bytes.len() >= MAGIC_BYTES_V12.len()
188 && bytes[..MAGIC_BYTES_V12.len()] == *MAGIC_BYTES_V12
189 {
190 return Some(Self::V12);
191 }
192 if bytes.len() >= MAGIC_BYTES_V11.len()
193 && bytes[..MAGIC_BYTES_V11.len()] == *MAGIC_BYTES_V11
194 {
195 return Some(Self::V11);
196 }
197 if bytes.len() >= MAGIC_BYTES_V10.len()
198 && bytes[..MAGIC_BYTES_V10.len()] == *MAGIC_BYTES_V10
199 {
200 return Some(Self::V10);
201 }
202 if bytes.len() < MAGIC_BYTES_V7.len() {
203 return None;
204 }
205 let prefix = &bytes[..MAGIC_BYTES_V7.len()];
206 if prefix == MAGIC_BYTES_V7 {
207 Some(Self::V7)
208 } else if prefix == MAGIC_BYTES_V8 {
209 Some(Self::V8)
210 } else if prefix == MAGIC_BYTES_V9 {
211 Some(Self::V9)
212 } else {
213 None
214 }
215 }
216}
217
218pub const CURRENT_VERSION: FormatVersion = FormatVersion::V13;
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct GraphHeader {
227 pub version: u32,
229
230 pub node_count: usize,
232
233 pub edge_count: usize,
235
236 pub string_count: usize,
238
239 pub file_count: usize,
241
242 pub timestamp: u64,
244
245 #[serde(default)]
247 pub config_provenance: Option<ConfigProvenance>,
248
249 #[serde(default)]
254 pub plugin_versions: HashMap<String, String>,
255
256 #[serde(default)]
270 pub fact_epoch: u64,
271}
272
273impl GraphHeader {
274 #[must_use]
276 pub fn new(
277 node_count: usize,
278 edge_count: usize,
279 string_count: usize,
280 file_count: usize,
281 ) -> Self {
282 Self {
283 version: VERSION,
284 node_count,
285 edge_count,
286 string_count,
287 file_count,
288 timestamp: std::time::SystemTime::now()
289 .duration_since(std::time::UNIX_EPOCH)
290 .unwrap_or_default()
291 .as_secs(),
292 config_provenance: None,
293 plugin_versions: HashMap::new(),
294 fact_epoch: 0,
295 }
296 }
297
298 #[must_use]
300 pub fn with_provenance(
301 node_count: usize,
302 edge_count: usize,
303 string_count: usize,
304 file_count: usize,
305 provenance: ConfigProvenance,
306 ) -> Self {
307 Self {
308 version: VERSION,
309 node_count,
310 edge_count,
311 string_count,
312 file_count,
313 timestamp: std::time::SystemTime::now()
314 .duration_since(std::time::UNIX_EPOCH)
315 .unwrap_or_default()
316 .as_secs(),
317 config_provenance: Some(provenance),
318 plugin_versions: HashMap::new(),
319 fact_epoch: 0,
320 }
321 }
322
323 #[must_use]
325 pub fn with_provenance_and_plugins(
326 node_count: usize,
327 edge_count: usize,
328 string_count: usize,
329 file_count: usize,
330 provenance: ConfigProvenance,
331 plugin_versions: HashMap<String, String>,
332 ) -> Self {
333 Self {
334 version: VERSION,
335 node_count,
336 edge_count,
337 string_count,
338 file_count,
339 timestamp: std::time::SystemTime::now()
340 .duration_since(std::time::UNIX_EPOCH)
341 .unwrap_or_default()
342 .as_secs(),
343 config_provenance: Some(provenance),
344 plugin_versions,
345 fact_epoch: 0,
346 }
347 }
348
349 #[must_use]
351 pub fn provenance(&self) -> Option<&ConfigProvenance> {
352 self.config_provenance.as_ref()
353 }
354
355 #[must_use]
357 pub fn has_provenance(&self) -> bool {
358 self.config_provenance.is_some()
359 }
360
361 #[must_use]
363 pub fn plugin_versions(&self) -> &HashMap<String, String> {
364 &self.plugin_versions
365 }
366
367 pub fn set_plugin_versions(&mut self, versions: HashMap<String, String>) {
369 self.plugin_versions = versions;
370 }
371
372 #[must_use]
379 pub fn fact_epoch(&self) -> u64 {
380 self.fact_epoch
381 }
382
383 pub fn set_fact_epoch(&mut self, epoch: u64) {
389 self.fact_epoch = epoch;
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use std::collections::HashMap;
397 use std::path::PathBuf;
398
399 fn make_test_provenance() -> ConfigProvenance {
400 ConfigProvenance {
401 config_file: PathBuf::from(".sqry/graph/config/config.json"),
402 config_checksum: "abc123def456".to_string(),
403 schema_version: 1,
404 overrides: HashMap::new(),
405 build_timestamp: std::time::SystemTime::now()
406 .duration_since(std::time::UNIX_EPOCH)
407 .unwrap_or_default()
408 .as_secs(),
409 build_host: Some("test-host".to_string()),
410 }
411 }
412
413 #[test]
414 fn test_magic_bytes() {
415 assert_eq!(MAGIC_BYTES, b"SQRY_GRAPH_V7");
416 assert_eq!(MAGIC_BYTES.len(), 13);
417 }
418
419 #[test]
420 fn test_version() {
421 assert_eq!(VERSION, 7);
422 }
423
424 #[test]
425 fn test_graph_header_new() {
426 let header = GraphHeader::new(100, 50, 200, 10);
427
428 assert_eq!(header.version, VERSION);
429 assert_eq!(header.node_count, 100);
430 assert_eq!(header.edge_count, 50);
431 assert_eq!(header.string_count, 200);
432 assert_eq!(header.file_count, 10);
433 assert!(header.timestamp > 0);
434 assert!(header.config_provenance.is_none());
435 }
436
437 #[test]
438 fn test_graph_header_with_provenance() {
439 let provenance = make_test_provenance();
440 let header = GraphHeader::with_provenance(100, 50, 200, 10, provenance);
441
442 assert_eq!(header.version, VERSION);
443 assert_eq!(header.node_count, 100);
444 assert_eq!(header.edge_count, 50);
445 assert!(header.config_provenance.is_some());
446 assert_eq!(
447 header.config_provenance.as_ref().unwrap().config_checksum,
448 "abc123def456"
449 );
450 }
451
452 #[test]
453 fn test_graph_header_provenance_method() {
454 let header = GraphHeader::new(10, 5, 20, 2);
455 assert!(header.provenance().is_none());
456
457 let provenance = make_test_provenance();
458 let header_with = GraphHeader::with_provenance(10, 5, 20, 2, provenance);
459 assert!(header_with.provenance().is_some());
460 assert_eq!(
461 header_with.provenance().unwrap().config_checksum,
462 "abc123def456"
463 );
464 }
465
466 #[test]
467 fn test_graph_header_has_provenance() {
468 let header = GraphHeader::new(10, 5, 20, 2);
469 assert!(!header.has_provenance());
470
471 let provenance = make_test_provenance();
472 let header_with = GraphHeader::with_provenance(10, 5, 20, 2, provenance);
473 assert!(header_with.has_provenance());
474 }
475
476 #[test]
477 fn test_graph_header_clone() {
478 let header = GraphHeader::new(100, 50, 200, 10);
479 let cloned = header.clone();
480
481 assert_eq!(header.version, cloned.version);
482 assert_eq!(header.node_count, cloned.node_count);
483 assert_eq!(header.edge_count, cloned.edge_count);
484 assert_eq!(header.string_count, cloned.string_count);
485 assert_eq!(header.file_count, cloned.file_count);
486 }
487
488 #[test]
489 fn test_graph_header_debug() {
490 let header = GraphHeader::new(100, 50, 200, 10);
491 let debug_str = format!("{header:?}");
492
493 assert!(debug_str.contains("GraphHeader"));
494 assert!(debug_str.contains("version"));
495 assert!(debug_str.contains("node_count"));
496 }
497
498 #[test]
499 fn test_graph_header_timestamp_is_recent() {
500 let header = GraphHeader::new(10, 5, 20, 2);
501 let now = std::time::SystemTime::now()
502 .duration_since(std::time::UNIX_EPOCH)
503 .unwrap()
504 .as_secs();
505
506 assert!(header.timestamp <= now);
508 assert!(header.timestamp >= now - 1);
509 }
510
511 #[test]
512 fn test_graph_header_zero_counts() {
513 let header = GraphHeader::new(0, 0, 0, 0);
514
515 assert_eq!(header.node_count, 0);
516 assert_eq!(header.edge_count, 0);
517 assert_eq!(header.string_count, 0);
518 assert_eq!(header.file_count, 0);
519 }
520
521 #[test]
522 fn test_graph_header_large_counts() {
523 let header = GraphHeader::new(1_000_000, 5_000_000, 10_000_000, 100_000);
524
525 assert_eq!(header.node_count, 1_000_000);
526 assert_eq!(header.edge_count, 5_000_000);
527 assert_eq!(header.string_count, 10_000_000);
528 assert_eq!(header.file_count, 100_000);
529 }
530
531 #[test]
532 fn test_graph_header_plugin_versions_empty_by_default() {
533 let header = GraphHeader::new(10, 5, 20, 2);
534 assert!(header.plugin_versions().is_empty());
535 }
536
537 #[test]
538 fn test_graph_header_set_plugin_versions() {
539 let mut header = GraphHeader::new(10, 5, 20, 2);
540
541 let mut versions = HashMap::new();
542 versions.insert("rust".to_string(), "3.3.0".to_string());
543 versions.insert("javascript".to_string(), "3.3.0".to_string());
544
545 header.set_plugin_versions(versions.clone());
546
547 assert_eq!(header.plugin_versions().len(), 2);
548 assert_eq!(
549 header.plugin_versions().get("rust"),
550 Some(&"3.3.0".to_string())
551 );
552 assert_eq!(
553 header.plugin_versions().get("javascript"),
554 Some(&"3.3.0".to_string())
555 );
556 }
557
558 #[test]
563 fn phase1_graph_header_new_defaults_fact_epoch_to_zero() {
564 let header = GraphHeader::new(10, 5, 20, 2);
565 assert_eq!(header.fact_epoch, 0);
566 assert_eq!(header.fact_epoch(), 0);
567 }
568
569 #[test]
570 fn phase1_graph_header_with_provenance_defaults_fact_epoch_to_zero() {
571 let header = GraphHeader::with_provenance(10, 5, 20, 2, make_test_provenance());
572 assert_eq!(header.fact_epoch, 0);
573 }
574
575 #[test]
576 fn phase1_graph_header_set_fact_epoch_round_trip() {
577 let mut header = GraphHeader::new(10, 5, 20, 2);
578 header.set_fact_epoch(42);
579 assert_eq!(header.fact_epoch(), 42);
580 }
581
582 #[test]
583 fn phase1_graph_header_postcard_round_trip_with_fact_epoch() {
584 let mut header = GraphHeader::new(100, 50, 200, 10);
585 header.set_fact_epoch(1_234_567);
586
587 let encoded = postcard::to_allocvec(&header).expect("encode");
588 let decoded: GraphHeader = postcard::from_bytes(&encoded).expect("decode");
589
590 assert_eq!(decoded.fact_epoch(), 1_234_567);
591 assert_eq!(decoded.node_count, 100);
592 assert_eq!(decoded.edge_count, 50);
593 }
594
595 #[test]
596 fn phase1_graph_header_fact_epoch_preserved_through_clone() {
597 let mut header = GraphHeader::new(10, 5, 20, 2);
598 header.set_fact_epoch(9_999);
599 let cloned = header.clone();
600 assert_eq!(cloned.fact_epoch(), 9_999);
601 }
602
603 #[test]
608 fn phase1_magic_bytes_v7_matches_legacy() {
609 assert_eq!(MAGIC_BYTES_V7, b"SQRY_GRAPH_V7");
610 assert_eq!(MAGIC_BYTES_V7, MAGIC_BYTES);
611 assert_eq!(MAGIC_BYTES_V7.len(), 13);
612 }
613
614 #[test]
615 fn phase1_magic_bytes_v8_is_distinct_and_13_bytes() {
616 assert_eq!(MAGIC_BYTES_V8, b"SQRY_GRAPH_V8");
617 assert_eq!(MAGIC_BYTES_V8.len(), 13);
618 assert_ne!(MAGIC_BYTES_V8, MAGIC_BYTES_V7);
619 }
620
621 #[test]
622 fn phase1_legacy_version_v7_equals_seven() {
623 assert_eq!(LEGACY_VERSION_V7, 7);
624 }
625
626 #[test]
627 fn phase1_format_version_discriminants() {
628 assert_eq!(FormatVersion::V7 as u32, 7);
629 assert_eq!(FormatVersion::V8 as u32, 8);
630 assert_eq!(FormatVersion::V9 as u32, 9);
631 assert_eq!(FormatVersion::V10 as u32, 10);
632 assert_eq!(FormatVersion::V11 as u32, 11);
633 assert_eq!(FormatVersion::V12 as u32, 12);
634 assert_eq!(FormatVersion::V13 as u32, 13);
635 }
636
637 #[test]
638 fn current_version_is_v13() {
639 assert_eq!(CURRENT_VERSION, FormatVersion::V13);
640 }
641
642 #[test]
643 fn t3_magic_bytes_v13_is_distinct_and_14_bytes() {
644 assert_eq!(MAGIC_BYTES_V13, b"SQRY_GRAPH_V13");
645 assert_eq!(MAGIC_BYTES_V13.len(), 14);
646 assert_ne!(MAGIC_BYTES_V13.as_slice(), MAGIC_BYTES_V12.as_slice());
647 assert_ne!(MAGIC_BYTES_V13.as_slice(), MAGIC_BYTES_V10.as_slice());
648 }
649
650 #[test]
651 fn t3_format_version_from_magic_v13() {
652 assert_eq!(
653 FormatVersion::from_magic(MAGIC_BYTES_V13),
654 Some(FormatVersion::V13),
655 );
656 }
657
658 #[test]
664 fn t3_format_version_dispatch_v13_before_v12_v11_v10() {
665 let mut buf = MAGIC_BYTES_V13.to_vec();
666 buf.extend_from_slice(&[0u8; 8]);
667 assert_eq!(FormatVersion::from_magic(&buf), Some(FormatVersion::V13));
668
669 let mut buf12 = MAGIC_BYTES_V12.to_vec();
670 buf12.extend_from_slice(&[0u8; 8]);
671 assert_eq!(FormatVersion::from_magic(&buf12), Some(FormatVersion::V12));
672 }
673
674 #[test]
675 fn t3_format_version_v13_magic_round_trip() {
676 let v = FormatVersion::V13;
677 let bytes = v.magic();
678 assert_eq!(bytes, MAGIC_BYTES_V13.as_slice());
679 assert_eq!(FormatVersion::from_magic(bytes), Some(v));
680 }
681
682 #[test]
683 fn phase_a_magic_bytes_v11_is_distinct_and_14_bytes() {
684 assert_eq!(MAGIC_BYTES_V11, b"SQRY_GRAPH_V11");
685 assert_eq!(MAGIC_BYTES_V11.len(), 14);
686 assert_ne!(MAGIC_BYTES_V11, MAGIC_BYTES_V10);
687 }
688
689 #[test]
690 fn phase_a_format_version_from_magic_v11() {
691 assert_eq!(
692 FormatVersion::from_magic(MAGIC_BYTES_V11),
693 Some(FormatVersion::V11),
694 );
695 }
696
697 #[test]
703 fn phase_a_format_version_dispatch_v11_before_v10() {
704 let mut buf = MAGIC_BYTES_V11.to_vec();
705 buf.extend_from_slice(&[0u8; 8]);
707 assert_eq!(FormatVersion::from_magic(&buf), Some(FormatVersion::V11));
708
709 let mut buf10 = MAGIC_BYTES_V10.to_vec();
710 buf10.extend_from_slice(&[0u8; 8]);
711 assert_eq!(FormatVersion::from_magic(&buf10), Some(FormatVersion::V10));
712 }
713
714 #[test]
715 fn phase_a_format_version_v11_magic_round_trip() {
716 let v = FormatVersion::V11;
717 let bytes = v.magic();
718 assert_eq!(bytes, MAGIC_BYTES_V11.as_slice());
719 assert_eq!(FormatVersion::from_magic(bytes), Some(v));
720 }
721
722 #[test]
723 fn phase1_format_version_from_magic_v7() {
724 assert_eq!(
725 FormatVersion::from_magic(MAGIC_BYTES_V7),
726 Some(FormatVersion::V7),
727 );
728 }
729
730 #[test]
731 fn phase1_format_version_from_magic_v8() {
732 assert_eq!(
733 FormatVersion::from_magic(MAGIC_BYTES_V8),
734 Some(FormatVersion::V8),
735 );
736 }
737
738 #[test]
739 fn phase2_magic_bytes_v9_is_distinct_and_13_bytes() {
740 assert_eq!(MAGIC_BYTES_V9, b"SQRY_GRAPH_V9");
741 assert_eq!(MAGIC_BYTES_V9.len(), 13);
742 assert_ne!(MAGIC_BYTES_V9, MAGIC_BYTES_V7);
743 assert_ne!(MAGIC_BYTES_V9, MAGIC_BYTES_V8);
744 }
745
746 #[test]
747 fn phase2_format_version_from_magic_v9() {
748 assert_eq!(
749 FormatVersion::from_magic(MAGIC_BYTES_V9),
750 Some(FormatVersion::V9),
751 );
752 }
753
754 #[test]
755 fn phase1_format_version_from_magic_unknown() {
756 assert_eq!(FormatVersion::from_magic(b"SQRY_GRAPH_V1"), None);
757 assert_eq!(FormatVersion::from_magic(b"NOT_A_GRAPH_!"), None);
758 }
759
760 #[test]
761 fn phase1_format_version_magic_round_trip() {
762 for version in [
763 FormatVersion::V7,
764 FormatVersion::V8,
765 FormatVersion::V9,
766 FormatVersion::V10,
767 FormatVersion::V11,
768 FormatVersion::V12,
769 FormatVersion::V13,
770 ] {
771 let bytes = version.magic();
772 assert_eq!(FormatVersion::from_magic(bytes), Some(version));
773 }
774 }
775
776 #[test]
781 fn phase_beta_magic_bytes_v12_is_distinct_and_14_bytes() {
782 assert_eq!(MAGIC_BYTES_V12, b"SQRY_GRAPH_V12");
783 assert_eq!(MAGIC_BYTES_V12.len(), 14);
784 assert_ne!(MAGIC_BYTES_V12, MAGIC_BYTES_V11);
785 assert_ne!(MAGIC_BYTES_V12, MAGIC_BYTES_V10);
786 }
787
788 #[test]
789 fn phase_beta_format_version_from_magic_v12() {
790 assert_eq!(
791 FormatVersion::from_magic(MAGIC_BYTES_V12),
792 Some(FormatVersion::V12),
793 );
794 }
795
796 #[test]
801 fn phase_beta_format_version_dispatch_v12_before_v11_v10() {
802 let mut buf = MAGIC_BYTES_V12.to_vec();
803 buf.extend_from_slice(&[0u8; 8]);
804 assert_eq!(FormatVersion::from_magic(&buf), Some(FormatVersion::V12));
805
806 let mut buf11 = MAGIC_BYTES_V11.to_vec();
807 buf11.extend_from_slice(&[0u8; 8]);
808 assert_eq!(FormatVersion::from_magic(&buf11), Some(FormatVersion::V11));
809
810 let mut buf10 = MAGIC_BYTES_V10.to_vec();
811 buf10.extend_from_slice(&[0u8; 8]);
812 assert_eq!(FormatVersion::from_magic(&buf10), Some(FormatVersion::V10));
813 }
814
815 #[test]
816 fn phase_beta_format_version_v12_magic_round_trip() {
817 let v = FormatVersion::V12;
818 let bytes = v.magic();
819 assert_eq!(bytes, MAGIC_BYTES_V12.as_slice());
820 assert_eq!(FormatVersion::from_magic(bytes), Some(v));
821 }
822
823 #[test]
824 fn phase1_format_version_copy_eq_debug() {
825 let v = FormatVersion::V8;
826 let copied = v;
827 assert_eq!(v, copied);
828 assert_eq!(format!("{v:?}"), "V8");
829 }
830
831 #[test]
832 fn phase2_format_version_v9_copy_eq_debug() {
833 let v = FormatVersion::V9;
834 let copied = v;
835 assert_eq!(v, copied);
836 assert_eq!(format!("{v:?}"), "V9");
837 }
838
839 #[test]
840 fn test_graph_header_with_provenance_and_plugins() {
841 let provenance = make_test_provenance();
842
843 let mut plugin_versions = HashMap::new();
844 plugin_versions.insert("rust".to_string(), "3.3.0".to_string());
845 plugin_versions.insert("python".to_string(), "3.3.0".to_string());
846
847 let header = GraphHeader::with_provenance_and_plugins(
848 100,
849 50,
850 200,
851 10,
852 provenance,
853 plugin_versions.clone(),
854 );
855
856 assert_eq!(header.version, VERSION);
857 assert_eq!(header.node_count, 100);
858 assert!(header.config_provenance.is_some());
859 assert_eq!(header.plugin_versions().len(), 2);
860 assert_eq!(
861 header.plugin_versions().get("rust"),
862 Some(&"3.3.0".to_string())
863 );
864 }
865}