Skip to main content

dirtydata_core/
storage.rs

1//! Filesystem-based storage for DirtyData.
2//!
3//! boring は美徳。FS で行く。
4//!
5//! Layout:
6//! .dirtydata/
7//! ├── HEAD                 # ref: refs/heads/main
8//! ├── refs/
9//! │   └── heads/
10//! │       └── main         # Latest PatchId for branch 'main'
11//! ├── ir/
12//! │   └── current.json       # Current Graph snapshot
13//! ├── patches/
14//! │   ├── {patch_id}.json    # Individual patches
15//! │   └── index.json         # Patch DAG metadata
16//! ├── intents/
17//! │   └── {intent_id}.json   # Intent metadata
18//! └── config.json            # Project config
19//! ```
20
21use std::fs;
22use std::path::{Path, PathBuf};
23
24use crate::ir::Graph;
25use crate::patch::Patch;
26use crate::types::PatchId;
27
28/// Root storage directory name.
29const DIRTYDATA_DIR: &str = ".dirtydata";
30
31/// Errors during storage operations.
32#[derive(Debug, thiserror::Error)]
33pub enum StorageError {
34    #[error("I/O error: {0}")]
35    Io(#[from] std::io::Error),
36
37    #[error("serialization error: {0}")]
38    Serialize(#[from] serde_json::Error),
39
40    #[error("project not initialized — run `dirtydata init`")]
41    NotInitialized,
42
43    #[error("patch {0} not found")]
44    PatchNotFound(PatchId),
45}
46
47/// Filesystem-based storage backend.
48pub struct Storage {
49    root: PathBuf,
50}
51
52impl Storage {
53    /// Open storage at the given project root.
54    pub fn open(project_root: &Path) -> Result<Self, StorageError> {
55        let root = project_root.join(DIRTYDATA_DIR);
56        if !root.exists() {
57            return Err(StorageError::NotInitialized);
58        }
59        Ok(Self { root })
60    }
61
62    /// Initialize a new DirtyData project.
63    pub fn init(project_root: &Path) -> Result<Self, StorageError> {
64        let root = project_root.join(DIRTYDATA_DIR);
65        fs::create_dir_all(root.join("ir"))?;
66        fs::create_dir_all(root.join("patches"))?;
67        fs::create_dir_all(root.join("intents"))?;
68        fs::create_dir_all(root.join("refs").join("heads"))?;
69
70        // Initialize HEAD and main branch
71        fs::write(root.join("HEAD"), "ref: refs/heads/main")?;
72
73        // Write default config
74        let config = serde_json::json!({
75            "version": "0.1.0",
76            "hash_algorithm": "blake3",
77            "id_scheme": "ulid"
78        });
79        fs::write(
80            root.join("config.json"),
81            serde_json::to_string_pretty(&config)?,
82        )?;
83
84        // Write empty patch index
85        let index = PatchIndex {
86            patches: Vec::new(),
87        };
88        fs::write(
89            root.join("patches").join("index.json"),
90            serde_json::to_string_pretty(&index)?,
91        )?;
92
93        // Write empty graph
94        let graph = Graph::new();
95        fs::write(
96            root.join("ir").join("current.json"),
97            serde_json::to_string_pretty(&graph)?,
98        )?;
99
100        Ok(Self { root })
101    }
102
103    // ── Graph ─────────────────────────────────
104
105    /// Load the current graph.
106    pub fn load_graph(&self) -> Result<Graph, StorageError> {
107        let path = self.root.join("ir").join("current.json");
108        let data = fs::read_to_string(&path)?;
109        let graph = serde_json::from_str(&data)?;
110        Ok(graph)
111    }
112
113    /// Save the current graph.
114    pub fn save_graph(&self, graph: &Graph) -> Result<(), StorageError> {
115        let path = self.root.join("ir").join("current.json");
116        let data = serde_json::to_string_pretty(graph)?;
117        fs::write(path, data)?;
118        Ok(())
119    }
120
121    // ── Branches (Timeline) ───────────────────
122
123    /// Read the current branch name from HEAD
124    pub fn read_head(&self) -> Result<String, StorageError> {
125        let head_path = self.root.join("HEAD");
126        if !head_path.exists() {
127            return Ok("main".to_string());
128        }
129        let content = fs::read_to_string(head_path)?;
130        let content = content.trim();
131        if content.starts_with("ref: refs/heads/") {
132            Ok(content.replace("ref: refs/heads/", ""))
133        } else {
134            Ok(content.to_string()) // detached HEAD support later if needed
135        }
136    }
137
138    /// Update HEAD to point to a branch
139    pub fn write_head(&self, branch: &str) -> Result<(), StorageError> {
140        let head_path = self.root.join("HEAD");
141        fs::write(head_path, format!("ref: refs/heads/{}", branch))?;
142        Ok(())
143    }
144
145    /// Get the PatchId a branch points to
146    pub fn read_branch(&self, branch: &str) -> Result<Option<PatchId>, StorageError> {
147        let path = self.root.join("refs").join("heads").join(branch);
148        if !path.exists() {
149            return Ok(None);
150        }
151        let content = fs::read_to_string(path)?;
152        let content = content.trim();
153        if content.is_empty() {
154            return Ok(None);
155        }
156        Ok(content.parse::<PatchId>().ok())
157    }
158
159    /// Update a branch to point to a PatchId
160    pub fn write_branch(&self, branch: &str, patch_id: PatchId) -> Result<(), StorageError> {
161        let path = self.root.join("refs").join("heads").join(branch);
162        fs::write(path, patch_id.to_string())?;
163        Ok(())
164    }
165
166    /// List all local branches
167    pub fn list_branches(&self) -> Result<Vec<String>, StorageError> {
168        let mut branches = Vec::new();
169        let heads_dir = self.root.join("refs").join("heads");
170        if heads_dir.exists() {
171            for entry in fs::read_dir(heads_dir)? {
172                let entry = entry?;
173                if entry.file_type()?.is_file() {
174                    if let Some(name) = entry.file_name().to_str() {
175                        branches.push(name.to_string());
176                    }
177                }
178            }
179        }
180        branches.sort();
181        Ok(branches)
182    }
183
184    // ── Patches ───────────────────────────────
185
186    /// Save a patch.
187    pub fn save_patch(&self, patch: &Patch) -> Result<(), StorageError> {
188        let filename = format!("{}.json", patch.identity);
189        let path = self.root.join("patches").join(&filename);
190        let data = serde_json::to_string_pretty(patch)?;
191        fs::write(path, data)?;
192
193        // Update index
194        let mut index = self.load_patch_index()?;
195        let entry = PatchIndexEntry {
196            id: patch.identity,
197            parents: patch.parents.clone(),
198            timestamp: patch.timestamp,
199            hash: patch.deterministic_hash,
200        };
201        // Avoid duplicates
202        if !index.patches.iter().any(|e| e.id == patch.identity) {
203            index.patches.push(entry);
204            self.save_patch_index(&index)?;
205        }
206
207        // Auto-update current branch pointer if applying a new patch
208        let current_branch = self.read_head()?;
209        self.write_branch(&current_branch, patch.identity)?;
210
211        Ok(())
212    }
213
214    /// Load a patch by ID.
215    pub fn load_patch(&self, id: &PatchId) -> Result<Patch, StorageError> {
216        let filename = format!("{}.json", id);
217        let path = self.root.join("patches").join(&filename);
218        if !path.exists() {
219            return Err(StorageError::PatchNotFound(*id));
220        }
221        let data = fs::read_to_string(path)?;
222        let patch = serde_json::from_str(&data)?;
223        Ok(patch)
224    }
225
226    /// Load all patches in order.
227    pub fn load_all_patches(&self) -> Result<Vec<Patch>, StorageError> {
228        let index = self.load_patch_index()?;
229        let mut patches = Vec::new();
230        for entry in &index.patches {
231            patches.push(self.load_patch(&entry.id)?);
232        }
233        Ok(patches)
234    }
235
236    /// Load the patch index.
237    fn load_patch_index(&self) -> Result<PatchIndex, StorageError> {
238        let path = self.root.join("patches").join("index.json");
239        let data = fs::read_to_string(path)?;
240        let index = serde_json::from_str(&data)?;
241        Ok(index)
242    }
243
244    /// Load patches starting from a tip, following parents backwards, then return in chronological order.
245    pub fn load_patch_ancestry(&self, tip: PatchId) -> Result<Vec<Patch>, StorageError> {
246        let mut ancestry = Vec::new();
247        let mut current = Some(tip);
248
249        while let Some(id) = current {
250            let patch = self.load_patch(&id)?;
251            // Simple linear history assumption for now (take first parent)
252            current = patch.parents.first().copied();
253            ancestry.push(patch);
254        }
255
256        // Reverse to get chronological order
257        ancestry.reverse();
258        Ok(ancestry)
259    }
260
261    /// Save the patch index.
262    fn save_patch_index(&self, index: &PatchIndex) -> Result<(), StorageError> {
263        let path = self.root.join("patches").join("index.json");
264        let data = serde_json::to_string_pretty(index)?;
265        fs::write(path, data)?;
266        Ok(())
267    }
268
269    /// Get the storage root path.
270    pub fn root(&self) -> &Path {
271        &self.root
272    }
273}
274
275/// Metadata index for the patch DAG.
276#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
277pub struct PatchIndex {
278    pub patches: Vec<PatchIndexEntry>,
279}
280
281/// An entry in the patch index.
282#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
283pub struct PatchIndexEntry {
284    pub id: PatchId,
285    pub parents: Vec<PatchId>,
286    pub timestamp: crate::types::Timestamp,
287    pub hash: crate::types::Hash,
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use crate::ir::Node;
294    use crate::patch::{Operation, Patch};
295
296    #[test]
297    fn test_init_and_load() {
298        let tmp = std::env::temp_dir().join(format!("dirtydata_test_{}", ulid::Ulid::new()));
299        fs::create_dir_all(&tmp).unwrap();
300
301        let storage = Storage::init(&tmp).unwrap();
302        let graph = storage.load_graph().unwrap();
303        assert!(graph.nodes.is_empty());
304        assert_eq!(graph.revision.0, 0);
305
306        // Cleanup
307        fs::remove_dir_all(&tmp).ok();
308    }
309
310    #[test]
311    fn test_save_and_load_patch() {
312        let tmp = std::env::temp_dir().join(format!("dirtydata_test_{}", ulid::Ulid::new()));
313        fs::create_dir_all(&tmp).unwrap();
314
315        let storage = Storage::init(&tmp).unwrap();
316
317        let node = Node::new_source("Sine");
318        let patch = Patch::from_operations(vec![Operation::AddNode(node)]);
319        storage.save_patch(&patch).unwrap();
320
321        let loaded = storage.load_patch(&patch.identity).unwrap();
322        assert_eq!(loaded.identity, patch.identity);
323        assert_eq!(loaded.operations.len(), 1);
324
325        // Cleanup
326        fs::remove_dir_all(&tmp).ok();
327    }
328
329    #[test]
330    fn test_full_roundtrip() {
331        let tmp = std::env::temp_dir().join(format!("dirtydata_test_{}", ulid::Ulid::new()));
332        fs::create_dir_all(&tmp).unwrap();
333
334        let storage = Storage::init(&tmp).unwrap();
335
336        // Build a graph
337        let mut graph = storage.load_graph().unwrap();
338        let node = Node::new_processor("Gain");
339        let patch = Patch::from_operations(vec![Operation::AddNode(node.clone())]);
340        graph.apply(&patch).unwrap();
341
342        // Save everything
343        storage.save_graph(&graph).unwrap();
344        storage.save_patch(&patch).unwrap();
345
346        // Reload and verify
347        let reloaded = storage.load_graph().unwrap();
348        assert_eq!(reloaded.nodes.len(), 1);
349        assert!(reloaded.nodes.contains_key(&node.id));
350
351        // Replay from patches
352        let patches = storage.load_all_patches().unwrap();
353        let replayed = Graph::replay(&patches).unwrap();
354        assert_eq!(replayed.nodes.len(), reloaded.nodes.len());
355
356        // Cleanup
357        fs::remove_dir_all(&tmp).ok();
358    }
359}