vibe_graph_core/
lib.rs

1//! Core domain types shared across the entire Vibe-Graph workspace.
2
3use petgraph::stable_graph::{NodeIndex, StableDiGraph};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::time::{Duration, Instant, SystemTime};
9
10// =============================================================================
11// Git Change Tracking Types
12// =============================================================================
13
14/// Type of change detected for a file in git.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum GitChangeKind {
17    /// File was modified (content changed)
18    Modified,
19    /// File was newly added (staged new file)
20    Added,
21    /// File was deleted
22    Deleted,
23    /// File was renamed (old path)
24    RenamedFrom,
25    /// File was renamed (new path)
26    RenamedTo,
27    /// File is untracked (new file not yet staged)
28    Untracked,
29}
30
31impl GitChangeKind {
32    /// Get a display label for the change kind.
33    pub fn label(&self) -> &'static str {
34        match self {
35            GitChangeKind::Modified => "Modified",
36            GitChangeKind::Added => "Added",
37            GitChangeKind::Deleted => "Deleted",
38            GitChangeKind::RenamedFrom => "Renamed (from)",
39            GitChangeKind::RenamedTo => "Renamed (to)",
40            GitChangeKind::Untracked => "Untracked",
41        }
42    }
43
44    /// Get a short symbol for the change kind.
45    pub fn symbol(&self) -> &'static str {
46        match self {
47            GitChangeKind::Modified => "M",
48            GitChangeKind::Added => "+",
49            GitChangeKind::Deleted => "-",
50            GitChangeKind::RenamedFrom => "R←",
51            GitChangeKind::RenamedTo => "R→",
52            GitChangeKind::Untracked => "?",
53        }
54    }
55}
56
57/// Represents a single file change detected in git.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct GitFileChange {
60    /// Relative path of the changed file.
61    pub path: PathBuf,
62    /// Kind of change.
63    pub kind: GitChangeKind,
64    /// Whether this is a staged change (vs working directory).
65    pub staged: bool,
66}
67
68/// Snapshot of git changes for an entire repository.
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70pub struct GitChangeSnapshot {
71    /// All detected changes.
72    pub changes: Vec<GitFileChange>,
73    /// Timestamp when this snapshot was taken.
74    #[serde(skip)]
75    pub captured_at: Option<Instant>,
76}
77
78impl GitChangeSnapshot {
79    /// Create a new empty snapshot.
80    pub fn new() -> Self {
81        Self {
82            changes: Vec::new(),
83            captured_at: Some(Instant::now()),
84        }
85    }
86
87    /// Check if a path has any changes.
88    pub fn has_changes(&self, path: &Path) -> bool {
89        self.changes.iter().any(|c| c.path == path)
90    }
91
92    /// Get the change kind for a path, if any.
93    pub fn get_change(&self, path: &Path) -> Option<&GitFileChange> {
94        self.changes.iter().find(|c| c.path == path)
95    }
96
97    /// Get all paths that have changes.
98    pub fn changed_paths(&self) -> impl Iterator<Item = &Path> {
99        self.changes.iter().map(|c| c.path.as_path())
100    }
101
102    /// Count changes by kind.
103    pub fn count_by_kind(&self, kind: GitChangeKind) -> usize {
104        self.changes.iter().filter(|c| c.kind == kind).count()
105    }
106
107    /// Check if snapshot is stale (older than given duration).
108    pub fn is_stale(&self, max_age: Duration) -> bool {
109        match self.captured_at {
110            Some(at) => at.elapsed() > max_age,
111            None => true,
112        }
113    }
114
115    /// Get age of this snapshot.
116    pub fn age(&self) -> Option<Duration> {
117        self.captured_at.map(|at| at.elapsed())
118    }
119}
120
121/// State for animating change indicators.
122#[derive(Debug, Clone)]
123pub struct ChangeIndicatorState {
124    /// Animation phase (0.0 to 1.0, loops).
125    pub phase: f32,
126    /// Animation speed multiplier.
127    pub speed: f32,
128    /// Whether animation is enabled.
129    pub enabled: bool,
130}
131
132impl Default for ChangeIndicatorState {
133    fn default() -> Self {
134        Self {
135            phase: 0.0,
136            speed: 1.0,
137            enabled: true,
138        }
139    }
140}
141
142impl ChangeIndicatorState {
143    /// Advance the animation by delta time.
144    pub fn tick(&mut self, dt: f32) {
145        if self.enabled {
146            self.phase = (self.phase + dt * self.speed) % 1.0;
147        }
148    }
149
150    /// Get the current pulse scale (1.0 to 1.3).
151    pub fn pulse_scale(&self) -> f32 {
152        // Smooth sine-based pulse
153        let t = self.phase * std::f32::consts::TAU;
154        1.0 + 0.15 * (t.sin() * 0.5 + 0.5)
155    }
156
157    /// Get the current alpha for outer ring (fades in/out).
158    pub fn ring_alpha(&self) -> f32 {
159        let t = self.phase * std::f32::consts::TAU;
160        0.3 + 0.4 * (t.sin() * 0.5 + 0.5)
161    }
162}
163
164/// Identifier for nodes within the `SourceCodeGraph`.
165#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
166pub struct NodeId(pub u64);
167
168/// Identifier for edges within the `SourceCodeGraph`.
169#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
170pub struct EdgeId(pub u64);
171
172/// Enumerates the kinds of nodes that can populate the `SourceCodeGraph`.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
174pub enum GraphNodeKind {
175    /// A logical module, typically aligned to a crate or package.
176    Module,
177    /// A discrete file in the repository.
178    File,
179    /// Directory or folder that contains additional nodes.
180    Directory,
181    /// A long-running service entry point.
182    Service,
183    /// Automated test suites or harnesses.
184    Test,
185    /// Any other kind that does not fit the curated list.
186    #[default]
187    Other,
188}
189
190/// Captures metadata for a node in the graph.
191#[derive(Debug, Default, Clone, Serialize, Deserialize)]
192pub struct GraphNode {
193    /// Unique identifier for this node.
194    pub id: NodeId,
195    /// Human readable name.
196    pub name: String,
197    /// Category associated with the node.
198    pub kind: GraphNodeKind,
199    /// Arbitrary metadata, e.g. language, path, ownership.
200    pub metadata: HashMap<String, String>,
201}
202
203/// Represents connections between graph nodes.
204#[derive(Debug, Default, Clone, Serialize, Deserialize)]
205pub struct GraphEdge {
206    /// Unique identifier for this edge.
207    pub id: EdgeId,
208    /// Originating node identifier.
209    pub from: NodeId,
210    /// Destination node identifier.
211    pub to: NodeId,
212    /// Description of the relationship ("imports", "calls", etc.).
213    pub relationship: String,
214    /// Arbitrary metadata for the relationship.
215    pub metadata: HashMap<String, String>,
216}
217
218/// Aggregate graph describing the full software project topology.
219#[derive(Debug, Default, Clone, Serialize, Deserialize)]
220pub struct SourceCodeGraph {
221    /// All nodes that make up the graph.
222    pub nodes: Vec<GraphNode>,
223    /// All edges that connect nodes in the graph.
224    pub edges: Vec<GraphEdge>,
225    /// Arbitrary metadata about the entire graph snapshot.
226    pub metadata: HashMap<String, String>,
227}
228
229impl SourceCodeGraph {
230    /// Creates an empty graph with no nodes or edges.
231    pub fn empty() -> Self {
232        Self::default()
233    }
234
235    /// Returns the number of nodes currently tracked.
236    pub fn node_count(&self) -> usize {
237        self.nodes.len()
238    }
239
240    /// Returns the number of edges currently tracked.
241    pub fn edge_count(&self) -> usize {
242        self.edges.len()
243    }
244
245    /// Convert to petgraph StableDiGraph for visualization/analysis.
246    /// Returns the graph and a mapping from NodeIndex to NodeId.
247    pub fn to_petgraph(&self) -> (StableDiGraph<GraphNode, String>, HashMap<NodeId, NodeIndex>) {
248        let mut graph = StableDiGraph::new();
249        let mut id_to_index = HashMap::new();
250
251        // Add all nodes
252        for node in &self.nodes {
253            let idx = graph.add_node(node.clone());
254            id_to_index.insert(node.id, idx);
255        }
256
257        // Add all edges
258        for edge in &self.edges {
259            if let (Some(&from_idx), Some(&to_idx)) =
260                (id_to_index.get(&edge.from), id_to_index.get(&edge.to))
261            {
262                graph.add_edge(from_idx, to_idx, edge.relationship.clone());
263            }
264        }
265
266        (graph, id_to_index)
267    }
268}
269
270/// Types of references detected between source files.
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
272pub enum ReferenceKind {
273    /// Rust `use` statement
274    Uses,
275    /// Python/JS/TS `import` statement
276    Imports,
277    /// Trait or interface implementation
278    Implements,
279    /// Filesystem hierarchy (parent->child)
280    Contains,
281}
282
283impl std::fmt::Display for ReferenceKind {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        match self {
286            ReferenceKind::Uses => write!(f, "uses"),
287            ReferenceKind::Imports => write!(f, "imports"),
288            ReferenceKind::Implements => write!(f, "implements"),
289            ReferenceKind::Contains => write!(f, "contains"),
290        }
291    }
292}
293
294/// A detected reference from one source to another.
295#[derive(Debug, Clone)]
296pub struct SourceReference {
297    /// Path of the source file containing the reference
298    pub source_path: PathBuf,
299    /// Type of reference
300    pub kind: ReferenceKind,
301    /// Target path (may be partial, resolved later)
302    pub target_route: PathBuf,
303}
304
305/// Builder for constructing a `SourceCodeGraph` from project data.
306#[derive(Debug, Default)]
307pub struct SourceCodeGraphBuilder {
308    nodes: Vec<GraphNode>,
309    edges: Vec<GraphEdge>,
310    path_to_node: HashMap<PathBuf, NodeId>,
311    next_node_id: u64,
312    next_edge_id: u64,
313    metadata: HashMap<String, String>,
314}
315
316impl SourceCodeGraphBuilder {
317    /// Create a new builder.
318    pub fn new() -> Self {
319        Self::default()
320    }
321
322    /// Set metadata for the graph.
323    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
324        self.metadata.insert(key.into(), value.into());
325        self
326    }
327
328    /// Add a directory node.
329    pub fn add_directory(&mut self, path: &Path) -> NodeId {
330        if let Some(&id) = self.path_to_node.get(path) {
331            return id;
332        }
333
334        let id = NodeId(self.next_node_id);
335        self.next_node_id += 1;
336
337        let name = path
338            .file_name()
339            .and_then(|n| n.to_str())
340            .unwrap_or_else(|| path.to_str().unwrap_or("."))
341            .to_string();
342
343        let mut metadata = HashMap::new();
344        metadata.insert("path".to_string(), path.to_string_lossy().to_string());
345
346        self.nodes.push(GraphNode {
347            id,
348            name,
349            kind: GraphNodeKind::Directory,
350            metadata,
351        });
352
353        self.path_to_node.insert(path.to_path_buf(), id);
354        id
355    }
356
357    /// Add a file node with optional language detection.
358    pub fn add_file(&mut self, path: &Path, relative_path: &str) -> NodeId {
359        if let Some(&id) = self.path_to_node.get(path) {
360            return id;
361        }
362
363        let id = NodeId(self.next_node_id);
364        self.next_node_id += 1;
365
366        let name = path
367            .file_name()
368            .and_then(|n| n.to_str())
369            .unwrap_or_else(|| path.to_str().unwrap_or("unknown"))
370            .to_string();
371
372        // Determine kind based on extension
373        let kind = match path.extension().and_then(|e| e.to_str()) {
374            Some("rs") | Some("py") | Some("js") | Some("ts") | Some("tsx") | Some("jsx")
375            | Some("go") | Some("java") | Some("c") | Some("cpp") | Some("h") | Some("hpp") => {
376                if relative_path.contains("test") || name.starts_with("test_") {
377                    GraphNodeKind::Test
378                } else if name == "mod.rs"
379                    || name == "__init__.py"
380                    || name == "index.ts"
381                    || name == "index.js"
382                {
383                    GraphNodeKind::Module
384                } else {
385                    GraphNodeKind::File
386                }
387            }
388            _ => GraphNodeKind::File,
389        };
390
391        let mut metadata = HashMap::new();
392        metadata.insert("path".to_string(), path.to_string_lossy().to_string());
393        metadata.insert("relative_path".to_string(), relative_path.to_string());
394
395        if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
396            metadata.insert("extension".to_string(), ext.to_string());
397            metadata.insert(
398                "language".to_string(),
399                extension_to_language(ext).to_string(),
400            );
401        }
402
403        self.nodes.push(GraphNode {
404            id,
405            name,
406            kind,
407            metadata,
408        });
409
410        self.path_to_node.insert(path.to_path_buf(), id);
411        id
412    }
413
414    /// Add a hierarchy edge (parent contains child).
415    pub fn add_hierarchy_edge(&mut self, parent_path: &Path, child_path: &Path) {
416        if let (Some(&parent_id), Some(&child_id)) = (
417            self.path_to_node.get(parent_path),
418            self.path_to_node.get(child_path),
419        ) {
420            if parent_id != child_id {
421                self.add_edge(parent_id, child_id, ReferenceKind::Contains);
422            }
423        }
424    }
425
426    /// Add an edge between two nodes.
427    pub fn add_edge(&mut self, from: NodeId, to: NodeId, kind: ReferenceKind) {
428        let id = EdgeId(self.next_edge_id);
429        self.next_edge_id += 1;
430
431        self.edges.push(GraphEdge {
432            id,
433            from,
434            to,
435            relationship: kind.to_string(),
436            metadata: HashMap::new(),
437        });
438    }
439
440    /// Get NodeId for a path if it exists.
441    pub fn get_node_id(&self, path: &Path) -> Option<NodeId> {
442        self.path_to_node.get(path).copied()
443    }
444
445    /// Find a node by matching path suffix (for reference resolution).
446    pub fn find_node_by_path_suffix(&self, route: &Path) -> Option<NodeId> {
447        let route_str = route.to_string_lossy();
448
449        for (path, &node_id) in &self.path_to_node {
450            let path_str = path.to_string_lossy();
451
452            // Strategy 1: Direct suffix match
453            if path_str.ends_with(route_str.as_ref()) {
454                return Some(node_id);
455            }
456
457            // Strategy 2: Normalized comparison
458            let normalized_path: String = path_str.trim_start_matches("./").replace('\\', "/");
459            let normalized_route: String = route_str.trim_start_matches("./").replace('\\', "/");
460            if normalized_path.ends_with(&normalized_route) {
461                return Some(node_id);
462            }
463
464            // Strategy 3: Module path matching (e.g., core/models.rs -> src/core/models.rs)
465            let route_parts: Vec<&str> = normalized_route.split('/').collect();
466            let path_parts: Vec<&str> = normalized_path.split('/').collect();
467            if route_parts.len() <= path_parts.len() {
468                for window in path_parts.windows(route_parts.len()) {
469                    if window == route_parts.as_slice() {
470                        return Some(node_id);
471                    }
472                }
473            }
474
475            // Strategy 4: Filename match
476            if let (Some(file_name), Some(route_name)) = (
477                path.file_name().and_then(|n| n.to_str()),
478                route.file_name().and_then(|n| n.to_str()),
479            ) {
480                if file_name == route_name {
481                    return Some(node_id);
482                }
483            }
484        }
485
486        None
487    }
488
489    /// Get the current node count.
490    pub fn node_count(&self) -> usize {
491        self.nodes.len()
492    }
493
494    /// Get the current edge count.
495    pub fn edge_count(&self) -> usize {
496        self.edges.len()
497    }
498
499    /// Build the final `SourceCodeGraph`.
500    pub fn build(self) -> SourceCodeGraph {
501        SourceCodeGraph {
502            nodes: self.nodes,
503            edges: self.edges,
504            metadata: self.metadata,
505        }
506    }
507}
508
509/// Detect references in Rust source code.
510pub fn detect_rust_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
511    let mut refs = Vec::new();
512
513    for line in content.lines() {
514        let trimmed = line.trim();
515
516        if !trimmed.starts_with("use ") {
517            continue;
518        }
519
520        // Extract module path from use statement
521        let use_part = trimmed
522            .strip_prefix("use ")
523            .unwrap_or("")
524            .split(';')
525            .next()
526            .unwrap_or("")
527            .split('{')
528            .next()
529            .unwrap_or("")
530            .trim();
531
532        if use_part.is_empty() {
533            continue;
534        }
535
536        // Only track local imports
537        let is_local = use_part.starts_with("crate::")
538            || use_part.starts_with("self::")
539            || use_part.starts_with("super::");
540
541        if !is_local {
542            continue;
543        }
544
545        let module_path = use_part
546            .strip_prefix("crate::")
547            .or_else(|| use_part.strip_prefix("self::"))
548            .or_else(|| use_part.strip_prefix("super::"))
549            .unwrap_or(use_part);
550
551        // Convert module path to file path
552        let path_str = module_path
553            .replace("::", "/")
554            .trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_')
555            .to_string();
556
557        refs.push(SourceReference {
558            source_path: source_path.to_path_buf(),
559            kind: ReferenceKind::Uses,
560            target_route: PathBuf::from(format!("{}.rs", path_str)),
561        });
562    }
563
564    refs
565}
566
567/// Detect references in Python source code.
568pub fn detect_python_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
569    let mut refs = Vec::new();
570
571    for line in content.lines() {
572        let trimmed = line.trim();
573
574        // Match "import module" statements
575        if trimmed.starts_with("import ") && !trimmed.starts_with("import(") {
576            let import_part = trimmed
577                .strip_prefix("import ")
578                .unwrap_or("")
579                .split_whitespace()
580                .next()
581                .unwrap_or("")
582                .split(',')
583                .next()
584                .unwrap_or("")
585                .trim();
586
587            if !import_part.is_empty() {
588                let path_str = import_part.replace('.', "/");
589                refs.push(SourceReference {
590                    source_path: source_path.to_path_buf(),
591                    kind: ReferenceKind::Imports,
592                    target_route: PathBuf::from(format!("{}.py", path_str)),
593                });
594            }
595        }
596
597        // Match "from module import something" statements
598        if let Some(module_part) = trimmed
599            .strip_prefix("from ")
600            .and_then(|s| s.split(" import ").next())
601        {
602            let module = module_part.trim();
603            if !module.is_empty() && module != "." && !module.starts_with("..") {
604                let path_str = module.replace('.', "/");
605                refs.push(SourceReference {
606                    source_path: source_path.to_path_buf(),
607                    kind: ReferenceKind::Imports,
608                    target_route: PathBuf::from(format!("{}.py", path_str)),
609                });
610            }
611        }
612    }
613
614    refs
615}
616
617/// Detect references in TypeScript/JavaScript source code.
618pub fn detect_ts_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
619    let mut refs = Vec::new();
620
621    for line in content.lines() {
622        let trimmed = line.trim();
623
624        // Match import statements: import X from 'path' or import 'path'
625        if trimmed.starts_with("import ") {
626            // Extract path from quotes
627            if let Some(path_start) = trimmed.find(['\'', '"']) {
628                let quote_char = trimmed.chars().nth(path_start).unwrap();
629                let rest = &trimmed[path_start + 1..];
630                if let Some(path_end) = rest.find(quote_char) {
631                    let import_path = &rest[..path_end];
632                    // Only track relative imports
633                    if import_path.starts_with('.') {
634                        refs.push(SourceReference {
635                            source_path: source_path.to_path_buf(),
636                            kind: ReferenceKind::Imports,
637                            target_route: PathBuf::from(import_path),
638                        });
639                    }
640                }
641            }
642        }
643    }
644
645    refs
646}
647
648/// Detect references based on file extension.
649pub fn detect_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
650    match source_path.extension().and_then(|e| e.to_str()) {
651        Some("rs") => detect_rust_references(content, source_path),
652        Some("py") => detect_python_references(content, source_path),
653        Some("ts") | Some("tsx") | Some("js") | Some("jsx") => {
654            detect_ts_references(content, source_path)
655        }
656        _ => Vec::new(),
657    }
658}
659
660/// Map file extension to language name.
661fn extension_to_language(ext: &str) -> &'static str {
662    match ext {
663        "rs" => "rust",
664        "py" => "python",
665        "js" => "javascript",
666        "ts" => "typescript",
667        "tsx" => "typescript",
668        "jsx" => "javascript",
669        "go" => "go",
670        "java" => "java",
671        "c" | "h" => "c",
672        "cpp" | "hpp" | "cc" | "cxx" => "cpp",
673        "md" => "markdown",
674        "json" => "json",
675        "yaml" | "yml" => "yaml",
676        "toml" => "toml",
677        _ => "unknown",
678    }
679}
680
681/// Represents an explicitly declared vibe (intent/spec/decision) attached to graph regions.
682#[derive(Debug, Clone, Serialize, Deserialize)]
683pub struct Vibe {
684    /// Stable identifier for referencing the vibe.
685    pub id: String,
686    /// Short title summarizing the intent.
687    pub title: String,
688    /// Richer description with context.
689    pub description: String,
690    /// Target nodes within the graph impacted by the vibe.
691    pub targets: Vec<NodeId>,
692    /// Actor (human or machine) responsible for the vibe.
693    pub created_by: String,
694    /// Timestamp when the vibe entered the system.
695    pub created_at: SystemTime,
696    /// Optional tags or attributes.
697    pub metadata: HashMap<String, String>,
698}
699
700/// Canonical definition of governing rules in effect for a graph.
701#[derive(Debug, Default, Clone, Serialize, Deserialize)]
702pub struct Constitution {
703    /// Unique name of the constitution.
704    pub name: String,
705    /// Semantic version or revision identifier.
706    pub version: String,
707    /// Human-readable description of the guardrails.
708    pub description: String,
709    /// Simple list of policies; future versions may embed richer data.
710    pub policies: Vec<String>,
711}
712
713/// Generic payload that cells in the automaton can store.
714pub type StatePayload = Value;
715
716/// Captures the state of an individual cell in the LLM cellular automaton.
717#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct CellState {
719    /// Node the cell is associated with.
720    pub node_id: NodeId,
721    /// Arbitrary structured state payload.
722    pub payload: StatePayload,
723    /// Tracking field for energy, confidence, or strength indicators.
724    pub activation: f32,
725    /// Timestamp for the most recent update.
726    pub last_updated: SystemTime,
727    /// Free-form annotations (signals, metrics, citations, etc.).
728    pub annotations: HashMap<String, String>,
729}
730
731impl CellState {
732    /// Creates a fresh `CellState` wrapping the provided payload.
733    pub fn new(node_id: NodeId, payload: StatePayload) -> Self {
734        Self {
735            node_id,
736            payload,
737            activation: 0.0,
738            last_updated: SystemTime::now(),
739            annotations: HashMap::new(),
740        }
741    }
742}
743
744/// Represents a snapshot of the entire system ready to be fossilized in Git.
745#[derive(Debug, Clone, Serialize, Deserialize)]
746pub struct Snapshot {
747    /// Identifier suitable for referencing the snapshot in storage.
748    pub id: String,
749    /// Captured graph at the time of snapshot.
750    pub graph: SourceCodeGraph,
751    /// All vibes considered part of the snapshot.
752    pub vibes: Vec<Vibe>,
753    /// Cell states for the automaton corresponding to the snapshot.
754    pub cell_states: Vec<CellState>,
755    /// Constitution in effect when the snapshot was created.
756    pub constitution: Constitution,
757    /// Timestamp for when the snapshot was recorded.
758    pub created_at: SystemTime,
759}