ggen_cli_lib/cmds/sync.rs
1//! Sync Command - The ONLY command in ggen v5
2//!
3//! `ggen sync` is the unified code synchronization pipeline that replaces ALL
4//! previous ggen commands. It transforms domain ontologies through inference
5//! rules into typed code via Tera templates.
6
7#![allow(clippy::unused_unit)] // clap-noun-verb macro generates this
8//!
9//! ## Architecture: Three-Layer Pattern
10//!
11//! - **Layer 3 (CLI)**: Input validation, output formatting, thin routing
12//! - **Layer 2 (Integration)**: Async execution, error handling
13//! - **Layer 1 (Domain)**: Pure generation logic from ggen_core::codegen
14//!
15//! ## Exit Codes
16//!
17//! | Code | Meaning |
18//! |------|---------|
19//! | 0 | Success |
20//! | 1 | Manifest validation error |
21//! | 2 | Ontology load error |
22//! | 3 | SPARQL query error |
23//! | 4 | Template rendering error |
24//! | 5 | File I/O error |
25//! | 6 | Timeout exceeded |
26
27use clap_noun_verb::Result as VerbResult;
28use clap_noun_verb_macros::verb;
29use ggen_core::codegen::{OutputFormat, SyncExecutor, SyncOptions, SyncResult};
30use serde::Serialize;
31use std::path::PathBuf;
32
33// ============================================================================
34// Output Types (re-exported for CLI compatibility)
35// ============================================================================
36
37/// Output for the `ggen sync` command
38#[derive(Debug, Clone, Serialize)]
39pub struct SyncOutput {
40 /// Overall status: "success" or "error"
41 pub status: String,
42
43 /// Number of files synced
44 pub files_synced: usize,
45
46 /// Total duration in milliseconds
47 pub duration_ms: u64,
48
49 /// Generated files with details
50 pub files: Vec<SyncedFile>,
51
52 /// Number of inference rules executed
53 pub inference_rules_executed: usize,
54
55 /// Number of generation rules executed
56 pub generation_rules_executed: usize,
57
58 /// Audit trail path (if enabled)
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub audit_trail: Option<String>,
61
62 /// Error message (if failed)
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub error: Option<String>,
65}
66
67/// Individual file sync result
68#[derive(Debug, Clone, Serialize)]
69pub struct SyncedFile {
70 /// File path relative to output directory
71 pub path: String,
72
73 /// File size in bytes
74 pub size_bytes: usize,
75
76 /// Action taken: "created", "updated", "unchanged"
77 pub action: String,
78}
79
80impl From<SyncResult> for SyncOutput {
81 fn from(result: SyncResult) -> Self {
82 Self {
83 status: result.status,
84 files_synced: result.files_synced,
85 duration_ms: result.duration_ms,
86 files: result
87 .files
88 .into_iter()
89 .map(|f| SyncedFile {
90 path: f.path,
91 size_bytes: f.size_bytes,
92 action: f.action,
93 })
94 .collect(),
95 inference_rules_executed: result.inference_rules_executed,
96 generation_rules_executed: result.generation_rules_executed,
97 audit_trail: result.audit_trail,
98 error: result.error,
99 }
100 }
101}
102
103// ============================================================================
104// The ONLY Command: ggen sync
105// ============================================================================
106
107/// Execute the complete code synchronization pipeline from a ggen.toml manifest.
108///
109/// This is THE ONLY command in ggen v5. It replaces all previous commands
110/// (`ggen generate`, `ggen validate`, `ggen template`, etc.) with a single
111/// unified pipeline.
112///
113/// ## Pipeline Flow
114///
115/// ```text
116/// ggen.toml → ontology → CONSTRUCT inference → SELECT → Template → Code
117/// ```
118///
119/// ## Flags
120///
121/// --manifest PATH Path to ggen.toml (default: ./ggen.toml)
122/// --output-dir PATH Override output directory from manifest
123/// --dry-run Preview changes without writing files
124/// --force Overwrite existing files (DESTRUCTIVE - use with --audit)
125/// --audit Create detailed audit trail in .ggen/audit/
126/// --rule NAME Execute only specific generation rule
127/// --verbose Show detailed execution logs
128/// --watch Continuous file monitoring and auto-regeneration
129/// --validate-only Run SHACL/SPARQL validation without generation
130/// --format FORMAT Output format: text, json, yaml (default: text)
131/// --timeout MS Maximum execution time in milliseconds (default: 30000)
132///
133/// ## Flag Combinations
134///
135/// Safe workflows:
136/// ggen sync --dry-run --audit Preview with audit
137/// ggen sync --force --audit Destructive overwrite with tracking
138/// ggen sync --watch --validate-only Continuous validation
139///
140/// CI/CD workflows:
141/// ggen sync --format json Machine-readable output
142/// ggen sync --validate-only Pre-flight checks
143///
144/// Development workflows:
145/// ggen sync --watch --verbose Live feedback
146/// ggen sync --rule structs Focused iteration
147///
148/// ## Flag Precedence
149///
150/// --validate-only overrides --force
151/// --dry-run prevents file writes (--force has no effect)
152/// --watch triggers continuous execution
153///
154/// ## Safety Notes
155///
156/// ⚠️ ALWAYS use --audit with --force to enable rollback
157/// ⚠️ ALWAYS use --dry-run before --force to preview changes
158/// ⚠️ Review docs/features/force-flag.md before using --force
159///
160/// ## Examples
161///
162/// ```bash
163/// # Basic sync (the primary workflow)
164/// ggen sync
165///
166/// # Sync from specific manifest
167/// ggen sync --manifest project/ggen.toml
168///
169/// # Dry-run to preview changes
170/// ggen sync --dry-run
171///
172/// # Sync specific rule only
173/// ggen sync --rule structs
174///
175/// # Force overwrite with audit trail (RECOMMENDED)
176/// ggen sync --force --audit
177///
178/// # Watch mode for development
179/// ggen sync --watch --verbose
180///
181/// # Validate without generating
182/// ggen sync --validate-only
183///
184/// # JSON output for CI/CD
185/// ggen sync --format json
186///
187/// # Complex: Watch, audit, verbose
188/// ggen sync --watch --audit --verbose --rule api_endpoints
189/// ```
190///
191/// ## Documentation
192///
193/// Full feature documentation:
194/// - docs/features/audit-trail.md Audit trail format and usage
195/// - docs/features/force-flag.md Safe destructive workflows
196/// - docs/features/merge-mode.md Hybrid manual/generated code
197/// - docs/features/watch-mode.md Continuous regeneration
198/// - docs/features/conditional-execution.md SPARQL ASK conditions
199/// - docs/features/validation.md SHACL/SPARQL constraints
200#[allow(clippy::unused_unit, clippy::too_many_arguments)]
201#[verb("sync", "root")]
202pub fn sync(
203 manifest: Option<String>, output_dir: Option<String>, dry_run: Option<bool>,
204 force: Option<bool>, audit: Option<bool>, rule: Option<String>, verbose: Option<bool>,
205 watch: Option<bool>, validate_only: Option<bool>, format: Option<String>, timeout: Option<u64>,
206) -> VerbResult<SyncOutput> {
207 // Build options from CLI args
208 let options = build_sync_options(
209 manifest,
210 output_dir,
211 dry_run,
212 force,
213 audit,
214 rule,
215 verbose,
216 watch,
217 validate_only,
218 format,
219 timeout,
220 );
221
222 // Execute via domain executor
223 let result = SyncExecutor::new(options)
224 .execute()
225 .map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))?;
226
227 Ok(SyncOutput::from(result))
228}
229
230/// Build SyncOptions from CLI arguments
231///
232/// This helper keeps the verb function thin by extracting option building.
233#[allow(clippy::too_many_arguments)]
234fn build_sync_options(
235 manifest: Option<String>, output_dir: Option<String>, dry_run: Option<bool>,
236 force: Option<bool>, audit: Option<bool>, rule: Option<String>, verbose: Option<bool>,
237 watch: Option<bool>, validate_only: Option<bool>, format: Option<String>, timeout: Option<u64>,
238) -> SyncOptions {
239 let mut options = SyncOptions::new();
240
241 // Set manifest path
242 options.manifest_path = PathBuf::from(manifest.unwrap_or_else(|| "ggen.toml".to_string()));
243
244 // Set optional output directory
245 if let Some(dir) = output_dir {
246 options.output_dir = Some(PathBuf::from(dir));
247 }
248
249 // Set boolean flags
250 options.dry_run = dry_run.unwrap_or(false);
251 options.force = force.unwrap_or(false);
252 options.audit = audit.unwrap_or(false);
253 options.verbose = verbose.unwrap_or(false);
254 options.watch = watch.unwrap_or(false);
255 options.validate_only = validate_only.unwrap_or(false);
256
257 // Set selected rules
258 if let Some(r) = rule {
259 options.selected_rules = Some(vec![r]);
260 }
261
262 // Set output format
263 if let Some(fmt) = format {
264 options.output_format = fmt.parse().unwrap_or(OutputFormat::Text);
265 }
266
267 // Set timeout
268 if let Some(t) = timeout {
269 options.timeout_ms = t;
270 }
271
272 options
273}