ggen-cli-lib 26.7.3

CLI interface for ggen
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
//! Sync Command - The ONLY command in ggen v26_5_19
//!
//! `ggen sync` is the unified code synchronization pipeline that replaces ALL
//! previous ggen commands. It transforms domain ontologies through inference
//! rules into typed code via Tera templates.
//!
//! ## A2A-RS μ Pipeline
//!
//! For A2A-RS integration, the sync command executes the full μ₁-μ₅ pipeline:
//!
//! - **μ₁ (CONSTRUCT)**: Normalize RDF ontology from .specify/specs/014-a2a-integration/
//! - **μ₂ (SELECT)**: Extract bindings for each module (agent, message, task, transport, skill)
//! - **μ₃ (Tera)**: Generate Rust code from templates
//! - **μ₄ (Canonicalize)**: Format and organize generated code
//! - **μ₅ (Receipt)**: Generate cryptographic receipt for verification
//!
//! Usage:
//!   ggen sync --audit              # Full A2A pipeline with receipt
//!   ggen sync --dry-run            # Preview without writing
//!   ggen sync --output crates/     # Custom output directory

#![allow(clippy::unused_unit)] // clap-noun-verb macro generates this
//!
//! ## Architecture: Three-Layer Pattern
//!
//! - **Layer 3 (CLI)**: Input validation, output formatting, thin routing
//! - **Layer 2 (Integration)**: Async execution, error handling
//! - **Layer 1 (Domain)**: Pure generation logic from ggen_core::codegen
//!
//! ## Exit Codes
//!
//! | Code | Meaning |
//! |------|---------|
//! | 0 | Success |
//! | 1 | Manifest validation error |
//! | 2 | Ontology load error |
//! | 3 | SPARQL query error |
//! | 4 | Template rendering error |
//! | 5 | File I/O error |
//! | 6 | Timeout exceeded |

use chrono::Utc;
use clap_noun_verb::{NounVerbError, Result as VerbResult};
use clap_noun_verb_macros::verb;
use ggen_core::codegen::{OutputFormat, SyncExecutor, SyncOptions, SyncResult};
use ggen_core::receipt::{generate_keypair, hash_data, Receipt};
use ggen_core::sync::{sync as low_level_sync, SyncConfig, SyncLanguage};
use serde::Serialize;
use std::path::PathBuf;

// ============================================================================
// Output Types (re-exported for CLI compatibility)
// ============================================================================

/// Output for the `ggen sync` command
#[derive(Debug, Clone, Serialize)]
pub struct SyncOutput {
    /// Overall status: "success" or "error"
    pub status: String,

    /// Number of files synced
    pub files_synced: usize,

    /// Total duration in milliseconds
    pub duration_ms: u64,

    /// Generated files with details
    pub files: Vec<SyncedFile>,

    /// Number of inference rules executed
    pub inference_rules_executed: usize,

    /// Number of generation rules executed
    pub generation_rules_executed: usize,

    /// Audit trail path (if enabled)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub audit_trail: Option<String>,

    /// Error message (if failed)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,

    /// Machine-parsable recovery steps for AGI remediation
    #[serde(skip_serializing_if = "Option::is_none")]
    pub recovery: Option<String>,

    /// JSON representation of the TPS Andon signal (if any)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub andon_signal: Option<serde_json::Value>,

    /// Path to the cryptographic receipt emitted after sync (if generated)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub receipt_path: Option<String>,

    /// Results of the manufacturing proof gates
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub gates: Vec<GateResult>,
}

/// Result of a single proof gate
#[derive(Debug, Clone, serde::Serialize)]
pub struct GateResult {
    /// Gate name
    pub name: String,
    /// Whether the gate passed
    pub passed: bool,
    /// Detailed message
    pub message: String,
}

/// Individual file sync result
#[derive(Debug, Clone, Serialize)]
pub struct SyncedFile {
    /// File path relative to output directory
    pub path: String,

    /// File size in bytes
    pub size_bytes: usize,

    /// Action taken: "created", "updated", "unchanged"
    pub action: String,

    /// Rule that generated this file
    pub rule: String,
}

impl From<SyncResult> for SyncOutput {
    fn from(result: SyncResult) -> Self {
        Self {
            status: result.status,
            files_synced: result.files_synced,
            duration_ms: result.duration_ms,
            files: result
                .files
                .into_iter()
                .map(|f| SyncedFile {
                    path: f.path,
                    size_bytes: f.size_bytes,
                    action: f.action,
                    rule: f.produced_by,
                })
                .collect(),
            inference_rules_executed: result.inference_rules_executed,
            generation_rules_executed: result.generation_rules_executed,
            audit_trail: result.audit_trail,
            error: result.error,
            recovery: result.recovery,
            andon_signal: result.andon_signal,
            receipt_path: None,
            gates: Vec::new(),
        }
    }
}

// ============================================================================
// The ONLY Command: ggen sync
// ============================================================================

/// Execute the complete code synchronization pipeline from a ggen.toml manifest.
///
/// This is THE ONLY command in ggen v26_5_19. It replaces all previous commands
/// (`ggen generate`, `ggen validate`, `ggen template`, etc.) with a single
/// unified pipeline.
///
/// ## A2A-RS μ Pipeline (μ₁ through μ₅)
///
/// When generating A2A-RS code from `.specify/specs/014-a2a-integration/`:
///
/// ```text
/// μ₁ CONSTRUCT: Normalize RDF ontology
///    Input: .specify/specs/014-a2a-integration/a2a-ontology.ttl
///    Query: crates/ggen-core/queries/a2a/construct-agents.rq
///    Output: Normalized A2A RDF with a2a: prefix
///
/// μ₂ SELECT: Extract bindings for each module
///    Queries: crates/ggen-core/queries/a2a/extract-*.rq
///      - extract-agents.rq → agent bindings
///      - extract-messages.rq → message bindings
///      - extract-tasks.rq → task bindings
///      - extract-transports.rq → transport bindings
///      - extract-skills.rq → skill bindings
///    Output: SPARQL result bindings
///
/// μ₃ Tera: Generate Rust code
///    Templates: crates/ggen-core/templates/a2a/*.tera
///      - agent.rs.tera → crates/a2a-generated/src/agent.rs
///      - message.rs.tera → crates/a2a-generated/src/message.rs
///      - task.rs.tera → crates/a2a-generated/src/task.rs
///      - transport.rs.tera → crates/a2a-generated/src/transport.rs
///      - skill.rs.tera → crates/a2a-generated/src/skill.rs
///      - lib.rs.tera → crates/a2a-generated/src/lib.rs
///    Output: Generated Rust source files
///
/// μ₄ Canonicalize: Format and organize
///    Action: rustfmt, organize imports, verify compilation
///    Output: Formatted, ready-to-compile code
///
/// μ₅ Receipt: Generate cryptographic verification
///    Output: .ggen/receipts/a2a-{timestamp}.json
///    Contains: SHA256 hashes, input ontology hash, timestamp
/// ```
///
/// ## Pipeline Flow
///
/// ```text
/// ggen.toml → ontology → CONSTRUCT inference → SELECT → Template → Code
/// ```
///
/// ## Flags
///
/// --manifest PATH         Path to ggen.toml (default: ./ggen.toml)
/// --output-dir PATH       Override output directory from manifest
/// --dry-run               Preview changes without writing files
/// --force                 Overwrite existing files (DESTRUCTIVE - use with --audit)
/// --audit                 Create detailed audit trail in .ggen/audit/
/// --rule NAME             Execute only specific generation rule
/// --verbose               Show detailed execution logs
/// --watch                 Continuous file monitoring and auto-regeneration
/// --validate-only         Run SHACL/SPARQL validation without generation
/// --format FORMAT         Output format: text, json, yaml (default: text)
/// --timeout MS            Maximum execution time in milliseconds (default: 30000)
/// --stage STAGE           Run specific μ stage only (μ₁, μ₂, μ₃, μ₄, μ₅)
/// --ontology PATH         Override ontology path (default: from manifest)
///
/// ## Flag Combinations
///
/// Safe workflows:
///   ggen sync --dry-run --audit         Preview with audit
///   ggen sync --force --audit           Destructive overwrite with tracking
///   ggen sync --watch --validate-only   Continuous validation
///
/// A2A-specific workflows:
///   ggen sync --audit                   Full A2A μ₁-μ₅ pipeline with receipt
///   ggen sync --stage μ₃                Only run template generation
///   ggen sync --ontology .specify/specs/014-a2a-integration/a2a-ontology.ttl
///
/// CI/CD workflows:
///   ggen sync --format json             Machine-readable output
///   ggen sync --validate-only           Pre-flight checks
///
/// Development workflows:
///   ggen sync --watch --verbose         Live feedback
///   ggen sync --rule structs            Focused iteration
///
/// ## Progress Reporting (A2A Pipeline)
///
/// When running A2A sync, progress is reported for each μ stage:
///
/// ```text
/// [μ₁/5] CONSTRUCT: Normalizing ontology...
///        Loaded 847 triples from a2a-ontology.ttl
///        +124 triples from construct-agents.rq
/// [μ₂/5] SELECT: Extracting bindings...
///        Agents: 8 bindings
///        Messages: 12 bindings
///        Tasks: 15 bindings
///        Transports: 3 bindings
///        Skills: 24 bindings
/// [μ₃/5] Tera: Generating code...
///        agent.rs (2.4 KB)
///        message.rs (3.1 KB)
///        task.rs (2.8 KB)
///        transport.rs (1.2 KB)
///        skill.rs (4.5 KB)
///        lib.rs (1.8 KB)
/// [μ₄/5] Canonicalizing: Formatting code...
///        Running rustfmt...
///        Verifying compilation...
/// [μ₅/5] Receipt: Generating verification...
///        Receipt: .ggen/receipts/a2a-20250208-143022.json
///        Ontology hash: a3f2e1b4...
///        Total: 6 files, 15.8 KB, 2.34s
/// ```
///
/// ## Flag Precedence
///
/// --validate-only overrides --force
/// --dry-run prevents file writes (--force has no effect)
/// --watch triggers continuous execution
/// --stage limits execution to specific μ stage
///
/// ## Safety Notes
///
/// ⚠️  ALWAYS use --audit with --force to enable rollback
/// ⚠️  ALWAYS use --dry-run before --force to preview changes
/// ⚠️  Review docs/features/force-flag.md before using --force
///
/// ## Examples
///
/// ```bash
/// # Basic sync (the primary workflow)
/// ggen sync
///
/// # Sync from specific manifest
/// ggen sync --manifest project/ggen.toml
///
/// # Dry-run to preview changes
/// ggen sync --dry-run
///
/// # Sync specific rule only
/// ggen sync --rule structs
///
/// # Force overwrite with audit trail (RECOMMENDED)
/// ggen sync --force --audit
///
/// # Watch mode for development
/// ggen sync --watch --verbose
///
/// # Validate without generating
/// ggen sync --validate-only
///
/// # JSON output for CI/CD
/// ggen sync --format json
///
/// # A2A generation with custom ontology
/// ggen sync --ontology .specify/specs/014-a2a-integration/a2a-ontology.ttl --audit
///
/// # Run specific μ stage only
/// ggen sync --stage μ₃
///
/// # Complex: Watch, audit, verbose
/// ggen sync --watch --audit --verbose --rule api_endpoints
/// ```
///
/// ## Documentation
///
/// Full feature documentation:
///   - docs/features/audit-trail.md          Audit trail format and usage
///   - docs/features/force-flag.md           Safe destructive workflows
///   - docs/features/merge-mode.md           Hybrid manual/generated code
///   - docs/features/watch-mode.md           Continuous regeneration
///   - docs/features/conditional-execution.md SPARQL ASK conditions
///   - docs/features/validation.md           SHACL/SPARQL constraints
///   - docs/features/a2a-pipeline.md         A2A μ₁-μ₅ pipeline details
#[allow(clippy::unused_unit, clippy::too_many_arguments)]
#[verb("sync", "root")]
pub fn sync(
    manifest: Option<String>,
    output_dir: Option<String>,
    dry_run: Option<bool>,
    force: Option<bool>,
    audit: Option<bool>,
    rule: Option<String>,
    verbose: Option<bool>,
    watch: Option<bool>,
    validate_only: Option<bool>,
    format: Option<String>,
    timeout: Option<u64>,
    stage: Option<String>,
    ontology: Option<String>,
    queries: Option<String>, // dir of .rq files — activates ontology-first pipeline (no ggen.toml needed)
    language: Option<String>, // go, elixir, rust, typescript, python, auto
    profile: Option<String>, // enforcement profile (e.g. enterprise-strict, permissive)
    locked: bool,            // require exact lock-file match (no implicit upgrades)
) -> VerbResult<SyncOutput> {
    check_profile_preconditions(profile.as_deref(), locked)?;

    // When --queries is supplied, bypass the manifest and run the low-level pipeline directly
    if let Some(ref queries_dir) = queries {
        return run_low_level_pipeline(
            ontology,
            queries_dir.clone(),
            output_dir,
            language,
            dry_run.unwrap_or(false),
        );
    }

    run_manifest_pipeline(
        manifest,
        output_dir,
        dry_run,
        force,
        audit,
        rule,
        verbose,
        watch,
        validate_only,
        format,
        timeout,
        stage,
        ontology,
    )
}

/// Check profile and locked preconditions before any pipeline work.
fn check_profile_preconditions(profile: Option<&str>, locked: bool) -> VerbResult<()> {
    if profile.is_some() || locked {
        let workspace =
            std::env::current_dir().map_err(|e| NounVerbError::execution_error(e.to_string()))?;
        ggen_core::domain::sync_profile::validate_sync_preconditions(profile, locked, &workspace)
            .map_err(NounVerbError::execution_error)?;
    }
    Ok(())
}

/// Execute the manifest-driven (ggen.toml) sync pipeline and emit a signed receipt.
#[allow(clippy::too_many_arguments)]
fn run_manifest_pipeline(
    manifest: Option<String>, output_dir: Option<String>, dry_run: Option<bool>,
    force: Option<bool>, audit: Option<bool>, rule: Option<String>, verbose: Option<bool>,
    watch: Option<bool>, validate_only: Option<bool>, format: Option<String>, timeout: Option<u64>,
    stage: Option<String>, ontology: Option<String>,
) -> VerbResult<SyncOutput> {
    let installed_packs = read_installed_packs(".ggen/packs.lock");

    let manifest_path = PathBuf::from(manifest.clone().unwrap_or_else(|| "ggen.toml".to_string()));

    let options = SyncOptions {
        manifest_path: manifest_path.clone(),
        output_dir: output_dir.map(PathBuf::from),
        use_cache: true,
        flags: ggen_core::codegen::executor::SyncFlags {
            mode: ggen_core::codegen::executor::ModeFlags {
                validate_only: validate_only.unwrap_or(false),
                dry_run: dry_run.unwrap_or(false),
                watch: watch.unwrap_or(false),
            },
            behavior: ggen_core::codegen::executor::BehaviorFlags {
                verbose: verbose.unwrap_or(false),
                force: force.unwrap_or(false),
                audit: audit.unwrap_or(false),
            },
        },
        output_format: match format.as_deref() {
            Some("json") => OutputFormat::Json,
            _ => OutputFormat::default(),
        },
        selected_rules: rule.map(|r| vec![r]),
        a2a_stage: stage,
        ontology_path: ontology.map(PathBuf::from),
        timeout_ms: timeout,
        ..SyncOptions::default()
    };

    let sync_result = ggen_core::codegen::executor::SyncExecutor::new(options)
        .execute()
        .map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))?;

    let files: Vec<SyncedFile> = sync_result
        .files
        .iter()
        .map(|f| SyncedFile {
            path: f.path.clone(),
            size_bytes: f.size_bytes,
            action: f.action.clone(),
            rule: f.produced_by.clone(),
        })
        .collect();

    let synced_file_paths: Vec<String> = files.iter().map(|f| f.path.clone()).collect();
    let files_synced = sync_result.files_synced;
    let duration_ms = sync_result.duration_ms;
    let generation_rules_executed = sync_result.generation_rules_executed;
    let inference_rules_executed = sync_result.inference_rules_executed;

    // A dry-run is a pure PREVIEW (sensing), not an actuation: it writes no
    // artifacts, so it must write no receipt. Emitting a receipt for a preview
    // would record a consequence that never happened (contract drift).
    let receipt_file_path = if dry_run.unwrap_or(false) {
        None
    } else {
        Some(
            emit_sync_receipt(&synced_file_paths, &installed_packs).map_err(|e| {
                clap_noun_verb::NounVerbError::execution_error(format!("Audit failure: {}", e))
            })?,
        )
    };

    Ok(SyncOutput {
        status: sync_result.status,
        files_synced,
        duration_ms,
        files,
        inference_rules_executed,
        generation_rules_executed,
        audit_trail: sync_result.audit_trail,
        error: sync_result.error,
        recovery: sync_result.recovery,
        andon_signal: sync_result.andon_signal,
        receipt_path: receipt_file_path,
        gates: Vec::new(),
    })
}

/// Read the installed packs from a packs.lock JSON file.
///
/// Returns a list of `"<id>@<version>"` strings, one per installed pack.
/// Returns an empty vec if the file is absent or cannot be parsed.
fn read_installed_packs(lock_path: &str) -> Vec<String> {
    let path = std::path::Path::new(lock_path);
    if !path.exists() {
        return vec![];
    }
    let content = match std::fs::read_to_string(path) {
        Ok(c) => c,
        Err(e) => {
            log::warn!("Failed to read lock file {}: {}", lock_path, e);
            return vec![];
        }
    };
    let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) else {
        return vec![];
    };
    val.get("packs")
        .and_then(|p| p.as_object())
        .map(|obj| {
            obj.iter()
                .map(|(id, entry)| {
                    let version = entry
                        .get("version")
                        .and_then(|v| v.as_str())
                        .unwrap_or("unknown");
                    format!("{}@{}", id, version)
                })
                .collect()
        })
        .unwrap_or_default()
}

/// Emit a cryptographically signed sync receipt to `.ggen/receipts/`.
///
/// Writes two files on success:
///   `.ggen/receipts/sync-<YYYYMMDD-HHMMSS>.json` — timestamped archive copy
///   `.ggen/receipts/latest.json`                  — always points at the latest receipt
///
/// The receipt captures:
///   - SHA-256 hash of `ggen.toml` as an input hash
///   - One entry per installed pack (`pack:<id>@<version>`) as input hashes
///   - SHA-256 hash of each generated file as output hashes
///   - Ed25519 signature over the entire payload
///   - Hash of the previous receipt (chaining)
///
/// Signing key is persisted at `.ggen/keys/signing.key` (hex).
/// A new keypair is generated only when no key file exists; existing keys are
/// never overwritten.
///
/// Returns the path written to `latest.json` on success, or a string error on
/// failure.
fn emit_sync_receipt(
    generated_file_paths: &[String], installed_packs: &[String],
) -> std::result::Result<String, String> {
    use std::fs;

    // 1. Ensure .ggen/keys/ directory exists.
    let keys_dir = std::path::Path::new(".ggen/keys");
    fs::create_dir_all(keys_dir).map_err(|e| e.to_string())?;

    // 2. Load or generate signing keypair — never overwrite an existing key.
    let signing_key_path = keys_dir.join("signing.key");
    let verifying_key_path = keys_dir.join("verifying.key");

    let signing_key = if signing_key_path.exists() {
        let hex_str = fs::read_to_string(&signing_key_path).map_err(|e| e.to_string())?;
        let bytes = hex::decode(hex_str.trim()).map_err(|e| e.to_string())?;
        let sk_bytes: [u8; 32] = bytes
            .try_into()
            .map_err(|_| "Invalid signing key length (expected 32 bytes)".to_string())?;
        ed25519_dalek::SigningKey::from_bytes(&sk_bytes)
    } else {
        let (sk, vk) = generate_keypair();
        fs::write(&signing_key_path, hex::encode(sk.to_bytes())).map_err(|e| e.to_string())?;
        fs::write(&verifying_key_path, hex::encode(vk.to_bytes())).map_err(|e| e.to_string())?;
        sk
    };

    // 3. Load previous receipt for chaining.
    let receipts_dir = std::path::Path::new(".ggen/receipts");
    let latest_path = receipts_dir.join("latest.json");
    let previous_receipt: Option<Receipt> = if latest_path.exists() {
        let content = fs::read_to_string(&latest_path).ok();
        content.and_then(|c| serde_json::from_str(&c).ok())
    } else {
        None
    };

    // 4. Build input hashes: the FULL O* closure — every input capable of changing
    //    the artifact, plus the actuator identity. R ⊢ A = μ(O*) is false unless R
    //    binds O*; hashing only the manifest would leave the ontology and templates
    //    (which determine the output) outside the witnessed closure.
    let mut input_hashes: Vec<String> = Vec::new();
    // Actuator identity: which μ (and version) produced this.
    input_hashes.push(format!("actuator:ggen-sync@{}", env!("CARGO_PKG_VERSION")));
    if let Ok(manifest_content) = std::fs::read_to_string("ggen.toml") {
        input_hashes.push(format!(
            "ggen.toml:{}",
            hash_data(manifest_content.as_bytes())
        ));
        // Bind the rest of the closure: ontology + imports + external query/template
        // files. Inline inference rules, queries, and templates live inside ggen.toml
        // and are therefore already bound by the manifest hash above.
        if let Ok(manifest) = ggen_core::manifest::ManifestParser::parse_str(&manifest_content) {
            let mut closure: Vec<PathBuf> = vec![manifest.ontology.source.clone()];
            closure.extend(manifest.ontology.imports.iter().cloned());
            for rule in &manifest.generation.rules {
                if let ggen_core::manifest::QuerySource::File { file } = &rule.query {
                    closure.push(file.clone());
                }
                if let ggen_core::manifest::TemplateSource::File { file } = &rule.template {
                    closure.push(file.clone());
                }
            }
            for p in closure {
                match std::fs::read(&p) {
                    // Honest: a closure input that cannot be read is recorded as MISSING,
                    // never silently dropped — a verifier must see the gap.
                    Ok(content) => {
                        input_hashes.push(format!("{}:{}", p.display(), hash_data(&content)));
                    }
                    Err(_) => input_hashes.push(format!("{}:MISSING", p.display())),
                }
            }
        }
    }
    for pack in installed_packs {
        input_hashes.push(format!("pack:{}", pack));
    }

    // 5. Build output hashes from generated file paths.
    let output_hashes: Vec<String> = generated_file_paths
        .iter()
        .filter_map(|path| {
            fs::read(path)
                .ok()
                .map(|content| format!("{}:{}", path, hash_data(&content)))
        })
        .collect();

    // 6. Create and sign the receipt.
    let operation_id = uuid::Uuid::new_v4().to_string();
    let mut receipt = Receipt::new(operation_id, input_hashes, output_hashes, None);

    // Chaining: link to previous receipt if it exists
    if let Some(prev) = previous_receipt {
        receipt = receipt.chain(&prev).map_err(|e| e.to_string())?;
    }

    let signed_receipt = receipt.sign(&signing_key).map_err(|e| e.to_string())?;

    // 7. Write timestamped archive copy.
    fs::create_dir_all(receipts_dir).map_err(|e| e.to_string())?;
    let timestamp = Utc::now().format("%Y%m%d-%H%M%S");
    let receipt_json = serde_json::to_string_pretty(&signed_receipt).map_err(|e| e.to_string())?;
    let timestamped_path = receipts_dir.join(format!("sync-{}.json", timestamp));
    fs::write(&timestamped_path, &receipt_json).map_err(|e| e.to_string())?;

    // 8. Overwrite latest.json so the golden-path verify command always works.
    fs::write(&latest_path, &receipt_json).map_err(|e| e.to_string())?;

    Ok(latest_path.to_string_lossy().into_owned())
}

/// Invoke the low-level `ggen_core::sync::sync()` pipeline directly.
///
/// Activated when the user supplies `--queries`.  Bypasses `ggen.toml` entirely.
///
/// Usage:
/// ```bash
/// ggen sync --ontology ./businessos.ttl --queries ./queries/businessos/ --output ./generated/ --language go
/// ```
#[allow(unused_variables)]
fn run_low_level_pipeline(
    ontology: Option<String>, queries_dir: String, output_dir: Option<String>,
    language: Option<String>, dry_run: bool,
) -> VerbResult<SyncOutput> {
    log::info!("[μ₁/5] CONSTRUCT: Loading ontology...");
    log::info!("[μ₂/5] SELECT: Running SPARQL queries...");
    log::info!("[μ₃/5] Tera: Generating code...");
    log::info!("[μ₄/5] Canonicalizing: Validating soundness...");

    let ontology_path = PathBuf::from(ontology.unwrap_or_else(|| "ontology.ttl".to_string()));
    let queries_path = PathBuf::from(queries_dir);
    let output_path = PathBuf::from(output_dir.unwrap_or_else(|| ".".to_string()));

    let lang: SyncLanguage =
        language.as_deref().unwrap_or("auto").parse().map_err(
            |e: ggen_core::sync::SyncError| NounVerbError::execution_error(e.to_string()),
        )?;

    let config = SyncConfig {
        ontology_path,
        queries_dir: queries_path,
        output_dir: output_path,
        language: lang,
        validate: true,
        dry_run,
    };

    let result =
        low_level_sync(config).map_err(|e| NounVerbError::execution_error(e.to_string()))?;

    let files: Vec<SyncedFile> = result
        .files_generated
        .iter()
        .map(|p| SyncedFile {
            path: p.display().to_string(),
            size_bytes: if dry_run {
                0
            } else {
                std::fs::metadata(p).map_or(0, |m| m.len() as usize)
            },
            action: "created".to_string(),
            rule: "low-level-generator".to_string(),
        })
        .collect();

    let synced_file_paths: Vec<String> = files.iter().map(|f| f.path.clone()).collect();
    let installed_packs = read_installed_packs(".ggen/packs.lock");

    // A dry-run is a pure PREVIEW (sensing): no artifacts written ⇒ no receipt.
    let receipt_file_path = if dry_run {
        None
    } else {
        log::info!("[μ₅/5] Receipt: Generating verification...");
        Some(
            emit_sync_receipt(&synced_file_paths, &installed_packs).map_err(|e| {
                clap_noun_verb::NounVerbError::execution_error(format!("Audit failure: {}", e))
            })?,
        )
    };

    let violation_msg = if result.soundness_violations.is_empty() {
        None
    } else {
        Some(format!(
            "{} soundness violation(s): {}",
            result.soundness_violations.len(),
            result
                .soundness_violations
                .iter()
                .map(|v| v.rule.as_str())
                .collect::<Vec<_>>()
                .join(", ")
        ))
    };

    Ok(SyncOutput {
        status: "success".to_string(),
        files_synced: files.len(),
        duration_ms: result.elapsed_ms,
        files,
        inference_rules_executed: 0,
        generation_rules_executed: result.files_generated.len(),
        audit_trail: None,
        error: violation_msg,
        recovery: None,
        andon_signal: None,
        receipt_path: receipt_file_path,
        gates: Vec::new(),
    })
}

/// Build SyncOptions from CLI arguments
///
/// This helper keeps the verb function thin by extracting option building.
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
fn build_sync_options(
    manifest: Option<String>, output_dir: Option<String>, dry_run: Option<bool>,
    force: Option<bool>, audit: Option<bool>, rule: Option<String>, verbose: Option<bool>,
    watch: Option<bool>, validate_only: Option<bool>, format: Option<String>, timeout: Option<u64>,
    stage: Option<String>, ontology: Option<String>,
) -> Result<SyncOptions, NounVerbError> {
    let mut options = SyncOptions::new();

    // Set manifest path
    options.manifest_path = PathBuf::from(manifest.unwrap_or_else(|| "ggen.toml".to_string()));

    // Set optional output directory
    if let Some(dir) = output_dir {
        options.output_dir = Some(PathBuf::from(dir));
    }

    // Set boolean flags (SyncOptions.flags was refactored into mode/behavior sub-structs)
    options.flags.mode.dry_run = dry_run.unwrap_or(false);
    options.flags.behavior.force = force.unwrap_or(false);
    options.flags.behavior.audit = audit.unwrap_or(false);
    options.flags.behavior.verbose = verbose.unwrap_or(false);
    options.flags.mode.watch = watch.unwrap_or(false);
    options.flags.mode.validate_only = validate_only.unwrap_or(false);

    // Set selected rules
    if let Some(r) = rule {
        options.selected_rules = Some(vec![r]);
    }

    // Set output format
    if let Some(fmt) = format {
        options.output_format = match fmt.to_lowercase().as_str() {
            "text" => OutputFormat::Text,
            "json" => OutputFormat::Json,
            _ => {
                return Err(NounVerbError::execution_error(format!(
                    "error[E0005]: Invalid output format '{}'\n  |\n  = help: Valid formats: text, json",
                    fmt
                )))
            }
        };
    }

    // Set timeout
    if let Some(t) = timeout {
        options.timeout_ms = Some(t);
    }

    // A2A-specific options
    if let Some(s) = stage {
        // Validate stage format: μ₁, μ₂, μ₃, μ₄, μ₅
        if !matches!(
            s.as_str(),
            "μ₁" | "μ₂" | "μ₃" | "μ₄" | "μ₅" | "mu1" | "mu2" | "mu3" | "mu4" | "mu5"
        ) {
            return Err(NounVerbError::execution_error(format!(
                "error[E0006]: Invalid stage '{}'\n  |\n  = help: Valid stages: μ₁, μ₂, μ₃, μ₄, μ₅ (or mu1-mu5)",
                s
            )));
        }
        options.a2a_stage = Some(s);
    }

    if let Some(ont) = ontology {
        options.ontology_path = Some(PathBuf::from(ont));
    }

    Ok(options)
}