ggen_core/codegen/
proof_archive.rs

1use crate::codegen::ExecutionProof;
2use ggen_utils::error::Result;
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Serialize, Deserialize, Clone)]
8pub struct ProofEntry {
9    pub execution_id: String,
10    pub timestamp_ms: u64,
11    pub proof: ExecutionProof,
12}
13
14pub struct ProofArchive {
15    archive_dir: PathBuf,
16}
17
18impl ProofArchive {
19    pub fn new(archive_dir: PathBuf) -> Result<Self> {
20        fs::create_dir_all(&archive_dir)?;
21        Ok(Self { archive_dir })
22    }
23
24    pub fn store_proof(&self, proof: &ExecutionProof) -> Result<()> {
25        let proof_file = self
26            .archive_dir
27            .join(format!("{}.json", proof.execution_id));
28        let json = serde_json::to_string_pretty(proof)?;
29        fs::write(proof_file, json)?;
30        Ok(())
31    }
32
33    pub fn load_proof(&self, execution_id: &str) -> Result<Option<ExecutionProof>> {
34        let proof_file = self.archive_dir.join(format!("{}.json", execution_id));
35        if !proof_file.exists() {
36            return Ok(None);
37        }
38
39        let json = fs::read_to_string(proof_file)?;
40        let proof = serde_json::from_str(&json)?;
41        Ok(Some(proof))
42    }
43
44    pub fn list_proofs(&self) -> Result<Vec<ProofEntry>> {
45        let mut entries = vec![];
46
47        for entry in fs::read_dir(&self.archive_dir)? {
48            let entry = entry?;
49            let path = entry.path();
50
51            if path.extension().is_some_and(|ext| ext == "json") {
52                if let Ok(json) = fs::read_to_string(&path) {
53                    if let Ok(proof) = serde_json::from_str::<ExecutionProof>(&json) {
54                        entries.push(ProofEntry {
55                            execution_id: proof.execution_id.clone(),
56                            timestamp_ms: proof.timestamp_ms,
57                            proof,
58                        });
59                    }
60                }
61            }
62        }
63
64        entries.sort_by_key(|e| e.timestamp_ms);
65        Ok(entries)
66    }
67
68    pub fn verify_chain(&self) -> Result<ChainVerification> {
69        let entries = self.list_proofs()?;
70
71        if entries.is_empty() {
72            return Ok(ChainVerification {
73                is_valid: true,
74                proof_count: 0,
75                determinism_violations: vec![],
76                last_manifest_hash: None,
77                last_output_hash: None,
78            });
79        }
80
81        let mut determinism_violations = vec![];
82        let mut last_manifest = entries[0].proof.manifest_hash.clone();
83        let mut last_output = entries[0].proof.output_hash.clone();
84
85        for i in 1..entries.len() {
86            let current = &entries[i];
87            let previous = &entries[i - 1];
88
89            if previous.proof.manifest_hash == current.proof.manifest_hash
90                && previous.proof.ontology_hash == current.proof.ontology_hash
91                && previous.proof.output_hash != current.proof.output_hash
92            {
93                determinism_violations.push(DeterminismViolation {
94                    execution_id_previous: previous.execution_id.clone(),
95                    execution_id_current: current.execution_id.clone(),
96                    reason: "Same manifest/ontology produced different output".to_string(),
97                });
98            }
99
100            last_manifest = current.proof.manifest_hash.clone();
101            last_output = current.proof.output_hash.clone();
102        }
103
104        let is_valid = determinism_violations.is_empty();
105
106        Ok(ChainVerification {
107            is_valid,
108            proof_count: entries.len(),
109            determinism_violations,
110            last_manifest_hash: Some(last_manifest),
111            last_output_hash: Some(last_output),
112        })
113    }
114
115    pub fn get_latest_proof(&self) -> Result<Option<ExecutionProof>> {
116        let entries = self.list_proofs()?;
117        Ok(entries.last().map(|e| e.proof.clone()))
118    }
119}
120
121pub struct ChainVerification {
122    pub is_valid: bool,
123    pub proof_count: usize,
124    pub determinism_violations: Vec<DeterminismViolation>,
125    pub last_manifest_hash: Option<String>,
126    pub last_output_hash: Option<String>,
127}
128
129pub struct DeterminismViolation {
130    pub execution_id_previous: String,
131    pub execution_id_current: String,
132    pub reason: String,
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use tempfile::TempDir;
139
140    #[test]
141    fn test_proof_archive_creation() {
142        let temp = TempDir::new().unwrap();
143        let _archive = ProofArchive::new(temp.path().to_path_buf()).unwrap();
144        assert!(temp.path().exists());
145    }
146
147    #[test]
148    fn test_proof_persistence() {
149        let temp = TempDir::new().unwrap();
150        let archive = ProofArchive::new(temp.path().to_path_buf()).unwrap();
151
152        let proof = ExecutionProof {
153            execution_id: "test-1".to_string(),
154            timestamp_ms: 1000,
155            manifest_hash: "hash1".to_string(),
156            ontology_hash: "ohash1".to_string(),
157            rules_executed: vec![],
158            output_hash: "out1".to_string(),
159            execution_duration_ms: 100,
160            determinism_signature: "sig1".to_string(),
161        };
162
163        archive.store_proof(&proof).unwrap();
164        let loaded = archive.load_proof("test-1").unwrap().unwrap();
165
166        assert_eq!(loaded.execution_id, proof.execution_id);
167        assert_eq!(loaded.manifest_hash, proof.manifest_hash);
168    }
169
170    #[test]
171    fn test_chain_verification_empty() {
172        let temp = TempDir::new().unwrap();
173        let archive = ProofArchive::new(temp.path().to_path_buf()).unwrap();
174        let verification = archive.verify_chain().unwrap();
175
176        assert!(verification.is_valid);
177        assert_eq!(verification.proof_count, 0);
178    }
179
180    #[test]
181    fn test_determinism_violation_detection() {
182        let temp = TempDir::new().unwrap();
183        let archive = ProofArchive::new(temp.path().to_path_buf()).unwrap();
184
185        let proof1 = ExecutionProof {
186            execution_id: "exec-1".to_string(),
187            timestamp_ms: 1000,
188            manifest_hash: "same-manifest".to_string(),
189            ontology_hash: "same-ontology".to_string(),
190            rules_executed: vec![],
191            output_hash: "output-1".to_string(),
192            execution_duration_ms: 100,
193            determinism_signature: "sig1".to_string(),
194        };
195
196        let proof2 = ExecutionProof {
197            execution_id: "exec-2".to_string(),
198            timestamp_ms: 2000,
199            manifest_hash: "same-manifest".to_string(),
200            ontology_hash: "same-ontology".to_string(),
201            rules_executed: vec![],
202            output_hash: "output-2".to_string(),
203            execution_duration_ms: 100,
204            determinism_signature: "sig2".to_string(),
205        };
206
207        archive.store_proof(&proof1).unwrap();
208        archive.store_proof(&proof2).unwrap();
209
210        let verification = archive.verify_chain().unwrap();
211
212        assert!(!verification.is_valid);
213        assert_eq!(verification.determinism_violations.len(), 1);
214    }
215}