Skip to main content

void_core/ops/
diagnostics.rs

1//! Debug helpers for inspecting shard layout and changes.
2
3use std::collections::HashMap;
4
5use crate::cid;
6use crate::crypto::CommitReader;
7use crate::ContentHash;
8use crate::VoidContext;
9use crate::metadata::{Commit, MetadataBundle};
10use crate::metadata::manifest_tree::TreeManifest;
11
12use crate::store::ObjectStoreExt;
13use crate::Result;
14
15/// Diagnostics snapshot for a sealed commit.
16#[derive(Debug, Clone)]
17pub struct ShardDiagnostics {
18    /// File path → shard ID.
19    pub file_to_shard: HashMap<String, u64>,
20    /// File path → content hash.
21    pub file_hashes: HashMap<String, ContentHash>,
22    /// Shard ID → CID string.
23    pub shard_cids: HashMap<u64, String>,
24    /// Shard ID → sorted file list.
25    pub shard_files: HashMap<u64, Vec<String>>,
26    /// Shard ID → compressed body size.
27    pub shard_sizes: HashMap<u64, u64>,
28    /// Shard ID → directory listing count.
29    pub dir_listing_counts: HashMap<u64, usize>,
30}
31
32/// Differences between two diagnostics snapshots.
33#[derive(Debug, Clone)]
34pub struct ShardDiff {
35    pub new_files: Vec<String>,
36    pub removed_files: Vec<String>,
37    pub moved_files: Vec<(String, u64, u64)>,
38    pub modified_files: Vec<String>,
39}
40
41impl ShardDiagnostics {
42    /// Builds diagnostics from a TreeManifest (no shard decryption needed).
43    pub fn from_manifest(manifest: &TreeManifest, metadata: &MetadataBundle) -> Result<Self> {
44        let mut file_to_shard = HashMap::new();
45        let mut file_hashes = HashMap::new();
46        let mut shard_cids: HashMap<u64, String> = HashMap::new();
47        let mut shard_files: HashMap<u64, Vec<String>> = HashMap::new();
48        let mut shard_sizes: HashMap<u64, u64> = HashMap::new();
49
50        // Build shard CID map from metadata ranges
51        for range in &metadata.shard_map.ranges {
52            if let Some(cid_bytes) = range.cid.as_ref() {
53                if let Ok(shard_cid) = crate::cid::from_bytes(cid_bytes.as_bytes()) {
54                    shard_cids.insert(range.shard_id, shard_cid.to_string());
55                }
56                shard_sizes.insert(range.shard_id, range.compressed_size);
57            }
58        }
59
60        // Build shard sizes from manifest shard references
61        for (idx, sref) in manifest.shards().iter().enumerate() {
62            shard_sizes.insert(idx as u64, sref.size_compressed);
63        }
64
65        // Build file maps from manifest entries
66        for entry_result in manifest.iter() {
67            let entry = entry_result?;
68            let shard_id = entry.shard_index as u64;
69            file_to_shard.insert(entry.path.clone(), shard_id);
70            file_hashes.insert(entry.path.clone(), entry.content_hash);
71            shard_files.entry(shard_id).or_default().push(entry.path.clone());
72        }
73
74        // Sort file lists within each shard
75        for files in shard_files.values_mut() {
76            files.sort();
77        }
78
79        Ok(Self {
80            file_to_shard,
81            file_hashes,
82            shard_cids,
83            shard_files,
84            shard_sizes,
85            dir_listing_counts: HashMap::new(), // No longer tracked
86        })
87    }
88
89    /// Builds diagnostics by loading the manifest from a commit.
90    pub fn from_commit(ctx: &VoidContext, commit: &Commit, metadata: &MetadataBundle, reader: &CommitReader) -> Result<Self> {
91        let store = ctx.open_store()?;
92        let manifest = TreeManifest::from_commit(&store, commit, reader)?
93            .ok_or_else(|| crate::VoidError::NotFound("commit has no manifest".into()))?;
94        Self::from_manifest(&manifest, metadata)
95    }
96
97    /// Convenience: loads HEAD commit and builds diagnostics.
98    ///
99    /// Used by tests that have a VoidContext + MetadataBundle but not the commit object.
100    pub fn from_metadata(ctx: &VoidContext, metadata: &MetadataBundle, reader: &CommitReader) -> Result<Self> {
101        let store = ctx.open_store()?;
102        // Load HEAD commit
103        let head_cid = crate::refs::resolve_head(&ctx.paths.void_dir)?
104            .ok_or_else(|| crate::VoidError::NotFound("HEAD".into()))?;
105        let commit_cid = cid::from_bytes(head_cid.as_bytes())?;
106        let commit_encrypted: void_crypto::EncryptedCommit = store.get_blob(&commit_cid)?;
107        let (commit_bytes, _) = CommitReader::open_with_vault(&ctx.crypto.vault, &commit_encrypted)?;
108        let commit = commit_bytes.parse()?;
109
110        let manifest = TreeManifest::from_commit(&store, &commit, reader)?
111            .ok_or_else(|| crate::VoidError::NotFound("commit has no manifest".into()))?;
112        Self::from_manifest(&manifest, metadata)
113    }
114
115    /// Returns the number of files indexed in the diagnostics snapshot.
116    pub fn total_files(&self) -> usize {
117        self.file_to_shard.len()
118    }
119
120    /// Returns the number of shards with data in the snapshot.
121    pub fn shard_count(&self) -> usize {
122        self.shard_files.len()
123    }
124
125    /// Computes differences between two snapshots.
126    pub fn diff(&self, next: &Self) -> ShardDiff {
127        let mut new_files = Vec::new();
128        let mut removed_files = Vec::new();
129        let mut moved_files = Vec::new();
130        let mut modified_files = Vec::new();
131
132        for (file, shard_id) in &next.file_to_shard {
133            let prev_id = match self.file_to_shard.get(file) {
134                Some(id) => id,
135                None => {
136                    new_files.push(file.clone());
137                    continue;
138                }
139            };
140
141            if prev_id != shard_id {
142                moved_files.push((file.clone(), *prev_id, *shard_id));
143            }
144
145            if self
146                .file_hashes
147                .get(file)
148                .zip(next.file_hashes.get(file))
149                .map(|(prev, next)| prev != next)
150                .unwrap_or(false)
151            {
152                modified_files.push(file.clone());
153            }
154        }
155
156        for file in self.file_to_shard.keys() {
157            if !next.file_to_shard.contains_key(file) {
158                removed_files.push(file.clone());
159            }
160        }
161
162        new_files.sort();
163        removed_files.sort();
164        moved_files.sort_by(|a, b| a.0.cmp(&b.0));
165        modified_files.sort();
166
167        ShardDiff {
168            new_files,
169            removed_files,
170            moved_files,
171            modified_files,
172        }
173    }
174}
175