use clap_noun_verb::{NounVerbError, Result};
use clap_noun_verb_macros::verb;
use ed25519_dalek::SigningKey;
use ggen_core::receipt::{generate_keypair, ProvenanceEnvelope};
use ggen_core::reverse_sync::inverse_pipeline::InversePipeline;
use ggen_graph::{CoherenceChecker, Pole, PoleState};
use serde::Serialize;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize)]
pub struct InverseSyncOutput {
pub status: String,
pub files_scanned: usize,
pub recovered_triples: usize,
pub inverse_operation_id: String,
pub coherence_admitted: bool,
pub envelope_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub coherence_drifts: Option<Vec<String>>,
}
fn load_signing_key(key_path: &PathBuf) -> std::result::Result<SigningKey, String> {
let key_content =
fs::read_to_string(key_path).map_err(|e| format!("Failed to read signing key: {}", e))?;
let key_bytes = hex::decode(key_content.trim())
.map_err(|e| format!("Failed to decode signing key hex: {}", e))?;
let key_array: [u8; 32] = key_bytes
.as_slice()
.try_into()
.map_err(|_| "Signing key must be exactly 32 bytes".to_string())?;
Ok(SigningKey::from_bytes(&key_array))
}
fn resolve_signing_key_path(signing_key: Option<String>) -> PathBuf {
signing_key
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(".ggen/keys/signing.key"))
}
fn resolve_envelope_path(output_envelope: Option<String>) -> PathBuf {
output_envelope
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(".ggen/envelopes/latest.json"))
}
fn collect_artifact_files(source_dir: &PathBuf) -> std::result::Result<Vec<PathBuf>, String> {
if !source_dir.exists() {
return Err(format!(
"Source directory not found: {}",
source_dir.display()
));
}
let mut files = Vec::new();
for entry in
fs::read_dir(source_dir).map_err(|e| format!("Failed to read source directory: {}", e))?
{
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
let path = entry.path();
if path.is_dir() {
let sub_files = collect_artifact_files(&path)?;
files.extend(sub_files);
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if matches!(ext, "rs" | "ex" | "exs" | "go") {
files.push(path);
}
}
}
Ok(files)
}
fn load_ontology_hash(ontology_path: &PathBuf) -> std::result::Result<(String, usize), String> {
let content =
fs::read_to_string(ontology_path).map_err(|e| format!("Failed to read ontology: {}", e))?;
let mut hasher = blake3::Hasher::new();
hasher.update(content.as_bytes());
let hash = hasher.finalize().to_hex().to_string();
let triple_count = content
.lines()
.filter(|l| !l.trim().is_empty() && !l.starts_with("@prefix"))
.count();
Ok((hash, triple_count))
}
fn do_inverse_sync(
source_dir: String, ontology: String, signing_key: Option<String>,
output_envelope: Option<String>,
) -> std::result::Result<InverseSyncOutput, NounVerbError> {
let source_dir = PathBuf::from(&source_dir);
let ontology_path = PathBuf::from(&ontology);
let signing_key_path = resolve_signing_key_path(signing_key);
let envelope_path = resolve_envelope_path(output_envelope);
let artifact_files = collect_artifact_files(&source_dir).map_err(|e| {
NounVerbError::execution_error(format!("Failed to collect artifacts: {}", e))
})?;
if artifact_files.is_empty() {
return Ok(InverseSyncOutput {
status: "error".to_string(),
files_scanned: 0,
recovered_triples: 0,
inverse_operation_id: String::new(),
coherence_admitted: false,
envelope_path: envelope_path.to_string_lossy().to_string(),
error: Some("No artifact files found in source directory".to_string()),
coherence_drifts: None,
});
}
let signing_key_obj = load_signing_key(&signing_key_path).map_err(|e| {
NounVerbError::execution_error(format!(
"Failed to load signing key from {}: {}",
signing_key_path.display(),
e
))
})?;
let inverse_receipt = InversePipeline::run_signed(&artifact_files, &signing_key_obj)
.map_err(|e| NounVerbError::execution_error(format!("Inverse pipeline failed: {}", e)))?;
let (ontology_hash, ontology_triple_count) =
load_ontology_hash(&ontology_path).map_err(|e| NounVerbError::execution_error(e))?;
let ocel_events = vec![
format!(
r#"{{"activity":"inverse-scan","timestamp":"{}","files":{}}}"#,
chrono::Utc::now().to_rfc3339(),
artifact_files.len()
),
format!(
r#"{{"activity":"inverse-extract","timestamp":"{}","triples":{}}}"#,
chrono::Utc::now().to_rfc3339(),
inverse_receipt.recovered_triple_count
),
format!(
r#"{{"activity":"inverse-validate","timestamp":"{}","valid":{}}}"#,
chrono::Utc::now().to_rfc3339(),
inverse_receipt.shacl_valid
),
];
let ocel_event_refs: Vec<&str> = ocel_events.iter().map(|s| s.as_str()).collect();
let l_pole = CoherenceChecker::fingerprint_event_log(&ocel_event_refs);
let artifact_paths: Vec<(String, u64)> = artifact_files
.iter()
.map(|p| {
let size = fs::metadata(p).map(|m| m.len()).unwrap_or(0);
(p.to_string_lossy().into_owned(), size)
})
.collect();
let artifact_data: Vec<(&str, u64)> = artifact_paths
.iter()
.map(|(s, n)| (s.as_str(), *n))
.collect();
let a_pole = CoherenceChecker::fingerprint_artifacts(&artifact_data);
let o_pole = PoleState {
pole: Pole::Ontology,
hash: ontology_hash.clone(),
item_count: ontology_triple_count,
timestamp: chrono::Utc::now(),
};
let mut expectations = HashMap::new();
expectations.insert(Pole::Ontology, ontology_hash);
let a_pole_hash = a_pole.hash.clone();
let coherence_report =
CoherenceChecker::check_with_expectations(&[o_pole, a_pole, l_pole], &expectations);
let coherence_admitted = coherence_report.admitted;
let coherence_drifts: Option<Vec<String>> = if !coherence_report.drifts.is_empty() {
Some(
coherence_report
.drifts
.iter()
.map(|d| format!("{:?}: {}", d.kind, d.detail))
.collect(),
)
} else {
None
};
let mut envelope = ProvenanceEnvelope::from_inverse(inverse_receipt.clone());
let envelope_coherence_report = {
use ggen_core::receipt::provenance_envelope::CoherenceReport as EnvelopeCoherenceReport;
EnvelopeCoherenceReport::new(
Uuid::new_v4().to_string(),
a_pole_hash,
inverse_receipt.output_hash.clone(),
coherence_admitted,
if coherence_admitted {
None
} else {
coherence_drifts.as_ref().map(|d| d.join("; "))
},
)
};
envelope = envelope.add_coherence(envelope_coherence_report);
let envelope_json = envelope.to_json().map_err(|e| {
NounVerbError::execution_error(format!("Failed to serialize envelope: {}", e))
})?;
if let Some(parent) = envelope_path.parent() {
fs::create_dir_all(parent).map_err(|e| {
NounVerbError::execution_error(format!("Failed to create envelope directory: {}", e))
})?;
}
fs::write(&envelope_path, &envelope_json).map_err(|e| {
NounVerbError::execution_error(format!(
"Failed to write envelope to {}: {}",
envelope_path.display(),
e
))
})?;
let status = if coherence_admitted && inverse_receipt.shacl_valid {
"success".to_string()
} else {
"incoherent".to_string()
};
Ok(InverseSyncOutput {
status,
files_scanned: artifact_files.len(),
recovered_triples: inverse_receipt.recovered_triple_count,
inverse_operation_id: inverse_receipt.operation_id,
coherence_admitted,
envelope_path: envelope_path.to_string_lossy().to_string(),
error: if !coherence_admitted {
Some("Coherence check failed".to_string())
} else {
None
},
coherence_drifts,
})
}
#[verb]
pub fn inverse_sync(
source_dir: String, ontology: String, signing_key: Option<String>,
output_envelope: Option<String>,
) -> Result<InverseSyncOutput> {
do_inverse_sync(source_dir, ontology, signing_key, output_envelope)
}