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::{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#[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
71#[derive(Debug, Clone, Copy, Default)]
73pub struct ModeFlags {
74 pub validate_only: bool,
76 pub dry_run: bool,
78 pub watch: bool,
80}
81
82#[derive(Debug, Clone, Copy, Default)]
84pub struct BehaviorFlags {
85 pub verbose: bool,
87 pub force: bool,
89 pub audit: bool,
91}
92
93#[derive(Debug, Clone, Copy, Default)]
95pub struct SyncFlags {
96 pub mode: ModeFlags,
98 pub behavior: BehaviorFlags,
100}
101
102pub struct SyncOptions {
104 pub manifest_path: PathBuf,
106
107 pub output_dir: Option<PathBuf>,
109
110 pub cache_dir: Option<PathBuf>,
112
113 pub use_cache: bool,
115
116 pub flags: SyncFlags,
118
119 pub output_format: OutputFormat,
121
122 pub selected_rules: Option<Vec<String>>,
124
125 pub a2a_stage: Option<String>,
128
129 pub ontology_path: Option<PathBuf>,
131
132 pub llm_service: Option<Box<dyn LlmService>>,
136
137 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, }
191 }
192}
193
194impl SyncOptions {
195 pub fn new() -> Self {
197 Self::default()
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Default)]
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 #[serde(skip_serializing_if = "Option::is_none")]
236 pub recovery: Option<String>,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub andon_signal: Option<serde_json::Value>,
241}
242
243#[derive(Debug, Clone, Serialize, Default)]
245pub struct SyncedFileInfo {
246 pub path: String,
248
249 pub size_bytes: usize,
251
252 pub action: String,
254
255 pub produced_by: String,
257}
258
259#[derive(Debug, Clone, Serialize)]
261pub struct ValidationCheck {
262 pub check: String,
264
265 pub passed: bool,
267
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub details: Option<String>,
271}
272
273pub struct SyncExecutor {
282 options: SyncOptions,
283 start_time: Instant,
284}
285
286impl SyncExecutor {
287 pub fn new(options: SyncOptions) -> Self {
289 Self {
290 options,
291 start_time: Instant::now(),
292 }
293 }
294
295 pub fn with_llm_service(mut self, service: Option<Box<dyn LlmService>>) -> Self {
300 self.options.llm_service = service;
301 self
302 }
303
304 pub fn execute(mut self) -> Result<SyncResult> {
308 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) .with_template_check(false) .with_git_check(false);
319
320 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 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 self.check_and_warn_drift(base_path);
341
342 if self.options.flags.mode.watch {
344 return self.execute_watch_mode(&self.options.manifest_path);
345 }
346
347 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 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 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 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 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 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 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 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 validations.push(ValidationCheck {
472 check: "Manifest schema".to_string(),
473 passed: true,
474 details: None,
475 });
476
477 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 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 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 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 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 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 fn execute_full_sync(
633 &mut self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
634 ) -> Result<SyncResult> {
635 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 let mut progress = ProgressIndicator::new(show_progress);
647
648 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(); 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 let mut pipeline = GenerationPipeline::new(manifest_data.clone(), base_path.to_path_buf());
682
683 if self.options.flags.behavior.force {
685 pipeline.set_force_overwrite(true);
686 }
687
688 if let Some(llm_service) = self.options.llm_service.take() {
690 pipeline.set_llm_service(Some(llm_service));
691 }
692
693 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 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 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 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 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 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 let mut builder = crate::codegen::audit::AuditTrailBuilder::new();
797
798 for file in &state.generated_files {
803 builder.record_output(&file.path, "", &format!("rule-{}", file.path.display()));
804 }
805
806 let audit_trail = builder.build(true); 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 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 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 if self.options.output_format == OutputFormat::Text {
872 if self.options.flags.behavior.verbose {
873 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 eprintln!();
895 eprintln!(
896 "{}",
897 success_message(&format!(
898 "Generated {} files in {}",
899 files_synced,
900 format_duration(duration)
901 ))
902 );
903
904 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 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 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 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 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; 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 let watcher = FileWatcher::new(watch_paths.clone());
979 let rx = watcher.start()?;
980
981 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 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 }
1013 Err(e) => {
1014 return Err(Error::new(&format!("Watch error: {}", e)));
1015 }
1016 }
1017 }
1018 }
1019
1020 fn check_and_warn_drift(&self, base_path: &Path) {
1022 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, };
1032
1033 if !detector.has_state() {
1035 return;
1036 }
1037
1038 let manifest_data = match ManifestParser::parse(&self.options.manifest_path) {
1040 Ok(m) => m,
1041 Err(_) => return, };
1043
1044 let ontology_path = base_path.join(&manifest_data.ontology.source);
1045
1046 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 }
1056 }
1057 }
1058
1059 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 let imports = manifest_data
1079 .ontology
1080 .imports
1081 .iter()
1082 .map(|imp| base_path.join(imp))
1083 .collect();
1084
1085 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 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 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}