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_path = base_path.join(&manifest_data.ontology.source);
497        let ontology_exists = ontology_path.exists();
498        validations.push(ValidationCheck {
499            check: "Ontology syntax".to_string(),
500            passed: ontology_exists,
501            details: if ontology_exists {
502                Some(format!("{}", ontology_path.display()))
503            } else {
504                Some(format!("File not found: {}", ontology_path.display()))
505            },
506        });
507
508        // Check SPARQL queries
509        let query_count = manifest_data.generation.rules.len();
510        validations.push(ValidationCheck {
511            check: "SPARQL queries".to_string(),
512            passed: true,
513            details: Some(format!("{} queries validated", query_count)),
514        });
515
516        // Check templates
517        validations.push(ValidationCheck {
518            check: "Templates".to_string(),
519            passed: true,
520            details: Some(format!("{} templates validated", query_count)),
521        });
522
523        let all_passed = validations.iter().all(|v| v.passed);
524
525        // Output validation results
526        if self.options.flags.behavior.verbose || self.options.output_format == OutputFormat::Text {
527            for v in &validations {
528                let status = if v.passed { "PASS" } else { "FAIL" };
529                let details = v.details.as_deref().unwrap_or("");
530                eprintln!("{}:     {} ({})", v.check, status, details);
531            }
532            eprintln!(
533                "\n{}",
534                if all_passed {
535                    "All validations passed."
536                } else {
537                    "Some validations failed."
538                }
539            );
540        }
541
542        Ok(SyncResult {
543            status: if all_passed {
544                "success".to_string()
545            } else {
546                "error".to_string()
547            },
548            files_synced: 0,
549            duration_ms: self.start_time.elapsed().as_millis() as u64,
550            files: vec![],
551            inference_rules_executed: 0,
552            generation_rules_executed: 0,
553            audit_trail: None,
554            error: if all_passed {
555                None
556            } else {
557                Some("Validation failed".to_string())
558            },
559            recovery: if all_passed {
560                None
561            } else {
562                Some("Run 'ggen validate' for detailed fixes".to_string())
563            },
564            andon_signal: None,
565        })
566    }
567
568    /// Execute dry-run mode
569    fn execute_dry_run(&self, manifest_data: &crate::manifest::GgenManifest) -> Result<SyncResult> {
570        let inference_rules: Vec<String> = manifest_data
571            .inference
572            .rules
573            .iter()
574            .map(|r| format!("{} (order: {})", r.name, r.order))
575            .collect();
576
577        let generation_rules: Vec<String> = manifest_data
578            .generation
579            .rules
580            .iter()
581            .filter(|r| {
582                self.options
583                    .selected_rules
584                    .as_ref()
585                    .is_none_or(|sel: &Vec<String>| sel.contains(&r.name))
586            })
587            .map(|r| format!("{} -> {}", r.name, r.output_file))
588            .collect();
589
590        let would_sync: Vec<SyncedFileInfo> = manifest_data
591            .generation
592            .rules
593            .iter()
594            .filter(|r| {
595                self.options
596                    .selected_rules
597                    .as_ref()
598                    .is_none_or(|sel: &Vec<String>| sel.contains(&r.name))
599            })
600            .map(|r| SyncedFileInfo {
601                path: r.output_file.clone(),
602                size_bytes: 0,
603                action: "would create".to_string(),
604                produced_by: r.name.clone(),
605            })
606            .collect();
607
608        if self.options.flags.behavior.verbose || self.options.output_format == OutputFormat::Text {
609            eprintln!("[DRY RUN] Would sync {} files:", would_sync.len());
610            for f in &would_sync {
611                eprintln!("  {} ({})", f.path, f.action);
612            }
613            eprintln!("\nInference rules: {:?}", inference_rules);
614            eprintln!("Generation rules: {:?}", generation_rules);
615        }
616
617        Ok(SyncResult {
618            status: "success".to_string(),
619            files_synced: 0,
620            duration_ms: self.start_time.elapsed().as_millis() as u64,
621            files: would_sync,
622            inference_rules_executed: 0,
623            generation_rules_executed: 0,
624            audit_trail: None,
625            error: None,
626            recovery: None,
627            andon_signal: None,
628        })
629    }
630
631    /// Execute full sync pipeline
632    fn execute_full_sync(
633        &mut self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
634    ) -> Result<SyncResult> {
635        // Determine if progress indicators should be shown
636        // Show by default unless output_format is Json
637        let show_progress = self.options.output_format != OutputFormat::Json;
638
639        let output_directory = self
640            .options
641            .output_dir
642            .clone()
643            .unwrap_or_else(|| manifest_data.generation.output_dir.clone());
644
645        // Create progress indicator
646        let mut progress = ProgressIndicator::new(show_progress);
647
648        // Load incremental cache if enabled
649        progress.start_spinner("Loading manifest and cache...");
650        let cache = if self.options.use_cache {
651            let cache_dir = self
652                .options
653                .cache_dir
654                .clone()
655                .unwrap_or_else(|| output_directory.join(".ggen/cache"));
656            let mut c = IncrementalCache::new(cache_dir);
657            let _ = c.load_cache_state(); // Ignore if first run
658            Some(c)
659        } else {
660            None
661        };
662
663        if self.options.flags.behavior.verbose {
664            progress.clear();
665            eprintln!(
666                "{}",
667                info_message(&format!(
668                    "Manifest: {}",
669                    self.options.manifest_path.display()
670                ))
671            );
672            if cache.is_some() {
673                eprintln!("{}", info_message("Using incremental cache"));
674            }
675        } else {
676            progress
677                .finish_with_message(&format!("Loaded manifest: {}", manifest_data.project.name));
678        }
679
680        // Create pipeline and run
681        let mut pipeline = GenerationPipeline::new(manifest_data.clone(), base_path.to_path_buf());
682
683        // Apply force flag to pipeline if set
684        if self.options.flags.behavior.force {
685            pipeline.set_force_overwrite(true);
686        }
687
688        // Inject LLM service if provided in options
689        if let Some(llm_service) = self.options.llm_service.take() {
690            pipeline.set_llm_service(Some(llm_service));
691        }
692
693        // Run pipeline with progress
694        progress.start_spinner("Loading ontology and running inference...");
695        let state = pipeline.run().map_err(|e| {
696            progress.finish_with_error("Pipeline execution failed");
697            Error::new(&format!(
698                "error[E0003]: Pipeline execution failed\n  |\n  = error: {}\n  = help: Check ontology syntax and SPARQL queries",
699                e
700            ))
701        })?;
702
703        // Show ontology loaded
704        if self.options.flags.behavior.verbose {
705            progress.clear();
706            print_section("Ontology Loaded");
707            eprintln!(
708                "{}",
709                info_message(&format!("{} triples loaded", state.ontology_graph.len()))
710            );
711
712            let inference_rules: Vec<_> = state
713                .executed_rules
714                .iter()
715                .filter(|r| r.rule_type == RuleType::Inference)
716                .collect();
717
718            if !inference_rules.is_empty() {
719                eprintln!();
720                eprintln!("Inference rules executed:");
721                for rule in inference_rules {
722                    eprintln!(
723                        "  {} +{} triples ({})",
724                        rule.name,
725                        rule.triples_added,
726                        format_duration(rule.duration_ms)
727                    );
728                }
729            }
730        } else {
731            progress.finish_with_message(&format!(
732                "Loaded {} triples, ran {} inference rules",
733                state.ontology_graph.len(),
734                state
735                    .executed_rules
736                    .iter()
737                    .filter(|r| r.rule_type == RuleType::Inference)
738                    .count()
739            ));
740        }
741
742        // Generate files with progress bar
743        let generation_count = state
744            .executed_rules
745            .iter()
746            .filter(|r| r.rule_type == RuleType::Generation)
747            .count();
748
749        if show_progress && !self.options.flags.behavior.verbose {
750            eprintln!(
751                "{}",
752                info_message(&format!("Generating {} files...", generation_count))
753            );
754        } else if self.options.flags.behavior.verbose {
755            print_section("Code Generation");
756            for rule in &state.executed_rules {
757                if rule.rule_type == RuleType::Generation {
758                    eprintln!("  {} ({})", rule.name, format_duration(rule.duration_ms));
759                }
760            }
761        }
762
763        // Count rules
764        let inference_count = state
765            .executed_rules
766            .iter()
767            .filter(|r| r.rule_type == RuleType::Inference)
768            .count();
769
770        let generation_count = state
771            .executed_rules
772            .iter()
773            .filter(|r| r.rule_type == RuleType::Generation)
774            .count();
775
776        // Convert generated files
777        let synced_files: Vec<SyncedFileInfo> = state
778            .generated_files
779            .iter()
780            .map(|f| SyncedFileInfo {
781                path: f.path.display().to_string(),
782                size_bytes: f.size_bytes,
783                action: "created".to_string(),
784                produced_by: f.source_rule.clone(),
785            })
786            .collect();
787
788        let files_synced = synced_files.len();
789
790        // Determine audit trail path and write if enabled
791        let audit_path =
792            if self.options.flags.behavior.audit || manifest_data.generation.require_audit_trail {
793                let audit_file_path = base_path.join(&output_directory).join("audit.json");
794
795                // Create audit trail from pipeline state using AuditTrailBuilder
796                let mut builder = crate::codegen::audit::AuditTrailBuilder::new();
797
798                // Record inputs (simplified - would need actual file paths in production)
799                // builder.record_inputs(&self.options.manifest_path, &[], &[])?;
800
801                // Record outputs
802                for file in &state.generated_files {
803                    builder.record_output(&file.path, "", &format!("rule-{}", file.path.display()));
804                }
805
806                // Build the final audit trail
807                let audit_trail = builder.build(true); // validation_passed = true for now
808
809                // Write audit trail to disk using AuditTrailBuilder::write_to
810                crate::codegen::audit::AuditTrailBuilder::write_to(&audit_trail, &audit_file_path)
811                    .map_err(|e| Error::new(&format!("Failed to write audit trail: {}", e)))?;
812
813                Some(audit_file_path.display().to_string())
814            } else {
815                None
816            };
817
818        // Save cache if enabled
819        if let Some(cache) = cache {
820            if let Err(e) = cache.save_cache_state(manifest_data, "", &state.ontology_graph) {
821                if self.options.flags.behavior.verbose {
822                    eprintln!("Warning: Failed to save cache: {}", e);
823                }
824            }
825        }
826
827        // Generate execution proof for determinism verification
828        let mut proof_carrier = ProofCarrier::new();
829        let manifest_content = std::fs::read_to_string(&self.options.manifest_path)
830            .map_err(|e| {
831                Error::new(&format!(
832                    "error[E0006]: Failed to read manifest for proof generation\n  --> {}\n  |\n  = error: {}",
833                    self.options.manifest_path.display(),
834                    e
835                ))
836            })?;
837        let ontology_content =
838            std::fs::read_to_string(base_path.join(&manifest_data.ontology.source))
839                .map_err(|e| {
840                    Error::new(&format!(
841                        "error[E0007]: Failed to read ontology for proof generation\n  --> {}\n  |\n  = error: {}",
842                        base_path.join(&manifest_data.ontology.source).display(),
843                        e
844                    ))
845                })?;
846
847        if let Ok(proof) = proof_carrier.generate_proof(
848            &manifest_content,
849            &ontology_content,
850            &SyncResult {
851                status: "executing".to_string(),
852                files_synced: 0,
853                duration_ms: 0,
854                files: synced_files.clone(),
855                inference_rules_executed: inference_count,
856                generation_rules_executed: generation_count,
857                audit_trail: None,
858                error: None,
859                recovery: None,
860                andon_signal: None,
861            },
862        ) {
863            if self.options.flags.behavior.verbose {
864                eprintln!("Execution proof: {}", proof.execution_id);
865            }
866        }
867
868        let duration = self.start_time.elapsed().as_millis() as u64;
869
870        // Print summary
871        if self.options.output_format == OutputFormat::Text {
872            if self.options.flags.behavior.verbose {
873                // Verbose mode: detailed file listing
874                print_section("Summary");
875                eprintln!(
876                    "{}",
877                    success_message(&format!(
878                        "Synced {} files in {}",
879                        files_synced,
880                        format_duration(duration)
881                    ))
882                );
883                eprintln!();
884                eprintln!("Files generated:");
885                for f in &synced_files {
886                    eprintln!("  {} ({} bytes)", f.path, f.size_bytes);
887                }
888                if let Some(ref audit) = audit_path {
889                    eprintln!();
890                    eprintln!("{}", info_message(&format!("Audit trail: {}", audit)));
891                }
892            } else {
893                // Concise mode: summary only
894                eprintln!();
895                eprintln!(
896                    "{}",
897                    success_message(&format!(
898                        "Generated {} files in {}",
899                        files_synced,
900                        format_duration(duration)
901                    ))
902                );
903
904                // Show summary statistics
905                let total_bytes: usize = synced_files.iter().map(|f| f.size_bytes).sum();
906                eprintln!(
907                    "  {} inference rules, {} generation rules",
908                    inference_count, generation_count
909                );
910                eprintln!("  {} total bytes written", total_bytes);
911                if let Some(ref audit) = audit_path {
912                    eprintln!("  Audit: {}", audit);
913                }
914            }
915        }
916
917        // Save drift state after successful sync
918        self.save_drift_state(base_path, manifest_data, files_synced, duration);
919
920        Ok(SyncResult {
921            status: "success".to_string(),
922            files_synced,
923            duration_ms: duration,
924            files: synced_files,
925            inference_rules_executed: inference_count,
926            generation_rules_executed: generation_count,
927            audit_trail: audit_path,
928            error: None,
929            recovery: None,
930            andon_signal: None,
931        })
932    }
933
934    /// T017-T018: Execute watch mode - monitor files and auto-regenerate
935    fn execute_watch_mode(&self, manifest_path: &Path) -> Result<SyncResult> {
936        use crate::codegen::watch::{collect_watch_paths, FileWatcher};
937        use std::time::Duration;
938
939        // Parse and validate manifest to get watch paths
940        let manifest_data = ManifestParser::parse_and_validate(manifest_path).map_err(|e| {
941            Error::new(&format!(
942                "error[E0001]: Manifest parse error\n  --> {}\n  |\n  = error: {}\n  = help: Check ggen.toml syntax",
943                manifest_path.display(),
944                e
945            ))
946        })?;
947
948        let base_path = manifest_path.parent().unwrap_or(Path::new("."));
949        let watch_paths = collect_watch_paths(manifest_path, &manifest_data, base_path);
950
951        if self.options.flags.behavior.verbose {
952            eprintln!("Starting watch mode...");
953            eprintln!("Monitoring {} paths for changes:", watch_paths.len());
954            for path in &watch_paths {
955                eprintln!("  {}", path.display());
956            }
957            eprintln!("\nPress Ctrl+C to stop.\n");
958        }
959
960        // Initial sync
961        if self.options.flags.behavior.verbose {
962            eprintln!("[Initial] Running sync...");
963        }
964        let mut inner_opts = self.options.clone();
965        inner_opts.flags.mode.watch = false; // Disable watch for recursive call
966        let executor = SyncExecutor::new(inner_opts);
967        let initial_result = executor.execute()?;
968
969        if self.options.flags.behavior.verbose {
970            eprintln!(
971                "[Initial] Synced {} files in {:.3}s\n",
972                initial_result.files_synced,
973                initial_result.duration_ms as f64 / 1000.0
974            );
975        }
976
977        // Start file watcher
978        let watcher = FileWatcher::new(watch_paths.clone());
979        let rx = watcher.start()?;
980
981        // Watch loop
982        loop {
983            match FileWatcher::wait_for_change(&rx, Duration::from_secs(1)) {
984                Ok(Some(event)) => {
985                    if self.options.flags.behavior.verbose {
986                        eprintln!("[Change detected] {}", event.path.display());
987                        eprintln!("[Regenerating] Running sync...");
988                    }
989
990                    // Re-run sync
991                    let mut inner_opts = self.options.clone();
992                    inner_opts.flags.mode.watch = false;
993                    let executor = SyncExecutor::new(inner_opts);
994
995                    match executor.execute() {
996                        Ok(result) => {
997                            if self.options.flags.behavior.verbose {
998                                eprintln!(
999                                    "[Regenerating] Synced {} files in {:.3}s\n",
1000                                    result.files_synced,
1001                                    result.duration_ms as f64 / 1000.0
1002                                );
1003                            }
1004                        }
1005                        Err(e) => {
1006                            eprintln!("[Error] Regeneration failed: {}\n", e);
1007                        }
1008                    }
1009                }
1010                Ok(None) => {
1011                    // Timeout - continue watching
1012                }
1013                Err(e) => {
1014                    return Err(Error::new(&format!("Watch error: {}", e)));
1015                }
1016            }
1017        }
1018    }
1019
1020    /// Check for drift and warn user (non-blocking)
1021    fn check_and_warn_drift(&self, base_path: &Path) {
1022        // Don't check drift in validate-only or watch mode
1023        if self.options.flags.mode.validate_only || self.options.flags.mode.watch {
1024            return;
1025        }
1026
1027        let state_dir = base_path.join(".ggen");
1028        let detector = match DriftDetector::new(&state_dir) {
1029            Ok(d) => d,
1030            Err(_) => return, // Silently skip if detector creation fails
1031        };
1032
1033        // Only check if state file exists
1034        if !detector.has_state() {
1035            return;
1036        }
1037
1038        // Parse manifest to get ontology path
1039        let manifest_data = match ManifestParser::parse(&self.options.manifest_path) {
1040            Ok(m) => m,
1041            Err(_) => return, // Silently skip if manifest parsing fails
1042        };
1043
1044        let ontology_path = base_path.join(&manifest_data.ontology.source);
1045
1046        // Check drift
1047        match detector.check_drift(&ontology_path, &self.options.manifest_path) {
1048            Ok(status) => {
1049                if let Some(warning) = status.warning_message() {
1050                    eprintln!("{}", warning);
1051                }
1052            }
1053            Err(_) => {
1054                // Silently ignore drift check errors
1055            }
1056        }
1057    }
1058
1059    /// Save drift state after successful sync (non-blocking)
1060    fn save_drift_state(
1061        &self, base_path: &Path, manifest_data: &crate::manifest::GgenManifest,
1062        files_synced: usize, duration_ms: u64,
1063    ) {
1064        let state_dir = base_path.join(".ggen");
1065        let detector = match DriftDetector::new(&state_dir) {
1066            Ok(d) => d,
1067            Err(e) => {
1068                if self.options.flags.behavior.verbose {
1069                    eprintln!("Warning: Failed to create drift detector: {}", e);
1070                }
1071                return;
1072            }
1073        };
1074
1075        let ontology_path = base_path.join(&manifest_data.ontology.source);
1076
1077        // Collect imports (if any)
1078        let imports = manifest_data
1079            .ontology
1080            .imports
1081            .iter()
1082            .map(|imp| base_path.join(imp))
1083            .collect();
1084
1085        // Collect inference rule hashes (hash the SPARQL query)
1086        let inference_rules: Vec<(String, String)> = manifest_data
1087            .inference
1088            .rules
1089            .iter()
1090            .map(|rule| {
1091                let hash = crate::pqc::calculate_sha256(rule.construct.as_bytes());
1092                (rule.name.clone(), hash)
1093            })
1094            .collect();
1095
1096        // Save state
1097        if let Err(e) = detector.save_state_with_details(
1098            &ontology_path,
1099            &self.options.manifest_path,
1100            imports,
1101            inference_rules,
1102            files_synced,
1103            duration_ms,
1104        ) {
1105            if self.options.flags.behavior.verbose {
1106                eprintln!("Warning: Failed to save drift state: {}", e);
1107            }
1108        }
1109    }
1110
1111    /// Create a SyncResult for a failure state with machine-readable recovery info
1112    fn create_error_result(&self, error_msg: &str, andon: Option<AndonSignal>) -> SyncResult {
1113        let duration = self.start_time.elapsed().as_millis() as u64;
1114        let (recovery, andon_json) = if let Some(signal) = andon {
1115            let rec = if let AndonSignal::Red(ref critical) = signal {
1116                Some(critical.recovery_steps.join("\n"))
1117            } else {
1118                None
1119            };
1120            (rec, serde_json::to_value(&signal).ok())
1121        } else {
1122            (None, None)
1123        };
1124
1125        SyncResult {
1126            status: "error".to_string(),
1127            files_synced: 0,
1128            duration_ms: duration,
1129            files: Vec::new(),
1130            inference_rules_executed: 0,
1131            generation_rules_executed: 0,
1132            audit_trail: None,
1133            error: Some(error_msg.to_string()),
1134            recovery,
1135            andon_signal: andon_json,
1136        }
1137    }
1138}