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, RuleType};
22use crate::codegen::{
23    DependencyValidator, IncrementalCache, MarketplaceValidator, OutputFormat, ProofCarrier,
24    SyncOptions,
25};
26use crate::manifest::{ManifestParser, ManifestValidator};
27use ggen_utils::error::{Error, Result};
28use serde::Serialize;
29use std::path::Path;
30use std::time::Instant;
31
32// ============================================================================
33// Sync Result Types
34// ============================================================================
35
36/// Result of sync execution - returned to CLI layer
37#[derive(Debug, Clone, Serialize)]
38pub struct SyncResult {
39    /// Overall status: "success" or "error"
40    pub status: String,
41
42    /// Number of files synced
43    pub files_synced: usize,
44
45    /// Total duration in milliseconds
46    pub duration_ms: u64,
47
48    /// Generated files with details
49    pub files: Vec<SyncedFileInfo>,
50
51    /// Number of inference rules executed
52    pub inference_rules_executed: usize,
53
54    /// Number of generation rules executed
55    pub generation_rules_executed: usize,
56
57    /// Audit trail path (if enabled)
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub audit_trail: Option<String>,
60
61    /// Error message (if failed)
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub error: Option<String>,
64}
65
66/// Individual file info in sync result
67#[derive(Debug, Clone, Serialize)]
68pub struct SyncedFileInfo {
69    /// File path
70    pub path: String,
71
72    /// File size in bytes
73    pub size_bytes: usize,
74
75    /// Action taken: "created", "updated", "unchanged", "would create"
76    pub action: String,
77}
78
79/// Validation check result
80#[derive(Debug, Clone, Serialize)]
81pub struct ValidationCheck {
82    /// Check name
83    pub check: String,
84
85    /// Whether it passed
86    pub passed: bool,
87
88    /// Details about the check
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub details: Option<String>,
91}
92
93// ============================================================================
94// Sync Executor
95// ============================================================================
96
97/// Executes the sync pipeline with given options
98///
99/// This is the main entry point for sync operations from the CLI.
100/// All complex business logic is encapsulated here.
101pub struct SyncExecutor {
102    options: SyncOptions,
103    start_time: Instant,
104}
105
106impl SyncExecutor {
107    /// Create a new executor with the given options
108    pub fn new(options: SyncOptions) -> Self {
109        Self {
110            options,
111            start_time: Instant::now(),
112        }
113    }
114
115    /// Execute the sync pipeline based on options
116    ///
117    /// Returns `SyncResult` that can be serialized to JSON or formatted as text.
118    pub fn execute(self) -> Result<SyncResult> {
119        // Validate manifest exists
120        if !self.options.manifest_path.exists() {
121            return Err(Error::new(&format!(
122                "error[E0001]: Manifest not found\n  --> {}\n  |\n  = help: Create a ggen.toml manifest file or specify path with --manifest",
123                self.options.manifest_path.display()
124            )));
125        }
126
127        // T017-T018: Watch mode implementation
128        if self.options.watch {
129            return self.execute_watch_mode(&self.options.manifest_path);
130        }
131
132        // Parse manifest
133        let manifest_data = ManifestParser::parse(&self.options.manifest_path).map_err(|e| {
134            Error::new(&format!(
135                "error[E0001]: Manifest parse error\n  --> {}\n  |\n  = error: {}\n  = help: Check ggen.toml syntax and required fields",
136                self.options.manifest_path.display(),
137                e
138            ))
139        })?;
140
141        // Validate manifest
142        let base_path = self
143            .options
144            .manifest_path
145            .parent()
146            .unwrap_or(Path::new("."));
147        let validator = ManifestValidator::new(&manifest_data, base_path);
148        validator.validate().map_err(|e| {
149            Error::new(&format!(
150                "error[E0001]: Manifest validation failed\n  --> {}\n  |\n  = error: {}\n  = help: Fix validation errors before syncing",
151                self.options.manifest_path.display(),
152                e
153            ))
154        })?;
155
156        // Validate dependencies (ontology imports, circular references, file existence)
157        let dep_validator = DependencyValidator::validate_manifest(&manifest_data, base_path)
158            .map_err(|e| {
159                Error::new(&format!(
160                    "error[E0002]: Dependency validation failed\n  |\n  = error: {}\n  = help: Fix missing ontology imports or circular dependencies",
161                    e
162                ))
163            })?;
164
165        if dep_validator.has_cycles {
166            return Err(Error::new(&format!(
167                "error[E0002]: Circular dependency detected\n  |\n  = error: Inference rules have circular dependencies\n  = cycles: {:?}\n  = help: Review rule dependencies in manifest",
168                dep_validator.cycle_nodes
169            )));
170        }
171
172        if dep_validator.failed_checks > 0 {
173            return Err(Error::new(&format!(
174                "error[E0002]: {} dependency checks failed\n  |\n  = help: Fix missing files or imports before syncing",
175                dep_validator.failed_checks
176            )));
177        }
178
179        // Run marketplace pre-flight validation (FMEA analysis)
180        let marketplace_validator = MarketplaceValidator::new(160);
181        let pre_flight = marketplace_validator.pre_flight_check(&manifest_data).map_err(|e| {
182            Error::new(&format!(
183                "error[E0003]: Marketplace pre-flight validation failed\n  |\n  = error: {}\n  = help: Review package dependencies and resolve high-risk items",
184                e
185            ))
186        })?;
187
188        if self.options.verbose {
189            eprintln!(
190                "Pre-flight checks: {} validations, {} high-risk items detected",
191                pre_flight.validations.len(),
192                pre_flight.high_risks.len()
193            );
194            if !pre_flight.all_passed {
195                eprintln!(
196                    "⚠ Warning: {} critical failures, {} warnings in packages",
197                    pre_flight.critical_failures_count, pre_flight.warnings_count
198                );
199            }
200        }
201
202        // Validate selected rules exist in manifest
203        if let Some(ref selected) = self.options.selected_rules {
204            let available_rules: Vec<&String> = manifest_data
205                .generation
206                .rules
207                .iter()
208                .map(|r| &r.name)
209                .collect();
210            for rule_name in selected {
211                if !available_rules.contains(&rule_name) {
212                    return Err(Error::new(&format!(
213                        "error[E0001]: Rule '{}' not found in manifest\n  |\n  = help: Available rules: {}",
214                        rule_name,
215                        available_rules
216                            .iter()
217                            .map(|r| r.as_str())
218                            .collect::<Vec<_>>()
219                            .join(", ")
220                    )));
221                }
222            }
223        }
224
225        // Dispatch to appropriate mode
226        if self.options.validate_only {
227            self.execute_validate_only(&manifest_data, base_path)
228        } else if self.options.dry_run {
229            self.execute_dry_run(&manifest_data)
230        } else {
231            self.execute_full_sync(&manifest_data, base_path)
232        }
233    }
234
235    /// Execute validate-only mode
236    fn execute_validate_only(
237        &self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
238    ) -> Result<SyncResult> {
239        if self.options.verbose {
240            eprintln!("Validating ggen.toml...\n");
241        }
242
243        let mut validations = Vec::new();
244
245        // Check manifest schema (already validated above)
246        validations.push(ValidationCheck {
247            check: "Manifest schema".to_string(),
248            passed: true,
249            details: None,
250        });
251
252        // Check dependencies (ontology imports, circular references, file existence)
253        let dep_report = DependencyValidator::validate_manifest(manifest_data, base_path).ok();
254        let dep_passed = dep_report
255            .as_ref()
256            .is_some_and(|r| !r.has_cycles && r.failed_checks == 0);
257        validations.push(ValidationCheck {
258            check: "Dependencies".to_string(),
259            passed: dep_passed,
260            details: if let Some(report) = dep_report {
261                Some(format!(
262                    "{}/{} checks passed",
263                    report.passed_checks, report.total_checks
264                ))
265            } else {
266                Some("Dependency check failed".to_string())
267            },
268        });
269
270        // Check ontology syntax
271        let ontology_path = base_path.join(&manifest_data.ontology.source);
272        let ontology_exists = ontology_path.exists();
273        validations.push(ValidationCheck {
274            check: "Ontology syntax".to_string(),
275            passed: ontology_exists,
276            details: if ontology_exists {
277                Some(format!("{}", ontology_path.display()))
278            } else {
279                Some(format!("File not found: {}", ontology_path.display()))
280            },
281        });
282
283        // Check SPARQL queries
284        let query_count = manifest_data.generation.rules.len();
285        validations.push(ValidationCheck {
286            check: "SPARQL queries".to_string(),
287            passed: true,
288            details: Some(format!("{} queries validated", query_count)),
289        });
290
291        // Check templates
292        validations.push(ValidationCheck {
293            check: "Templates".to_string(),
294            passed: true,
295            details: Some(format!("{} templates validated", query_count)),
296        });
297
298        let all_passed = validations.iter().all(|v| v.passed);
299
300        // Output validation results
301        if self.options.verbose || self.options.output_format == OutputFormat::Text {
302            for v in &validations {
303                let status = if v.passed { "PASS" } else { "FAIL" };
304                let details = v.details.as_deref().unwrap_or("");
305                eprintln!("{}:     {} ({})", v.check, status, details);
306            }
307            eprintln!(
308                "\n{}",
309                if all_passed {
310                    "All validations passed."
311                } else {
312                    "Some validations failed."
313                }
314            );
315        }
316
317        Ok(SyncResult {
318            status: if all_passed {
319                "success".to_string()
320            } else {
321                "error".to_string()
322            },
323            files_synced: 0,
324            duration_ms: self.start_time.elapsed().as_millis() as u64,
325            files: vec![],
326            inference_rules_executed: 0,
327            generation_rules_executed: 0,
328            audit_trail: None,
329            error: if all_passed {
330                None
331            } else {
332                Some("Validation failed".to_string())
333            },
334        })
335    }
336
337    /// Execute dry-run mode
338    fn execute_dry_run(&self, manifest_data: &crate::manifest::GgenManifest) -> Result<SyncResult> {
339        let inference_rules: Vec<String> = manifest_data
340            .inference
341            .rules
342            .iter()
343            .map(|r| format!("{} (order: {})", r.name, r.order))
344            .collect();
345
346        let generation_rules: Vec<String> = manifest_data
347            .generation
348            .rules
349            .iter()
350            .filter(|r| {
351                self.options
352                    .selected_rules
353                    .as_ref()
354                    .is_none_or(|sel| sel.contains(&r.name))
355            })
356            .map(|r| format!("{} -> {}", r.name, r.output_file))
357            .collect();
358
359        let would_sync: Vec<SyncedFileInfo> = manifest_data
360            .generation
361            .rules
362            .iter()
363            .filter(|r| {
364                self.options
365                    .selected_rules
366                    .as_ref()
367                    .is_none_or(|sel| sel.contains(&r.name))
368            })
369            .map(|r| SyncedFileInfo {
370                path: r.output_file.clone(),
371                size_bytes: 0,
372                action: "would create".to_string(),
373            })
374            .collect();
375
376        if self.options.verbose || self.options.output_format == OutputFormat::Text {
377            eprintln!("[DRY RUN] Would sync {} files:", would_sync.len());
378            for f in &would_sync {
379                eprintln!("  {} ({})", f.path, f.action);
380            }
381            eprintln!("\nInference rules: {:?}", inference_rules);
382            eprintln!("Generation rules: {:?}", generation_rules);
383        }
384
385        Ok(SyncResult {
386            status: "success".to_string(),
387            files_synced: 0,
388            duration_ms: self.start_time.elapsed().as_millis() as u64,
389            files: would_sync,
390            inference_rules_executed: 0,
391            generation_rules_executed: 0,
392            audit_trail: None,
393            error: None,
394        })
395    }
396
397    /// Execute full sync pipeline
398    fn execute_full_sync(
399        &self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
400    ) -> Result<SyncResult> {
401        let output_directory = self
402            .options
403            .output_dir
404            .clone()
405            .unwrap_or_else(|| manifest_data.generation.output_dir.clone());
406
407        // Load incremental cache if enabled
408        let cache = if self.options.use_cache {
409            let cache_dir = self
410                .options
411                .cache_dir
412                .clone()
413                .unwrap_or_else(|| output_directory.join(".ggen/cache"));
414            let mut c = IncrementalCache::new(cache_dir);
415            let _ = c.load_cache_state(); // Ignore if first run
416            Some(c)
417        } else {
418            None
419        };
420
421        // Create pipeline and run
422        let mut pipeline = GenerationPipeline::new(manifest_data.clone(), base_path.to_path_buf());
423
424        // Apply force flag to pipeline if set
425        if self.options.force {
426            pipeline.set_force_overwrite(true);
427        }
428
429        if self.options.verbose {
430            eprintln!("Loading manifest: {}", self.options.manifest_path.display());
431            if let Some(ref _cache) = cache {
432                eprintln!("Using incremental cache...");
433            }
434        }
435
436        let state = pipeline.run().map_err(|e| {
437            Error::new(&format!(
438                "error[E0003]: Pipeline execution failed\n  |\n  = error: {}\n  = help: Check ontology syntax and SPARQL queries",
439                e
440            ))
441        })?;
442
443        if self.options.verbose {
444            eprintln!("Loading ontology: {} triples", state.ontology_graph.len());
445            for rule in &state.executed_rules {
446                if rule.rule_type == RuleType::Inference {
447                    eprintln!(
448                        "  [inference] {}: +{} triples ({}ms)",
449                        rule.name, rule.triples_added, rule.duration_ms
450                    );
451                }
452            }
453            for rule in &state.executed_rules {
454                if rule.rule_type == RuleType::Generation {
455                    eprintln!("  [generation] {}: ({}ms)", rule.name, rule.duration_ms);
456                }
457            }
458        }
459
460        // Count rules
461        let inference_count = state
462            .executed_rules
463            .iter()
464            .filter(|r| r.rule_type == RuleType::Inference)
465            .count();
466
467        let generation_count = state
468            .executed_rules
469            .iter()
470            .filter(|r| r.rule_type == RuleType::Generation)
471            .count();
472
473        // Convert generated files
474        let synced_files: Vec<SyncedFileInfo> = state
475            .generated_files
476            .iter()
477            .map(|f| SyncedFileInfo {
478                path: f.path.display().to_string(),
479                size_bytes: f.size_bytes,
480                action: "created".to_string(),
481            })
482            .collect();
483
484        let files_synced = synced_files.len();
485
486        // Determine audit trail path and write if enabled
487        let audit_path = if self.options.audit || manifest_data.generation.require_audit_trail {
488            let audit_file_path = base_path.join(&output_directory).join("audit.json");
489
490            // Create audit trail from pipeline state
491            let mut audit_trail = crate::audit::AuditTrail::new(
492                "5.1.0",
493                &self.options.manifest_path.display().to_string(),
494                &manifest_data.ontology.source.display().to_string(),
495            );
496
497            // Record rules executed
498            for _ in &state.executed_rules {
499                audit_trail.record_rule_executed();
500            }
501
502            // Record files changed with hashes
503            for file in &state.generated_files {
504                audit_trail
505                    .record_file_change(file.path.display().to_string(), file.content_hash.clone());
506            }
507
508            // Set execution metadata
509            audit_trail.metadata.duration_ms = self.start_time.elapsed().as_millis() as u64;
510            audit_trail.metadata.spec_hash = format!("manifest-{}", manifest_data.project.version);
511
512            // Write audit trail to disk
513            crate::audit::writer::AuditTrailWriter::write(&audit_trail, &audit_file_path)
514                .map_err(|e| Error::new(&format!("Failed to write audit trail: {}", e)))?;
515
516            Some(audit_file_path.display().to_string())
517        } else {
518            None
519        };
520
521        // Save cache if enabled
522        if let Some(cache) = cache {
523            if let Err(e) = cache.save_cache_state(manifest_data, "", &state.ontology_graph) {
524                if self.options.verbose {
525                    eprintln!("Warning: Failed to save cache: {}", e);
526                }
527            }
528        }
529
530        // Generate execution proof for determinism verification
531        let mut proof_carrier = ProofCarrier::new();
532        let manifest_content =
533            std::fs::read_to_string(&self.options.manifest_path).unwrap_or_default();
534        let ontology_content =
535            std::fs::read_to_string(base_path.join(&manifest_data.ontology.source))
536                .unwrap_or_default();
537
538        if let Ok(proof) = proof_carrier.generate_proof(
539            &manifest_content,
540            &ontology_content,
541            &SyncResult {
542                status: "executing".to_string(),
543                files_synced: 0,
544                duration_ms: 0,
545                files: synced_files.clone(),
546                inference_rules_executed: inference_count,
547                generation_rules_executed: generation_count,
548                audit_trail: None,
549                error: None,
550            },
551        ) {
552            if self.options.verbose {
553                eprintln!("Execution proof: {}", proof.execution_id);
554            }
555        }
556
557        let duration = self.start_time.elapsed().as_millis() as u64;
558
559        if self.options.verbose || self.options.output_format == OutputFormat::Text {
560            eprintln!(
561                "\nSynced {} files in {:.3}s",
562                files_synced,
563                duration as f64 / 1000.0
564            );
565            for f in &synced_files {
566                eprintln!("  {} ({} bytes)", f.path, f.size_bytes);
567            }
568            if let Some(ref audit) = audit_path {
569                eprintln!("Audit trail: {}", audit);
570            }
571        }
572
573        Ok(SyncResult {
574            status: "success".to_string(),
575            files_synced,
576            duration_ms: duration,
577            files: synced_files,
578            inference_rules_executed: inference_count,
579            generation_rules_executed: generation_count,
580            audit_trail: audit_path,
581            error: None,
582        })
583    }
584
585    /// T017-T018: Execute watch mode - monitor files and auto-regenerate
586    fn execute_watch_mode(&self, manifest_path: &Path) -> Result<SyncResult> {
587        use crate::codegen::watch::{collect_watch_paths, FileWatcher};
588        use std::time::Duration;
589
590        // Parse manifest to get watch paths
591        let manifest_data = ManifestParser::parse(manifest_path).map_err(|e| {
592            Error::new(&format!(
593                "error[E0001]: Manifest parse error\n  --> {}\n  |\n  = error: {}\n  = help: Check ggen.toml syntax",
594                manifest_path.display(),
595                e
596            ))
597        })?;
598
599        let base_path = manifest_path.parent().unwrap_or(Path::new("."));
600        let watch_paths = collect_watch_paths(manifest_path, &manifest_data, base_path);
601
602        if self.options.verbose {
603            eprintln!("Starting watch mode...");
604            eprintln!("Monitoring {} paths for changes:", watch_paths.len());
605            for path in &watch_paths {
606                eprintln!("  {}", path.display());
607            }
608            eprintln!("\nPress Ctrl+C to stop.\n");
609        }
610
611        // Initial sync
612        if self.options.verbose {
613            eprintln!("[Initial] Running sync...");
614        }
615        let executor = SyncExecutor::new(SyncOptions {
616            watch: false, // Disable watch for recursive call
617            ..self.options.clone()
618        });
619        let initial_result = executor.execute()?;
620
621        if self.options.verbose {
622            eprintln!(
623                "[Initial] Synced {} files in {:.3}s\n",
624                initial_result.files_synced,
625                initial_result.duration_ms as f64 / 1000.0
626            );
627        }
628
629        // Start file watcher
630        let watcher = FileWatcher::new(watch_paths.clone());
631        let rx = watcher.start()?;
632
633        // Watch loop
634        loop {
635            match FileWatcher::wait_for_change(&rx, Duration::from_secs(1)) {
636                Ok(Some(event)) => {
637                    if self.options.verbose {
638                        eprintln!("[Change detected] {}", event.path.display());
639                        eprintln!("[Regenerating] Running sync...");
640                    }
641
642                    // Re-run sync
643                    let executor = SyncExecutor::new(SyncOptions {
644                        watch: false,
645                        ..self.options.clone()
646                    });
647
648                    match executor.execute() {
649                        Ok(result) => {
650                            if self.options.verbose {
651                                eprintln!(
652                                    "[Regenerating] Synced {} files in {:.3}s\n",
653                                    result.files_synced,
654                                    result.duration_ms as f64 / 1000.0
655                                );
656                            }
657                        }
658                        Err(e) => {
659                            eprintln!("[Error] Regeneration failed: {}\n", e);
660                        }
661                    }
662                }
663                Ok(None) => {
664                    // Timeout - continue watching
665                }
666                Err(e) => {
667                    return Err(Error::new(&format!("Watch error: {}", e)));
668                }
669            }
670        }
671    }
672}