1use 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#[derive(Debug, Clone, Serialize)]
38pub struct SyncResult {
39 pub status: String,
41
42 pub files_synced: usize,
44
45 pub duration_ms: u64,
47
48 pub files: Vec<SyncedFileInfo>,
50
51 pub inference_rules_executed: usize,
53
54 pub generation_rules_executed: usize,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub audit_trail: Option<String>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub error: Option<String>,
64}
65
66#[derive(Debug, Clone, Serialize)]
68pub struct SyncedFileInfo {
69 pub path: String,
71
72 pub size_bytes: usize,
74
75 pub action: String,
77}
78
79#[derive(Debug, Clone, Serialize)]
81pub struct ValidationCheck {
82 pub check: String,
84
85 pub passed: bool,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub details: Option<String>,
91}
92
93pub struct SyncExecutor {
102 options: SyncOptions,
103 start_time: Instant,
104}
105
106impl SyncExecutor {
107 pub fn new(options: SyncOptions) -> Self {
109 Self {
110 options,
111 start_time: Instant::now(),
112 }
113 }
114
115 pub fn execute(self) -> Result<SyncResult> {
119 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 if self.options.watch {
129 return self.execute_watch_mode(&self.options.manifest_path);
130 }
131
132 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 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 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 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 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 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 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 validations.push(ValidationCheck {
247 check: "Manifest schema".to_string(),
248 passed: true,
249 details: None,
250 });
251
252 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 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 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 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 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 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 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 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(); Some(c)
417 } else {
418 None
419 };
420
421 let mut pipeline = GenerationPipeline::new(manifest_data.clone(), base_path.to_path_buf());
423
424 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 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 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 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 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 for _ in &state.executed_rules {
499 audit_trail.record_rule_executed();
500 }
501
502 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 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 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 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 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 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 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 if self.options.verbose {
613 eprintln!("[Initial] Running sync...");
614 }
615 let executor = SyncExecutor::new(SyncOptions {
616 watch: false, ..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 let watcher = FileWatcher::new(watch_paths.clone());
631 let rx = watcher.start()?;
632
633 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 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 }
666 Err(e) => {
667 return Err(Error::new(&format!("Watch error: {}", e)));
668 }
669 }
670 }
671 }
672}