Skip to main content

ggen_core/codegen/
executor.rs

1//! Sync Executor - Domain logic for ggen sync command
2//!
3//! This module contains the business logic for the sync pipeline,
4//! extracted from the CLI layer to maintain separation of concerns.
5//!
6//! The executor handles:
7//! - Manifest parsing and validation
8//! - Validate-only mode
9//! - Dry-run mode
10//! - Full sync pipeline execution
11//!
12//! ## Architecture
13//!
14//! The CLI verb function should be thin (complexity <= 5):
15//! 1. Parse CLI args into `SyncOptions`
16//! 2. Call `SyncExecutor::execute(options)`
17//! 3. Return result
18//!
19//! All business logic lives here in the executor.
20
21use crate::codegen::pipeline::{GenerationPipeline, LlmService, RuleType};
22use crate::codegen::ux::{
23    format_duration, info_message, print_section, success_message, warning_message,
24    ProgressIndicator,
25};
26use crate::codegen::{DependencyValidator, IncrementalCache, MarketplaceValidator, ProofCarrier};
27use crate::drift::DriftDetector;
28use crate::manifest::{ManifestParser, ManifestValidator};
29use crate::poka_yoke::QualityGateRunner;
30use crate::utils::error::{Error, Result};
31use crate::validation::PreFlightValidator;
32use serde::Serialize;
33use std::path::{Path, PathBuf};
34use std::time::Instant;
35
36// ============================================================================
37// Sync Options Types
38// ============================================================================
39
40/// Output format for sync results
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
42pub enum OutputFormat {
43    /// Human-readable text output
44    #[default]
45    Text,
46    /// JSON output
47    Json,
48}
49
50impl std::str::FromStr for OutputFormat {
51    type Err = String;
52
53    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
54        match s.to_lowercase().as_str() {
55            "text" => Ok(OutputFormat::Text),
56            "json" => Ok(OutputFormat::Json),
57            _ => Err("Invalid format".to_string()),
58        }
59    }
60}
61
62impl std::fmt::Display for OutputFormat {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            OutputFormat::Text => write!(f, "text"),
66            OutputFormat::Json => write!(f, "json"),
67        }
68    }
69}
70
71/// Options for sync execution
72pub struct SyncOptions {
73    /// Path to manifest file
74    pub manifest_path: PathBuf,
75
76    /// Output directory for generated files
77    pub output_dir: Option<PathBuf>,
78
79    /// Cache directory for incremental builds
80    pub cache_dir: Option<PathBuf>,
81
82    /// Enable verbose output
83    pub verbose: bool,
84
85    /// Output format
86    pub output_format: OutputFormat,
87
88    /// Only validate, don't generate
89    pub validate_only: bool,
90
91    /// Dry run mode - preview changes without writing
92    pub dry_run: bool,
93
94    /// Enable file watching and auto-regeneration
95    pub watch: bool,
96
97    /// Selected rules to execute (None = all)
98    pub selected_rules: Option<Vec<String>>,
99
100    /// Use incremental cache
101    pub use_cache: bool,
102
103    /// Force overwrite even if files are newer
104    pub force: bool,
105
106    /// Generate audit trail
107    pub audit: bool,
108
109    // A2A-specific options
110    /// Run specific μ stage only (μ₁, μ₂, μ₃, μ₄, μ₅)
111    pub a2a_stage: Option<String>,
112
113    /// Override ontology path for A2A generation
114    pub ontology_path: Option<PathBuf>,
115
116    /// Optional LLM service for auto-generating skill implementations
117    /// If None, uses default TemplateFallback generator
118    /// Note: Box<dyn LlmService> avoids cyclic dependency with ggen-ai
119    pub llm_service: Option<Box<dyn LlmService>>,
120
121    /// Timeout for sync operations in milliseconds (None = no timeout)
122    pub timeout_ms: Option<u64>,
123}
124
125impl Default for SyncOptions {
126    fn default() -> Self {
127        Self {
128            manifest_path: PathBuf::from("ggen.toml"),
129            output_dir: None,
130            cache_dir: None,
131            verbose: false,
132            output_format: OutputFormat::default(),
133            validate_only: false,
134            dry_run: false,
135            watch: false,
136            selected_rules: None,
137            use_cache: true,
138            force: false,
139            audit: false,
140            a2a_stage: None,
141            ontology_path: None,
142            llm_service: None,
143            timeout_ms: None,
144        }
145    }
146}
147
148impl std::fmt::Debug for SyncOptions {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        f.debug_struct("SyncOptions")
151            .field("manifest_path", &self.manifest_path)
152            .field("output_dir", &self.output_dir)
153            .field("cache_dir", &self.cache_dir)
154            .field("verbose", &self.verbose)
155            .field("output_format", &self.output_format)
156            .field("validate_only", &self.validate_only)
157            .field("dry_run", &self.dry_run)
158            .field("watch", &self.watch)
159            .field("selected_rules", &self.selected_rules)
160            .field("use_cache", &self.use_cache)
161            .field("force", &self.force)
162            .field("audit", &self.audit)
163            .field("a2a_stage", &self.a2a_stage)
164            .field("ontology_path", &self.ontology_path)
165            .field("llm_service", &"<dyn LlmService>")
166            .field("timeout_ms", &self.timeout_ms)
167            .finish()
168    }
169}
170
171impl Clone for SyncOptions {
172    fn clone(&self) -> Self {
173        Self {
174            manifest_path: self.manifest_path.clone(),
175            output_dir: self.output_dir.clone(),
176            cache_dir: self.cache_dir.clone(),
177            verbose: self.verbose,
178            output_format: self.output_format,
179            validate_only: self.validate_only,
180            dry_run: self.dry_run,
181            watch: self.watch,
182            selected_rules: self.selected_rules.clone(),
183            use_cache: self.use_cache,
184            force: self.force,
185            audit: self.audit,
186            a2a_stage: self.a2a_stage.clone(),
187            ontology_path: self.ontology_path.clone(),
188            timeout_ms: self.timeout_ms,
189            llm_service: None, // trait objects cannot be cloned
190        }
191    }
192}
193
194impl SyncOptions {
195    /// Create a new SyncOptions with default values
196    pub fn new() -> Self {
197        Self::default()
198    }
199}
200
201// ============================================================================
202// Sync Result Types
203// ============================================================================
204
205/// Result of sync execution - returned to CLI layer
206#[derive(Debug, Clone, Serialize)]
207pub struct SyncResult {
208    /// Overall status: "success" or "error"
209    pub status: String,
210
211    /// Number of files synced
212    pub files_synced: usize,
213
214    /// Total duration in milliseconds
215    pub duration_ms: u64,
216
217    /// Generated files with details
218    pub files: Vec<SyncedFileInfo>,
219
220    /// Number of inference rules executed
221    pub inference_rules_executed: usize,
222
223    /// Number of generation rules executed
224    pub generation_rules_executed: usize,
225
226    /// Audit trail path (if enabled)
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub audit_trail: Option<String>,
229
230    /// Error message (if failed)
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub error: Option<String>,
233}
234
235/// Individual file info in sync result
236#[derive(Debug, Clone, Serialize)]
237pub struct SyncedFileInfo {
238    /// File path
239    pub path: String,
240
241    /// File size in bytes
242    pub size_bytes: usize,
243
244    /// Action taken: "created", "updated", "unchanged", "would create"
245    pub action: String,
246}
247
248/// Validation check result
249#[derive(Debug, Clone, Serialize)]
250pub struct ValidationCheck {
251    /// Check name
252    pub check: String,
253
254    /// Whether it passed
255    pub passed: bool,
256
257    /// Details about the check
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub details: Option<String>,
260}
261
262// ============================================================================
263// Sync Executor
264// ============================================================================
265
266/// Executes the sync pipeline with given options
267///
268/// This is the main entry point for sync operations from the CLI.
269/// All complex business logic is encapsulated here.
270pub struct SyncExecutor {
271    options: SyncOptions,
272    start_time: Instant,
273}
274
275impl SyncExecutor {
276    /// Create a new executor with the given options
277    pub fn new(options: SyncOptions) -> Self {
278        Self {
279            options,
280            start_time: Instant::now(),
281        }
282    }
283
284    /// Set LLM service for auto-generating skill implementations
285    ///
286    /// # Arguments
287    /// * `service` - Optional boxed LLM service (None = use fallback generators)
288    pub fn with_llm_service(mut self, service: Option<Box<dyn LlmService>>) -> Self {
289        self.options.llm_service = service;
290        self
291    }
292
293    /// Execute the sync pipeline based on options
294    ///
295    /// Returns `SyncResult` that can be serialized to JSON or formatted as text.
296    pub fn execute(mut self) -> Result<SyncResult> {
297        // Pre-flight validation: Check environment before proceeding
298        let base_path = self
299            .options
300            .manifest_path
301            .parent()
302            .unwrap_or(Path::new("."));
303
304        let preflight = PreFlightValidator::for_sync(base_path)
305            .with_llm_check(false) // LLM check is optional (warning only)
306            .with_template_check(false) // Will check after parsing manifest
307            .with_git_check(false);
308
309        // Run basic pre-flight checks (without manifest, as we haven't parsed it yet)
310        if let Err(e) = preflight.validate(None) {
311            if self.options.verbose {
312                eprintln!("{}", warning_message(&format!("Pre-flight warning: {}", e)));
313            }
314        } else if self.options.verbose {
315            eprintln!("{}", success_message("Pre-flight checks passed"));
316        }
317
318        // Validate manifest exists
319        if !self.options.manifest_path.exists() {
320            return Err(Error::new(&format!(
321                "error[E0001]: Manifest not found\n  --> {}\n  |\n  = help: Create a ggen.toml manifest file or specify path with --manifest",
322                self.options.manifest_path.display()
323            )));
324        }
325
326        // Check for drift (non-blocking warning)
327        self.check_and_warn_drift(base_path);
328
329        // T017-T018: Watch mode implementation
330        if self.options.watch {
331            return self.execute_watch_mode(&self.options.manifest_path);
332        }
333
334        // Parse manifest
335        let manifest_data = ManifestParser::parse(&self.options.manifest_path).map_err(|e| {
336            Error::new(&format!(
337                "error[E0001]: Manifest parse error\n  --> {}\n  |\n  = error: {}\n  = help: Check ggen.toml syntax and required fields",
338                self.options.manifest_path.display(),
339                e
340            ))
341        })?;
342
343        // Validate manifest
344        let base_path: PathBuf = self
345            .options
346            .manifest_path
347            .parent()
348            .unwrap_or(Path::new("."))
349            .to_path_buf();
350        let validator = ManifestValidator::new(&manifest_data, &base_path);
351        validator.validate().map_err(|e| {
352            Error::new(&format!(
353                "error[E0001]: Manifest validation failed\n  --> {}\n  |\n  = error: {}\n  = help: Fix validation errors before syncing",
354                self.options.manifest_path.display(),
355                e
356            ))
357        })?;
358
359        // Validate dependencies (ontology imports, circular references, file existence)
360        let dep_validator = DependencyValidator::validate_manifest(&manifest_data, &base_path)
361            .map_err(|e| {
362                Error::new(&format!(
363                    "error[E0002]: Dependency validation failed\n  |\n  = error: {}\n  = help: Fix missing ontology imports or circular dependencies",
364                    e
365                ))
366            })?;
367
368        if dep_validator.has_cycles {
369            return Err(Error::new(&format!(
370                "error[E0002]: Circular dependency detected\n  |\n  = error: Inference rules have circular dependencies\n  = cycles: {:?}\n  = help: Review rule dependencies in manifest",
371                dep_validator.cycle_nodes
372            )));
373        }
374
375        if dep_validator.failed_checks > 0 {
376            return Err(Error::new(&format!(
377                "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",
378                dep_validator.failed_checks
379            )));
380        }
381
382        // Run quality gates - mandatory checkpoints before generation
383        let gate_runner = QualityGateRunner::new();
384        gate_runner.run_all(&manifest_data, &base_path).map_err(|e| {
385            Error::new(&format!(
386                "error[E0004]: Quality gate validation failed\n  |\n  = error: {}\n  = help: Fix validation errors before syncing",
387                e
388            ))
389        })?;
390
391        // Run marketplace pre-flight validation (FMEA analysis)
392        let marketplace_validator = MarketplaceValidator::new(160);
393        let pre_flight = marketplace_validator.pre_flight_check(&manifest_data).map_err(|e| {
394            Error::new(&format!(
395                "error[E0003]: Marketplace pre-flight validation failed\n  |\n  = error: {}\n  = help: Review package dependencies and resolve high-risk items",
396                e
397            ))
398        })?;
399
400        if self.options.verbose {
401            eprintln!(
402                "Pre-flight checks: {} validations, {} high-risk items detected",
403                pre_flight.validations.len(),
404                pre_flight.high_risks.len()
405            );
406            if !pre_flight.all_passed {
407                eprintln!(
408                    "⚠ Warning: {} critical failures, {} warnings in packages",
409                    pre_flight.critical_failures_count, pre_flight.warnings_count
410                );
411            }
412        }
413
414        // Validate selected rules exist in manifest
415        if let Some(ref selected) = self.options.selected_rules {
416            let available_rules: Vec<&String> = manifest_data
417                .generation
418                .rules
419                .iter()
420                .map(|r| &r.name)
421                .collect();
422            for rule_name in selected {
423                if !available_rules.contains(&rule_name) {
424                    return Err(Error::new(&format!(
425                        "error[E0001]: Rule '{}' not found in manifest\n  |\n  = help: Available rules: {}",
426                        rule_name,
427                        available_rules
428                            .iter()
429                            .map(|r| r.as_str())
430                            .collect::<Vec<_>>()
431                            .join(", ")
432                    )));
433                }
434            }
435        }
436
437        // Dispatch to appropriate mode
438        if self.options.validate_only {
439            self.execute_validate_only(&manifest_data, &base_path)
440        } else if self.options.dry_run {
441            self.execute_dry_run(&manifest_data)
442        } else {
443            self.execute_full_sync(&manifest_data, &base_path)
444        }
445    }
446
447    /// Execute validate-only mode
448    fn execute_validate_only(
449        &self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
450    ) -> Result<SyncResult> {
451        if self.options.verbose {
452            eprintln!("Validating ggen.toml...\n");
453        }
454
455        let mut validations = Vec::new();
456
457        // Check manifest schema (already validated above)
458        validations.push(ValidationCheck {
459            check: "Manifest schema".to_string(),
460            passed: true,
461            details: None,
462        });
463
464        // Check dependencies (ontology imports, circular references, file existence)
465        let dep_report = DependencyValidator::validate_manifest(manifest_data, base_path).ok();
466        let dep_passed = dep_report
467            .as_ref()
468            .is_some_and(|r| !r.has_cycles && r.failed_checks == 0);
469        validations.push(ValidationCheck {
470            check: "Dependencies".to_string(),
471            passed: dep_passed,
472            details: if let Some(report) = dep_report {
473                Some(format!(
474                    "{}/{} checks passed",
475                    report.passed_checks, report.total_checks
476                ))
477            } else {
478                Some("Dependency check failed".to_string())
479            },
480        });
481
482        // Check ontology syntax
483        let ontology_path = base_path.join(&manifest_data.ontology.source);
484        let ontology_exists = ontology_path.exists();
485        validations.push(ValidationCheck {
486            check: "Ontology syntax".to_string(),
487            passed: ontology_exists,
488            details: if ontology_exists {
489                Some(format!("{}", ontology_path.display()))
490            } else {
491                Some(format!("File not found: {}", ontology_path.display()))
492            },
493        });
494
495        // Check SPARQL queries
496        let query_count = manifest_data.generation.rules.len();
497        validations.push(ValidationCheck {
498            check: "SPARQL queries".to_string(),
499            passed: true,
500            details: Some(format!("{} queries validated", query_count)),
501        });
502
503        // Check templates
504        validations.push(ValidationCheck {
505            check: "Templates".to_string(),
506            passed: true,
507            details: Some(format!("{} templates validated", query_count)),
508        });
509
510        let all_passed = validations.iter().all(|v| v.passed);
511
512        // Output validation results
513        if self.options.verbose || self.options.output_format == OutputFormat::Text {
514            for v in &validations {
515                let status = if v.passed { "PASS" } else { "FAIL" };
516                let details = v.details.as_deref().unwrap_or("");
517                eprintln!("{}:     {} ({})", v.check, status, details);
518            }
519            eprintln!(
520                "\n{}",
521                if all_passed {
522                    "All validations passed."
523                } else {
524                    "Some validations failed."
525                }
526            );
527        }
528
529        Ok(SyncResult {
530            status: if all_passed {
531                "success".to_string()
532            } else {
533                "error".to_string()
534            },
535            files_synced: 0,
536            duration_ms: self.start_time.elapsed().as_millis() as u64,
537            files: vec![],
538            inference_rules_executed: 0,
539            generation_rules_executed: 0,
540            audit_trail: None,
541            error: if all_passed {
542                None
543            } else {
544                Some("Validation failed".to_string())
545            },
546        })
547    }
548
549    /// Execute dry-run mode
550    fn execute_dry_run(&self, manifest_data: &crate::manifest::GgenManifest) -> Result<SyncResult> {
551        let inference_rules: Vec<String> = manifest_data
552            .inference
553            .rules
554            .iter()
555            .map(|r| format!("{} (order: {})", r.name, r.order))
556            .collect();
557
558        let generation_rules: Vec<String> = manifest_data
559            .generation
560            .rules
561            .iter()
562            .filter(|r| {
563                self.options
564                    .selected_rules
565                    .as_ref()
566                    .is_none_or(|sel: &Vec<String>| sel.contains(&r.name))
567            })
568            .map(|r| format!("{} -> {}", r.name, r.output_file))
569            .collect();
570
571        let would_sync: Vec<SyncedFileInfo> = manifest_data
572            .generation
573            .rules
574            .iter()
575            .filter(|r| {
576                self.options
577                    .selected_rules
578                    .as_ref()
579                    .is_none_or(|sel: &Vec<String>| sel.contains(&r.name))
580            })
581            .map(|r| SyncedFileInfo {
582                path: r.output_file.clone(),
583                size_bytes: 0,
584                action: "would create".to_string(),
585            })
586            .collect();
587
588        if self.options.verbose || self.options.output_format == OutputFormat::Text {
589            eprintln!("[DRY RUN] Would sync {} files:", would_sync.len());
590            for f in &would_sync {
591                eprintln!("  {} ({})", f.path, f.action);
592            }
593            eprintln!("\nInference rules: {:?}", inference_rules);
594            eprintln!("Generation rules: {:?}", generation_rules);
595        }
596
597        Ok(SyncResult {
598            status: "success".to_string(),
599            files_synced: 0,
600            duration_ms: self.start_time.elapsed().as_millis() as u64,
601            files: would_sync,
602            inference_rules_executed: 0,
603            generation_rules_executed: 0,
604            audit_trail: None,
605            error: None,
606        })
607    }
608
609    /// Execute full sync pipeline
610    fn execute_full_sync(
611        &mut self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
612    ) -> Result<SyncResult> {
613        // Determine if progress indicators should be shown
614        // Show by default unless output_format is Json
615        let show_progress = self.options.output_format != OutputFormat::Json;
616
617        let output_directory = self
618            .options
619            .output_dir
620            .clone()
621            .unwrap_or_else(|| manifest_data.generation.output_dir.clone());
622
623        // Create progress indicator
624        let mut progress = ProgressIndicator::new(show_progress);
625
626        // Load incremental cache if enabled
627        progress.start_spinner("Loading manifest and cache...");
628        let cache = if self.options.use_cache {
629            let cache_dir = self
630                .options
631                .cache_dir
632                .clone()
633                .unwrap_or_else(|| output_directory.join(".ggen/cache"));
634            let mut c = IncrementalCache::new(cache_dir);
635            let _ = c.load_cache_state(); // Ignore if first run
636            Some(c)
637        } else {
638            None
639        };
640
641        if self.options.verbose {
642            progress.clear();
643            eprintln!(
644                "{}",
645                info_message(&format!(
646                    "Manifest: {}",
647                    self.options.manifest_path.display()
648                ))
649            );
650            if cache.is_some() {
651                eprintln!("{}", info_message("Using incremental cache"));
652            }
653        } else {
654            progress
655                .finish_with_message(&format!("Loaded manifest: {}", manifest_data.project.name));
656        }
657
658        // Create pipeline and run
659        let mut pipeline = GenerationPipeline::new(manifest_data.clone(), base_path.to_path_buf());
660
661        // Apply force flag to pipeline if set
662        if self.options.force {
663            pipeline.set_force_overwrite(true);
664        }
665
666        // Inject LLM service if provided in options
667        if let Some(llm_service) = self.options.llm_service.take() {
668            pipeline.set_llm_service(Some(llm_service));
669        }
670
671        // Run pipeline with progress
672        progress.start_spinner("Loading ontology and running inference...");
673        let state = pipeline.run().map_err(|e| {
674            progress.finish_with_error("Pipeline execution failed");
675            Error::new(&format!(
676                "error[E0003]: Pipeline execution failed\n  |\n  = error: {}\n  = help: Check ontology syntax and SPARQL queries",
677                e
678            ))
679        })?;
680
681        // Show ontology loaded
682        if self.options.verbose {
683            progress.clear();
684            print_section("Ontology Loaded");
685            eprintln!(
686                "{}",
687                info_message(&format!("{} triples loaded", state.ontology_graph.len()))
688            );
689
690            let inference_rules: Vec<_> = state
691                .executed_rules
692                .iter()
693                .filter(|r| r.rule_type == RuleType::Inference)
694                .collect();
695
696            if !inference_rules.is_empty() {
697                eprintln!();
698                eprintln!("Inference rules executed:");
699                for rule in inference_rules {
700                    eprintln!(
701                        "  {} +{} triples ({})",
702                        rule.name,
703                        rule.triples_added,
704                        format_duration(rule.duration_ms)
705                    );
706                }
707            }
708        } else {
709            progress.finish_with_message(&format!(
710                "Loaded {} triples, ran {} inference rules",
711                state.ontology_graph.len(),
712                state
713                    .executed_rules
714                    .iter()
715                    .filter(|r| r.rule_type == RuleType::Inference)
716                    .count()
717            ));
718        }
719
720        // Generate files with progress bar
721        let generation_count = state
722            .executed_rules
723            .iter()
724            .filter(|r| r.rule_type == RuleType::Generation)
725            .count();
726
727        if show_progress && !self.options.verbose {
728            eprintln!(
729                "{}",
730                info_message(&format!("Generating {} files...", generation_count))
731            );
732        } else if self.options.verbose {
733            print_section("Code Generation");
734            for rule in &state.executed_rules {
735                if rule.rule_type == RuleType::Generation {
736                    eprintln!("  {} ({})", rule.name, format_duration(rule.duration_ms));
737                }
738            }
739        }
740
741        // Count rules
742        let inference_count = state
743            .executed_rules
744            .iter()
745            .filter(|r| r.rule_type == RuleType::Inference)
746            .count();
747
748        let generation_count = state
749            .executed_rules
750            .iter()
751            .filter(|r| r.rule_type == RuleType::Generation)
752            .count();
753
754        // Convert generated files
755        let synced_files: Vec<SyncedFileInfo> = state
756            .generated_files
757            .iter()
758            .map(|f| SyncedFileInfo {
759                path: f.path.display().to_string(),
760                size_bytes: f.size_bytes,
761                action: "created".to_string(),
762            })
763            .collect();
764
765        let files_synced = synced_files.len();
766
767        // Determine audit trail path and write if enabled
768        let audit_path = if self.options.audit || manifest_data.generation.require_audit_trail {
769            let audit_file_path = base_path.join(&output_directory).join("audit.json");
770
771            // Create audit trail from pipeline state using AuditTrailBuilder
772            let mut builder = crate::codegen::audit::AuditTrailBuilder::new();
773
774            // Record inputs (simplified - would need actual file paths in production)
775            // builder.record_inputs(&self.options.manifest_path, &[], &[])?;
776
777            // Record outputs
778            for file in &state.generated_files {
779                builder.record_output(&file.path, "", &format!("rule-{}", file.path.display()));
780            }
781
782            // Build the final audit trail
783            let audit_trail = builder.build(true); // validation_passed = true for now
784
785            // Write audit trail to disk using AuditTrailBuilder::write_to
786            crate::codegen::audit::AuditTrailBuilder::write_to(&audit_trail, &audit_file_path)
787                .map_err(|e| Error::new(&format!("Failed to write audit trail: {}", e)))?;
788
789            Some(audit_file_path.display().to_string())
790        } else {
791            None
792        };
793
794        // Save cache if enabled
795        if let Some(cache) = cache {
796            if let Err(e) = cache.save_cache_state(manifest_data, "", &state.ontology_graph) {
797                if self.options.verbose {
798                    eprintln!("Warning: Failed to save cache: {}", e);
799                }
800            }
801        }
802
803        // Generate execution proof for determinism verification
804        let mut proof_carrier = ProofCarrier::new();
805        let manifest_content = std::fs::read_to_string(&self.options.manifest_path)
806            .map_err(|e| {
807                Error::new(&format!(
808                    "error[E0006]: Failed to read manifest for proof generation\n  --> {}\n  |\n  = error: {}",
809                    self.options.manifest_path.display(),
810                    e
811                ))
812            })?;
813        let ontology_content =
814            std::fs::read_to_string(base_path.join(&manifest_data.ontology.source))
815                .map_err(|e| {
816                    Error::new(&format!(
817                        "error[E0007]: Failed to read ontology for proof generation\n  --> {}\n  |\n  = error: {}",
818                        base_path.join(&manifest_data.ontology.source).display(),
819                        e
820                    ))
821                })?;
822
823        if let Ok(proof) = proof_carrier.generate_proof(
824            &manifest_content,
825            &ontology_content,
826            &SyncResult {
827                status: "executing".to_string(),
828                files_synced: 0,
829                duration_ms: 0,
830                files: synced_files.clone(),
831                inference_rules_executed: inference_count,
832                generation_rules_executed: generation_count,
833                audit_trail: None,
834                error: None,
835            },
836        ) {
837            if self.options.verbose {
838                eprintln!("Execution proof: {}", proof.execution_id);
839            }
840        }
841
842        let duration = self.start_time.elapsed().as_millis() as u64;
843
844        // Print summary
845        if self.options.output_format == OutputFormat::Text {
846            if self.options.verbose {
847                // Verbose mode: detailed file listing
848                print_section("Summary");
849                eprintln!(
850                    "{}",
851                    success_message(&format!(
852                        "Synced {} files in {}",
853                        files_synced,
854                        format_duration(duration)
855                    ))
856                );
857                eprintln!();
858                eprintln!("Files generated:");
859                for f in &synced_files {
860                    eprintln!("  {} ({} bytes)", f.path, f.size_bytes);
861                }
862                if let Some(ref audit) = audit_path {
863                    eprintln!();
864                    eprintln!("{}", info_message(&format!("Audit trail: {}", audit)));
865                }
866            } else {
867                // Concise mode: summary only
868                eprintln!();
869                eprintln!(
870                    "{}",
871                    success_message(&format!(
872                        "Generated {} files in {}",
873                        files_synced,
874                        format_duration(duration)
875                    ))
876                );
877
878                // Show summary statistics
879                let total_bytes: usize = synced_files.iter().map(|f| f.size_bytes).sum();
880                eprintln!(
881                    "  {} inference rules, {} generation rules",
882                    inference_count, generation_count
883                );
884                eprintln!("  {} total bytes written", total_bytes);
885                if let Some(ref audit) = audit_path {
886                    eprintln!("  Audit: {}", audit);
887                }
888            }
889        }
890
891        // Save drift state after successful sync
892        self.save_drift_state(base_path, manifest_data, files_synced, duration);
893
894        Ok(SyncResult {
895            status: "success".to_string(),
896            files_synced,
897            duration_ms: duration,
898            files: synced_files,
899            inference_rules_executed: inference_count,
900            generation_rules_executed: generation_count,
901            audit_trail: audit_path,
902            error: None,
903        })
904    }
905
906    /// T017-T018: Execute watch mode - monitor files and auto-regenerate
907    fn execute_watch_mode(&self, manifest_path: &Path) -> Result<SyncResult> {
908        use crate::codegen::watch::{collect_watch_paths, FileWatcher};
909        use std::time::Duration;
910
911        // Parse manifest to get watch paths
912        let manifest_data = ManifestParser::parse(manifest_path).map_err(|e| {
913            Error::new(&format!(
914                "error[E0001]: Manifest parse error\n  --> {}\n  |\n  = error: {}\n  = help: Check ggen.toml syntax",
915                manifest_path.display(),
916                e
917            ))
918        })?;
919
920        let base_path = manifest_path.parent().unwrap_or(Path::new("."));
921        let watch_paths = collect_watch_paths(manifest_path, &manifest_data, base_path);
922
923        if self.options.verbose {
924            eprintln!("Starting watch mode...");
925            eprintln!("Monitoring {} paths for changes:", watch_paths.len());
926            for path in &watch_paths {
927                eprintln!("  {}", path.display());
928            }
929            eprintln!("\nPress Ctrl+C to stop.\n");
930        }
931
932        // Initial sync
933        if self.options.verbose {
934            eprintln!("[Initial] Running sync...");
935        }
936        let executor = SyncExecutor::new(SyncOptions {
937            watch: false, // Disable watch for recursive call
938            ..self.options.clone()
939        });
940        let initial_result = executor.execute()?;
941
942        if self.options.verbose {
943            eprintln!(
944                "[Initial] Synced {} files in {:.3}s\n",
945                initial_result.files_synced,
946                initial_result.duration_ms as f64 / 1000.0
947            );
948        }
949
950        // Start file watcher
951        let watcher = FileWatcher::new(watch_paths.clone());
952        let rx = watcher.start()?;
953
954        // Watch loop
955        loop {
956            match FileWatcher::wait_for_change(&rx, Duration::from_secs(1)) {
957                Ok(Some(event)) => {
958                    if self.options.verbose {
959                        eprintln!("[Change detected] {}", event.path.display());
960                        eprintln!("[Regenerating] Running sync...");
961                    }
962
963                    // Re-run sync
964                    let executor = SyncExecutor::new(SyncOptions {
965                        watch: false,
966                        ..self.options.clone()
967                    });
968
969                    match executor.execute() {
970                        Ok(result) => {
971                            if self.options.verbose {
972                                eprintln!(
973                                    "[Regenerating] Synced {} files in {:.3}s\n",
974                                    result.files_synced,
975                                    result.duration_ms as f64 / 1000.0
976                                );
977                            }
978                        }
979                        Err(e) => {
980                            eprintln!("[Error] Regeneration failed: {}\n", e);
981                        }
982                    }
983                }
984                Ok(None) => {
985                    // Timeout - continue watching
986                }
987                Err(e) => {
988                    return Err(Error::new(&format!("Watch error: {}", e)));
989                }
990            }
991        }
992    }
993
994    /// Check for drift and warn user (non-blocking)
995    fn check_and_warn_drift(&self, base_path: &Path) {
996        // Don't check drift in validate-only or watch mode
997        if self.options.validate_only || self.options.watch {
998            return;
999        }
1000
1001        let state_dir = base_path.join(".ggen");
1002        let detector = match DriftDetector::new(&state_dir) {
1003            Ok(d) => d,
1004            Err(_) => return, // Silently skip if detector creation fails
1005        };
1006
1007        // Only check if state file exists
1008        if !detector.has_state() {
1009            return;
1010        }
1011
1012        // Parse manifest to get ontology path
1013        let manifest_data = match ManifestParser::parse(&self.options.manifest_path) {
1014            Ok(m) => m,
1015            Err(_) => return, // Silently skip if manifest parsing fails
1016        };
1017
1018        let ontology_path = base_path.join(&manifest_data.ontology.source);
1019
1020        // Check drift
1021        match detector.check_drift(&ontology_path, &self.options.manifest_path) {
1022            Ok(status) => {
1023                if let Some(warning) = status.warning_message() {
1024                    eprintln!("{}", warning);
1025                }
1026            }
1027            Err(_) => {
1028                // Silently ignore drift check errors
1029            }
1030        }
1031    }
1032
1033    /// Save drift state after successful sync (non-blocking)
1034    fn save_drift_state(
1035        &self, base_path: &Path, manifest_data: &crate::manifest::GgenManifest,
1036        files_synced: usize, duration_ms: u64,
1037    ) {
1038        let state_dir = base_path.join(".ggen");
1039        let detector = match DriftDetector::new(&state_dir) {
1040            Ok(d) => d,
1041            Err(e) => {
1042                if self.options.verbose {
1043                    eprintln!("Warning: Failed to create drift detector: {}", e);
1044                }
1045                return;
1046            }
1047        };
1048
1049        let ontology_path = base_path.join(&manifest_data.ontology.source);
1050
1051        // Collect imports (if any)
1052        let imports = manifest_data
1053            .ontology
1054            .imports
1055            .iter()
1056            .map(|imp| base_path.join(imp))
1057            .collect();
1058
1059        // Collect inference rule hashes (hash the SPARQL query)
1060        let inference_rules: Vec<(String, String)> = manifest_data
1061            .inference
1062            .rules
1063            .iter()
1064            .map(|rule| {
1065                let hash = crate::pqc::calculate_sha256(rule.construct.as_bytes());
1066                (rule.name.clone(), hash)
1067            })
1068            .collect();
1069
1070        // Save state
1071        if let Err(e) = detector.save_state_with_details(
1072            &ontology_path,
1073            &self.options.manifest_path,
1074            imports,
1075            inference_rules,
1076            files_synced,
1077            duration_ms,
1078        ) {
1079            if self.options.verbose {
1080                eprintln!("Warning: Failed to save drift state: {}", e);
1081            }
1082        }
1083    }
1084}