use crate::utils::error::{Error, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::path::Path;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditTrail {
pub generated_at: String,
pub ggen_version: String,
pub inputs: AuditInputs,
pub pipeline: Vec<AuditStep>,
pub outputs: Vec<AuditOutput>,
pub validation_passed: bool,
pub total_duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditInputs {
pub manifest_hash: String,
pub ontology_hashes: BTreeMap<String, String>,
pub template_hashes: BTreeMap<String, String>,
pub query_hashes: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditStep {
pub step_type: String,
pub name: String,
pub duration_ms: u64,
#[serde(default)]
pub triples_added: Option<usize>,
pub status: String,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditOutput {
pub path: String,
pub content_hash: String,
pub size_bytes: usize,
pub source_rule: String,
}
pub struct AuditTrailBuilder {
ggen_version: String,
inputs: AuditInputs,
pipeline: Vec<AuditStep>,
outputs: Vec<AuditOutput>,
started_at: std::time::Instant,
}
impl AuditTrailBuilder {
pub fn new() -> Self {
Self {
ggen_version: env!("CARGO_PKG_VERSION").to_string(),
inputs: AuditInputs {
manifest_hash: String::new(),
ontology_hashes: BTreeMap::new(),
template_hashes: BTreeMap::new(),
query_hashes: BTreeMap::new(),
},
pipeline: Vec::new(),
outputs: Vec::new(),
started_at: std::time::Instant::now(),
}
}
pub fn record_inputs(
&mut self, manifest: &Path, ontologies: &[&Path], templates: &[&Path],
) -> Result<&mut Self> {
self.inputs.manifest_hash = Self::hash_file(manifest)?;
for ont in ontologies {
let hash = Self::hash_file(ont)?;
self.inputs
.ontology_hashes
.insert(ont.display().to_string(), hash);
}
for tmpl in templates {
let hash = Self::hash_file(tmpl)?;
self.inputs
.template_hashes
.insert(tmpl.display().to_string(), hash);
}
Ok(self)
}
pub fn record_step(
&mut self, step_type: &str, name: &str, duration: Duration, triples: Option<usize>,
status: &str,
) -> &mut Self {
self.pipeline.push(AuditStep {
step_type: step_type.to_string(),
name: name.to_string(),
duration_ms: duration.as_millis() as u64,
triples_added: triples,
status: status.to_string(),
error: None,
});
self
}
pub fn record_step_error(
&mut self, step_type: &str, name: &str, duration: Duration, error: &str,
) -> &mut Self {
self.pipeline.push(AuditStep {
step_type: step_type.to_string(),
name: name.to_string(),
duration_ms: duration.as_millis() as u64,
triples_added: None,
status: "error".to_string(),
error: Some(error.to_string()),
});
self
}
pub fn record_output(&mut self, path: &Path, content: &str, source_rule: &str) -> &mut Self {
let hash = Self::hash_string(content);
self.outputs.push(AuditOutput {
path: path.display().to_string(),
content_hash: hash,
size_bytes: content.len(),
source_rule: source_rule.to_string(),
});
self
}
pub fn build(&self, validation_passed: bool) -> AuditTrail {
let total_duration = self.started_at.elapsed();
AuditTrail {
generated_at: chrono::Utc::now().to_rfc3339(),
ggen_version: self.ggen_version.clone(),
inputs: self.inputs.clone(),
pipeline: self.pipeline.clone(),
outputs: self.outputs.clone(),
validation_passed,
total_duration_ms: total_duration.as_millis() as u64,
}
}
pub fn write_to(trail: &AuditTrail, path: &Path) -> Result<()> {
let json = serde_json::to_string_pretty(trail)
.map_err(|e| Error::new(&format!("Failed to serialize audit trail: {}", e)))?;
std::fs::write(path, json)
.map_err(|e| Error::new(&format!("Failed to write audit trail: {}", e)))?;
Ok(())
}
fn hash_file(path: &Path) -> Result<String> {
let content = std::fs::read(path)
.map_err(|e| Error::new(&format!("Failed to read '{}': {}", path.display(), e)))?;
Ok(Self::hash_bytes(&content))
}
fn hash_string(content: &str) -> String {
Self::hash_bytes(content.as_bytes())
}
fn hash_bytes(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
format!("{:x}", hasher.finalize())
}
}
impl Default for AuditTrailBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_audit_builder() {
let mut builder = AuditTrailBuilder::new();
builder.record_step(
"inference",
"auditable_fields",
Duration::from_millis(5),
Some(10),
"success",
);
builder.record_step(
"render",
"structs",
Duration::from_millis(15),
None,
"success",
);
let trail = builder.build(true);
assert_eq!(trail.pipeline.len(), 2);
assert!(trail.validation_passed);
}
#[test]
fn test_hash_string() {
let hash1 = AuditTrailBuilder::hash_string("hello world");
let hash2 = AuditTrailBuilder::hash_string("hello world");
let hash3 = AuditTrailBuilder::hash_string("different");
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
}
#[test]
fn test_record_output() {
let mut builder = AuditTrailBuilder::new();
builder.record_output(Path::new("test.rs"), "fn main() {}", "structs");
let trail = builder.build(true);
assert_eq!(trail.outputs.len(), 1);
assert_eq!(trail.outputs[0].path, "test.rs");
assert_eq!(trail.outputs[0].source_rule, "structs");
}
}