use crate::codegen::pipeline::{GenerationPipeline, LlmService, RuleType};
use crate::codegen::ux::{
format_duration, info_message, print_section, success_message, warning_message,
ProgressIndicator,
};
use crate::codegen::{DependencyValidator, IncrementalCache, MarketplaceValidator, ProofCarrier};
use crate::drift::DriftDetector;
use crate::manifest::{ManifestParser, ManifestValidator};
use crate::poka_yoke::{AndonSignal, CriticalError, QualityGateRunner};
use crate::utils::error::{Error, Result};
use crate::validation::PreFlightValidator;
use serde::Serialize;
use std::path::{Path, PathBuf};
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
pub enum OutputFormat {
#[default]
Text,
Json,
}
impl std::str::FromStr for OutputFormat {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"text" => Ok(OutputFormat::Text),
"json" => Ok(OutputFormat::Json),
_ => Err("Invalid format".to_string()),
}
}
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OutputFormat::Text => write!(f, "text"),
OutputFormat::Json => write!(f, "json"),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ModeFlags {
pub validate_only: bool,
pub dry_run: bool,
pub watch: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BehaviorFlags {
pub verbose: bool,
pub force: bool,
pub audit: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SyncFlags {
pub mode: ModeFlags,
pub behavior: BehaviorFlags,
}
pub struct SyncOptions {
pub manifest_path: PathBuf,
pub output_dir: Option<PathBuf>,
pub cache_dir: Option<PathBuf>,
pub use_cache: bool,
pub flags: SyncFlags,
pub output_format: OutputFormat,
pub selected_rules: Option<Vec<String>>,
pub a2a_stage: Option<String>,
pub ontology_path: Option<PathBuf>,
pub llm_service: Option<Box<dyn LlmService>>,
pub timeout_ms: Option<u64>,
}
impl Default for SyncOptions {
fn default() -> Self {
Self {
manifest_path: PathBuf::from("ggen.toml"),
output_dir: None,
cache_dir: None,
use_cache: true,
flags: SyncFlags::default(),
output_format: OutputFormat::default(),
selected_rules: None,
a2a_stage: None,
ontology_path: None,
llm_service: None,
timeout_ms: None,
}
}
}
impl std::fmt::Debug for SyncOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SyncOptions")
.field("manifest_path", &self.manifest_path)
.field("output_dir", &self.output_dir)
.field("cache_dir", &self.cache_dir)
.field("flags", &self.flags)
.field("output_format", &self.output_format)
.field("selected_rules", &self.selected_rules)
.field("a2a_stage", &self.a2a_stage)
.field("ontology_path", &self.ontology_path)
.field("llm_service", &"<dyn LlmService>")
.field("timeout_ms", &self.timeout_ms)
.finish()
}
}
impl Clone for SyncOptions {
fn clone(&self) -> Self {
Self {
manifest_path: self.manifest_path.clone(),
output_dir: self.output_dir.clone(),
cache_dir: self.cache_dir.clone(),
use_cache: self.use_cache,
flags: self.flags,
output_format: self.output_format,
selected_rules: self.selected_rules.clone(),
a2a_stage: self.a2a_stage.clone(),
ontology_path: self.ontology_path.clone(),
timeout_ms: self.timeout_ms,
llm_service: None, }
}
}
impl SyncOptions {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct SyncResult {
pub status: String,
pub files_synced: usize,
pub duration_ms: u64,
pub files: Vec<SyncedFileInfo>,
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 recovery: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub andon_signal: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct SyncedFileInfo {
pub path: String,
pub size_bytes: usize,
pub action: String,
pub produced_by: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ValidationCheck {
pub check: String,
pub passed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
}
pub struct SyncExecutor {
options: SyncOptions,
start_time: Instant,
}
impl SyncExecutor {
pub fn new(options: SyncOptions) -> Self {
Self {
options,
start_time: Instant::now(),
}
}
pub fn with_llm_service(mut self, service: Option<Box<dyn LlmService>>) -> Self {
self.options.llm_service = service;
self
}
pub fn execute(mut self) -> Result<SyncResult> {
let base_path = self
.options
.manifest_path
.parent()
.unwrap_or(Path::new("."));
let preflight = PreFlightValidator::for_sync(base_path)
.with_llm_check(false) .with_template_check(false) .with_git_check(false);
if let Err(e) = preflight.validate(None) {
if self.options.flags.behavior.verbose {
eprintln!("{}", warning_message(&format!("Pre-flight warning: {}", e)));
}
} else if self.options.flags.behavior.verbose {
eprintln!("{}", success_message("Pre-flight checks passed"));
}
if !self.options.manifest_path.exists() {
let error_msg = format!(
"error[E0001]: Manifest not found\n --> {}",
self.options.manifest_path.display()
);
let andon = AndonSignal::manifest_error("ggen.toml", "File does not exist");
return Ok(self.create_error_result(&error_msg, Some(andon)));
}
self.check_and_warn_drift(base_path);
if self.options.flags.mode.watch {
return self.execute_watch_mode(&self.options.manifest_path);
}
let manifest_data = ManifestParser::parse(&self.options.manifest_path).map_err(|e| {
Error::new(&format!(
"error[E0001]: Manifest parse error\n --> {}\n |\n = error: {}\n = help: Check ggen.toml syntax and required fields",
self.options.manifest_path.display(),
e
))
})?;
let base_path: PathBuf = self
.options
.manifest_path
.parent()
.unwrap_or(Path::new("."))
.to_path_buf();
let validator = ManifestValidator::new(&manifest_data, &base_path);
validator.validate().map_err(|e| {
Error::new(&format!(
"error[E0001]: Manifest validation failed\n --> {}\n |\n = error: {}\n = help: Fix validation errors before syncing",
self.options.manifest_path.display(),
e
))
})?;
let dep_validator = DependencyValidator::validate_manifest(&manifest_data, &base_path)
.map_err(|e| {
Error::new(&format!(
"error[E0002]: Dependency validation failed\n |\n = error: {}\n = help: Fix missing ontology imports or circular dependencies",
e
))
})?;
if dep_validator.has_cycles {
let error_msg = format!("error[E0002]: Circular dependency detected\n |\n = error: Inference rules have circular dependencies\n = cycles: {:?}", dep_validator.cycle_nodes);
let andon = AndonSignal::circular_dependency(vec![dep_validator.cycle_nodes.clone()]);
return Ok(self.create_error_result(&error_msg, Some(andon)));
}
if dep_validator.failed_checks > 0 {
let error_msg = format!(
"error[E0002]: {} dependency validation checks failed\n |\n = help: Common issues:\n = 1. Query file not found: Check ontology.source and ontology.imports paths\n = 2. Template file not found: Check generation.rules[].template paths\n = 3. Import cycle: Check if imported files reference each other\n = help: Run 'ggen validate' for detailed dependency analysis",
dep_validator.failed_checks
);
return Ok(self.create_error_result(&error_msg, None));
}
let gate_runner = QualityGateRunner::new();
gate_runner.run_all(&manifest_data, &base_path).map_err(|e| {
Error::new(&format!(
"error[E0004]: Quality gate validation failed\n |\n = error: {}\n = help: Fix validation errors before syncing",
e
))
})?;
let marketplace_validator = MarketplaceValidator::new(160);
let pre_flight = marketplace_validator.pre_flight_check(&manifest_data).map_err(|e| {
Error::new(&format!(
"error[E0003]: Marketplace pre-flight validation failed\n |\n = error: {}\n = help: Review package dependencies and resolve high-risk items",
e
))
})?;
if self.options.flags.behavior.verbose {
eprintln!(
"Pre-flight checks: {} validations, {} high-risk items detected",
pre_flight.validations.len(),
pre_flight.high_risks.len()
);
if !pre_flight.all_passed {
eprintln!(
"⚠ Warning: {} critical failures, {} warnings in packages",
pre_flight.critical_failures_count, pre_flight.warnings_count
);
}
}
if let Some(ref selected) = self.options.selected_rules {
let available_rules: Vec<&String> = manifest_data
.generation
.rules
.iter()
.map(|r| &r.name)
.collect();
for rule_name in selected {
if !available_rules.contains(&rule_name) {
return Err(Error::new(&format!(
"error[E0001]: Rule '{}' not found in manifest\n |\n = help: Available rules: {}",
rule_name,
available_rules
.iter()
.map(|r| r.as_str())
.collect::<Vec<_>>()
.join(", ")
)));
}
}
}
if self.options.flags.mode.validate_only {
self.execute_validate_only(&manifest_data, &base_path)
} else if self.options.flags.mode.dry_run {
self.execute_dry_run(&manifest_data)
} else {
self.execute_full_sync(&manifest_data, &base_path)
}
}
fn execute_validate_only(
&self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
) -> Result<SyncResult> {
if self.options.flags.behavior.verbose {
eprintln!("Validating ggen.toml...\n");
}
let mut validations = Vec::new();
validations.push(ValidationCheck {
check: "Manifest schema".to_string(),
passed: true,
details: None,
});
let dep_report = DependencyValidator::validate_manifest(manifest_data, base_path).ok();
let dep_passed = dep_report
.as_ref()
.is_some_and(|r| !r.has_cycles && r.failed_checks == 0);
validations.push(ValidationCheck {
check: "Dependencies".to_string(),
passed: dep_passed,
details: if let Some(report) = dep_report {
Some(format!(
"{}/{} checks passed",
report.passed_checks, report.total_checks
))
} else {
Some("Dependency check failed".to_string())
},
});
let ontology_paths = manifest_data.ontology.resolved_sources(&base_path);
let ontology_exists =
!ontology_paths.is_empty() && ontology_paths.iter().all(|p| p.exists());
validations.push(ValidationCheck {
check: "Ontology syntax".to_string(),
passed: ontology_exists,
details: if ontology_exists {
Some(format!(
"{}",
ontology_paths
.first()
.map(|p| p.display().to_string())
.unwrap_or_default()
))
} else {
Some(format!(
"File not found: {}",
ontology_paths
.first()
.map(|p| p.display().to_string())
.unwrap_or_default()
))
},
});
let query_count = manifest_data.generation.rules.len();
validations.push(ValidationCheck {
check: "SPARQL queries".to_string(),
passed: true,
details: Some(format!("{} queries validated", query_count)),
});
validations.push(ValidationCheck {
check: "Templates".to_string(),
passed: true,
details: Some(format!("{} templates validated", query_count)),
});
if !manifest_data.validation.rules.is_empty() {
let mut rules_passed = true;
let mut rules_detail = format!("{} rules", manifest_data.validation.rules.len());
let mut pipeline =
GenerationPipeline::new(manifest_data.clone(), base_path.to_path_buf());
match pipeline
.load_ontology()
.and_then(|_| pipeline.execute_inference_rules().map(|_| ()))
.and_then(|_| pipeline.execute_validation_rules())
{
Ok(()) => {}
Err(e) => {
rules_passed = false;
rules_detail = e.to_string();
}
}
validations.push(ValidationCheck {
check: "Custom validation rules".to_string(),
passed: rules_passed,
details: Some(rules_detail),
});
}
let all_passed = validations.iter().all(|v| v.passed);
if self.options.flags.behavior.verbose || self.options.output_format == OutputFormat::Text {
for v in &validations {
let status = if v.passed { "PASS" } else { "FAIL" };
let details = v.details.as_deref().unwrap_or("");
eprintln!("{}: {} ({})", v.check, status, details);
}
eprintln!(
"\n{}",
if all_passed {
"All validations passed."
} else {
"Some validations failed."
}
);
}
Ok(SyncResult {
status: if all_passed {
"success".to_string()
} else {
"error".to_string()
},
files_synced: 0,
duration_ms: self.start_time.elapsed().as_millis() as u64,
files: vec![],
inference_rules_executed: 0,
generation_rules_executed: 0,
audit_trail: None,
error: if all_passed {
None
} else {
Some("Validation failed".to_string())
},
recovery: if all_passed {
None
} else {
Some("Run 'ggen validate' for detailed fixes".to_string())
},
andon_signal: None,
})
}
fn execute_dry_run(&self, manifest_data: &crate::manifest::GgenManifest) -> Result<SyncResult> {
let inference_rules: Vec<String> = manifest_data
.inference
.rules
.iter()
.map(|r| format!("{} (order: {})", r.name, r.order))
.collect();
let generation_rules: Vec<String> = manifest_data
.generation
.rules
.iter()
.filter(|r| {
self.options
.selected_rules
.as_ref()
.is_none_or(|sel: &Vec<String>| sel.contains(&r.name))
})
.map(|r| format!("{} -> {}", r.name, r.output_file))
.collect();
let would_sync: Vec<SyncedFileInfo> = manifest_data
.generation
.rules
.iter()
.filter(|r| {
self.options
.selected_rules
.as_ref()
.is_none_or(|sel: &Vec<String>| sel.contains(&r.name))
})
.map(|r| SyncedFileInfo {
path: r.output_file.clone(),
size_bytes: 0,
action: "would create".to_string(),
produced_by: r.name.clone(),
})
.collect();
if self.options.flags.behavior.verbose || self.options.output_format == OutputFormat::Text {
eprintln!("[DRY RUN] Would sync {} files:", would_sync.len());
for f in &would_sync {
eprintln!(" {} ({})", f.path, f.action);
}
eprintln!("\nInference rules: {:?}", inference_rules);
eprintln!("Generation rules: {:?}", generation_rules);
}
Ok(SyncResult {
status: "success".to_string(),
files_synced: 0,
duration_ms: self.start_time.elapsed().as_millis() as u64,
files: would_sync,
inference_rules_executed: 0,
generation_rules_executed: 0,
audit_trail: None,
error: None,
recovery: None,
andon_signal: None,
})
}
fn execute_full_sync(
&mut self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
) -> Result<SyncResult> {
let show_progress = self.options.output_format != OutputFormat::Json;
let output_directory = self
.options
.output_dir
.clone()
.unwrap_or_else(|| manifest_data.generation.output_dir.clone());
let mut progress = ProgressIndicator::new(show_progress);
progress.start_spinner("Loading manifest and cache...");
let cache = if self.options.use_cache {
let cache_dir = self
.options
.cache_dir
.clone()
.unwrap_or_else(|| output_directory.join(".ggen/cache"));
let mut c = IncrementalCache::new(cache_dir);
let _ = c.load_cache_state(); Some(c)
} else {
None
};
if self.options.flags.behavior.verbose {
progress.clear();
eprintln!(
"{}",
info_message(&format!(
"Manifest: {}",
self.options.manifest_path.display()
))
);
if cache.is_some() {
eprintln!("{}", info_message("Using incremental cache"));
}
} else {
progress
.finish_with_message(&format!("Loaded manifest: {}", manifest_data.project.name));
}
let mut pipeline = GenerationPipeline::new(manifest_data.clone(), base_path.to_path_buf());
if self.options.flags.behavior.force {
pipeline.set_force_overwrite(true);
}
if let Some(llm_service) = self.options.llm_service.take() {
pipeline.set_llm_service(Some(llm_service));
}
progress.start_spinner("Loading ontology and running inference...");
let state = pipeline.run().map_err(|e| {
progress.finish_with_error("Pipeline execution failed");
Error::new(&format!(
"error[E0003]: Pipeline execution failed\n |\n = error: {}\n = help: Check ontology syntax and SPARQL queries",
e
))
})?;
if self.options.flags.behavior.verbose {
progress.clear();
print_section("Ontology Loaded");
eprintln!(
"{}",
info_message(&format!("{} triples loaded", state.ontology_graph.len()))
);
let inference_rules: Vec<_> = state
.executed_rules
.iter()
.filter(|r| r.rule_type == RuleType::Inference)
.collect();
if !inference_rules.is_empty() {
eprintln!();
eprintln!("Inference rules executed:");
for rule in inference_rules {
eprintln!(
" {} +{} triples ({})",
rule.name,
rule.triples_added,
format_duration(rule.duration_ms)
);
}
}
} else {
progress.finish_with_message(&format!(
"Loaded {} triples, ran {} inference rules",
state.ontology_graph.len(),
state
.executed_rules
.iter()
.filter(|r| r.rule_type == RuleType::Inference)
.count()
));
}
let generation_count = state
.executed_rules
.iter()
.filter(|r| r.rule_type == RuleType::Generation)
.count();
if show_progress && !self.options.flags.behavior.verbose {
eprintln!(
"{}",
info_message(&format!("Generating {} files...", generation_count))
);
} else if self.options.flags.behavior.verbose {
print_section("Code Generation");
for rule in &state.executed_rules {
if rule.rule_type == RuleType::Generation {
eprintln!(" {} ({})", rule.name, format_duration(rule.duration_ms));
}
}
}
let inference_count = state
.executed_rules
.iter()
.filter(|r| r.rule_type == RuleType::Inference)
.count();
let generation_count = state
.executed_rules
.iter()
.filter(|r| r.rule_type == RuleType::Generation)
.count();
let synced_files: Vec<SyncedFileInfo> = state
.generated_files
.iter()
.map(|f| SyncedFileInfo {
path: f.path.display().to_string(),
size_bytes: f.size_bytes,
action: "created".to_string(),
produced_by: f.source_rule.clone(),
})
.collect();
let files_synced = synced_files.len();
let audit_path = if self.options.flags.behavior.audit
|| manifest_data.generation.require_audit_trail
{
let audit_file_path = base_path.join(&output_directory).join("audit.json");
let mut builder = crate::codegen::audit::AuditTrailBuilder::new();
{
let ontology_paths = manifest_data.ontology.resolved_sources(&base_path);
let template_paths: Vec<PathBuf> = manifest_data
.generation
.rules
.iter()
.filter_map(|r| {
if let crate::manifest::TemplateSource::File { file } = &r.template {
Some(base_path.join(file))
} else {
None
}
})
.collect();
let template_refs: Vec<&std::path::Path> =
template_paths.iter().map(|p| p.as_path()).collect();
let ontology_refs: Vec<&std::path::Path> =
ontology_paths.iter().map(|p| p.as_path()).collect();
builder
.record_inputs(&self.options.manifest_path, &ontology_refs, &template_refs)
.map_err(|e| Error::new(&format!("Failed to record audit inputs: {}", e)))?;
}
for file in &state.generated_files {
let content = std::fs::read_to_string(&file.path).unwrap_or_default();
builder.record_output(
&file.path,
&content,
&format!("rule-{}", file.path.display()),
);
}
for rule in &state.executed_rules {
let step_type = match rule.rule_type {
RuleType::Inference => "inference",
RuleType::Generation => "render",
};
let triples = if rule.triples_added > 0 {
Some(rule.triples_added)
} else {
None
};
builder.record_step(
step_type,
&rule.name,
std::time::Duration::from_millis(rule.duration_ms),
triples,
"success",
);
}
let audit_trail = builder.build(true);
crate::codegen::audit::AuditTrailBuilder::write_to(&audit_trail, &audit_file_path)
.map_err(|e| Error::new(&format!("Failed to write audit trail: {}", e)))?;
Some(audit_file_path.display().to_string())
} else {
None
};
if let Some(cache) = cache {
if let Err(e) = cache.save_cache_state(manifest_data, "", &state.ontology_graph) {
if self.options.flags.behavior.verbose {
eprintln!("Warning: Failed to save cache: {}", e);
}
}
}
let mut proof_carrier = ProofCarrier::new();
let manifest_content = std::fs::read_to_string(&self.options.manifest_path)
.map_err(|e| {
Error::new(&format!(
"error[E0006]: Failed to read manifest for proof generation\n --> {}\n |\n = error: {}",
self.options.manifest_path.display(),
e
))
})?;
let mut ontology_content = String::new();
for path in manifest_data.ontology.resolved_sources(&base_path) {
let content = std::fs::read_to_string(&path).map_err(|e| {
Error::new(&format!(
"error[E0007]: Failed to read ontology for proof generation\n --> {}\n |\n = error: {}",
path.display(),
e
))
})?;
ontology_content.push_str(&content);
ontology_content.push('\n');
}
if let Ok(proof) = proof_carrier.generate_proof(
&manifest_content,
&ontology_content,
&SyncResult {
status: "executing".to_string(),
files_synced: 0,
duration_ms: 0,
files: synced_files.clone(),
inference_rules_executed: inference_count,
generation_rules_executed: generation_count,
audit_trail: None,
error: None,
recovery: None,
andon_signal: None,
},
) {
if self.options.flags.behavior.verbose {
eprintln!("Execution proof: {}", proof.execution_id);
}
}
let duration = self.start_time.elapsed().as_millis() as u64;
if self.options.output_format == OutputFormat::Text {
if self.options.flags.behavior.verbose {
print_section("Summary");
eprintln!(
"{}",
success_message(&format!(
"Synced {} files in {}",
files_synced,
format_duration(duration)
))
);
eprintln!();
eprintln!("Files generated:");
for f in &synced_files {
eprintln!(" {} ({} bytes)", f.path, f.size_bytes);
}
if let Some(ref audit) = audit_path {
eprintln!();
eprintln!("{}", info_message(&format!("Audit trail: {}", audit)));
}
} else {
eprintln!();
eprintln!(
"{}",
success_message(&format!(
"Generated {} files in {}",
files_synced,
format_duration(duration)
))
);
let total_bytes: usize = synced_files.iter().map(|f| f.size_bytes).sum();
eprintln!(
" {} inference rules, {} generation rules",
inference_count, generation_count
);
eprintln!(" {} total bytes written", total_bytes);
if let Some(ref audit) = audit_path {
eprintln!(" Audit: {}", audit);
}
}
}
self.save_drift_state(base_path, manifest_data, files_synced, duration);
Ok(SyncResult {
status: "success".to_string(),
files_synced,
duration_ms: duration,
files: synced_files,
inference_rules_executed: inference_count,
generation_rules_executed: generation_count,
audit_trail: audit_path,
error: None,
recovery: None,
andon_signal: None,
})
}
fn execute_watch_mode(&self, manifest_path: &Path) -> Result<SyncResult> {
use crate::codegen::watch::{collect_watch_paths, FileWatcher};
use std::time::Duration;
let manifest_data = ManifestParser::parse_and_validate(manifest_path).map_err(|e| {
Error::new(&format!(
"error[E0001]: Manifest parse error\n --> {}\n |\n = error: {}\n = help: Check ggen.toml syntax",
manifest_path.display(),
e
))
})?;
let base_path = manifest_path.parent().unwrap_or(Path::new("."));
let watch_paths = collect_watch_paths(manifest_path, &manifest_data, base_path);
if self.options.flags.behavior.verbose {
eprintln!("Starting watch mode...");
eprintln!("Monitoring {} paths for changes:", watch_paths.len());
for path in &watch_paths {
eprintln!(" {}", path.display());
}
eprintln!("\nPress Ctrl+C to stop.\n");
}
if self.options.flags.behavior.verbose {
eprintln!("[Initial] Running sync...");
}
let mut inner_opts = self.options.clone();
inner_opts.flags.mode.watch = false; let executor = SyncExecutor::new(inner_opts);
let initial_result = executor.execute()?;
if self.options.flags.behavior.verbose {
eprintln!(
"[Initial] Synced {} files in {:.3}s\n",
initial_result.files_synced,
initial_result.duration_ms as f64 / 1000.0
);
}
let watcher = FileWatcher::new(watch_paths.clone());
let rx = watcher.start()?;
loop {
match FileWatcher::wait_for_change(&rx, Duration::from_secs(1)) {
Ok(Some(event)) => {
if self.options.flags.behavior.verbose {
eprintln!("[Change detected] {}", event.path.display());
eprintln!("[Regenerating] Running sync...");
}
let mut inner_opts = self.options.clone();
inner_opts.flags.mode.watch = false;
let executor = SyncExecutor::new(inner_opts);
match executor.execute() {
Ok(result) => {
if self.options.flags.behavior.verbose {
eprintln!(
"[Regenerating] Synced {} files in {:.3}s\n",
result.files_synced,
result.duration_ms as f64 / 1000.0
);
}
}
Err(e) => {
eprintln!("[Error] Regeneration failed: {}\n", e);
}
}
}
Ok(None) => {
}
Err(e) => {
return Err(Error::new(&format!("Watch error: {}", e)));
}
}
}
}
fn check_and_warn_drift(&self, base_path: &Path) {
if self.options.flags.mode.validate_only || self.options.flags.mode.watch {
return;
}
let state_dir = base_path.join(".ggen");
let detector = match DriftDetector::new(&state_dir) {
Ok(d) => d,
Err(_) => return, };
if !detector.has_state() {
return;
}
let manifest_data = match ManifestParser::parse(&self.options.manifest_path) {
Ok(m) => m,
Err(_) => return, };
let ontology_path = manifest_data
.ontology
.resolved_sources(&base_path)
.into_iter()
.next()
.unwrap_or_else(|| base_path.join(&manifest_data.ontology.source));
match detector.check_drift(&ontology_path, &self.options.manifest_path) {
Ok(status) => {
if let Some(warning) = status.warning_message() {
eprintln!("{}", warning);
}
}
Err(_) => {
}
}
}
fn save_drift_state(
&self, base_path: &Path, manifest_data: &crate::manifest::GgenManifest,
files_synced: usize, duration_ms: u64,
) {
let state_dir = base_path.join(".ggen");
let detector = match DriftDetector::new(&state_dir) {
Ok(d) => d,
Err(e) => {
if self.options.flags.behavior.verbose {
eprintln!("Warning: Failed to create drift detector: {}", e);
}
return;
}
};
let ontology_path = manifest_data
.ontology
.resolved_sources(&base_path)
.into_iter()
.next()
.unwrap_or_else(|| base_path.join(&manifest_data.ontology.source));
let imports = manifest_data
.ontology
.imports
.iter()
.map(|imp| base_path.join(imp))
.collect();
let inference_rules: Vec<(String, String)> = manifest_data
.inference
.rules
.iter()
.map(|rule| {
let hash = crate::pqc::calculate_sha256(rule.construct.as_bytes());
(rule.name.clone(), hash)
})
.collect();
if let Err(e) = detector.save_state_with_details(
&ontology_path,
&self.options.manifest_path,
imports,
inference_rules,
files_synced,
duration_ms,
) {
if self.options.flags.behavior.verbose {
eprintln!("Warning: Failed to save drift state: {}", e);
}
}
}
fn create_error_result(&self, error_msg: &str, andon: Option<AndonSignal>) -> SyncResult {
let duration = self.start_time.elapsed().as_millis() as u64;
let (recovery, andon_json) = if let Some(signal) = andon {
let rec = if let AndonSignal::Red(ref critical) = signal {
Some(critical.recovery_steps.join("\n"))
} else {
None
};
(rec, serde_json::to_value(&signal).ok())
} else {
(None, None)
};
SyncResult {
status: "error".to_string(),
files_synced: 0,
duration_ms: duration,
files: Vec::new(),
inference_rules_executed: 0,
generation_rules_executed: 0,
audit_trail: None,
error: Some(error_msg.to_string()),
recovery,
andon_signal: andon_json,
}
}
}