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 LEGACY_VERSION_V7: u32 = 7;
81
82#[repr(u32)]
89#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
90pub enum FormatVersion {
91 V7 = 7,
93 V8 = 8,
95 V9 = 9,
99 V10 = 10,
103 V11 = 11,
109}
110
111impl FormatVersion {
112 #[must_use]
114 pub const fn magic(self) -> &'static [u8] {
115 match self {
116 Self::V7 => MAGIC_BYTES_V7.as_slice(),
117 Self::V8 => MAGIC_BYTES_V8.as_slice(),
118 Self::V9 => MAGIC_BYTES_V9.as_slice(),
119 Self::V10 => MAGIC_BYTES_V10.as_slice(),
120 Self::V11 => MAGIC_BYTES_V11.as_slice(),
121 }
122 }
123
124 #[must_use]
126 pub const fn as_u32(self) -> u32 {
127 self as u32
128 }
129
130 #[must_use]
134 pub fn from_magic(bytes: &[u8]) -> Option<Self> {
135 if bytes.len() >= MAGIC_BYTES_V11.len()
139 && bytes[..MAGIC_BYTES_V11.len()] == *MAGIC_BYTES_V11
140 {
141 return Some(Self::V11);
142 }
143 if bytes.len() >= MAGIC_BYTES_V10.len()
144 && bytes[..MAGIC_BYTES_V10.len()] == *MAGIC_BYTES_V10
145 {
146 return Some(Self::V10);
147 }
148 if bytes.len() < MAGIC_BYTES_V7.len() {
149 return None;
150 }
151 let prefix = &bytes[..MAGIC_BYTES_V7.len()];
152 if prefix == MAGIC_BYTES_V7 {
153 Some(Self::V7)
154 } else if prefix == MAGIC_BYTES_V8 {
155 Some(Self::V8)
156 } else if prefix == MAGIC_BYTES_V9 {
157 Some(Self::V9)
158 } else {
159 None
160 }
161 }
162}
163
164pub const CURRENT_VERSION: FormatVersion = FormatVersion::V11;
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct GraphHeader {
173 pub version: u32,
175
176 pub node_count: usize,
178
179 pub edge_count: usize,
181
182 pub string_count: usize,
184
185 pub file_count: usize,
187
188 pub timestamp: u64,
190
191 #[serde(default)]
193 pub config_provenance: Option<ConfigProvenance>,
194
195 #[serde(default)]
200 pub plugin_versions: HashMap<String, String>,
201
202 #[serde(default)]
216 pub fact_epoch: u64,
217}
218
219impl GraphHeader {
220 #[must_use]
222 pub fn new(
223 node_count: usize,
224 edge_count: usize,
225 string_count: usize,
226 file_count: usize,
227 ) -> Self {
228 Self {
229 version: VERSION,
230 node_count,
231 edge_count,
232 string_count,
233 file_count,
234 timestamp: std::time::SystemTime::now()
235 .duration_since(std::time::UNIX_EPOCH)
236 .unwrap_or_default()
237 .as_secs(),
238 config_provenance: None,
239 plugin_versions: HashMap::new(),
240 fact_epoch: 0,
241 }
242 }
243
244 #[must_use]
246 pub fn with_provenance(
247 node_count: usize,
248 edge_count: usize,
249 string_count: usize,
250 file_count: usize,
251 provenance: ConfigProvenance,
252 ) -> Self {
253 Self {
254 version: VERSION,
255 node_count,
256 edge_count,
257 string_count,
258 file_count,
259 timestamp: std::time::SystemTime::now()
260 .duration_since(std::time::UNIX_EPOCH)
261 .unwrap_or_default()
262 .as_secs(),
263 config_provenance: Some(provenance),
264 plugin_versions: HashMap::new(),
265 fact_epoch: 0,
266 }
267 }
268
269 #[must_use]
271 pub fn with_provenance_and_plugins(
272 node_count: usize,
273 edge_count: usize,
274 string_count: usize,
275 file_count: usize,
276 provenance: ConfigProvenance,
277 plugin_versions: HashMap<String, String>,
278 ) -> Self {
279 Self {
280 version: VERSION,
281 node_count,
282 edge_count,
283 string_count,
284 file_count,
285 timestamp: std::time::SystemTime::now()
286 .duration_since(std::time::UNIX_EPOCH)
287 .unwrap_or_default()
288 .as_secs(),
289 config_provenance: Some(provenance),
290 plugin_versions,
291 fact_epoch: 0,
292 }
293 }
294
295 #[must_use]
297 pub fn provenance(&self) -> Option<&ConfigProvenance> {
298 self.config_provenance.as_ref()
299 }
300
301 #[must_use]
303 pub fn has_provenance(&self) -> bool {
304 self.config_provenance.is_some()
305 }
306
307 #[must_use]
309 pub fn plugin_versions(&self) -> &HashMap<String, String> {
310 &self.plugin_versions
311 }
312
313 pub fn set_plugin_versions(&mut self, versions: HashMap<String, String>) {
315 self.plugin_versions = versions;
316 }
317
318 #[must_use]
325 pub fn fact_epoch(&self) -> u64 {
326 self.fact_epoch
327 }
328
329 pub fn set_fact_epoch(&mut self, epoch: u64) {
335 self.fact_epoch = epoch;
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use std::collections::HashMap;
343 use std::path::PathBuf;
344
345 fn make_test_provenance() -> ConfigProvenance {
346 ConfigProvenance {
347 config_file: PathBuf::from(".sqry/graph/config/config.json"),
348 config_checksum: "abc123def456".to_string(),
349 schema_version: 1,
350 overrides: HashMap::new(),
351 build_timestamp: std::time::SystemTime::now()
352 .duration_since(std::time::UNIX_EPOCH)
353 .unwrap_or_default()
354 .as_secs(),
355 build_host: Some("test-host".to_string()),
356 }
357 }
358
359 #[test]
360 fn test_magic_bytes() {
361 assert_eq!(MAGIC_BYTES, b"SQRY_GRAPH_V7");
362 assert_eq!(MAGIC_BYTES.len(), 13);
363 }
364
365 #[test]
366 fn test_version() {
367 assert_eq!(VERSION, 7);
368 }
369
370 #[test]
371 fn test_graph_header_new() {
372 let header = GraphHeader::new(100, 50, 200, 10);
373
374 assert_eq!(header.version, VERSION);
375 assert_eq!(header.node_count, 100);
376 assert_eq!(header.edge_count, 50);
377 assert_eq!(header.string_count, 200);
378 assert_eq!(header.file_count, 10);
379 assert!(header.timestamp > 0);
380 assert!(header.config_provenance.is_none());
381 }
382
383 #[test]
384 fn test_graph_header_with_provenance() {
385 let provenance = make_test_provenance();
386 let header = GraphHeader::with_provenance(100, 50, 200, 10, provenance);
387
388 assert_eq!(header.version, VERSION);
389 assert_eq!(header.node_count, 100);
390 assert_eq!(header.edge_count, 50);
391 assert!(header.config_provenance.is_some());
392 assert_eq!(
393 header.config_provenance.as_ref().unwrap().config_checksum,
394 "abc123def456"
395 );
396 }
397
398 #[test]
399 fn test_graph_header_provenance_method() {
400 let header = GraphHeader::new(10, 5, 20, 2);
401 assert!(header.provenance().is_none());
402
403 let provenance = make_test_provenance();
404 let header_with = GraphHeader::with_provenance(10, 5, 20, 2, provenance);
405 assert!(header_with.provenance().is_some());
406 assert_eq!(
407 header_with.provenance().unwrap().config_checksum,
408 "abc123def456"
409 );
410 }
411
412 #[test]
413 fn test_graph_header_has_provenance() {
414 let header = GraphHeader::new(10, 5, 20, 2);
415 assert!(!header.has_provenance());
416
417 let provenance = make_test_provenance();
418 let header_with = GraphHeader::with_provenance(10, 5, 20, 2, provenance);
419 assert!(header_with.has_provenance());
420 }
421
422 #[test]
423 fn test_graph_header_clone() {
424 let header = GraphHeader::new(100, 50, 200, 10);
425 let cloned = header.clone();
426
427 assert_eq!(header.version, cloned.version);
428 assert_eq!(header.node_count, cloned.node_count);
429 assert_eq!(header.edge_count, cloned.edge_count);
430 assert_eq!(header.string_count, cloned.string_count);
431 assert_eq!(header.file_count, cloned.file_count);
432 }
433
434 #[test]
435 fn test_graph_header_debug() {
436 let header = GraphHeader::new(100, 50, 200, 10);
437 let debug_str = format!("{header:?}");
438
439 assert!(debug_str.contains("GraphHeader"));
440 assert!(debug_str.contains("version"));
441 assert!(debug_str.contains("node_count"));
442 }
443
444 #[test]
445 fn test_graph_header_timestamp_is_recent() {
446 let header = GraphHeader::new(10, 5, 20, 2);
447 let now = std::time::SystemTime::now()
448 .duration_since(std::time::UNIX_EPOCH)
449 .unwrap()
450 .as_secs();
451
452 assert!(header.timestamp <= now);
454 assert!(header.timestamp >= now - 1);
455 }
456
457 #[test]
458 fn test_graph_header_zero_counts() {
459 let header = GraphHeader::new(0, 0, 0, 0);
460
461 assert_eq!(header.node_count, 0);
462 assert_eq!(header.edge_count, 0);
463 assert_eq!(header.string_count, 0);
464 assert_eq!(header.file_count, 0);
465 }
466
467 #[test]
468 fn test_graph_header_large_counts() {
469 let header = GraphHeader::new(1_000_000, 5_000_000, 10_000_000, 100_000);
470
471 assert_eq!(header.node_count, 1_000_000);
472 assert_eq!(header.edge_count, 5_000_000);
473 assert_eq!(header.string_count, 10_000_000);
474 assert_eq!(header.file_count, 100_000);
475 }
476
477 #[test]
478 fn test_graph_header_plugin_versions_empty_by_default() {
479 let header = GraphHeader::new(10, 5, 20, 2);
480 assert!(header.plugin_versions().is_empty());
481 }
482
483 #[test]
484 fn test_graph_header_set_plugin_versions() {
485 let mut header = GraphHeader::new(10, 5, 20, 2);
486
487 let mut versions = HashMap::new();
488 versions.insert("rust".to_string(), "3.3.0".to_string());
489 versions.insert("javascript".to_string(), "3.3.0".to_string());
490
491 header.set_plugin_versions(versions.clone());
492
493 assert_eq!(header.plugin_versions().len(), 2);
494 assert_eq!(
495 header.plugin_versions().get("rust"),
496 Some(&"3.3.0".to_string())
497 );
498 assert_eq!(
499 header.plugin_versions().get("javascript"),
500 Some(&"3.3.0".to_string())
501 );
502 }
503
504 #[test]
509 fn phase1_graph_header_new_defaults_fact_epoch_to_zero() {
510 let header = GraphHeader::new(10, 5, 20, 2);
511 assert_eq!(header.fact_epoch, 0);
512 assert_eq!(header.fact_epoch(), 0);
513 }
514
515 #[test]
516 fn phase1_graph_header_with_provenance_defaults_fact_epoch_to_zero() {
517 let header = GraphHeader::with_provenance(10, 5, 20, 2, make_test_provenance());
518 assert_eq!(header.fact_epoch, 0);
519 }
520
521 #[test]
522 fn phase1_graph_header_set_fact_epoch_round_trip() {
523 let mut header = GraphHeader::new(10, 5, 20, 2);
524 header.set_fact_epoch(42);
525 assert_eq!(header.fact_epoch(), 42);
526 }
527
528 #[test]
529 fn phase1_graph_header_postcard_round_trip_with_fact_epoch() {
530 let mut header = GraphHeader::new(100, 50, 200, 10);
531 header.set_fact_epoch(1_234_567);
532
533 let encoded = postcard::to_allocvec(&header).expect("encode");
534 let decoded: GraphHeader = postcard::from_bytes(&encoded).expect("decode");
535
536 assert_eq!(decoded.fact_epoch(), 1_234_567);
537 assert_eq!(decoded.node_count, 100);
538 assert_eq!(decoded.edge_count, 50);
539 }
540
541 #[test]
542 fn phase1_graph_header_fact_epoch_preserved_through_clone() {
543 let mut header = GraphHeader::new(10, 5, 20, 2);
544 header.set_fact_epoch(9_999);
545 let cloned = header.clone();
546 assert_eq!(cloned.fact_epoch(), 9_999);
547 }
548
549 #[test]
554 fn phase1_magic_bytes_v7_matches_legacy() {
555 assert_eq!(MAGIC_BYTES_V7, b"SQRY_GRAPH_V7");
556 assert_eq!(MAGIC_BYTES_V7, MAGIC_BYTES);
557 assert_eq!(MAGIC_BYTES_V7.len(), 13);
558 }
559
560 #[test]
561 fn phase1_magic_bytes_v8_is_distinct_and_13_bytes() {
562 assert_eq!(MAGIC_BYTES_V8, b"SQRY_GRAPH_V8");
563 assert_eq!(MAGIC_BYTES_V8.len(), 13);
564 assert_ne!(MAGIC_BYTES_V8, MAGIC_BYTES_V7);
565 }
566
567 #[test]
568 fn phase1_legacy_version_v7_equals_seven() {
569 assert_eq!(LEGACY_VERSION_V7, 7);
570 }
571
572 #[test]
573 fn phase1_format_version_discriminants() {
574 assert_eq!(FormatVersion::V7 as u32, 7);
575 assert_eq!(FormatVersion::V8 as u32, 8);
576 assert_eq!(FormatVersion::V9 as u32, 9);
577 assert_eq!(FormatVersion::V10 as u32, 10);
578 assert_eq!(FormatVersion::V11 as u32, 11);
579 }
580
581 #[test]
582 fn current_version_is_v11() {
583 assert_eq!(CURRENT_VERSION, FormatVersion::V11);
584 }
585
586 #[test]
587 fn phase_a_magic_bytes_v11_is_distinct_and_14_bytes() {
588 assert_eq!(MAGIC_BYTES_V11, b"SQRY_GRAPH_V11");
589 assert_eq!(MAGIC_BYTES_V11.len(), 14);
590 assert_ne!(MAGIC_BYTES_V11, MAGIC_BYTES_V10);
591 }
592
593 #[test]
594 fn phase_a_format_version_from_magic_v11() {
595 assert_eq!(
596 FormatVersion::from_magic(MAGIC_BYTES_V11),
597 Some(FormatVersion::V11),
598 );
599 }
600
601 #[test]
607 fn phase_a_format_version_dispatch_v11_before_v10() {
608 let mut buf = MAGIC_BYTES_V11.to_vec();
609 buf.extend_from_slice(&[0u8; 8]);
611 assert_eq!(FormatVersion::from_magic(&buf), Some(FormatVersion::V11));
612
613 let mut buf10 = MAGIC_BYTES_V10.to_vec();
614 buf10.extend_from_slice(&[0u8; 8]);
615 assert_eq!(FormatVersion::from_magic(&buf10), Some(FormatVersion::V10));
616 }
617
618 #[test]
619 fn phase_a_format_version_v11_magic_round_trip() {
620 let v = FormatVersion::V11;
621 let bytes = v.magic();
622 assert_eq!(bytes, MAGIC_BYTES_V11.as_slice());
623 assert_eq!(FormatVersion::from_magic(bytes), Some(v));
624 }
625
626 #[test]
627 fn phase1_format_version_from_magic_v7() {
628 assert_eq!(
629 FormatVersion::from_magic(MAGIC_BYTES_V7),
630 Some(FormatVersion::V7),
631 );
632 }
633
634 #[test]
635 fn phase1_format_version_from_magic_v8() {
636 assert_eq!(
637 FormatVersion::from_magic(MAGIC_BYTES_V8),
638 Some(FormatVersion::V8),
639 );
640 }
641
642 #[test]
643 fn phase2_magic_bytes_v9_is_distinct_and_13_bytes() {
644 assert_eq!(MAGIC_BYTES_V9, b"SQRY_GRAPH_V9");
645 assert_eq!(MAGIC_BYTES_V9.len(), 13);
646 assert_ne!(MAGIC_BYTES_V9, MAGIC_BYTES_V7);
647 assert_ne!(MAGIC_BYTES_V9, MAGIC_BYTES_V8);
648 }
649
650 #[test]
651 fn phase2_format_version_from_magic_v9() {
652 assert_eq!(
653 FormatVersion::from_magic(MAGIC_BYTES_V9),
654 Some(FormatVersion::V9),
655 );
656 }
657
658 #[test]
659 fn phase1_format_version_from_magic_unknown() {
660 assert_eq!(FormatVersion::from_magic(b"SQRY_GRAPH_V1"), None);
661 assert_eq!(FormatVersion::from_magic(b"NOT_A_GRAPH_!"), None);
662 }
663
664 #[test]
665 fn phase1_format_version_magic_round_trip() {
666 for version in [
667 FormatVersion::V7,
668 FormatVersion::V8,
669 FormatVersion::V9,
670 FormatVersion::V10,
671 FormatVersion::V11,
672 ] {
673 let bytes = version.magic();
674 assert_eq!(FormatVersion::from_magic(bytes), Some(version));
675 }
676 }
677
678 #[test]
679 fn phase1_format_version_copy_eq_debug() {
680 let v = FormatVersion::V8;
681 let copied = v;
682 assert_eq!(v, copied);
683 assert_eq!(format!("{v:?}"), "V8");
684 }
685
686 #[test]
687 fn phase2_format_version_v9_copy_eq_debug() {
688 let v = FormatVersion::V9;
689 let copied = v;
690 assert_eq!(v, copied);
691 assert_eq!(format!("{v:?}"), "V9");
692 }
693
694 #[test]
695 fn test_graph_header_with_provenance_and_plugins() {
696 let provenance = make_test_provenance();
697
698 let mut plugin_versions = HashMap::new();
699 plugin_versions.insert("rust".to_string(), "3.3.0".to_string());
700 plugin_versions.insert("python".to_string(), "3.3.0".to_string());
701
702 let header = GraphHeader::with_provenance_and_plugins(
703 100,
704 50,
705 200,
706 10,
707 provenance,
708 plugin_versions.clone(),
709 );
710
711 assert_eq!(header.version, VERSION);
712 assert_eq!(header.node_count, 100);
713 assert!(header.config_provenance.is_some());
714 assert_eq!(header.plugin_versions().len(), 2);
715 assert_eq!(
716 header.plugin_versions().get("rust"),
717 Some(&"3.3.0".to_string())
718 );
719 }
720}