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}