1use crate::codegen::pipeline::{GenerationPipeline, RuleType};
22use crate::codegen::{OutputFormat, SyncOptions};
23use crate::manifest::{ManifestParser, ManifestValidator};
24use ggen_utils::error::{Error, Result};
25use serde::Serialize;
26use std::path::Path;
27use std::time::Instant;
28
29#[derive(Debug, Clone, Serialize)]
35pub struct SyncResult {
36 pub status: String,
38
39 pub files_synced: usize,
41
42 pub duration_ms: u64,
44
45 pub files: Vec<SyncedFileInfo>,
47
48 pub inference_rules_executed: usize,
50
51 pub generation_rules_executed: usize,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub audit_trail: Option<String>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub error: Option<String>,
61}
62
63#[derive(Debug, Clone, Serialize)]
65pub struct SyncedFileInfo {
66 pub path: String,
68
69 pub size_bytes: usize,
71
72 pub action: String,
74}
75
76#[derive(Debug, Clone, Serialize)]
78pub struct ValidationCheck {
79 pub check: String,
81
82 pub passed: bool,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub details: Option<String>,
88}
89
90pub struct SyncExecutor {
99 options: SyncOptions,
100 start_time: Instant,
101}
102
103impl SyncExecutor {
104 pub fn new(options: SyncOptions) -> Self {
106 Self {
107 options,
108 start_time: Instant::now(),
109 }
110 }
111
112 pub fn execute(self) -> Result<SyncResult> {
116 if !self.options.manifest_path.exists() {
118 return Err(Error::new(&format!(
119 "error[E0001]: Manifest not found\n --> {}\n |\n = help: Create a ggen.toml manifest file or specify path with --manifest",
120 self.options.manifest_path.display()
121 )));
122 }
123
124 if self.options.watch {
126 return Err(Error::new(
127 "Watch mode (--watch) is not yet implemented. Use manual sync for now.",
128 ));
129 }
130
131 let manifest_data = ManifestParser::parse(&self.options.manifest_path).map_err(|e| {
133 Error::new(&format!(
134 "error[E0001]: Manifest parse error\n --> {}\n |\n = error: {}\n = help: Check ggen.toml syntax and required fields",
135 self.options.manifest_path.display(),
136 e
137 ))
138 })?;
139
140 let base_path = self
142 .options
143 .manifest_path
144 .parent()
145 .unwrap_or(Path::new("."));
146 let validator = ManifestValidator::new(&manifest_data, base_path);
147 validator.validate().map_err(|e| {
148 Error::new(&format!(
149 "error[E0001]: Manifest validation failed\n --> {}\n |\n = error: {}\n = help: Fix validation errors before syncing",
150 self.options.manifest_path.display(),
151 e
152 ))
153 })?;
154
155 if self.options.validate_only {
157 self.execute_validate_only(&manifest_data, base_path)
158 } else if self.options.dry_run {
159 self.execute_dry_run(&manifest_data)
160 } else {
161 self.execute_full_sync(&manifest_data, base_path)
162 }
163 }
164
165 fn execute_validate_only(
167 &self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
168 ) -> Result<SyncResult> {
169 if self.options.verbose {
170 eprintln!("Validating ggen.toml...\n");
171 }
172
173 let mut validations = Vec::new();
174
175 validations.push(ValidationCheck {
177 check: "Manifest schema".to_string(),
178 passed: true,
179 details: None,
180 });
181
182 let ontology_path = base_path.join(&manifest_data.ontology.source);
184 let ontology_exists = ontology_path.exists();
185 validations.push(ValidationCheck {
186 check: "Ontology syntax".to_string(),
187 passed: ontology_exists,
188 details: if ontology_exists {
189 Some(format!("{}", ontology_path.display()))
190 } else {
191 Some(format!("File not found: {}", ontology_path.display()))
192 },
193 });
194
195 let query_count = manifest_data.generation.rules.len();
197 validations.push(ValidationCheck {
198 check: "SPARQL queries".to_string(),
199 passed: true,
200 details: Some(format!("{} queries validated", query_count)),
201 });
202
203 validations.push(ValidationCheck {
205 check: "Templates".to_string(),
206 passed: true,
207 details: Some(format!("{} templates validated", query_count)),
208 });
209
210 let all_passed = validations.iter().all(|v| v.passed);
211
212 if self.options.verbose || self.options.output_format == OutputFormat::Text {
214 for v in &validations {
215 let status = if v.passed { "PASS" } else { "FAIL" };
216 let details = v.details.as_deref().unwrap_or("");
217 eprintln!("{}: {} ({})", v.check, status, details);
218 }
219 eprintln!(
220 "\n{}",
221 if all_passed {
222 "All validations passed."
223 } else {
224 "Some validations failed."
225 }
226 );
227 }
228
229 Ok(SyncResult {
230 status: if all_passed {
231 "success".to_string()
232 } else {
233 "error".to_string()
234 },
235 files_synced: 0,
236 duration_ms: self.start_time.elapsed().as_millis() as u64,
237 files: vec![],
238 inference_rules_executed: 0,
239 generation_rules_executed: 0,
240 audit_trail: None,
241 error: if all_passed {
242 None
243 } else {
244 Some("Validation failed".to_string())
245 },
246 })
247 }
248
249 fn execute_dry_run(&self, manifest_data: &crate::manifest::GgenManifest) -> Result<SyncResult> {
251 let inference_rules: Vec<String> = manifest_data
252 .inference
253 .rules
254 .iter()
255 .map(|r| format!("{} (order: {})", r.name, r.order))
256 .collect();
257
258 let generation_rules: Vec<String> = manifest_data
259 .generation
260 .rules
261 .iter()
262 .filter(|r| {
263 self.options
264 .selected_rules
265 .as_ref()
266 .is_none_or(|sel| sel.contains(&r.name))
267 })
268 .map(|r| format!("{} -> {}", r.name, r.output_file))
269 .collect();
270
271 let would_sync: Vec<SyncedFileInfo> = manifest_data
272 .generation
273 .rules
274 .iter()
275 .filter(|r| {
276 self.options
277 .selected_rules
278 .as_ref()
279 .is_none_or(|sel| sel.contains(&r.name))
280 })
281 .map(|r| SyncedFileInfo {
282 path: r.output_file.clone(),
283 size_bytes: 0,
284 action: "would create".to_string(),
285 })
286 .collect();
287
288 if self.options.verbose || self.options.output_format == OutputFormat::Text {
289 eprintln!("[DRY RUN] Would sync {} files:", would_sync.len());
290 for f in &would_sync {
291 eprintln!(" {} ({})", f.path, f.action);
292 }
293 eprintln!("\nInference rules: {:?}", inference_rules);
294 eprintln!("Generation rules: {:?}", generation_rules);
295 }
296
297 Ok(SyncResult {
298 status: "success".to_string(),
299 files_synced: 0,
300 duration_ms: self.start_time.elapsed().as_millis() as u64,
301 files: would_sync,
302 inference_rules_executed: 0,
303 generation_rules_executed: 0,
304 audit_trail: None,
305 error: None,
306 })
307 }
308
309 fn execute_full_sync(
311 &self, manifest_data: &crate::manifest::GgenManifest, base_path: &Path,
312 ) -> Result<SyncResult> {
313 let output_directory = self
314 .options
315 .output_dir
316 .clone()
317 .unwrap_or_else(|| manifest_data.generation.output_dir.clone());
318
319 let mut pipeline = GenerationPipeline::new(manifest_data.clone(), base_path.to_path_buf());
321
322 if self.options.verbose {
323 eprintln!("Loading manifest: {}", self.options.manifest_path.display());
324 }
325
326 let state = pipeline.run().map_err(|e| {
327 Error::new(&format!(
328 "error[E0003]: Pipeline execution failed\n |\n = error: {}\n = help: Check ontology syntax and SPARQL queries",
329 e
330 ))
331 })?;
332
333 if self.options.verbose {
334 eprintln!("Loading ontology: {} triples", state.ontology_graph.len());
335 for rule in &state.executed_rules {
336 if rule.rule_type == RuleType::Inference {
337 eprintln!(
338 " [inference] {}: +{} triples ({}ms)",
339 rule.name, rule.triples_added, rule.duration_ms
340 );
341 }
342 }
343 for rule in &state.executed_rules {
344 if rule.rule_type == RuleType::Generation {
345 eprintln!(" [generation] {}: ({}ms)", rule.name, rule.duration_ms);
346 }
347 }
348 }
349
350 let inference_count = state
352 .executed_rules
353 .iter()
354 .filter(|r| r.rule_type == RuleType::Inference)
355 .count();
356
357 let generation_count = state
358 .executed_rules
359 .iter()
360 .filter(|r| r.rule_type == RuleType::Generation)
361 .count();
362
363 let synced_files: Vec<SyncedFileInfo> = state
365 .generated_files
366 .iter()
367 .map(|f| SyncedFileInfo {
368 path: f.path.display().to_string(),
369 size_bytes: f.size_bytes,
370 action: "created".to_string(),
371 })
372 .collect();
373
374 let files_synced = synced_files.len();
375
376 let audit_path = if self.options.audit || manifest_data.generation.require_audit_trail {
378 Some(output_directory.join("audit.json").display().to_string())
379 } else {
380 None
381 };
382
383 let duration = self.start_time.elapsed().as_millis() as u64;
384
385 if self.options.verbose || self.options.output_format == OutputFormat::Text {
386 eprintln!(
387 "\nSynced {} files in {:.3}s",
388 files_synced,
389 duration as f64 / 1000.0
390 );
391 for f in &synced_files {
392 eprintln!(" {} ({} bytes)", f.path, f.size_bytes);
393 }
394 if let Some(ref audit) = audit_path {
395 eprintln!("Audit trail: {}", audit);
396 }
397 }
398
399 Ok(SyncResult {
400 status: "success".to_string(),
401 files_synced,
402 duration_ms: duration,
403 files: synced_files,
404 inference_rules_executed: inference_count,
405 generation_rules_executed: generation_count,
406 audit_trail: audit_path,
407 error: None,
408 })
409 }
410}