Skip to main content

dirtydata_host/
lib.rs

1use std::io::{Read, Write};
2use std::process::{Child, Command, Stdio};
3use std::collections::BTreeMap;
4
5#[derive(Debug, thiserror::Error)]
6pub enum HostError {
7    #[error("IO error: {0}")]
8    Io(#[from] std::io::Error),
9    #[error("Plugin crashed or closed connection")]
10    Crashed,
11    #[error("Plugin produced NaN (NaN storm)")]
12    NanStorm,
13    #[error("Serialization error: {0}")]
14    Serialization(#[from] serde_json::Error),
15}
16#[derive(Debug, serde::Serialize)]
17pub struct AuditReport {
18    pub root_hash_valid: bool,
19    pub manifest_valid: bool,
20    pub lineage_intact: bool,
21    pub cas_complete: bool,
22    pub determinism_verified: bool,
23    pub issues: Vec<String>,
24}
25
26impl AuditReport {
27    pub fn is_healthy(&self) -> bool {
28        self.root_hash_valid && self.lineage_intact && self.cas_complete
29    }
30}
31
32use std::path::{Path, PathBuf};
33use serde::Serialize;
34
35/// §SSS: Workspace — The self-contained session manager.
36/// "設計図、製造履歴、意図。そのすべてを一つの宇宙に閉じ込める。"
37pub struct Workspace {
38    root: PathBuf,
39    graph: dirtydata_core::ir::Graph,
40    intent_state: dirtydata_intent::IntentState,
41}
42
43impl Workspace {
44    pub fn open(root: impl Into<PathBuf>) -> Result<Self, HostError> {
45        let root = root.into();
46        let dot_dirty = root.join(".dirtydata");
47        
48        if !dot_dirty.exists() {
49            std::fs::create_dir_all(&dot_dirty)?;
50            return Ok(Self { root, graph: dirtydata_core::ir::Graph::new(), intent_state: dirtydata_intent::IntentState::default() });
51        }
52
53        // 1. Layer 5: Manifest
54        let manifest_path = dot_dirty.join("manifest.json");
55        let manifest: dirtydata_core::types::Manifest = if manifest_path.exists() {
56            serde_json::from_str(&std::fs::read_to_string(manifest_path)?)?
57        } else {
58            // Legacy fallback
59            return Err(HostError::Crashed); // Or handle appropriately
60        };
61
62        // 2. Layer 1: Topology
63        let topo_path = dot_dirty.join("topology.ir");
64        let topology: dirtydata_core::ir::Topology = serde_json::from_str(&std::fs::read_to_string(topo_path)?)?;
65
66        // 3. Layer 3: Lineage
67        let lineage_path = dot_dirty.join("lineage.dag");
68        let lineage: dirtydata_core::ir::Lineage = serde_json::from_str(&std::fs::read_to_string(lineage_path)?)?;
69
70        // 4. Layer 2: Circuit Registry (Loaded via references from Topology/Manifest or walking CAS)
71        let mut registry = dirtydata_core::ir::CircuitRegistry::default();
72        let cas_root = dot_dirty.join("circuits").join("blake3");
73        if cas_root.exists() {
74            // For now, we'll walk all files in the CAS and load them
75            for entry in walkdir::WalkDir::new(cas_root) {
76                let entry = entry.map_err(|_| HostError::Crashed)?;
77                if entry.file_type().is_file() {
78                    let data = std::fs::read_to_string(entry.path())?;
79                    let def: dirtydata_core::types::CircuitDefinition = serde_json::from_str(&data)?;
80                    registry.definitions.insert(def.id, def);
81                }
82            }
83        }
84
85        let mut graph = dirtydata_core::ir::Graph {
86            spec_version: manifest.spec_version,
87            topology,
88            lineage,
89            registry,
90            verification: manifest.verification,
91            revision: dirtydata_core::types::Revision(manifest.last_revision),
92            nodes: BTreeMap::new(),
93            edges: BTreeMap::new(),
94            modulations: BTreeMap::new(),
95        };
96        graph.sync();
97
98        let intent_state = dirtydata_intent::IntentState::load(&root)?;
99
100        Ok(Self { root, graph, intent_state })
101    }
102
103    /// Performs an atomic save of the entire forensic record using CAS.
104    pub fn save(&self) -> Result<(), HostError> {
105        let dot_dirty = self.root.join(".dirtydata");
106        std::fs::create_dir_all(&dot_dirty)?;
107
108        // 1. Layer 1: Topology
109        self.save_atomic(&dot_dirty.join("topology.ir"), &self.graph.topology)?;
110
111        // 2. Layer 2: Circuit Registry (CAS Storage)
112        for def in self.graph.registry.definitions.values() {
113            let hash = def.hash();
114            let hash_hex = hex::encode(hash);
115            let cas_path = dot_dirty.join("circuits").join("blake3")
116                .join(&hash_hex[0..2])
117                .join(&hash_hex[2..4]);
118            std::fs::create_dir_all(&cas_path)?;
119            self.save_atomic(&cas_path.join(&hash_hex), def)?;
120        }
121
122        // 3. Layer 3: Lineage
123        self.save_atomic(&dot_dirty.join("lineage.dag"), &self.graph.lineage)?;
124
125        // 4. Layer 4: Intents
126        self.intent_state.save(&self.root)?;
127
128        // 5. Layer 5: Verification (Manifest with Root Hash & Signature)
129        let root_hash = self.calculate_root_hash()?;
130        let manifest = dirtydata_core::types::Manifest {
131            spec_version: self.graph.spec_version.clone(),
132            last_revision: self.graph.revision.0,
133            timestamp: dirtydata_core::types::Timestamp::now().0,
134            verification: dirtydata_core::types::Verification {
135                null_test: true,
136                hash: hex::encode(root_hash),
137                trust_state: "verified".into(),
138            },
139            author_id: "dirtydata-host-local".into(),
140            public_key: "ed25519:stub_key".into(),
141            signature: "stub_signature".into(),
142        };
143        self.save_atomic(&dot_dirty.join("manifest.json"), &manifest)?;
144
145        tracing::info!("Forensic record (Merkle DAG) saved successfully. Root Hash: {}", hex::encode(root_hash));
146        Ok(())
147    }
148
149    pub fn calculate_root_hash(&self) -> Result<dirtydata_core::types::Hash, HostError> {
150        let mut hasher = blake3::Hasher::new();
151        hasher.update(serde_json::to_string(&self.graph.topology)?.as_bytes());
152        // For Merkle DAG, we hash the head of the lineage
153        hasher.update(serde_json::to_string(&self.graph.lineage)?.as_bytes());
154        hasher.update(serde_json::to_string(&self.intent_state)?.as_bytes());
155        
156        // Include the hash of the last applied patch as the 'tip' of the Merkle chain
157        if let Some(&last_id) = self.graph.lineage.applied_patches.last() {
158            if let Some(patch) = self.graph.lineage.history.get(&last_id) {
159                hasher.update(&patch.deterministic_hash);
160            }
161        }
162
163        for def in self.graph.registry.definitions.values() {
164            hasher.update(&def.hash());
165        }
166        Ok(*hasher.finalize().as_bytes())
167    }
168
169    fn save_atomic<T: Serialize>(&self, path: &Path, data: &T) -> Result<(), HostError> {
170        let mut temp = tempfile::NamedTempFile::new_in(path.parent().unwrap())?;
171        let json = serde_json::to_string_pretty(data)?;
172        temp.write_all(json.as_bytes())?;
173        temp.persist(path).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
174        Ok(())
175    }
176
177    // --- Accessors ---
178
179    pub fn graph(&self) -> &dirtydata_core::ir::Graph {
180        &self.graph
181    }
182
183    pub fn graph_mut(&mut self) -> &mut dirtydata_core::ir::Graph {
184        &mut self.graph
185    }
186
187    pub fn intent_state(&self) -> &dirtydata_intent::IntentState {
188        &self.intent_state
189    }
190
191    pub fn root(&self) -> &Path {
192        &self.root
193    }
194
195    // --- High Level API ---
196
197    pub fn apply_patch(&mut self, patch: dirtydata_core::patch::Patch) -> Result<(), HostError> {
198        self.graph.apply_patch(&patch)
199            .map_err(|_| HostError::Crashed)?; // TODO: Better error mapping
200        self.save()?;
201        Ok(())
202    }
203
204    /// Audits the forensic integrity of the workspace.
205    pub fn audit(&self) -> Result<AuditReport, HostError> {
206        let mut report = AuditReport {
207            root_hash_valid: false,
208            manifest_valid: true, // Placeholder for signature check
209            lineage_intact: true,
210            cas_complete: true,
211            determinism_verified: false,
212            issues: Vec::new(),
213        };
214
215        // 1. Verify Root Hash
216        let actual_hash = self.calculate_root_hash()?;
217        let manifest_path = self.root.join(".dirtydata").join("manifest.json");
218        if manifest_path.exists() {
219            let manifest: dirtydata_core::types::Manifest = serde_json::from_str(&std::fs::read_to_string(manifest_path)?)?;
220            if hex::encode(actual_hash) == manifest.verification.hash {
221                report.root_hash_valid = true;
222            } else {
223                report.issues.push(format!("Root hash mismatch! Manifest: {}, Actual: {}", manifest.verification.hash, hex::encode(actual_hash)));
224            }
225        }
226
227        // 2. Audit Lineage (Merkle chain verification)
228        let mut prev_hash = None;
229        for patch_id in &self.graph.lineage.applied_patches {
230            if let Some(patch) = self.graph.lineage.history.get(patch_id) {
231                if !patch.verify_hash() {
232                    report.lineage_intact = false;
233                    report.issues.push(format!("Patch {} has corrupted hash", patch_id));
234                }
235                // Verify parent linkage
236                if let Some(p_hash) = prev_hash {
237                    if !patch.parent_hashes.contains(&p_hash) {
238                        // In a simple chain this would be an error, but in a DAG it's more complex.
239                        // For now, just logging suspicious gaps.
240                    }
241                }
242                prev_hash = Some(patch.deterministic_hash);
243            }
244        }
245
246        // 3. CAS Completeness
247        for node in self.graph.topology.nodes.values() {
248            if let dirtydata_core::types::NodeKind::Processor = node.kind {
249                // If we implement circuit definitions in registry, check them here
250            }
251        }
252
253        // 4. Determinism Check (Quick null-test replay)
254        // This requires dirtydata-runtime, so we might move it to a high-level helper
255        // For now, mark as pending
256
257        Ok(report)
258    }
259}
260
261// #[derive(Serialize, Deserialize)]
262// struct WorkspaceMeta {
263//     last_revision: u64,
264//     timestamp: i64,
265// }
266
267
268#[repr(u8)]
269#[derive(Debug, Clone, Copy)]
270pub enum HostCommand {
271    Process = 0,
272    SetParameter = 1,
273    GetState = 2,
274    SetState = 3,
275}
276
277pub struct PluginHost {
278    child: Child,
279    fallback_buffer: Vec<f32>,
280}
281
282impl PluginHost {
283    pub fn new(plugin_name: &str, buffer_size: usize) -> Result<Self, HostError> {
284        let exe = std::env::current_exe().unwrap_or_default();
285        let dir = exe.parent().unwrap_or(std::path::Path::new("."));
286        let worker_path = dir.join("dirtydata-plugin-worker");
287
288        let child = Command::new(&worker_path)
289            .arg(plugin_name)
290            .stdin(Stdio::piped())
291            .stdout(Stdio::piped())
292            .spawn()
293            .map_err(|e| {
294                tracing::error!("Failed to spawn plugin worker '{}' at {:?}: {}", plugin_name, worker_path, e);
295                e
296            })?;
297
298        tracing::info!("Spawned plugin worker '{}' (pid={})", plugin_name, child.id());
299        Ok(Self {
300            child,
301            fallback_buffer: vec![0.0; buffer_size],
302        })
303    }
304
305    pub fn set_parameter(&mut self, param_id: u32, value: f32) -> Result<(), HostError> {
306        let mut stdin = self.child.stdin.as_ref().ok_or(HostError::Crashed)?;
307        
308        let cmd = HostCommand::SetParameter as u8;
309        stdin.write_all(&[cmd])?;
310        stdin.write_all(&param_id.to_le_bytes())?;
311        stdin.write_all(&value.to_le_bytes())?;
312        stdin.flush()?;
313        
314        Ok(())
315    }
316
317    pub fn process(&mut self, input: &[f32], output: &mut [f32]) -> Result<(), HostError> {
318        let mut stdin = self.child.stdin.as_ref().ok_or(HostError::Crashed)?;
319        let stdout = self.child.stdout.as_mut().ok_or(HostError::Crashed)?;
320
321        // Send Command
322        let cmd = HostCommand::Process as u8;
323        stdin.write_all(&[cmd])?;
324        
325        // Send size (u32)
326        let size = input.len() as u32;
327        stdin.write_all(&size.to_le_bytes())?;
328
329        // Write input buffer as bytes
330        let in_bytes = bytemuck::cast_slice(input);
331        if stdin.write_all(in_bytes).is_err() {
332            tracing::error!("Failed to write to plugin stdin — plugin likely crashed");
333            return Err(HostError::Crashed);
334        }
335        if stdin.flush().is_err() {
336            tracing::error!("Failed to flush plugin stdin");
337            return Err(HostError::Crashed);
338        }
339
340        // Read output buffer as bytes
341        let out_bytes = bytemuck::cast_slice_mut(output);
342        if stdout.read_exact(out_bytes).is_err() {
343            return Err(HostError::Crashed);
344        }
345
346        // Check for NaN Storm
347        for sample in output.iter() {
348            if sample.is_nan() {
349                tracing::warn!("NaN detected in plugin output! Entering NaN storm protocol.");
350                return Err(HostError::NanStorm);
351            }
352        }
353
354        // Update fallback buffer
355        if self.fallback_buffer.len() != output.len() {
356            self.fallback_buffer.resize(output.len(), 0.0);
357        }
358        self.fallback_buffer.copy_from_slice(output);
359
360        Ok(())
361    }
362
363    pub fn get_fallback(&self) -> &[f32] {
364        &self.fallback_buffer
365    }
366}