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::{AndonSignal, CriticalError, 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/// Execution mode flags — mutually exclusive sync behaviors (≤3 bools)
72#[derive(Debug, Clone, Copy, Default)]
73pub struct ModeFlags {
74    /// Only validate, don't generate
75    pub validate_only: bool,
76    /// Dry run mode - preview changes without writing
77    pub dry_run: bool,
78    /// Enable file watching and auto-regeneration
79    pub watch: bool,
80}
81
82/// Behavioral modifier flags (≤3 bools)
83#[derive(Debug, Clone, Copy, Default)]
84pub struct BehaviorFlags {
85    /// Enable verbose output
86    pub verbose: bool,
87    /// Force overwrite even if files are newer
88    pub force: bool,
89    /// Generate audit trail
90    pub audit: bool,
91}
92
93/// Combined sync flags (groups mode and behavior sub-structs)
94#[derive(Debug, Clone, Copy, Default)]
95pub struct SyncFlags {
96    /// Execution mode (validate_only, dry_run, watch)
97    pub mode: ModeFlags,
98    /// Behavioral modifiers (verbose, force, audit)
99    pub behavior: BehaviorFlags,
100}
101
102/// Options for sync execution
103pub struct SyncOptions {
104    /// Path to manifest file
105    pub manifest_path: PathBuf,
106
107    /// Output directory for generated files
108    pub output_dir: Option<PathBuf>,
109
110    /// Cache directory for incremental builds
111    pub cache_dir: Option<PathBuf>,
112
113    /// Use incremental cache
114    pub use_cache: bool,
115
116    /// Boolean execution flags
117    pub flags: SyncFlags,
118
119    /// Output format
120    pub output_format: OutputFormat,
121
122    /// Selected rules to execute (None = all)
123    pub selected_rules: Option<Vec<String>>,
124
125    // A2A-specific options
126    /// Run specific μ stage only (μ₁, μ₂, μ₃, μ₄, μ₅)
127    pub a2a_stage: Option<String>,
128
129    /// Override ontology path for A2A generation
130    pub ontology_path: Option<PathBuf>,
131
132    /// Optional LLM service for auto-generating skill implementations
133    /// If None, uses default TemplateFallback generator
134    /// Note: `Box<dyn LlmService>` avoids cyclic dependency with ggen-ai
135    pub llm_service: Option<Box<dyn LlmService>>,
136
137    /// Timeout for sync operations in milliseconds (None = no timeout)
138    pub timeout_ms: Option<u64>,
139}
140
141impl Default for SyncOptions {
142    fn default() -> Self {
143        Self {
144            manifest_path: PathBuf::from("ggen.toml"),
145            output_dir: None,
146            cache_dir: None,
147            use_cache: true,
148            flags: SyncFlags::default(),
149            output_format: OutputFormat::default(),
150            selected_rules: None,
151            a2a_stage: None,
152            ontology_path: None,
153            llm_service: None,
154            timeout_ms: None,
155        }
156    }
157}
158
159impl std::fmt::Debug for SyncOptions {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        f.debug_struct("SyncOptions")
162            .field("manifest_path", &self.manifest_path)
163            .field("output_dir", &self.output_dir)
164            .field("cache_dir", &self.cache_dir)
165            .field("flags", &self.flags)
166            .field("output_format", &self.output_format)
167            .field("selected_rules", &self.selected_rules)
168            .field("a2a_stage", &self.a2a_stage)
169            .field("ontology_path", &self.ontology_path)
170            .field("llm_service", &"<dyn LlmService>")
171            .field("timeout_ms", &self.timeout_ms)
172            .finish()
173    }
174}
175
176impl Clone for SyncOptions {
177    fn clone(&self) -> Self {
178        Self {
179            manifest_path: self.manifest_path.clone(),
180            output_dir: self.output_dir.clone(),
181            cache_dir: self.cache_dir.clone(),
182            use_cache: self.use_cache,
183            flags: self.flags,
184            output_format: self.output_format,
185            selected_rules: self.selected_rules.clone(),
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, Default)]
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    /// Machine-parsable recovery steps for AGI remediation
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub recovery: Option<String>,
237
238    /// JSON representation of the TPS Andon signal (if any)
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub andon_signal: Option<serde_json::Value>,
241}
242
243/// Individual file info in sync result
244#[derive(Debug, Clone, Serialize, Default)]
245pub struct SyncedFileInfo {
246    /// File path
247    pub path: String,
248
249    /// File size in bytes
250    pub size_bytes: usize,
251
252    /// Action taken: "created", "updated", "unchanged", "would create"
253    pub action: String,
254
255    /// Rule that generated this file
256    pub produced_by: String,
257}
258
259/// Validation check result
260#[derive(Debug, Clone, Serialize)]
261pub struct ValidationCheck {
262    /// Check name
263    pub check: String,
264
265    /// Whether it passed
266    pub passed: bool,
267
268    /// Details about the check
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub details: Option<String>,
271}
272
273// ============================================================================
274// Sync Executor
275// ============================================================================
276
277/// Executes the sync pipeline with given options
278///
279/// This is the main entry point for sync operations from the CLI.
280/// All complex business logic is encapsulated here.
281pub struct SyncExecutor {
282    options: SyncOptions,
283    start_time: Instant,
284}
285
286impl SyncExecutor {
287    /// Create a new executor with the given options
288    pub fn new(options: SyncOptions) -> Self {
289        Self {
290            options,
291            start_time: Instant::now(),
292        }
293    }
294
295    /// Set LLM service for auto-generating skill implementations
296    ///
297    /// # Arguments
298    /// * `service` - Optional boxed LLM service (None = use fallback generators)
299    pub fn with_llm_service(mut self, service: Option<Box<dyn LlmService>>) -> Self {
300        self.options.llm_service = service;
301        self
302    }
303
304    /// Execute the sync pipeline based on options
305    ///
306    /// Returns `SyncResult` that can be serialized to JSON or formatted as text.
307    pub fn execute(mut self) -> Result<SyncResult> {
308        // Pre-flight validation: Check environment before proceeding
309        let base_path = self
310            .options
311            .manifest_path
312            .parent()
313            .unwrap_or(Path::new("."));
314
315        let preflight = PreFlightValidator::for_sync(base_path)
316            .with_llm_check(false) // LLM check is optional (warning only)
317            .with_template_check(false) // Will check after parsing manifest
318            .with_git_check(false);
319
320        // Run basic pre-flight checks (without manifest, as we haven't parsed it yet)
321        if let Err(e) = preflight.validate(None) {
322            if self.options.flags.behavior.verbose {
323                eprintln!("{}", warning_message(&format!("Pre-flight warning: {}", e)));
324            }
325        } else if self.options.flags.behavior.verbose {
326            eprintln!("{}", success_message("Pre-flight checks passed"));
327        }
328
329        // Validate manifest exists
330        if !self.options.manifest_path.exists() {
331            let error_msg = format!(
332                "error[E0001]: Manifest not found\n  --> {}",
333                self.options.manifest_path.display()
334            );
335            let andon = AndonSignal::manifest_error("ggen.toml", "File does not exist");
336            return Ok(self.create_error_result(&error_msg, Some(andon)));
337        }
338
339        // Check for drift (non-blocking warning)
340        self.check_and_warn_drift(base_path);
341
342        // T017-T018: Watch mode implementation
343        if self.options.flags.mode.watch {
344            return self.execute_watch_mode(&self.options.manifest_path);
345        }
346
347        // Parse manifest
348        let manifest_data = ManifestParser::parse(&self.options.manifest_path).map_err(|e| {
349            Error::new(&format!(
350                "error[E0001]: Manifest parse error\n  --> {}\n  |\n  = error: {}\n  = help: Check ggen.toml syntax and required fields",
351                self.options.manifest_path.display(),
352                e
353            ))
354        })?;
355
356        // Validate manifest
357        let base_path: PathBuf = self
358            .options
359            .manifest_path
360            .parent()
361            .unwrap_or(Path::new("."))
362            .to_path_buf();
363        let validator = ManifestValidator::new(&manifest_data, &base_path);
364        validator.validate().map_err(|e| {
365            Error::new(&format!(
366                "error[E0001]: Manifest validation failed\n  --> {}\n  |\n  = error: {}\n  = help: Fix validation errors before syncing",
367                self.options.manifest_path.display(),
368                e
369            ))
370        })?;
371
372        // Validate dependencies (ontology imports, circular references, file existence)
373        let dep_validator = DependencyValidator::validate_manifest(&manifest_data, &base_path)
374            .map_err(|e| {
375                Error::new(&format!(
376                    "error[E0002]: Dependency validation failed\n  |\n  = error: {}\n  = help: Fix missing ontology imports or circular dependencies",
377                    e
378                ))
379            })?;
380
381        if dep_validator.has_cycles {
382            let error_msg = format!("error[E0002]: Circular dependency detected\n  |\n  = error: Inference rules have circular dependencies\n  = cycles: {:?}", dep_validator.cycle_nodes);
383            let andon = AndonSignal::circular_dependency(vec![dep_validator.cycle_nodes.clone()]);
384            return Ok(self.create_error_result(&error_msg, Some(andon)));
385        }
386
387        if dep_validator.failed_checks > 0 {
388            let error_msg = format!(
389                "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",
390                dep_validator.failed_checks
391            );
392            return Ok(self.create_error_result(&error_msg, None));
393        }
394
395        // Run quality gates - mandatory checkpoints before generation
396        let gate_runner = QualityGateRunner::new();
397        gate_runner.run_all(&manifest_data, &base_path).map_err(|e| {
398            Error::new(&format!(
399                "error[E0004]: Quality gate validation failed\n  |\n  = error: {}\n  = help: Fix validation errors before syncing",
400                e
401            ))
402        })?;
403
404        // Run marketplace pre-flight validation (FMEA analysis)
405        let marketplace_validator = MarketplaceValidator::new(160);
406        let pre_flight = marketplace_validator.pre_flight_check(&manifest_data).map_err(|e| {
407            Error::new(&format!(
408                "error[E0003]: Marketplace pre-flight validation failed\n  |\n  = error: {}\n  = help: Review package dependencies and resolve high-risk items",
409                e
410            ))
411        })?;
412
413        if self.options.flags.behavior.verbose {
414            eprintln!(
415                "Pre-flight checks: {} validations, {} high-risk items detected",
416                pre_flight.validations.len(),
417                pre_flight.high_risks.len()
418            );
419            if !pre_flight.all_passed {
420                eprintln!(
421                    "⚠ Warning: {} critical failures, {} warnings in packages",
422                    pre_flight.critical_failures_count, pre_flight.warnings_count
423                );
424            }
425        }
426
427        // Validate selected rules exist in manifest
428        if let Some(ref selected) = self.options.selected_rules {
429            let available_rules: Vec<&String> = manifest_data
430                .generation
431                .rules
432                .iter()
433                .map(|r| &r.name)
434                .collect();
435            for rule_name in selected {
436                if !available_rules.contains(&rule_name) {
437                    return Err(Error::new(&format!(
438                        "error[E0001]: Rule '{}' not found in manifest\n  |\n  = help: Available rules: {}",
439                        rule_name,
440                        available_rules
441                            .iter()
442                            .map(|r| r.as_str())
443                            .collect::<Vec<_>>()
444                            .join(", ")
445                    )));
446                }
447            }
448        }
449
450        // Dispatch to appropriate mode
451        if self.options.flags.mode.validate_only {
452            self.execute_validate_only(&manifest_data, &base_path)
453        } else if self.options.flags.mode.dry_run {
454            self.execute_dry_run(&manifest_data)
455        } else {
456            self.execute_full_sync(&manifest_data, &base_path)
457        }
458    }
459
460    /// Execute validate-only mode
461    fn execute_validate_only(
462        &self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
463    ) -> Result<SyncResult> {
464        if self.options.flags.behavior.verbose {
465            eprintln!("Validating ggen.toml...\n");
466        }
467
468        let mut validations = Vec::new();
469
470        // Check manifest schema (already validated above)
471        validations.push(ValidationCheck {
472            check: "Manifest schema".to_string(),
473            passed: true,
474            details: None,
475        });
476
477        // Check dependencies (ontology imports, circular references, file existence)
478        let dep_report = DependencyValidator::validate_manifest(manifest_data, base_path).ok();
479        let dep_passed = dep_report
480            .as_ref()
481            .is_some_and(|r| !r.has_cycles && r.failed_checks == 0);
482        validations.push(ValidationCheck {
483            check: "Dependencies".to_string(),
484            passed: dep_passed,
485            details: if let Some(report) = dep_report {
486                Some(format!(
487                    "{}/{} checks passed",
488                    report.passed_checks, report.total_checks
489                ))
490            } else {
491                Some("Dependency check failed".to_string())
492            },
493        });
494
495        // Check ontology syntax
496        let ontology_paths = manifest_data.ontology.resolved_sources(&base_path);
497        let ontology_exists =
498            !ontology_paths.is_empty() && ontology_paths.iter().all(|p| p.exists());
499        validations.push(ValidationCheck {
500            check: "Ontology syntax".to_string(),
501            passed: ontology_exists,
502            details: if ontology_exists {
503                Some(format!(
504                    "{}",
505                    ontology_paths
506                        .first()
507                        .map(|p| p.display().to_string())
508                        .unwrap_or_default()
509                ))
510            } else {
511                Some(format!(
512                    "File not found: {}",
513                    ontology_paths
514                        .first()
515                        .map(|p| p.display().to_string())
516                        .unwrap_or_default()
517                ))
518            },
519        });
520
521        // Check SPARQL queries
522        let query_count = manifest_data.generation.rules.len();
523        validations.push(ValidationCheck {
524            check: "SPARQL queries".to_string(),
525            passed: true,
526            details: Some(format!("{} queries validated", query_count)),
527        });
528
529        // Check templates
530        validations.push(ValidationCheck {
531            check: "Templates".to_string(),
532            passed: true,
533            details: Some(format!("{} templates validated", query_count)),
534        });
535
536        // Check custom validation rules ([[validation.rules]] SPARQL ASK/SELECT).
537        // These run against the inferred graph, alongside the structural checks
538        // above. ASK polarity follows the executor convention: ASK=true => valid,
539        // ASK=false => violation (matches the manifest field doc "true = valid").
540        if !manifest_data.validation.rules.is_empty() {
541            let mut rules_passed = true;
542            let mut rules_detail = format!("{} rules", manifest_data.validation.rules.len());
543            let mut pipeline =
544                GenerationPipeline::new(manifest_data.clone(), base_path.to_path_buf());
545            match pipeline
546                .load_ontology()
547                .and_then(|_| pipeline.execute_inference_rules().map(|_| ()))
548                .and_then(|_| pipeline.execute_validation_rules())
549            {
550                Ok(()) => {}
551                Err(e) => {
552                    rules_passed = false;
553                    rules_detail = e.to_string();
554                }
555            }
556            validations.push(ValidationCheck {
557                check: "Custom validation rules".to_string(),
558                passed: rules_passed,
559                details: Some(rules_detail),
560            });
561        }
562
563        let all_passed = validations.iter().all(|v| v.passed);
564
565        // Output validation results
566        if self.options.flags.behavior.verbose || self.options.output_format == OutputFormat::Text {
567            for v in &validations {
568                let status = if v.passed { "PASS" } else { "FAIL" };
569                let details = v.details.as_deref().unwrap_or("");
570                eprintln!("{}:     {} ({})", v.check, status, details);
571            }
572            eprintln!(
573                "\n{}",
574                if all_passed {
575                    "All validations passed."
576                } else {
577                    "Some validations failed."
578                }
579            );
580        }
581
582        Ok(SyncResult {
583            status: if all_passed {
584                "success".to_string()
585            } else {
586                "error".to_string()
587            },
588            files_synced: 0,
589            duration_ms: self.start_time.elapsed().as_millis() as u64,
590            files: vec![],
591            inference_rules_executed: 0,
592            generation_rules_executed: 0,
593            audit_trail: None,
594            error: if all_passed {
595                None
596            } else {
597                Some("Validation failed".to_string())
598            },
599            recovery: if all_passed {
600                None
601            } else {
602                Some("Run 'ggen validate' for detailed fixes".to_string())
603            },
604            andon_signal: None,
605        })
606    }
607
608    /// Execute dry-run mode
609    fn execute_dry_run(&self, manifest_data: &crate::manifest::GgenManifest) -> Result<SyncResult> {
610        let inference_rules: Vec<String> = manifest_data
611            .inference
612            .rules
613            .iter()
614            .map(|r| format!("{} (order: {})", r.name, r.order))
615            .collect();
616
617        let generation_rules: Vec<String> = manifest_data
618            .generation
619            .rules
620            .iter()
621            .filter(|r| {
622                self.options
623                    .selected_rules
624                    .as_ref()
625                    .is_none_or(|sel: &Vec<String>| sel.contains(&r.name))
626            })
627            .map(|r| format!("{} -> {}", r.name, r.output_file))
628            .collect();
629
630        let would_sync: Vec<SyncedFileInfo> = manifest_data
631            .generation
632            .rules
633            .iter()
634            .filter(|r| {
635                self.options
636                    .selected_rules
637                    .as_ref()
638                    .is_none_or(|sel: &Vec<String>| sel.contains(&r.name))
639            })
640            .map(|r| SyncedFileInfo {
641                path: r.output_file.clone(),
642                size_bytes: 0,
643                action: "would create".to_string(),
644                produced_by: r.name.clone(),
645            })
646            .collect();
647
648        if self.options.flags.behavior.verbose || self.options.output_format == OutputFormat::Text {
649            eprintln!("[DRY RUN] Would sync {} files:", would_sync.len());
650            for f in &would_sync {
651                eprintln!("  {} ({})", f.path, f.action);
652            }
653            eprintln!("\nInference rules: {:?}", inference_rules);
654            eprintln!("Generation rules: {:?}", generation_rules);
655        }
656
657        Ok(SyncResult {
658            status: "success".to_string(),
659            files_synced: 0,
660            duration_ms: self.start_time.elapsed().as_millis() as u64,
661            files: would_sync,
662            inference_rules_executed: 0,
663            generation_rules_executed: 0,
664            audit_trail: None,
665            error: None,
666            recovery: None,
667            andon_signal: None,
668        })
669    }
670
671    /// Execute full sync pipeline
672    fn execute_full_sync(
673        &mut self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
674    ) -> Result<SyncResult> {
675        // Determine if progress indicators should be shown
676        // Show by default unless output_format is Json
677        let show_progress = self.options.output_format != OutputFormat::Json;
678
679        let output_directory = self
680            .options
681            .output_dir
682            .clone()
683            .unwrap_or_else(|| manifest_data.generation.output_dir.clone());
684
685        // Create progress indicator
686        let mut progress = ProgressIndicator::new(show_progress);
687
688        // Load incremental cache if enabled
689        progress.start_spinner("Loading manifest and cache...");
690        let cache = if self.options.use_cache {
691            let cache_dir = self
692                .options
693                .cache_dir
694                .clone()
695                .unwrap_or_else(|| output_directory.join(".ggen/cache"));
696            let mut c = IncrementalCache::new(cache_dir);
697            let _ = c.load_cache_state(); // Ignore if first run
698            Some(c)
699        } else {
700            None
701        };
702
703        if self.options.flags.behavior.verbose {
704            progress.clear();
705            eprintln!(
706                "{}",
707                info_message(&format!(
708                    "Manifest: {}",
709                    self.options.manifest_path.display()
710                ))
711            );
712            if cache.is_some() {
713                eprintln!("{}", info_message("Using incremental cache"));
714            }
715        } else {
716            progress
717                .finish_with_message(&format!("Loaded manifest: {}", manifest_data.project.name));
718        }
719
720        // Create pipeline and run
721        let mut pipeline = GenerationPipeline::new(manifest_data.clone(), base_path.to_path_buf());
722
723        // Apply force flag to pipeline if set
724        if self.options.flags.behavior.force {
725            pipeline.set_force_overwrite(true);
726        }
727
728        // Apply explicit output_dir override (used by ggen-daemon for cross-repo generation).
729        if let Some(ref out) = self.options.output_dir {
730            pipeline.set_output_dir(out.clone());
731        }
732
733        // Inject LLM service if provided in options
734        if let Some(llm_service) = self.options.llm_service.take() {
735            pipeline.set_llm_service(Some(llm_service));
736        }
737
738        // Run pipeline with progress
739        progress.start_spinner("Loading ontology and running inference...");
740        let state = pipeline.run().map_err(|e| {
741            progress.finish_with_error("Pipeline execution failed");
742            Error::new(&format!(
743                "error[E0003]: Pipeline execution failed\n  |\n  = error: {}\n  = help: Check ontology syntax and SPARQL queries",
744                e
745            ))
746        })?;
747
748        // Show ontology loaded
749        if self.options.flags.behavior.verbose {
750            progress.clear();
751            print_section("Ontology Loaded");
752            eprintln!(
753                "{}",
754                info_message(&format!("{} triples loaded", state.ontology_graph.len()))
755            );
756
757            let inference_rules: Vec<_> = state
758                .executed_rules
759                .iter()
760                .filter(|r| r.rule_type == RuleType::Inference)
761                .collect();
762
763            if !inference_rules.is_empty() {
764                eprintln!();
765                eprintln!("Inference rules executed:");
766                for rule in inference_rules {
767                    eprintln!(
768                        "  {} +{} triples ({})",
769                        rule.name,
770                        rule.triples_added,
771                        format_duration(rule.duration_ms)
772                    );
773                }
774            }
775        } else {
776            progress.finish_with_message(&format!(
777                "Loaded {} triples, ran {} inference rules",
778                state.ontology_graph.len(),
779                state
780                    .executed_rules
781                    .iter()
782                    .filter(|r| r.rule_type == RuleType::Inference)
783                    .count()
784            ));
785        }
786
787        // Generate files with progress bar
788        let generation_count = state
789            .executed_rules
790            .iter()
791            .filter(|r| r.rule_type == RuleType::Generation)
792            .count();
793
794        if show_progress && !self.options.flags.behavior.verbose {
795            eprintln!(
796                "{}",
797                info_message(&format!("Generating {} files...", generation_count))
798            );
799        } else if self.options.flags.behavior.verbose {
800            print_section("Code Generation");
801            for rule in &state.executed_rules {
802                if rule.rule_type == RuleType::Generation {
803                    eprintln!("  {} ({})", rule.name, format_duration(rule.duration_ms));
804                }
805            }
806        }
807
808        // Count rules
809        let inference_count = state
810            .executed_rules
811            .iter()
812            .filter(|r| r.rule_type == RuleType::Inference)
813            .count();
814
815        let generation_count = state
816            .executed_rules
817            .iter()
818            .filter(|r| r.rule_type == RuleType::Generation)
819            .count();
820
821        // Convert generated files
822        let synced_files: Vec<SyncedFileInfo> = state
823            .generated_files
824            .iter()
825            .map(|f| SyncedFileInfo {
826                path: f.path.display().to_string(),
827                size_bytes: f.size_bytes,
828                action: "created".to_string(),
829                produced_by: f.source_rule.clone(),
830            })
831            .collect();
832
833        let files_synced = synced_files.len();
834
835        // Determine audit trail path and write if enabled
836        let audit_path = if self.options.flags.behavior.audit
837            || manifest_data.generation.require_audit_trail
838        {
839            let audit_file_path = base_path.join(&output_directory).join("audit.json");
840
841            // Create audit trail from pipeline state using AuditTrailBuilder
842            let mut builder = crate::codegen::audit::AuditTrailBuilder::new();
843
844            // Record real input hashes: manifest + ontology source + all template files.
845            {
846                let ontology_paths = manifest_data.ontology.resolved_sources(&base_path);
847                let template_paths: Vec<PathBuf> = manifest_data
848                    .generation
849                    .rules
850                    .iter()
851                    .filter_map(|r| {
852                        if let crate::manifest::TemplateSource::File { file } = &r.template {
853                            Some(base_path.join(file))
854                        } else {
855                            None
856                        }
857                    })
858                    .collect();
859                let template_refs: Vec<&std::path::Path> =
860                    template_paths.iter().map(|p| p.as_path()).collect();
861                let ontology_refs: Vec<&std::path::Path> =
862                    ontology_paths.iter().map(|p| p.as_path()).collect();
863                // Failure to hash an input is a hard error: an audit with missing
864                // input hashes is contract drift, not a degraded audit.
865                builder
866                    .record_inputs(&self.options.manifest_path, &ontology_refs, &template_refs)
867                    .map_err(|e| Error::new(&format!("Failed to record audit inputs: {}", e)))?;
868            }
869
870            // Record real output hashes by reading each generated file from disk.
871            for file in &state.generated_files {
872                let content = std::fs::read_to_string(&file.path).unwrap_or_default();
873                builder.record_output(
874                    &file.path,
875                    &content,
876                    &format!("rule-{}", file.path.display()),
877                );
878            }
879
880            // Record μ₁–μ₅ pipeline steps from executed rules so audit.pipeline is populated.
881            for rule in &state.executed_rules {
882                let step_type = match rule.rule_type {
883                    RuleType::Inference => "inference",
884                    RuleType::Generation => "render",
885                };
886                let triples = if rule.triples_added > 0 {
887                    Some(rule.triples_added)
888                } else {
889                    None
890                };
891                builder.record_step(
892                    step_type,
893                    &rule.name,
894                    std::time::Duration::from_millis(rule.duration_ms),
895                    triples,
896                    "success",
897                );
898            }
899
900            // validation_passed is true iff we reached this point: every validation
901            // gate above returns Err and short-circuits before this block executes.
902            let audit_trail = builder.build(true);
903
904            // Write audit trail to disk using AuditTrailBuilder::write_to
905            crate::codegen::audit::AuditTrailBuilder::write_to(&audit_trail, &audit_file_path)
906                .map_err(|e| Error::new(&format!("Failed to write audit trail: {}", e)))?;
907
908            Some(audit_file_path.display().to_string())
909        } else {
910            None
911        };
912
913        // Save cache if enabled
914        if let Some(cache) = cache {
915            if let Err(e) = cache.save_cache_state(manifest_data, "", &state.ontology_graph) {
916                if self.options.flags.behavior.verbose {
917                    eprintln!("Warning: Failed to save cache: {}", e);
918                }
919            }
920        }
921
922        // Generate execution proof for determinism verification
923        let mut proof_carrier = ProofCarrier::new();
924        let manifest_content = std::fs::read_to_string(&self.options.manifest_path)
925            .map_err(|e| {
926                Error::new(&format!(
927                    "error[E0006]: Failed to read manifest for proof generation\n  --> {}\n  |\n  = error: {}",
928                    self.options.manifest_path.display(),
929                    e
930                ))
931            })?;
932        let mut ontology_content = String::new();
933        for path in manifest_data.ontology.resolved_sources(&base_path) {
934            let content = std::fs::read_to_string(&path).map_err(|e| {
935                Error::new(&format!(
936                    "error[E0007]: Failed to read ontology for proof generation\n  --> {}\n  |\n  = error: {}",
937                    path.display(),
938                    e
939                ))
940            })?;
941            ontology_content.push_str(&content);
942            ontology_content.push('\n');
943        }
944
945        if let Ok(proof) = proof_carrier.generate_proof(
946            &manifest_content,
947            &ontology_content,
948            &SyncResult {
949                status: "executing".to_string(),
950                files_synced: 0,
951                duration_ms: 0,
952                files: synced_files.clone(),
953                inference_rules_executed: inference_count,
954                generation_rules_executed: generation_count,
955                audit_trail: None,
956                error: None,
957                recovery: None,
958                andon_signal: None,
959            },
960        ) {
961            if self.options.flags.behavior.verbose {
962                eprintln!("Execution proof: {}", proof.execution_id);
963            }
964        }
965
966        let duration = self.start_time.elapsed().as_millis() as u64;
967
968        // Print summary
969        if self.options.output_format == OutputFormat::Text {
970            if self.options.flags.behavior.verbose {
971                // Verbose mode: detailed file listing
972                print_section("Summary");
973                eprintln!(
974                    "{}",
975                    success_message(&format!(
976                        "Synced {} files in {}",
977                        files_synced,
978                        format_duration(duration)
979                    ))
980                );
981                eprintln!();
982                eprintln!("Files generated:");
983                for f in &synced_files {
984                    eprintln!("  {} ({} bytes)", f.path, f.size_bytes);
985                }
986                if let Some(ref audit) = audit_path {
987                    eprintln!();
988                    eprintln!("{}", info_message(&format!("Audit trail: {}", audit)));
989                }
990            } else {
991                // Concise mode: summary only
992                eprintln!();
993                eprintln!(
994                    "{}",
995                    success_message(&format!(
996                        "Generated {} files in {}",
997                        files_synced,
998                        format_duration(duration)
999                    ))
1000                );
1001
1002                // Show summary statistics
1003                let total_bytes: usize = synced_files.iter().map(|f| f.size_bytes).sum();
1004                eprintln!(
1005                    "  {} inference rules, {} generation rules",
1006                    inference_count, generation_count
1007                );
1008                eprintln!("  {} total bytes written", total_bytes);
1009                if let Some(ref audit) = audit_path {
1010                    eprintln!("  Audit: {}", audit);
1011                }
1012            }
1013        }
1014
1015        // Save drift state after successful sync
1016        self.save_drift_state(base_path, manifest_data, files_synced, duration);
1017
1018        Ok(SyncResult {
1019            status: "success".to_string(),
1020            files_synced,
1021            duration_ms: duration,
1022            files: synced_files,
1023            inference_rules_executed: inference_count,
1024            generation_rules_executed: generation_count,
1025            audit_trail: audit_path,
1026            error: None,
1027            recovery: None,
1028            andon_signal: None,
1029        })
1030    }
1031
1032    /// T017-T018: Execute watch mode - monitor files and auto-regenerate
1033    fn execute_watch_mode(&self, manifest_path: &Path) -> Result<SyncResult> {
1034        use crate::codegen::watch::{collect_watch_paths, FileWatcher};
1035        use std::time::Duration;
1036
1037        // Parse and validate manifest to get watch paths
1038        let manifest_data = ManifestParser::parse_and_validate(manifest_path).map_err(|e| {
1039            Error::new(&format!(
1040                "error[E0001]: Manifest parse error\n  --> {}\n  |\n  = error: {}\n  = help: Check ggen.toml syntax",
1041                manifest_path.display(),
1042                e
1043            ))
1044        })?;
1045
1046        let base_path = manifest_path.parent().unwrap_or(Path::new("."));
1047        let watch_paths = collect_watch_paths(manifest_path, &manifest_data, base_path);
1048
1049        if self.options.flags.behavior.verbose {
1050            eprintln!("Starting watch mode...");
1051            eprintln!("Monitoring {} paths for changes:", watch_paths.len());
1052            for path in &watch_paths {
1053                eprintln!("  {}", path.display());
1054            }
1055            eprintln!("\nPress Ctrl+C to stop.\n");
1056        }
1057
1058        // Initial sync
1059        if self.options.flags.behavior.verbose {
1060            eprintln!("[Initial] Running sync...");
1061        }
1062        let mut inner_opts = self.options.clone();
1063        inner_opts.flags.mode.watch = false; // Disable watch for recursive call
1064        let executor = SyncExecutor::new(inner_opts);
1065        let initial_result = executor.execute()?;
1066
1067        if self.options.flags.behavior.verbose {
1068            eprintln!(
1069                "[Initial] Synced {} files in {:.3}s\n",
1070                initial_result.files_synced,
1071                initial_result.duration_ms as f64 / 1000.0
1072            );
1073        }
1074
1075        // Start file watcher
1076        let watcher = FileWatcher::new(watch_paths.clone());
1077        let rx = watcher.start()?;
1078
1079        // Watch loop
1080        loop {
1081            match FileWatcher::wait_for_change(&rx, Duration::from_secs(1)) {
1082                Ok(Some(event)) => {
1083                    if self.options.flags.behavior.verbose {
1084                        eprintln!("[Change detected] {}", event.path.display());
1085                        eprintln!("[Regenerating] Running sync...");
1086                    }
1087
1088                    // Re-run sync
1089                    let mut inner_opts = self.options.clone();
1090                    inner_opts.flags.mode.watch = false;
1091                    let executor = SyncExecutor::new(inner_opts);
1092
1093                    match executor.execute() {
1094                        Ok(result) => {
1095                            if self.options.flags.behavior.verbose {
1096                                eprintln!(
1097                                    "[Regenerating] Synced {} files in {:.3}s\n",
1098                                    result.files_synced,
1099                                    result.duration_ms as f64 / 1000.0
1100                                );
1101                            }
1102                        }
1103                        Err(e) => {
1104                            eprintln!("[Error] Regeneration failed: {}\n", e);
1105                        }
1106                    }
1107                }
1108                Ok(None) => {
1109                    // Timeout - continue watching
1110                }
1111                Err(e) => {
1112                    return Err(Error::new(&format!("Watch error: {}", e)));
1113                }
1114            }
1115        }
1116    }
1117
1118    /// Check for drift and warn user (non-blocking)
1119    fn check_and_warn_drift(&self, base_path: &Path) {
1120        // Don't check drift in validate-only or watch mode
1121        if self.options.flags.mode.validate_only || self.options.flags.mode.watch {
1122            return;
1123        }
1124
1125        let state_dir = base_path.join(".ggen");
1126        let detector = match DriftDetector::new(&state_dir) {
1127            Ok(d) => d,
1128            Err(_) => return, // Silently skip if detector creation fails
1129        };
1130
1131        // Only check if state file exists
1132        if !detector.has_state() {
1133            return;
1134        }
1135
1136        // Parse manifest to get ontology path
1137        let manifest_data = match ManifestParser::parse(&self.options.manifest_path) {
1138            Ok(m) => m,
1139            Err(_) => return, // Silently skip if manifest parsing fails
1140        };
1141
1142        // Note: Drift detector currently takes a single path, so we just use the first resolved path for now or skip if multiple.
1143        let ontology_path = manifest_data
1144            .ontology
1145            .resolved_sources(&base_path)
1146            .into_iter()
1147            .next()
1148            .unwrap_or_else(|| base_path.join(&manifest_data.ontology.source));
1149
1150        // Check drift
1151        match detector.check_drift(&ontology_path, &self.options.manifest_path) {
1152            Ok(status) => {
1153                if let Some(warning) = status.warning_message() {
1154                    eprintln!("{}", warning);
1155                }
1156            }
1157            Err(_) => {
1158                // Silently ignore drift check errors
1159            }
1160        }
1161    }
1162
1163    /// Save drift state after successful sync (non-blocking)
1164    fn save_drift_state(
1165        &self, base_path: &Path, manifest_data: &crate::manifest::GgenManifest,
1166        files_synced: usize, duration_ms: u64,
1167    ) {
1168        let state_dir = base_path.join(".ggen");
1169        let detector = match DriftDetector::new(&state_dir) {
1170            Ok(d) => d,
1171            Err(e) => {
1172                if self.options.flags.behavior.verbose {
1173                    eprintln!("Warning: Failed to create drift detector: {}", e);
1174                }
1175                return;
1176            }
1177        };
1178
1179        // Note: Drift detector currently takes a single path, so we just use the first resolved path for now or skip if multiple.
1180        let ontology_path = manifest_data
1181            .ontology
1182            .resolved_sources(&base_path)
1183            .into_iter()
1184            .next()
1185            .unwrap_or_else(|| base_path.join(&manifest_data.ontology.source));
1186
1187        // Collect imports (if any)
1188        let imports = manifest_data
1189            .ontology
1190            .imports
1191            .iter()
1192            .map(|imp| base_path.join(imp))
1193            .collect();
1194
1195        // Collect inference rule hashes (hash the SPARQL query)
1196        let inference_rules: Vec<(String, String)> = manifest_data
1197            .inference
1198            .rules
1199            .iter()
1200            .map(|rule| {
1201                let hash = crate::pqc::calculate_sha256(rule.construct.as_bytes());
1202                (rule.name.clone(), hash)
1203            })
1204            .collect();
1205
1206        // Save state
1207        if let Err(e) = detector.save_state_with_details(
1208            &ontology_path,
1209            &self.options.manifest_path,
1210            imports,
1211            inference_rules,
1212            files_synced,
1213            duration_ms,
1214        ) {
1215            if self.options.flags.behavior.verbose {
1216                eprintln!("Warning: Failed to save drift state: {}", e);
1217            }
1218        }
1219    }
1220
1221    /// Create a SyncResult for a failure state with machine-readable recovery info
1222    fn create_error_result(&self, error_msg: &str, andon: Option<AndonSignal>) -> SyncResult {
1223        let duration = self.start_time.elapsed().as_millis() as u64;
1224        let (recovery, andon_json) = if let Some(signal) = andon {
1225            let rec = if let AndonSignal::Red(ref critical) = signal {
1226                Some(critical.recovery_steps.join("\n"))
1227            } else {
1228                None
1229            };
1230            (rec, serde_json::to_value(&signal).ok())
1231        } else {
1232            (None, None)
1233        };
1234
1235        SyncResult {
1236            status: "error".to_string(),
1237            files_synced: 0,
1238            duration_ms: duration,
1239            files: Vec::new(),
1240            inference_rules_executed: 0,
1241            generation_rules_executed: 0,
1242            audit_trail: None,
1243            error: Some(error_msg.to_string()),
1244            recovery,
1245            andon_signal: andon_json,
1246        }
1247    }
1248}