1use crate::codegen::pipeline::{GenerationPipeline, LlmService, RuleType};
22use crate::codegen::ux::{
23 format_duration, info_message, print_section, success_message, warning_message,
24 ProgressIndicator,
25};
26use crate::codegen::{DependencyValidator, IncrementalCache, MarketplaceValidator, ProofCarrier};
27use crate::drift::DriftDetector;
28use crate::manifest::{ManifestParser, ManifestValidator};
29use crate::poka_yoke::QualityGateRunner;
30use crate::utils::error::{Error, Result};
31use crate::validation::PreFlightValidator;
32use serde::Serialize;
33use std::path::{Path, PathBuf};
34use std::time::Instant;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
42pub enum OutputFormat {
43 #[default]
45 Text,
46 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
71pub struct SyncOptions {
73 pub manifest_path: PathBuf,
75
76 pub output_dir: Option<PathBuf>,
78
79 pub cache_dir: Option<PathBuf>,
81
82 pub verbose: bool,
84
85 pub output_format: OutputFormat,
87
88 pub validate_only: bool,
90
91 pub dry_run: bool,
93
94 pub watch: bool,
96
97 pub selected_rules: Option<Vec<String>>,
99
100 pub use_cache: bool,
102
103 pub force: bool,
105
106 pub audit: bool,
108
109 pub a2a_stage: Option<String>,
112
113 pub ontology_path: Option<PathBuf>,
115
116 pub llm_service: Option<Box<dyn LlmService>>,
120
121 pub timeout_ms: Option<u64>,
123}
124
125impl Default for SyncOptions {
126 fn default() -> Self {
127 Self {
128 manifest_path: PathBuf::from("ggen.toml"),
129 output_dir: None,
130 cache_dir: None,
131 verbose: false,
132 output_format: OutputFormat::default(),
133 validate_only: false,
134 dry_run: false,
135 watch: false,
136 selected_rules: None,
137 use_cache: true,
138 force: false,
139 audit: false,
140 a2a_stage: None,
141 ontology_path: None,
142 llm_service: None,
143 timeout_ms: None,
144 }
145 }
146}
147
148impl std::fmt::Debug for SyncOptions {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("SyncOptions")
151 .field("manifest_path", &self.manifest_path)
152 .field("output_dir", &self.output_dir)
153 .field("cache_dir", &self.cache_dir)
154 .field("verbose", &self.verbose)
155 .field("output_format", &self.output_format)
156 .field("validate_only", &self.validate_only)
157 .field("dry_run", &self.dry_run)
158 .field("watch", &self.watch)
159 .field("selected_rules", &self.selected_rules)
160 .field("use_cache", &self.use_cache)
161 .field("force", &self.force)
162 .field("audit", &self.audit)
163 .field("a2a_stage", &self.a2a_stage)
164 .field("ontology_path", &self.ontology_path)
165 .field("llm_service", &"<dyn LlmService>")
166 .field("timeout_ms", &self.timeout_ms)
167 .finish()
168 }
169}
170
171impl Clone for SyncOptions {
172 fn clone(&self) -> Self {
173 Self {
174 manifest_path: self.manifest_path.clone(),
175 output_dir: self.output_dir.clone(),
176 cache_dir: self.cache_dir.clone(),
177 verbose: self.verbose,
178 output_format: self.output_format,
179 validate_only: self.validate_only,
180 dry_run: self.dry_run,
181 watch: self.watch,
182 selected_rules: self.selected_rules.clone(),
183 use_cache: self.use_cache,
184 force: self.force,
185 audit: self.audit,
186 a2a_stage: self.a2a_stage.clone(),
187 ontology_path: self.ontology_path.clone(),
188 timeout_ms: self.timeout_ms,
189 llm_service: None, }
191 }
192}
193
194impl SyncOptions {
195 pub fn new() -> Self {
197 Self::default()
198 }
199}
200
201#[derive(Debug, Clone, Serialize)]
207pub struct SyncResult {
208 pub status: String,
210
211 pub files_synced: usize,
213
214 pub duration_ms: u64,
216
217 pub files: Vec<SyncedFileInfo>,
219
220 pub inference_rules_executed: usize,
222
223 pub generation_rules_executed: usize,
225
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub audit_trail: Option<String>,
229
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub error: Option<String>,
233}
234
235#[derive(Debug, Clone, Serialize)]
237pub struct SyncedFileInfo {
238 pub path: String,
240
241 pub size_bytes: usize,
243
244 pub action: String,
246}
247
248#[derive(Debug, Clone, Serialize)]
250pub struct ValidationCheck {
251 pub check: String,
253
254 pub passed: bool,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub details: Option<String>,
260}
261
262pub struct SyncExecutor {
271 options: SyncOptions,
272 start_time: Instant,
273}
274
275impl SyncExecutor {
276 pub fn new(options: SyncOptions) -> Self {
278 Self {
279 options,
280 start_time: Instant::now(),
281 }
282 }
283
284 pub fn with_llm_service(mut self, service: Option<Box<dyn LlmService>>) -> Self {
289 self.options.llm_service = service;
290 self
291 }
292
293 pub fn execute(mut self) -> Result<SyncResult> {
297 let base_path = self
299 .options
300 .manifest_path
301 .parent()
302 .unwrap_or(Path::new("."));
303
304 let preflight = PreFlightValidator::for_sync(base_path)
305 .with_llm_check(false) .with_template_check(false) .with_git_check(false);
308
309 if let Err(e) = preflight.validate(None) {
311 if self.options.verbose {
312 eprintln!("{}", warning_message(&format!("Pre-flight warning: {}", e)));
313 }
314 } else if self.options.verbose {
315 eprintln!("{}", success_message("Pre-flight checks passed"));
316 }
317
318 if !self.options.manifest_path.exists() {
320 return Err(Error::new(&format!(
321 "error[E0001]: Manifest not found\n --> {}\n |\n = help: Create a ggen.toml manifest file or specify path with --manifest",
322 self.options.manifest_path.display()
323 )));
324 }
325
326 self.check_and_warn_drift(base_path);
328
329 if self.options.watch {
331 return self.execute_watch_mode(&self.options.manifest_path);
332 }
333
334 let manifest_data = ManifestParser::parse(&self.options.manifest_path).map_err(|e| {
336 Error::new(&format!(
337 "error[E0001]: Manifest parse error\n --> {}\n |\n = error: {}\n = help: Check ggen.toml syntax and required fields",
338 self.options.manifest_path.display(),
339 e
340 ))
341 })?;
342
343 let base_path: PathBuf = self
345 .options
346 .manifest_path
347 .parent()
348 .unwrap_or(Path::new("."))
349 .to_path_buf();
350 let validator = ManifestValidator::new(&manifest_data, &base_path);
351 validator.validate().map_err(|e| {
352 Error::new(&format!(
353 "error[E0001]: Manifest validation failed\n --> {}\n |\n = error: {}\n = help: Fix validation errors before syncing",
354 self.options.manifest_path.display(),
355 e
356 ))
357 })?;
358
359 let dep_validator = DependencyValidator::validate_manifest(&manifest_data, &base_path)
361 .map_err(|e| {
362 Error::new(&format!(
363 "error[E0002]: Dependency validation failed\n |\n = error: {}\n = help: Fix missing ontology imports or circular dependencies",
364 e
365 ))
366 })?;
367
368 if dep_validator.has_cycles {
369 return Err(Error::new(&format!(
370 "error[E0002]: Circular dependency detected\n |\n = error: Inference rules have circular dependencies\n = cycles: {:?}\n = help: Review rule dependencies in manifest",
371 dep_validator.cycle_nodes
372 )));
373 }
374
375 if dep_validator.failed_checks > 0 {
376 return Err(Error::new(&format!(
377 "error[E0002]: {} dependency validation checks failed\n |\n = help: Common issues:\n = 1. Query file not found: Check ontology.source and ontology.imports paths\n = 2. Template file not found: Check generation.rules[].template paths\n = 3. Import cycle: Check if imported files reference each other\n = help: Run 'ggen validate' for detailed dependency analysis",
378 dep_validator.failed_checks
379 )));
380 }
381
382 let gate_runner = QualityGateRunner::new();
384 gate_runner.run_all(&manifest_data, &base_path).map_err(|e| {
385 Error::new(&format!(
386 "error[E0004]: Quality gate validation failed\n |\n = error: {}\n = help: Fix validation errors before syncing",
387 e
388 ))
389 })?;
390
391 let marketplace_validator = MarketplaceValidator::new(160);
393 let pre_flight = marketplace_validator.pre_flight_check(&manifest_data).map_err(|e| {
394 Error::new(&format!(
395 "error[E0003]: Marketplace pre-flight validation failed\n |\n = error: {}\n = help: Review package dependencies and resolve high-risk items",
396 e
397 ))
398 })?;
399
400 if self.options.verbose {
401 eprintln!(
402 "Pre-flight checks: {} validations, {} high-risk items detected",
403 pre_flight.validations.len(),
404 pre_flight.high_risks.len()
405 );
406 if !pre_flight.all_passed {
407 eprintln!(
408 "⚠ Warning: {} critical failures, {} warnings in packages",
409 pre_flight.critical_failures_count, pre_flight.warnings_count
410 );
411 }
412 }
413
414 if let Some(ref selected) = self.options.selected_rules {
416 let available_rules: Vec<&String> = manifest_data
417 .generation
418 .rules
419 .iter()
420 .map(|r| &r.name)
421 .collect();
422 for rule_name in selected {
423 if !available_rules.contains(&rule_name) {
424 return Err(Error::new(&format!(
425 "error[E0001]: Rule '{}' not found in manifest\n |\n = help: Available rules: {}",
426 rule_name,
427 available_rules
428 .iter()
429 .map(|r| r.as_str())
430 .collect::<Vec<_>>()
431 .join(", ")
432 )));
433 }
434 }
435 }
436
437 if self.options.validate_only {
439 self.execute_validate_only(&manifest_data, &base_path)
440 } else if self.options.dry_run {
441 self.execute_dry_run(&manifest_data)
442 } else {
443 self.execute_full_sync(&manifest_data, &base_path)
444 }
445 }
446
447 fn execute_validate_only(
449 &self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
450 ) -> Result<SyncResult> {
451 if self.options.verbose {
452 eprintln!("Validating ggen.toml...\n");
453 }
454
455 let mut validations = Vec::new();
456
457 validations.push(ValidationCheck {
459 check: "Manifest schema".to_string(),
460 passed: true,
461 details: None,
462 });
463
464 let dep_report = DependencyValidator::validate_manifest(manifest_data, base_path).ok();
466 let dep_passed = dep_report
467 .as_ref()
468 .is_some_and(|r| !r.has_cycles && r.failed_checks == 0);
469 validations.push(ValidationCheck {
470 check: "Dependencies".to_string(),
471 passed: dep_passed,
472 details: if let Some(report) = dep_report {
473 Some(format!(
474 "{}/{} checks passed",
475 report.passed_checks, report.total_checks
476 ))
477 } else {
478 Some("Dependency check failed".to_string())
479 },
480 });
481
482 let ontology_path = base_path.join(&manifest_data.ontology.source);
484 let ontology_exists = ontology_path.exists();
485 validations.push(ValidationCheck {
486 check: "Ontology syntax".to_string(),
487 passed: ontology_exists,
488 details: if ontology_exists {
489 Some(format!("{}", ontology_path.display()))
490 } else {
491 Some(format!("File not found: {}", ontology_path.display()))
492 },
493 });
494
495 let query_count = manifest_data.generation.rules.len();
497 validations.push(ValidationCheck {
498 check: "SPARQL queries".to_string(),
499 passed: true,
500 details: Some(format!("{} queries validated", query_count)),
501 });
502
503 validations.push(ValidationCheck {
505 check: "Templates".to_string(),
506 passed: true,
507 details: Some(format!("{} templates validated", query_count)),
508 });
509
510 let all_passed = validations.iter().all(|v| v.passed);
511
512 if self.options.verbose || self.options.output_format == OutputFormat::Text {
514 for v in &validations {
515 let status = if v.passed { "PASS" } else { "FAIL" };
516 let details = v.details.as_deref().unwrap_or("");
517 eprintln!("{}: {} ({})", v.check, status, details);
518 }
519 eprintln!(
520 "\n{}",
521 if all_passed {
522 "All validations passed."
523 } else {
524 "Some validations failed."
525 }
526 );
527 }
528
529 Ok(SyncResult {
530 status: if all_passed {
531 "success".to_string()
532 } else {
533 "error".to_string()
534 },
535 files_synced: 0,
536 duration_ms: self.start_time.elapsed().as_millis() as u64,
537 files: vec![],
538 inference_rules_executed: 0,
539 generation_rules_executed: 0,
540 audit_trail: None,
541 error: if all_passed {
542 None
543 } else {
544 Some("Validation failed".to_string())
545 },
546 })
547 }
548
549 fn execute_dry_run(&self, manifest_data: &crate::manifest::GgenManifest) -> Result<SyncResult> {
551 let inference_rules: Vec<String> = manifest_data
552 .inference
553 .rules
554 .iter()
555 .map(|r| format!("{} (order: {})", r.name, r.order))
556 .collect();
557
558 let generation_rules: Vec<String> = manifest_data
559 .generation
560 .rules
561 .iter()
562 .filter(|r| {
563 self.options
564 .selected_rules
565 .as_ref()
566 .is_none_or(|sel: &Vec<String>| sel.contains(&r.name))
567 })
568 .map(|r| format!("{} -> {}", r.name, r.output_file))
569 .collect();
570
571 let would_sync: Vec<SyncedFileInfo> = manifest_data
572 .generation
573 .rules
574 .iter()
575 .filter(|r| {
576 self.options
577 .selected_rules
578 .as_ref()
579 .is_none_or(|sel: &Vec<String>| sel.contains(&r.name))
580 })
581 .map(|r| SyncedFileInfo {
582 path: r.output_file.clone(),
583 size_bytes: 0,
584 action: "would create".to_string(),
585 })
586 .collect();
587
588 if self.options.verbose || self.options.output_format == OutputFormat::Text {
589 eprintln!("[DRY RUN] Would sync {} files:", would_sync.len());
590 for f in &would_sync {
591 eprintln!(" {} ({})", f.path, f.action);
592 }
593 eprintln!("\nInference rules: {:?}", inference_rules);
594 eprintln!("Generation rules: {:?}", generation_rules);
595 }
596
597 Ok(SyncResult {
598 status: "success".to_string(),
599 files_synced: 0,
600 duration_ms: self.start_time.elapsed().as_millis() as u64,
601 files: would_sync,
602 inference_rules_executed: 0,
603 generation_rules_executed: 0,
604 audit_trail: None,
605 error: None,
606 })
607 }
608
609 fn execute_full_sync(
611 &mut self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
612 ) -> Result<SyncResult> {
613 let show_progress = self.options.output_format != OutputFormat::Json;
616
617 let output_directory = self
618 .options
619 .output_dir
620 .clone()
621 .unwrap_or_else(|| manifest_data.generation.output_dir.clone());
622
623 let mut progress = ProgressIndicator::new(show_progress);
625
626 progress.start_spinner("Loading manifest and cache...");
628 let cache = if self.options.use_cache {
629 let cache_dir = self
630 .options
631 .cache_dir
632 .clone()
633 .unwrap_or_else(|| output_directory.join(".ggen/cache"));
634 let mut c = IncrementalCache::new(cache_dir);
635 let _ = c.load_cache_state(); Some(c)
637 } else {
638 None
639 };
640
641 if self.options.verbose {
642 progress.clear();
643 eprintln!(
644 "{}",
645 info_message(&format!(
646 "Manifest: {}",
647 self.options.manifest_path.display()
648 ))
649 );
650 if cache.is_some() {
651 eprintln!("{}", info_message("Using incremental cache"));
652 }
653 } else {
654 progress
655 .finish_with_message(&format!("Loaded manifest: {}", manifest_data.project.name));
656 }
657
658 let mut pipeline = GenerationPipeline::new(manifest_data.clone(), base_path.to_path_buf());
660
661 if self.options.force {
663 pipeline.set_force_overwrite(true);
664 }
665
666 if let Some(llm_service) = self.options.llm_service.take() {
668 pipeline.set_llm_service(Some(llm_service));
669 }
670
671 progress.start_spinner("Loading ontology and running inference...");
673 let state = pipeline.run().map_err(|e| {
674 progress.finish_with_error("Pipeline execution failed");
675 Error::new(&format!(
676 "error[E0003]: Pipeline execution failed\n |\n = error: {}\n = help: Check ontology syntax and SPARQL queries",
677 e
678 ))
679 })?;
680
681 if self.options.verbose {
683 progress.clear();
684 print_section("Ontology Loaded");
685 eprintln!(
686 "{}",
687 info_message(&format!("{} triples loaded", state.ontology_graph.len()))
688 );
689
690 let inference_rules: Vec<_> = state
691 .executed_rules
692 .iter()
693 .filter(|r| r.rule_type == RuleType::Inference)
694 .collect();
695
696 if !inference_rules.is_empty() {
697 eprintln!();
698 eprintln!("Inference rules executed:");
699 for rule in inference_rules {
700 eprintln!(
701 " {} +{} triples ({})",
702 rule.name,
703 rule.triples_added,
704 format_duration(rule.duration_ms)
705 );
706 }
707 }
708 } else {
709 progress.finish_with_message(&format!(
710 "Loaded {} triples, ran {} inference rules",
711 state.ontology_graph.len(),
712 state
713 .executed_rules
714 .iter()
715 .filter(|r| r.rule_type == RuleType::Inference)
716 .count()
717 ));
718 }
719
720 let generation_count = state
722 .executed_rules
723 .iter()
724 .filter(|r| r.rule_type == RuleType::Generation)
725 .count();
726
727 if show_progress && !self.options.verbose {
728 eprintln!(
729 "{}",
730 info_message(&format!("Generating {} files...", generation_count))
731 );
732 } else if self.options.verbose {
733 print_section("Code Generation");
734 for rule in &state.executed_rules {
735 if rule.rule_type == RuleType::Generation {
736 eprintln!(" {} ({})", rule.name, format_duration(rule.duration_ms));
737 }
738 }
739 }
740
741 let inference_count = state
743 .executed_rules
744 .iter()
745 .filter(|r| r.rule_type == RuleType::Inference)
746 .count();
747
748 let generation_count = state
749 .executed_rules
750 .iter()
751 .filter(|r| r.rule_type == RuleType::Generation)
752 .count();
753
754 let synced_files: Vec<SyncedFileInfo> = state
756 .generated_files
757 .iter()
758 .map(|f| SyncedFileInfo {
759 path: f.path.display().to_string(),
760 size_bytes: f.size_bytes,
761 action: "created".to_string(),
762 })
763 .collect();
764
765 let files_synced = synced_files.len();
766
767 let audit_path = if self.options.audit || manifest_data.generation.require_audit_trail {
769 let audit_file_path = base_path.join(&output_directory).join("audit.json");
770
771 let mut builder = crate::codegen::audit::AuditTrailBuilder::new();
773
774 for file in &state.generated_files {
779 builder.record_output(&file.path, "", &format!("rule-{}", file.path.display()));
780 }
781
782 let audit_trail = builder.build(true); crate::codegen::audit::AuditTrailBuilder::write_to(&audit_trail, &audit_file_path)
787 .map_err(|e| Error::new(&format!("Failed to write audit trail: {}", e)))?;
788
789 Some(audit_file_path.display().to_string())
790 } else {
791 None
792 };
793
794 if let Some(cache) = cache {
796 if let Err(e) = cache.save_cache_state(manifest_data, "", &state.ontology_graph) {
797 if self.options.verbose {
798 eprintln!("Warning: Failed to save cache: {}", e);
799 }
800 }
801 }
802
803 let mut proof_carrier = ProofCarrier::new();
805 let manifest_content = std::fs::read_to_string(&self.options.manifest_path)
806 .map_err(|e| {
807 Error::new(&format!(
808 "error[E0006]: Failed to read manifest for proof generation\n --> {}\n |\n = error: {}",
809 self.options.manifest_path.display(),
810 e
811 ))
812 })?;
813 let ontology_content =
814 std::fs::read_to_string(base_path.join(&manifest_data.ontology.source))
815 .map_err(|e| {
816 Error::new(&format!(
817 "error[E0007]: Failed to read ontology for proof generation\n --> {}\n |\n = error: {}",
818 base_path.join(&manifest_data.ontology.source).display(),
819 e
820 ))
821 })?;
822
823 if let Ok(proof) = proof_carrier.generate_proof(
824 &manifest_content,
825 &ontology_content,
826 &SyncResult {
827 status: "executing".to_string(),
828 files_synced: 0,
829 duration_ms: 0,
830 files: synced_files.clone(),
831 inference_rules_executed: inference_count,
832 generation_rules_executed: generation_count,
833 audit_trail: None,
834 error: None,
835 },
836 ) {
837 if self.options.verbose {
838 eprintln!("Execution proof: {}", proof.execution_id);
839 }
840 }
841
842 let duration = self.start_time.elapsed().as_millis() as u64;
843
844 if self.options.output_format == OutputFormat::Text {
846 if self.options.verbose {
847 print_section("Summary");
849 eprintln!(
850 "{}",
851 success_message(&format!(
852 "Synced {} files in {}",
853 files_synced,
854 format_duration(duration)
855 ))
856 );
857 eprintln!();
858 eprintln!("Files generated:");
859 for f in &synced_files {
860 eprintln!(" {} ({} bytes)", f.path, f.size_bytes);
861 }
862 if let Some(ref audit) = audit_path {
863 eprintln!();
864 eprintln!("{}", info_message(&format!("Audit trail: {}", audit)));
865 }
866 } else {
867 eprintln!();
869 eprintln!(
870 "{}",
871 success_message(&format!(
872 "Generated {} files in {}",
873 files_synced,
874 format_duration(duration)
875 ))
876 );
877
878 let total_bytes: usize = synced_files.iter().map(|f| f.size_bytes).sum();
880 eprintln!(
881 " {} inference rules, {} generation rules",
882 inference_count, generation_count
883 );
884 eprintln!(" {} total bytes written", total_bytes);
885 if let Some(ref audit) = audit_path {
886 eprintln!(" Audit: {}", audit);
887 }
888 }
889 }
890
891 self.save_drift_state(base_path, manifest_data, files_synced, duration);
893
894 Ok(SyncResult {
895 status: "success".to_string(),
896 files_synced,
897 duration_ms: duration,
898 files: synced_files,
899 inference_rules_executed: inference_count,
900 generation_rules_executed: generation_count,
901 audit_trail: audit_path,
902 error: None,
903 })
904 }
905
906 fn execute_watch_mode(&self, manifest_path: &Path) -> Result<SyncResult> {
908 use crate::codegen::watch::{collect_watch_paths, FileWatcher};
909 use std::time::Duration;
910
911 let manifest_data = ManifestParser::parse(manifest_path).map_err(|e| {
913 Error::new(&format!(
914 "error[E0001]: Manifest parse error\n --> {}\n |\n = error: {}\n = help: Check ggen.toml syntax",
915 manifest_path.display(),
916 e
917 ))
918 })?;
919
920 let base_path = manifest_path.parent().unwrap_or(Path::new("."));
921 let watch_paths = collect_watch_paths(manifest_path, &manifest_data, base_path);
922
923 if self.options.verbose {
924 eprintln!("Starting watch mode...");
925 eprintln!("Monitoring {} paths for changes:", watch_paths.len());
926 for path in &watch_paths {
927 eprintln!(" {}", path.display());
928 }
929 eprintln!("\nPress Ctrl+C to stop.\n");
930 }
931
932 if self.options.verbose {
934 eprintln!("[Initial] Running sync...");
935 }
936 let executor = SyncExecutor::new(SyncOptions {
937 watch: false, ..self.options.clone()
939 });
940 let initial_result = executor.execute()?;
941
942 if self.options.verbose {
943 eprintln!(
944 "[Initial] Synced {} files in {:.3}s\n",
945 initial_result.files_synced,
946 initial_result.duration_ms as f64 / 1000.0
947 );
948 }
949
950 let watcher = FileWatcher::new(watch_paths.clone());
952 let rx = watcher.start()?;
953
954 loop {
956 match FileWatcher::wait_for_change(&rx, Duration::from_secs(1)) {
957 Ok(Some(event)) => {
958 if self.options.verbose {
959 eprintln!("[Change detected] {}", event.path.display());
960 eprintln!("[Regenerating] Running sync...");
961 }
962
963 let executor = SyncExecutor::new(SyncOptions {
965 watch: false,
966 ..self.options.clone()
967 });
968
969 match executor.execute() {
970 Ok(result) => {
971 if self.options.verbose {
972 eprintln!(
973 "[Regenerating] Synced {} files in {:.3}s\n",
974 result.files_synced,
975 result.duration_ms as f64 / 1000.0
976 );
977 }
978 }
979 Err(e) => {
980 eprintln!("[Error] Regeneration failed: {}\n", e);
981 }
982 }
983 }
984 Ok(None) => {
985 }
987 Err(e) => {
988 return Err(Error::new(&format!("Watch error: {}", e)));
989 }
990 }
991 }
992 }
993
994 fn check_and_warn_drift(&self, base_path: &Path) {
996 if self.options.validate_only || self.options.watch {
998 return;
999 }
1000
1001 let state_dir = base_path.join(".ggen");
1002 let detector = match DriftDetector::new(&state_dir) {
1003 Ok(d) => d,
1004 Err(_) => return, };
1006
1007 if !detector.has_state() {
1009 return;
1010 }
1011
1012 let manifest_data = match ManifestParser::parse(&self.options.manifest_path) {
1014 Ok(m) => m,
1015 Err(_) => return, };
1017
1018 let ontology_path = base_path.join(&manifest_data.ontology.source);
1019
1020 match detector.check_drift(&ontology_path, &self.options.manifest_path) {
1022 Ok(status) => {
1023 if let Some(warning) = status.warning_message() {
1024 eprintln!("{}", warning);
1025 }
1026 }
1027 Err(_) => {
1028 }
1030 }
1031 }
1032
1033 fn save_drift_state(
1035 &self, base_path: &Path, manifest_data: &crate::manifest::GgenManifest,
1036 files_synced: usize, duration_ms: u64,
1037 ) {
1038 let state_dir = base_path.join(".ggen");
1039 let detector = match DriftDetector::new(&state_dir) {
1040 Ok(d) => d,
1041 Err(e) => {
1042 if self.options.verbose {
1043 eprintln!("Warning: Failed to create drift detector: {}", e);
1044 }
1045 return;
1046 }
1047 };
1048
1049 let ontology_path = base_path.join(&manifest_data.ontology.source);
1050
1051 let imports = manifest_data
1053 .ontology
1054 .imports
1055 .iter()
1056 .map(|imp| base_path.join(imp))
1057 .collect();
1058
1059 let inference_rules: Vec<(String, String)> = manifest_data
1061 .inference
1062 .rules
1063 .iter()
1064 .map(|rule| {
1065 let hash = crate::pqc::calculate_sha256(rule.construct.as_bytes());
1066 (rule.name.clone(), hash)
1067 })
1068 .collect();
1069
1070 if let Err(e) = detector.save_state_with_details(
1072 &ontology_path,
1073 &self.options.manifest_path,
1074 imports,
1075 inference_rules,
1076 files_synced,
1077 duration_ms,
1078 ) {
1079 if self.options.verbose {
1080 eprintln!("Warning: Failed to save drift state: {}", e);
1081 }
1082 }
1083 }
1084}