#![allow(clippy::unused_unit)]
use chrono::Utc;
use clap_noun_verb::{NounVerbError, Result as VerbResult};
use clap_noun_verb_macros::verb;
use ggen_core::codegen::{OutputFormat, SyncExecutor, SyncOptions, SyncResult};
use ggen_core::receipt::{generate_keypair, hash_data, Receipt};
use ggen_core::sync::{sync as low_level_sync, SyncConfig, SyncLanguage};
use serde::Serialize;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize)]
pub struct SyncOutput {
pub status: String,
pub files_synced: usize,
pub duration_ms: u64,
pub files: Vec<SyncedFile>,
pub inference_rules_executed: usize,
pub generation_rules_executed: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub audit_trail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub receipt_path: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub gates: Vec<GateResult>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct GateResult {
pub name: String,
pub passed: bool,
pub message: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct SyncedFile {
pub path: String,
pub size_bytes: usize,
pub action: String,
}
impl From<SyncResult> for SyncOutput {
fn from(result: SyncResult) -> Self {
Self {
status: result.status,
files_synced: result.files_synced,
duration_ms: result.duration_ms,
files: result
.files
.into_iter()
.map(|f| SyncedFile {
path: f.path,
size_bytes: f.size_bytes,
action: f.action,
})
.collect(),
inference_rules_executed: result.inference_rules_executed,
generation_rules_executed: result.generation_rules_executed,
audit_trail: result.audit_trail,
error: result.error,
receipt_path: None,
gates: Vec::new(),
}
}
}
#[allow(clippy::unused_unit, clippy::too_many_arguments)]
#[verb("sync", "root")]
pub fn sync(
manifest: Option<String>,
output_dir: Option<String>,
dry_run: Option<bool>,
force: Option<bool>,
audit: Option<bool>,
rule: Option<String>,
verbose: Option<bool>,
watch: Option<bool>,
validate_only: Option<bool>,
format: Option<String>,
timeout: Option<u64>,
stage: Option<String>,
ontology: Option<String>,
queries: Option<String>, language: Option<String>, profile: Option<String>, locked: bool, ) -> VerbResult<SyncOutput> {
check_profile_preconditions(profile.as_deref(), locked)?;
if let Some(ref queries_dir) = queries {
return run_low_level_pipeline(
ontology,
queries_dir.clone(),
output_dir,
language,
dry_run.unwrap_or(false),
);
}
run_manifest_pipeline(
manifest,
output_dir,
dry_run,
force,
audit,
rule,
verbose,
watch,
validate_only,
format,
timeout,
stage,
ontology,
)
}
fn check_profile_preconditions(profile: Option<&str>, locked: bool) -> VerbResult<()> {
if profile.is_some() || locked {
let workspace =
std::env::current_dir().map_err(|e| NounVerbError::execution_error(e.to_string()))?;
ggen_core::domain::sync_profile::validate_sync_preconditions(profile, locked, &workspace)
.map_err(NounVerbError::execution_error)?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn run_manifest_pipeline(
manifest: Option<String>, output_dir: Option<String>, dry_run: Option<bool>,
force: Option<bool>, audit: Option<bool>, rule: Option<String>, verbose: Option<bool>,
watch: Option<bool>, validate_only: Option<bool>, format: Option<String>, timeout: Option<u64>,
stage: Option<String>, ontology: Option<String>,
) -> VerbResult<SyncOutput> {
let installed_packs = read_installed_packs(".ggen/packs.lock");
let manifest_path = PathBuf::from(manifest.clone().unwrap_or_else(|| "ggen.toml".to_string()));
let options = SyncOptions {
manifest_path: manifest_path.clone(),
output_dir: output_dir.map(PathBuf::from),
verbose: verbose.unwrap_or(false),
output_format: match format.as_deref() {
Some("json") => OutputFormat::Json,
_ => OutputFormat::default(),
},
validate_only: validate_only.unwrap_or(false),
dry_run: dry_run.unwrap_or(false),
watch: watch.unwrap_or(false),
selected_rules: rule.map(|r| vec![r]),
force: force.unwrap_or(false),
audit: audit.unwrap_or(false),
a2a_stage: stage,
ontology_path: ontology.map(PathBuf::from),
timeout_ms: timeout,
..SyncOptions::default()
};
let sync_result = ggen_core::codegen::executor::SyncExecutor::new(options)
.execute()
.map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))?;
let files: Vec<SyncedFile> = sync_result
.files
.iter()
.map(|f| SyncedFile {
path: f.path.clone(),
size_bytes: f.size_bytes,
action: f.action.clone(),
})
.collect();
let synced_file_paths: Vec<String> = files.iter().map(|f| f.path.clone()).collect();
let files_synced = sync_result.files_synced;
let duration_ms = sync_result.duration_ms;
let generation_rules_executed = sync_result.generation_rules_executed;
let inference_rules_executed = sync_result.inference_rules_executed;
let receipt_file_path =
emit_sync_receipt(&synced_file_paths, &installed_packs).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Audit failure: {}", e))
})?;
Ok(SyncOutput {
status: sync_result.status,
files_synced,
duration_ms,
files,
inference_rules_executed,
generation_rules_executed,
audit_trail: sync_result.audit_trail,
error: sync_result.error,
receipt_path: Some(receipt_file_path),
gates: Vec::new(),
})
}
fn read_installed_packs(lock_path: &str) -> Vec<String> {
let path = std::path::Path::new(lock_path);
if !path.exists() {
return vec![];
}
let content = std::fs::read_to_string(path).unwrap_or_default();
let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) else {
return vec![];
};
val.get("packs")
.and_then(|p| p.as_object())
.map(|obj| {
obj.iter()
.map(|(id, entry)| {
let version = entry
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
format!("{}@{}", id, version)
})
.collect()
})
.unwrap_or_default()
}
fn emit_sync_receipt(
generated_file_paths: &[String], installed_packs: &[String],
) -> std::result::Result<String, String> {
use std::fs;
let keys_dir = std::path::Path::new(".ggen/keys");
fs::create_dir_all(keys_dir).map_err(|e| e.to_string())?;
let signing_key_path = keys_dir.join("signing.key");
let verifying_key_path = keys_dir.join("verifying.key");
let signing_key = if signing_key_path.exists() {
let hex_str = fs::read_to_string(&signing_key_path).map_err(|e| e.to_string())?;
let bytes = hex::decode(hex_str.trim()).map_err(|e| e.to_string())?;
let sk_bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| "Invalid signing key length (expected 32 bytes)".to_string())?;
ed25519_dalek::SigningKey::from_bytes(&sk_bytes)
} else {
let (sk, vk) = generate_keypair();
fs::write(&signing_key_path, hex::encode(sk.to_bytes())).map_err(|e| e.to_string())?;
fs::write(&verifying_key_path, hex::encode(vk.to_bytes())).map_err(|e| e.to_string())?;
sk
};
let receipts_dir = std::path::Path::new(".ggen/receipts");
let latest_path = receipts_dir.join("latest.json");
let previous_receipt: Option<Receipt> = if latest_path.exists() {
let content = fs::read_to_string(&latest_path).ok();
content.and_then(|c| serde_json::from_str(&c).ok())
} else {
None
};
let mut input_hashes: Vec<String> = Vec::new();
if let Ok(manifest_content) = std::fs::read_to_string("ggen.toml") {
input_hashes.push(format!(
"ggen.toml:{}",
hash_data(manifest_content.as_bytes())
));
}
for pack in installed_packs {
input_hashes.push(format!("pack:{}", pack));
}
let output_hashes: Vec<String> = generated_file_paths
.iter()
.filter_map(|path| {
fs::read(path)
.ok()
.map(|content| format!("{}:{}", path, hash_data(&content)))
})
.collect();
let operation_id = uuid::Uuid::new_v4().to_string();
let mut receipt = Receipt::new(operation_id, input_hashes, output_hashes, None);
if let Some(prev) = previous_receipt {
receipt = receipt.chain(&prev).map_err(|e| e.to_string())?;
}
let signed_receipt = receipt.sign(&signing_key).map_err(|e| e.to_string())?;
fs::create_dir_all(receipts_dir).map_err(|e| e.to_string())?;
let timestamp = Utc::now().format("%Y%m%d-%H%M%S");
let receipt_json = serde_json::to_string_pretty(&signed_receipt).map_err(|e| e.to_string())?;
let timestamped_path = receipts_dir.join(format!("sync-{}.json", timestamp));
fs::write(×tamped_path, &receipt_json).map_err(|e| e.to_string())?;
fs::write(&latest_path, &receipt_json).map_err(|e| e.to_string())?;
Ok(latest_path.to_string_lossy().into_owned())
}
#[allow(unused_variables)]
fn run_low_level_pipeline(
ontology: Option<String>, queries_dir: String, output_dir: Option<String>,
language: Option<String>, dry_run: bool,
) -> VerbResult<SyncOutput> {
log::info!("[μ₁/5] CONSTRUCT: Loading ontology...");
log::info!("[μ₂/5] SELECT: Running SPARQL queries...");
log::info!("[μ₃/5] Tera: Generating code...");
log::info!("[μ₄/5] Canonicalizing: Validating soundness...");
let ontology_path = PathBuf::from(ontology.unwrap_or_else(|| "ontology.ttl".to_string()));
let queries_path = PathBuf::from(queries_dir);
let output_path = PathBuf::from(output_dir.unwrap_or_else(|| ".".to_string()));
let lang: SyncLanguage =
language.as_deref().unwrap_or("auto").parse().map_err(
|e: ggen_core::sync::SyncError| NounVerbError::execution_error(e.to_string()),
)?;
let config = SyncConfig {
ontology_path,
queries_dir: queries_path,
output_dir: output_path,
language: lang,
validate: true,
dry_run,
};
let result =
low_level_sync(config).map_err(|e| NounVerbError::execution_error(e.to_string()))?;
let files: Vec<SyncedFile> = result
.files_generated
.iter()
.map(|p| SyncedFile {
path: p.display().to_string(),
size_bytes: if dry_run {
0
} else {
std::fs::metadata(p).map_or(0, |m| m.len() as usize)
},
action: if dry_run {
"would create".to_string()
} else {
"created".to_string()
},
})
.collect();
let synced_file_paths: Vec<String> = files.iter().map(|f| f.path.clone()).collect();
let installed_packs = read_installed_packs(".ggen/packs.lock");
log::info!("[μ₅/5] Receipt: Generating verification...");
let receipt_file_path =
emit_sync_receipt(&synced_file_paths, &installed_packs).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Audit failure: {}", e))
})?;
let violation_msg = if result.soundness_violations.is_empty() {
None
} else {
Some(format!(
"{} soundness violation(s): {}",
result.soundness_violations.len(),
result
.soundness_violations
.iter()
.map(|v| v.rule.as_str())
.collect::<Vec<_>>()
.join(", ")
))
};
Ok(SyncOutput {
status: "success".to_string(),
files_synced: files.len(),
duration_ms: result.elapsed_ms,
files,
inference_rules_executed: 0,
generation_rules_executed: result.files_generated.len(),
audit_trail: None,
error: violation_msg,
receipt_path: Some(receipt_file_path),
gates: Vec::new(),
})
}
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
fn build_sync_options(
manifest: Option<String>, output_dir: Option<String>, dry_run: Option<bool>,
force: Option<bool>, audit: Option<bool>, rule: Option<String>, verbose: Option<bool>,
watch: Option<bool>, validate_only: Option<bool>, format: Option<String>, timeout: Option<u64>,
stage: Option<String>, ontology: Option<String>,
) -> Result<SyncOptions, NounVerbError> {
let mut options = SyncOptions::new();
options.manifest_path = PathBuf::from(manifest.unwrap_or_else(|| "ggen.toml".to_string()));
if let Some(dir) = output_dir {
options.output_dir = Some(PathBuf::from(dir));
}
options.dry_run = dry_run.unwrap_or(false);
options.force = force.unwrap_or(false);
options.audit = audit.unwrap_or(false);
options.verbose = verbose.unwrap_or(false);
options.watch = watch.unwrap_or(false);
options.validate_only = validate_only.unwrap_or(false);
if let Some(r) = rule {
options.selected_rules = Some(vec![r]);
}
if let Some(fmt) = format {
options.output_format = match fmt.to_lowercase().as_str() {
"text" => OutputFormat::Text,
"json" => OutputFormat::Json,
_ => {
return Err(NounVerbError::execution_error(format!(
"error[E0005]: Invalid output format '{}'\n |\n = help: Valid formats: text, json",
fmt
)))
}
};
}
if let Some(t) = timeout {
options.timeout_ms = Some(t);
}
if let Some(s) = stage {
if !matches!(
s.as_str(),
"μ₁" | "μ₂" | "μ₃" | "μ₄" | "μ₅" | "mu1" | "mu2" | "mu3" | "mu4" | "mu5"
) {
return Err(NounVerbError::execution_error(format!(
"error[E0006]: Invalid stage '{}'\n |\n = help: Valid stages: μ₁, μ₂, μ₃, μ₄, μ₅ (or mu1-mu5)",
s
)));
}
options.a2a_stage = Some(s);
}
if let Some(ont) = ontology {
options.ontology_path = Some(PathBuf::from(ont));
}
Ok(options)
}