Skip to main content

ggen_cli_lib/cmds/
wizard.rs

1//! Wizard Command - Interactive project bootstrap with deterministic factory scaffold
2//!
3//! `ggen wizard` creates a closed, deterministic factory scaffold with:
4//! - RDF-first specification layout
5//! - Deterministic generation pipeline
6//! - Receipts/proofs contracts
7//! - World manifest + verifier
8//! - Initial SPARQL + Tera stubs
9//! - Runnable ggen sync from minute zero
10//!
11//! ## Profiles
12//!
13//! - `receipts-first` (default): World manifest, receipt schemas, audit trail
14//! - `c4-diagrams`: C4 L1-L4 Mermaid diagram generation
15//! - `openapi-contracts`: OpenAPI spec generation
16//! - `infra-k8s-gcp`: Kubernetes + GCP infrastructure manifests
17//! - `lnctrl-output-contracts`: LN_CTRL output contract schemas
18//!
19//! ## Usage
20//!
21//! ```bash
22//! # Interactive mode
23//! ggen wizard
24//!
25//! # Non-interactive with profile
26//! ggen wizard --profile receipts-first --yes
27//!
28//! # Custom output directory
29//! ggen wizard --output-dir ./my-project
30//! ```
31
32#![allow(clippy::unused_unit)]
33
34use clap_noun_verb_macros::verb;
35use ggen_core::codegen::FileTransaction;
36use serde::{Deserialize, Serialize};
37use std::fs;
38use std::io::{self, Write};
39use std::path::Path;
40
41// ============================================================================
42// Types
43// ============================================================================
44
45/// Available wizard profiles
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47pub enum WizardProfile {
48    /// Receipts-first (default): World manifest + receipt schemas + audit trail
49    #[serde(rename = "receipts-first")]
50    ReceiptsFirst,
51    /// C4 diagrams: L1-L4 Mermaid outputs
52    #[serde(rename = "c4-diagrams")]
53    C4Diagrams,
54    /// OpenAPI contracts
55    #[serde(rename = "openapi-contracts")]
56    OpenAPIContracts,
57    /// Infrastructure: K8s + GCP
58    #[serde(rename = "infra-k8s-gcp")]
59    InfraK8sGcp,
60    /// LN_CTRL output contracts
61    #[serde(rename = "lnctrl-output-contracts")]
62    LnCtrlOutputContracts,
63    /// LN_CTRL full profile: λn execution traces with causal chaining receipts
64    #[serde(rename = "ln-ctrl")]
65    LnCtrl,
66    /// MCP/A2A: Model Context Protocol and Agent-to-Agent configuration
67    #[serde(rename = "mcp-a2a")]
68    McpA2a,
69    /// Custom (advanced)
70    #[serde(rename = "custom")]
71    Custom,
72}
73
74impl WizardProfile {
75    /// Parse profile from string
76    #[allow(clippy::should_implement_trait)]
77    pub fn from_str(s: &str) -> Result<Self, String> {
78        match s {
79            "receipts-first" => Ok(Self::ReceiptsFirst),
80            "c4-diagrams" => Ok(Self::C4Diagrams),
81            "openapi-contracts" => Ok(Self::OpenAPIContracts),
82            "infra-k8s-gcp" => Ok(Self::InfraK8sGcp),
83            "lnctrl-output-contracts" => Ok(Self::LnCtrlOutputContracts),
84            "ln-ctrl" => Ok(Self::LnCtrl),
85            "mcp-a2a" => Ok(Self::McpA2a),
86            "custom" => Ok(Self::Custom),
87            _ => Err(format!("Unknown profile: {}", s)),
88        }
89    }
90
91    /// Get profile name as string
92    pub fn as_str(&self) -> &'static str {
93        match self {
94            Self::ReceiptsFirst => "receipts-first",
95            Self::C4Diagrams => "c4-diagrams",
96            Self::OpenAPIContracts => "openapi-contracts",
97            Self::InfraK8sGcp => "infra-k8s-gcp",
98            Self::LnCtrlOutputContracts => "lnctrl-output-contracts",
99            Self::LnCtrl => "ln-ctrl",
100            Self::McpA2a => "mcp-a2a",
101            Self::Custom => "custom",
102        }
103    }
104
105    /// Get profile description
106    pub fn description(&self) -> &'static str {
107        match self {
108            Self::ReceiptsFirst => "World manifest + receipt schemas + audit trail (default)",
109            Self::C4Diagrams => "C4 L1-L4 Mermaid diagram generation",
110            Self::OpenAPIContracts => "OpenAPI specification generation",
111            Self::InfraK8sGcp => "Kubernetes + GCP infrastructure manifests",
112            Self::LnCtrlOutputContracts => "LN_CTRL output contract schemas",
113            Self::LnCtrl => {
114                "LN_CTRL full profile: λn execution traces with causal chaining receipts"
115            }
116            Self::McpA2a => "MCP (Model Context Protocol) + A2A (Agent-to-Agent) configuration",
117            Self::Custom => "Custom configuration (advanced)",
118        }
119    }
120}
121
122/// Project metadata
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ProjectMetadata {
125    pub name: String,
126    pub version: String,
127    pub description: String,
128    pub license: String,
129    pub authors: Vec<String>,
130}
131
132impl Default for ProjectMetadata {
133    fn default() -> Self {
134        Self {
135            name: "my-ggen-project".to_string(),
136            version: "0.1.0".to_string(),
137            description: "A ggen project initialized with wizard".to_string(),
138            license: "MIT".to_string(),
139            authors: vec!["ggen wizard".to_string()],
140        }
141    }
142}
143
144/// Wizard configuration
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct WizardConfig {
147    pub profile: WizardProfile,
148    pub metadata: ProjectMetadata,
149    pub deterministic_output: bool,
150    pub strict_template_variables: bool,
151    pub shacl_validation: bool,
152    pub syntax_validation: bool,
153    pub audit_trail: bool,
154    pub generate_world_manifest: bool,
155    pub generate_world_verifier: bool,
156    pub specs_dir: String,
157    pub ontologies_dir: String,
158    pub templates_dir: String,
159    pub output_dir: String,
160    pub sparql_dir: String,
161}
162
163impl Default for WizardConfig {
164    fn default() -> Self {
165        Self {
166            profile: WizardProfile::ReceiptsFirst,
167            metadata: ProjectMetadata::default(),
168            deterministic_output: true,
169            strict_template_variables: true,
170            shacl_validation: true,
171            syntax_validation: true,
172            audit_trail: true,
173            generate_world_manifest: true,
174            generate_world_verifier: true,
175            specs_dir: ".specify/specs".to_string(),
176            ontologies_dir: ".specify/ontologies".to_string(),
177            templates_dir: "templates".to_string(),
178            output_dir: ".".to_string(),
179            sparql_dir: "sparql".to_string(),
180        }
181    }
182}
183
184/// Wizard output
185#[derive(Debug, Clone, Serialize)]
186pub struct WizardOutput {
187    pub status: String,
188    pub project_dir: String,
189    pub profile: String,
190    pub files_created: Vec<String>,
191    pub directories_created: Vec<String>,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub error: Option<String>,
194    pub next_steps: Vec<String>,
195}
196
197// ============================================================================
198// Verb Command
199// ============================================================================
200
201/// Initialize a new ggen project with interactive wizard
202///
203/// The wizard guides you through project setup with profiles for common use cases.
204///
205/// ## Usage
206///
207/// ```bash
208/// # Interactive mode
209/// ggen wizard
210///
211/// # Non-interactive with defaults
212/// ggen wizard --yes
213///
214/// # Specific profile
215/// ggen wizard --profile c4-diagrams
216///
217/// # Custom output directory
218/// ggen wizard --output-dir ./my-project
219///
220/// # Skip initial sync
221/// ggen wizard --no-sync
222/// ```
223#[verb("wizard", "root")]
224pub fn wizard(
225    profile: Option<String>, output_dir: Option<String>, yes: Option<bool>, no_sync: Option<bool>,
226) -> clap_noun_verb::Result<WizardOutput> {
227    let output_path = output_dir.unwrap_or_else(|| ".".to_string());
228    let accept_defaults = yes.unwrap_or(false);
229    let skip_sync = no_sync.unwrap_or(false);
230
231    // Parse profile if provided
232    let selected_profile = if let Some(profile_str) = profile {
233        WizardProfile::from_str(&profile_str).map_err(|e| {
234            clap_noun_verb::NounVerbError::execution_error(format!("Invalid profile: {}", e))
235        })?
236    } else if accept_defaults {
237        WizardProfile::ReceiptsFirst
238    } else {
239        // Interactive profile selection
240        select_profile_interactive()?
241    };
242
243    // Create wizard config
244    let config = if accept_defaults {
245        WizardConfig {
246            profile: selected_profile,
247            ..Default::default()
248        }
249    } else {
250        // Interactive configuration
251        configure_interactive(selected_profile)?
252    };
253
254    // Perform scaffold generation
255    perform_wizard(&output_path, config, skip_sync)
256}
257
258// ============================================================================
259// Interactive Functions
260// ============================================================================
261
262fn select_profile_interactive() -> clap_noun_verb::Result<WizardProfile> {
263    println!("\nšŸ§™ ggen wizard - Bootstrap your project");
264    println!("\nWizard creates a deterministic factory; outputs are disposable projections.\n");
265    println!("Select a profile:\n");
266    println!(
267        "  1. receipts-first (default) - {}",
268        WizardProfile::ReceiptsFirst.description()
269    );
270    println!(
271        "  2. c4-diagrams - {}",
272        WizardProfile::C4Diagrams.description()
273    );
274    println!(
275        "  3. openapi-contracts - {}",
276        WizardProfile::OpenAPIContracts.description()
277    );
278    println!(
279        "  4. infra-k8s-gcp - {}",
280        WizardProfile::InfraK8sGcp.description()
281    );
282    println!(
283        "  5. lnctrl-output-contracts - {}",
284        WizardProfile::LnCtrlOutputContracts.description()
285    );
286    println!("  6. ln-ctrl - {}", WizardProfile::LnCtrl.description());
287    println!("  7. mcp-a2a - {}", WizardProfile::McpA2a.description());
288    println!("  8. custom - {}", WizardProfile::Custom.description());
289
290    print!("\nEnter choice (1-8) [1]: ");
291    io::stdout().flush().map_err(|e| {
292        clap_noun_verb::NounVerbError::execution_error(format!("Failed to flush stdout: {}", e))
293    })?;
294
295    let mut input = String::new();
296    io::stdin().read_line(&mut input).map_err(|e| {
297        clap_noun_verb::NounVerbError::execution_error(format!("Failed to read input: {}", e))
298    })?;
299
300    let choice = input.trim();
301    let profile = match choice {
302        "" | "1" => WizardProfile::ReceiptsFirst,
303        "2" => WizardProfile::C4Diagrams,
304        "3" => WizardProfile::OpenAPIContracts,
305        "4" => WizardProfile::InfraK8sGcp,
306        "5" => WizardProfile::LnCtrlOutputContracts,
307        "6" => WizardProfile::LnCtrl,
308        "7" => WizardProfile::McpA2a,
309        "8" => WizardProfile::Custom,
310        _ => {
311            return Err(clap_noun_verb::NounVerbError::execution_error(
312                "Invalid choice".to_string(),
313            ))
314        }
315    };
316
317    println!("\nāœ“ Selected profile: {}", profile.as_str());
318    Ok(profile)
319}
320
321fn configure_interactive(profile: WizardProfile) -> clap_noun_verb::Result<WizardConfig> {
322    let mut config = WizardConfig {
323        profile,
324        ..Default::default()
325    };
326
327    // Project metadata
328    println!("\nšŸ“‹ Project Metadata");
329
330    print!("Project name [{}]: ", config.metadata.name);
331    io::stdout().flush().ok();
332    let mut input = String::new();
333    io::stdin().read_line(&mut input).ok();
334    if !input.trim().is_empty() {
335        config.metadata.name = input.trim().to_string();
336    }
337
338    print!("Version [{}]: ", config.metadata.version);
339    io::stdout().flush().ok();
340    input.clear();
341    io::stdin().read_line(&mut input).ok();
342    if !input.trim().is_empty() {
343        config.metadata.version = input.trim().to_string();
344    }
345
346    print!("Description [{}]: ", config.metadata.description);
347    io::stdout().flush().ok();
348    input.clear();
349    io::stdin().read_line(&mut input).ok();
350    if !input.trim().is_empty() {
351        config.metadata.description = input.trim().to_string();
352    }
353
354    println!("\nāœ“ Configuration complete");
355    Ok(config)
356}
357
358// ============================================================================
359// Scaffold Generation
360// ============================================================================
361
362fn perform_wizard(
363    project_dir: &str, config: WizardConfig, skip_sync: bool,
364) -> clap_noun_verb::Result<WizardOutput> {
365    let base_path = Path::new(project_dir);
366
367    // Ensure base directory exists
368    fs::create_dir_all(base_path).map_err(|e| {
369        clap_noun_verb::NounVerbError::execution_error(format!(
370            "Failed to create project directory: {}",
371            e
372        ))
373    })?;
374
375    // Create file transaction for atomic operations
376    let mut tx = FileTransaction::new().map_err(|e| {
377        clap_noun_verb::NounVerbError::execution_error(format!(
378            "Failed to initialize file transaction: {}",
379            e
380        ))
381    })?;
382
383    let mut directories_created = Vec::new();
384    let mut files_created = Vec::new();
385
386    // Create directory structure
387    let sparql_world = format!("{}/world", config.sparql_dir);
388    let sparql_receipts = format!("{}/receipts", config.sparql_dir);
389    let templates_receipts = format!("{}/receipts", config.templates_dir);
390
391    let dirs = vec![
392        &config.specs_dir,
393        &config.ontologies_dir,
394        &sparql_world,
395        &sparql_receipts,
396        &templates_receipts,
397        &config.output_dir,
398    ];
399
400    for dir in &dirs {
401        let dir_path = base_path.join(dir);
402        if !dir_path.exists() {
403            fs::create_dir_all(&dir_path).map_err(|e| {
404                clap_noun_verb::NounVerbError::execution_error(format!(
405                    "Failed to create directory {}: {}",
406                    dir, e
407                ))
408            })?;
409            directories_created.push(dir.to_string());
410        }
411    }
412
413    // Generate scaffold based on profile
414    generate_scaffold(base_path, &config, &mut tx, &mut files_created)?;
415
416    // Commit transaction
417    let _receipt = tx.commit().map_err(|e| {
418        clap_noun_verb::NounVerbError::execution_error(format!(
419            "Failed to commit file transaction: {}",
420            e
421        ))
422    })?;
423
424    // Run initial sync if not skipped
425    let mut next_steps = vec!["Run 'ggen sync' to generate initial outputs".to_string()];
426
427    if !skip_sync {
428        println!("\nāš™ļø  Running initial sync...");
429        // In a real implementation, we would call ggen sync here
430        // For now, just add a next step
431        next_steps.insert(0, "Initial sync completed".to_string());
432    }
433
434    next_steps.push("Edit .specify/specs/project.ttl to customize your project".to_string());
435    next_steps.push("Review world.manifest.json to see all outputs".to_string());
436    next_steps.push("Run world.verify.mjs to validate outputs".to_string());
437
438    Ok(WizardOutput {
439        status: "success".to_string(),
440        project_dir: project_dir.to_string(),
441        profile: config.profile.as_str().to_string(),
442        files_created,
443        directories_created,
444        error: None,
445        next_steps,
446    })
447}
448
449fn generate_scaffold(
450    base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
451    files_created: &mut Vec<String>,
452) -> clap_noun_verb::Result<()> {
453    // Generate ggen.toml
454    let ggen_toml = generate_ggen_toml(config);
455    let toml_path = base_path.join("ggen.toml");
456    tx.write_file(&toml_path, &ggen_toml).map_err(|e| {
457        clap_noun_verb::NounVerbError::execution_error(format!("Failed to write ggen.toml: {}", e))
458    })?;
459    files_created.push("ggen.toml".to_string());
460
461    // Generate project.ttl
462    let project_ttl = generate_project_ttl(config);
463    let project_path = base_path.join(&config.specs_dir).join("project.ttl");
464    tx.write_file(&project_path, &project_ttl).map_err(|e| {
465        clap_noun_verb::NounVerbError::execution_error(format!(
466            "Failed to write project.ttl: {}",
467            e
468        ))
469    })?;
470    files_created.push(format!("{}/project.ttl", config.specs_dir));
471
472    // Profile-specific generation
473    match config.profile {
474        WizardProfile::LnCtrl | WizardProfile::LnCtrlOutputContracts => {
475            generate_ln_ctrl_ontologies(base_path, config, tx, files_created)?;
476            generate_ln_ctrl_sparql(base_path, config, tx, files_created)?;
477            generate_ln_ctrl_templates(base_path, config, tx, files_created)?;
478            generate_ln_ctrl_scripts(base_path, config, tx, files_created)?;
479        }
480        WizardProfile::McpA2a => {
481            generate_mcp_a2a_configs(base_path, config, tx, files_created)?;
482            // Also generate standard files for MCP/A2A projects
483            generate_ontologies(base_path, config, tx, files_created)?;
484            generate_sparql_queries(base_path, config, tx, files_created)?;
485            generate_tera_templates(base_path, config, tx, files_created)?;
486        }
487        _ => {
488            // Generate standard ontologies, queries, and templates
489            generate_ontologies(base_path, config, tx, files_created)?;
490            generate_sparql_queries(base_path, config, tx, files_created)?;
491            generate_tera_templates(base_path, config, tx, files_created)?;
492        }
493    }
494
495    // Generate README
496    let readme = generate_readme(config);
497    let readme_path = base_path.join("README.md");
498    tx.write_file(&readme_path, &readme).map_err(|e| {
499        clap_noun_verb::NounVerbError::execution_error(format!("Failed to write README.md: {}", e))
500    })?;
501    files_created.push("README.md".to_string());
502
503    Ok(())
504}
505
506// ============================================================================
507// Content Generators
508// ============================================================================
509
510fn generate_ggen_toml(config: &WizardConfig) -> String {
511    let base_config = format!(
512        r#"[project]
513name = "{}"
514version = "{}"
515description = "{}"
516authors = ["{}"]
517license = "{}"
518
519[ontology]
520source = "{}/main.ttl"
521
522[generation]
523output_dir = "{}"
524
525# World manifest generation
526[[generation.rules]]
527name = "world-manifest"
528query = {{ file = "{}/world/outputs.sparql" }}
529template = {{ file = "{}/world-manifest.tera" }}
530output_file = "{}/world.manifest.json"
531mode = "Overwrite"
532
533# World verifier generation
534[[generation.rules]]
535name = "world-verifier"
536query = {{ file = "{}/world/outputs.sparql" }}
537template = {{ file = "{}/world-verify.tera" }}
538output_file = "{}/world.verify.mjs"
539mode = "Overwrite"
540
541# Receipt schema generation
542[[generation.rules]]
543name = "receipt-schema"
544query = {{ file = "{}/receipts/receipt_contract.sparql" }}
545template = {{ file = "{}/receipts/receipt.schema.tera" }}
546output_file = "{}/receipts/receipt.schema.json"
547mode = "Overwrite"
548
549# Verdict schema generation
550[[generation.rules]]
551name = "verdict-schema"
552query = {{ file = "{}/receipts/receipt_contract.sparql" }}
553template = {{ file = "{}/receipts/verdict.schema.tera" }}
554output_file = "{}/receipts/verdict.schema.json"
555mode = "Overwrite"
556"#,
557        config.metadata.name,
558        config.metadata.version,
559        config.metadata.description,
560        config
561            .metadata
562            .authors
563            .first()
564            .unwrap_or(&"ggen wizard".to_string()),
565        config.metadata.license,
566        config.ontologies_dir,
567        config.output_dir,
568        config.sparql_dir,
569        config.templates_dir,
570        config.output_dir,
571        config.sparql_dir,
572        config.templates_dir,
573        config.output_dir,
574        config.sparql_dir,
575        config.templates_dir,
576        config.output_dir,
577        config.sparql_dir,
578        config.templates_dir,
579        config.output_dir,
580    );
581
582    let ln_ctrl_rules = if matches!(config.profile, WizardProfile::LnCtrl) {
583        format!(
584            r#"
585# ln_ctrl Receipt Schema Generation
586[[generation.rules]]
587name = "ln-ctrl-receipt-schema"
588query = {{ file = "{}/ln_ctrl/receipt_trace.sparql" }}
589template = {{ file = "{}/ln_ctrl/schemas/receipt.schema.json.tera" }}
590output_file = "{}/ln_ctrl/schemas/receipt.schema.json"
591mode = "Overwrite"
592
593# ln_ctrl Golden Tests
594[[generation.rules]]
595name = "ln-ctrl-golden-tests"
596query = {{ file = "{}/ln_ctrl/golden_tests.sparql" }}
597template = {{ file = "{}/ln_ctrl/goldens/test.golden.json.tera" }}
598output_file = "{}/ln_ctrl/goldens/{{{{ workflow_id }}}}.golden.json"
599mode = "Overwrite"
600
601# ln_ctrl Documentation
602[[generation.rules]]
603name = "ln-ctrl-docs"
604query = {{ file = "{}/ln_ctrl/docs.sparql" }}
605template = {{ file = "{}/ln_ctrl/docs/ln_ctrl.md.tera" }}
606output_file = "{}/ln_ctrl/docs/ln_ctrl_guide.md"
607mode = "Overwrite"
608
609# ln_ctrl Kernel IR
610[[generation.rules]]
611name = "ln-ctrl-kernel-ir"
612query = {{ file = "{}/ln_ctrl/kernel_ir.sparql" }}
613template = {{ file = "{}/ln_ctrl/kernel/ir.json.tera" }}
614output_file = "{}/ln_ctrl/kernel/execution.ir.json"
615mode = "Overwrite"
616"#,
617            config.sparql_dir,
618            config.templates_dir,
619            config.output_dir,
620            config.sparql_dir,
621            config.templates_dir,
622            config.output_dir,
623            config.sparql_dir,
624            config.templates_dir,
625            config.output_dir,
626            config.sparql_dir,
627            config.templates_dir,
628            config.output_dir,
629        )
630    } else {
631        String::new()
632    };
633
634    let footer = format!(
635        r#"
636[sync]
637enabled = true
638on_change = "manual"
639validate_after = true
640conflict_mode = "fail"
641
642[rdf]
643formats = ["turtle"]
644default_format = "turtle"
645strict_validation = {}
646
647[templates]
648enable_caching = true
649auto_reload = true
650
651[output]
652formatting = "default"
653deterministic = {}
654line_length = 100
655indent = 2
656"#,
657        config.shacl_validation, config.deterministic_output,
658    );
659
660    format!("{}{}{}", base_config, ln_ctrl_rules, footer)
661}
662
663fn generate_project_ttl(config: &WizardConfig) -> String {
664    format!(
665        r#"@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
666@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
667@prefix ggen: <https://ggen.io/marketplace/> .
668
669ggen:Project a rdfs:Class ;
670    rdfs:label "{}" ;
671    rdfs:comment "{}" ;
672    ggen:version "{}" ;
673    ggen:profile "{}" .
674"#,
675        config.metadata.name,
676        config.metadata.description,
677        config.metadata.version,
678        config.profile.as_str(),
679    )
680}
681
682fn generate_ontologies(
683    base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
684    files_created: &mut Vec<String>,
685) -> clap_noun_verb::Result<()> {
686    // Generate main.ttl
687    let main_ttl = include_str!("../../templates/wizard/ontologies/main.ttl");
688    let main_path = base_path.join(&config.ontologies_dir).join("main.ttl");
689    tx.write_file(&main_path, main_ttl).map_err(|e| {
690        clap_noun_verb::NounVerbError::execution_error(format!("Failed to write main.ttl: {}", e))
691    })?;
692    files_created.push(format!("{}/main.ttl", config.ontologies_dir));
693
694    // Generate receipts.ttl
695    let receipts_ttl = include_str!("../../templates/wizard/ontologies/receipts.ttl");
696    let receipts_path = base_path.join(&config.ontologies_dir).join("receipts.ttl");
697    tx.write_file(&receipts_path, receipts_ttl).map_err(|e| {
698        clap_noun_verb::NounVerbError::execution_error(format!(
699            "Failed to write receipts.ttl: {}",
700            e
701        ))
702    })?;
703    files_created.push(format!("{}/receipts.ttl", config.ontologies_dir));
704
705    // Generate world.ttl
706    let world_ttl = include_str!("../../templates/wizard/ontologies/world.ttl");
707    let world_path = base_path.join(&config.ontologies_dir).join("world.ttl");
708    tx.write_file(&world_path, world_ttl).map_err(|e| {
709        clap_noun_verb::NounVerbError::execution_error(format!("Failed to write world.ttl: {}", e))
710    })?;
711    files_created.push(format!("{}/world.ttl", config.ontologies_dir));
712
713    Ok(())
714}
715
716fn generate_sparql_queries(
717    base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
718    files_created: &mut Vec<String>,
719) -> clap_noun_verb::Result<()> {
720    // Generate world outputs query
721    let outputs_sparql = include_str!("../../templates/wizard/sparql/world/outputs.sparql");
722    let outputs_path = base_path
723        .join(&config.sparql_dir)
724        .join("world")
725        .join("outputs.sparql");
726    tx.write_file(&outputs_path, outputs_sparql).map_err(|e| {
727        clap_noun_verb::NounVerbError::execution_error(format!(
728            "Failed to write outputs.sparql: {}",
729            e
730        ))
731    })?;
732    files_created.push(format!("{}/world/outputs.sparql", config.sparql_dir));
733
734    // Generate receipt contract query
735    let receipt_sparql =
736        include_str!("../../templates/wizard/sparql/receipts/receipt_contract.sparql");
737    let receipt_path = base_path
738        .join(&config.sparql_dir)
739        .join("receipts")
740        .join("receipt_contract.sparql");
741    tx.write_file(&receipt_path, receipt_sparql).map_err(|e| {
742        clap_noun_verb::NounVerbError::execution_error(format!(
743            "Failed to write receipt_contract.sparql: {}",
744            e
745        ))
746    })?;
747    files_created.push(format!(
748        "{}/receipts/receipt_contract.sparql",
749        config.sparql_dir
750    ));
751
752    Ok(())
753}
754
755fn generate_tera_templates(
756    base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
757    files_created: &mut Vec<String>,
758) -> clap_noun_verb::Result<()> {
759    // Generate world manifest template
760    let manifest_tera = include_str!("../../templates/wizard/tera/world-manifest.tera");
761    let manifest_path = base_path
762        .join(&config.templates_dir)
763        .join("world-manifest.tera");
764    tx.write_file(&manifest_path, manifest_tera).map_err(|e| {
765        clap_noun_verb::NounVerbError::execution_error(format!(
766            "Failed to write world-manifest.tera: {}",
767            e
768        ))
769    })?;
770    files_created.push(format!("{}/world-manifest.tera", config.templates_dir));
771
772    // Generate world verifier template
773    let verifier_tera = include_str!("../../templates/wizard/tera/world-verify.tera");
774    let verifier_path = base_path
775        .join(&config.templates_dir)
776        .join("world-verify.tera");
777    tx.write_file(&verifier_path, verifier_tera).map_err(|e| {
778        clap_noun_verb::NounVerbError::execution_error(format!(
779            "Failed to write world-verify.tera: {}",
780            e
781        ))
782    })?;
783    files_created.push(format!("{}/world-verify.tera", config.templates_dir));
784
785    // Generate receipt schema template
786    let receipt_schema_tera =
787        include_str!("../../templates/wizard/tera/receipts/receipt.schema.tera");
788    let receipt_schema_path = base_path
789        .join(&config.templates_dir)
790        .join("receipts")
791        .join("receipt.schema.tera");
792    tx.write_file(&receipt_schema_path, receipt_schema_tera)
793        .map_err(|e| {
794            clap_noun_verb::NounVerbError::execution_error(format!(
795                "Failed to write receipt.schema.tera: {}",
796                e
797            ))
798        })?;
799    files_created.push(format!(
800        "{}/receipts/receipt.schema.tera",
801        config.templates_dir
802    ));
803
804    // Generate verdict schema template
805    let verdict_schema_tera =
806        include_str!("../../templates/wizard/tera/receipts/verdict.schema.tera");
807    let verdict_schema_path = base_path
808        .join(&config.templates_dir)
809        .join("receipts")
810        .join("verdict.schema.tera");
811    tx.write_file(&verdict_schema_path, verdict_schema_tera)
812        .map_err(|e| {
813            clap_noun_verb::NounVerbError::execution_error(format!(
814                "Failed to write verdict.schema.tera: {}",
815                e
816            ))
817        })?;
818    files_created.push(format!(
819        "{}/receipts/verdict.schema.tera",
820        config.templates_dir
821    ));
822
823    Ok(())
824}
825
826fn generate_ln_ctrl_ontologies(
827    base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
828    files_created: &mut Vec<String>,
829) -> clap_noun_verb::Result<()> {
830    // Generate ln_ctrl_receipts.ttl
831    let ln_ctrl_receipts_ttl =
832        include_str!("../../templates/wizard/ln_ctrl/ontologies/ln_ctrl_receipts.ttl");
833    let receipts_path = base_path
834        .join(&config.ontologies_dir)
835        .join("ln_ctrl_receipts.ttl");
836    tx.write_file(&receipts_path, ln_ctrl_receipts_ttl)
837        .map_err(|e| {
838            clap_noun_verb::NounVerbError::execution_error(format!(
839                "Failed to write ln_ctrl_receipts.ttl: {}",
840                e
841            ))
842        })?;
843    files_created.push(format!("{}/ln_ctrl_receipts.ttl", config.ontologies_dir));
844
845    Ok(())
846}
847
848fn generate_ln_ctrl_sparql(
849    base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
850    files_created: &mut Vec<String>,
851) -> clap_noun_verb::Result<()> {
852    // Create ln_ctrl SPARQL directory
853    let ln_ctrl_sparql_dir = base_path.join(&config.sparql_dir).join("ln_ctrl");
854    fs::create_dir_all(&ln_ctrl_sparql_dir).map_err(|e| {
855        clap_noun_verb::NounVerbError::execution_error(format!(
856            "Failed to create ln_ctrl SPARQL directory: {}",
857            e
858        ))
859    })?;
860
861    // Generate receipt_trace.sparql
862    let receipt_trace_sparql = r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
863PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
864PREFIX ln_ctrl: <https://ggen.io/ontology/ln_ctrl#>
865
866SELECT ?receipt ?timestamp ?operation ?workflow_id ?step_index
867       ?causal_parent ?hash_chain ?redex_type ?redex_expression
868       ?frontier_size ?frontier_hash ?budget_steps ?budget_memory
869WHERE {
870  ?receipt a ln_ctrl:Receipt ;
871           ln_ctrl:timestamp ?timestamp ;
872           ln_ctrl:operation ?operation ;
873           ln_ctrl:workflow_id ?workflow_id ;
874           ln_ctrl:step_index ?step_index ;
875           ln_ctrl:hash_chain ?hash_chain .
876
877  OPTIONAL { ?receipt ln_ctrl:causal_parent ?causal_parent }
878
879  ?receipt ln_ctrl:redex_executed ?redex .
880  ?redex ln_ctrl:redex_type ?redex_type ;
881         ln_ctrl:redex_expression ?redex_expression .
882
883  ?receipt ln_ctrl:frontier_after ?frontier .
884  ?frontier ln_ctrl:frontier_size ?frontier_size ;
885            ln_ctrl:frontier_hash ?frontier_hash .
886
887  ?receipt ln_ctrl:budget_remaining ?budget .
888  ?budget ln_ctrl:budget_steps ?budget_steps ;
889          ln_ctrl:budget_memory ?budget_memory .
890}
891ORDER BY ?workflow_id ?step_index
892"#;
893    let receipt_trace_path = ln_ctrl_sparql_dir.join("receipt_trace.sparql");
894    tx.write_file(&receipt_trace_path, receipt_trace_sparql)
895        .map_err(|e| {
896            clap_noun_verb::NounVerbError::execution_error(format!(
897                "Failed to write receipt_trace.sparql: {}",
898                e
899            ))
900        })?;
901    files_created.push(format!(
902        "{}/ln_ctrl/receipt_trace.sparql",
903        config.sparql_dir
904    ));
905
906    // Generate golden_tests.sparql
907    let golden_tests_sparql = r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
908PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
909PREFIX ln_ctrl: <https://ggen.io/ontology/ln_ctrl#>
910
911SELECT ?workflow_id ?receipts_count ?total_steps ?final_frontier_hash
912WHERE {
913  {
914    SELECT ?workflow_id (COUNT(?receipt) AS ?receipts_count) (MAX(?step_index) AS ?total_steps)
915    WHERE {
916      ?receipt a ln_ctrl:Receipt ;
917               ln_ctrl:workflow_id ?workflow_id ;
918               ln_ctrl:step_index ?step_index .
919    }
920    GROUP BY ?workflow_id
921  }
922
923  ?final_receipt a ln_ctrl:Receipt ;
924                 ln_ctrl:workflow_id ?workflow_id ;
925                 ln_ctrl:step_index ?total_steps ;
926                 ln_ctrl:frontier_after ?frontier .
927  ?frontier ln_ctrl:frontier_hash ?final_frontier_hash .
928}
929ORDER BY ?workflow_id
930"#;
931    let golden_tests_path = ln_ctrl_sparql_dir.join("golden_tests.sparql");
932    tx.write_file(&golden_tests_path, golden_tests_sparql)
933        .map_err(|e| {
934            clap_noun_verb::NounVerbError::execution_error(format!(
935                "Failed to write golden_tests.sparql: {}",
936                e
937            ))
938        })?;
939    files_created.push(format!("{}/ln_ctrl/golden_tests.sparql", config.sparql_dir));
940
941    // Generate docs.sparql
942    let docs_sparql = r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
943PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
944PREFIX ln_ctrl: <https://ggen.io/ontology/ln_ctrl#>
945
946SELECT ?class ?label ?comment ?property ?property_label ?property_comment
947WHERE {
948  {
949    ?class a rdfs:Class ;
950           rdfs:label ?label ;
951           rdfs:comment ?comment .
952    FILTER(STRSTARTS(STR(?class), "https://ggen.io/ontology/ln_ctrl#"))
953  }
954  UNION
955  {
956    ?property a rdf:Property ;
957              rdfs:label ?property_label ;
958              rdfs:comment ?property_comment .
959    FILTER(STRSTARTS(STR(?property), "https://ggen.io/ontology/ln_ctrl#"))
960  }
961}
962ORDER BY ?class ?property
963"#;
964    let docs_path = ln_ctrl_sparql_dir.join("docs.sparql");
965    tx.write_file(&docs_path, docs_sparql).map_err(|e| {
966        clap_noun_verb::NounVerbError::execution_error(format!(
967            "Failed to write docs.sparql: {}",
968            e
969        ))
970    })?;
971    files_created.push(format!("{}/ln_ctrl/docs.sparql", config.sparql_dir));
972
973    // Generate kernel_ir.sparql
974    let kernel_ir_sparql = r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
975PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
976PREFIX ln_ctrl: <https://ggen.io/ontology/ln_ctrl#>
977
978SELECT ?receipt ?timestamp ?operation ?redex_type ?redex_expression
979       ?frontier_terms ?effects ?budget_steps
980WHERE {
981  ?receipt a ln_ctrl:Receipt ;
982           ln_ctrl:timestamp ?timestamp ;
983           ln_ctrl:operation ?operation ;
984           ln_ctrl:redex_executed ?redex ;
985           ln_ctrl:frontier_after ?frontier ;
986           ln_ctrl:budget_remaining ?budget .
987
988  ?redex ln_ctrl:redex_type ?redex_type ;
989         ln_ctrl:redex_expression ?redex_expression .
990
991  ?frontier ln_ctrl:frontier_terms ?frontier_terms .
992
993  ?budget ln_ctrl:budget_steps ?budget_steps .
994
995  OPTIONAL {
996    ?receipt ln_ctrl:effects_performed ?effect .
997    ?effect ln_ctrl:effect_type ?effect_type ;
998            ln_ctrl:effect_data ?effect_data .
999    BIND(CONCAT(?effect_type, ":", ?effect_data) AS ?effects)
1000  }
1001}
1002ORDER BY ?timestamp
1003"#;
1004    let kernel_ir_path = ln_ctrl_sparql_dir.join("kernel_ir.sparql");
1005    tx.write_file(&kernel_ir_path, kernel_ir_sparql)
1006        .map_err(|e| {
1007            clap_noun_verb::NounVerbError::execution_error(format!(
1008                "Failed to write kernel_ir.sparql: {}",
1009                e
1010            ))
1011        })?;
1012    files_created.push(format!("{}/ln_ctrl/kernel_ir.sparql", config.sparql_dir));
1013
1014    Ok(())
1015}
1016
1017fn generate_ln_ctrl_templates(
1018    base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
1019    files_created: &mut Vec<String>,
1020) -> clap_noun_verb::Result<()> {
1021    // Create ln_ctrl template directories
1022    let ln_ctrl_schemas_dir = base_path
1023        .join(&config.templates_dir)
1024        .join("ln_ctrl")
1025        .join("schemas");
1026    fs::create_dir_all(&ln_ctrl_schemas_dir).map_err(|e| {
1027        clap_noun_verb::NounVerbError::execution_error(format!(
1028            "Failed to create ln_ctrl schemas directory: {}",
1029            e
1030        ))
1031    })?;
1032
1033    let ln_ctrl_goldens_dir = base_path
1034        .join(&config.templates_dir)
1035        .join("ln_ctrl")
1036        .join("goldens");
1037    fs::create_dir_all(&ln_ctrl_goldens_dir).map_err(|e| {
1038        clap_noun_verb::NounVerbError::execution_error(format!(
1039            "Failed to create ln_ctrl goldens directory: {}",
1040            e
1041        ))
1042    })?;
1043
1044    let ln_ctrl_docs_dir = base_path
1045        .join(&config.templates_dir)
1046        .join("ln_ctrl")
1047        .join("docs");
1048    fs::create_dir_all(&ln_ctrl_docs_dir).map_err(|e| {
1049        clap_noun_verb::NounVerbError::execution_error(format!(
1050            "Failed to create ln_ctrl docs directory: {}",
1051            e
1052        ))
1053    })?;
1054
1055    let ln_ctrl_kernel_dir = base_path
1056        .join(&config.templates_dir)
1057        .join("ln_ctrl")
1058        .join("kernel");
1059    fs::create_dir_all(&ln_ctrl_kernel_dir).map_err(|e| {
1060        clap_noun_verb::NounVerbError::execution_error(format!(
1061            "Failed to create ln_ctrl kernel directory: {}",
1062            e
1063        ))
1064    })?;
1065
1066    // Generate receipt schema template
1067    let receipt_schema_tera =
1068        include_str!("../../templates/wizard/ln_ctrl/templates/schemas/receipt.schema.json.tera");
1069    let receipt_schema_path = ln_ctrl_schemas_dir.join("receipt.schema.json.tera");
1070    tx.write_file(&receipt_schema_path, receipt_schema_tera)
1071        .map_err(|e| {
1072            clap_noun_verb::NounVerbError::execution_error(format!(
1073                "Failed to write receipt.schema.json.tera: {}",
1074                e
1075            ))
1076        })?;
1077    files_created.push(format!(
1078        "{}/ln_ctrl/schemas/receipt.schema.json.tera",
1079        config.templates_dir
1080    ));
1081
1082    // Generate golden test template
1083    let golden_test_tera = r#"{
1084  "workflow_id": "{{ workflow_id }}",
1085  "total_receipts": {{ receipts_count }},
1086  "total_steps": {{ total_steps }},
1087  "final_frontier_hash": "{{ final_frontier_hash }}",
1088  "generated_at": "{{ now() | date(format='%Y-%m-%dT%H:%M:%SZ') }}",
1089  "deterministic": true
1090}
1091"#;
1092    let golden_test_path = ln_ctrl_goldens_dir.join("test.golden.json.tera");
1093    tx.write_file(&golden_test_path, golden_test_tera)
1094        .map_err(|e| {
1095            clap_noun_verb::NounVerbError::execution_error(format!(
1096                "Failed to write test.golden.json.tera: {}",
1097                e
1098            ))
1099        })?;
1100    files_created.push(format!(
1101        "{}/ln_ctrl/goldens/test.golden.json.tera",
1102        config.templates_dir
1103    ));
1104
1105    // Generate docs template
1106    let docs_tera = r#"# ln_ctrl Ontology Documentation
1107
1108Generated: {{ now() | date(format='%Y-%m-%d %H:%M:%S UTC') }}
1109
1110## Classes
1111
1112{% for result in results -%}
1113{% if result.class -%}
1114### {{ result.label }}
1115
1116**URI**: `{{ result.class }}`
1117
1118{{ result.comment }}
1119
1120{% endif -%}
1121{% endfor %}
1122
1123## Properties
1124
1125{% for result in results -%}
1126{% if result.property -%}
1127### {{ result.property_label }}
1128
1129**URI**: `{{ result.property }}`
1130
1131{{ result.property_comment }}
1132
1133{% endif -%}
1134{% endfor %}
1135
1136---
1137
1138*This documentation is generated from the ln_ctrl RDF ontology.*
1139"#;
1140    let docs_path = ln_ctrl_docs_dir.join("ln_ctrl.md.tera");
1141    tx.write_file(&docs_path, docs_tera).map_err(|e| {
1142        clap_noun_verb::NounVerbError::execution_error(format!(
1143            "Failed to write ln_ctrl.md.tera: {}",
1144            e
1145        ))
1146    })?;
1147    files_created.push(format!(
1148        "{}/ln_ctrl/docs/ln_ctrl.md.tera",
1149        config.templates_dir
1150    ));
1151
1152    // Generate kernel IR template
1153    let kernel_ir_tera = r#"{
1154  "version": "1.0",
1155  "generated_at": "{{ now() | date(format='%Y-%m-%dT%H:%M:%SZ') }}",
1156  "execution_trace": [
1157    {% for result in results -%}
1158    {
1159      "receipt_id": "{{ result.receipt }}",
1160      "timestamp": "{{ result.timestamp }}",
1161      "operation": "{{ result.operation }}",
1162      "redex": {
1163        "type": "{{ result.redex_type }}",
1164        "expression": "{{ result.redex_expression }}"
1165      },
1166      "frontier_terms": {{ result.frontier_terms | json_encode }},
1167      "effects": [{{ result.effects | default(value="") }}],
1168      "budget_remaining": {
1169        "steps": {{ result.budget_steps }}
1170      }
1171    }{% if not loop.last %},{% endif %}
1172    {% endfor -%}
1173  ]
1174}
1175"#;
1176    let kernel_ir_path = ln_ctrl_kernel_dir.join("ir.json.tera");
1177    tx.write_file(&kernel_ir_path, kernel_ir_tera)
1178        .map_err(|e| {
1179            clap_noun_verb::NounVerbError::execution_error(format!(
1180                "Failed to write ir.json.tera: {}",
1181                e
1182            ))
1183        })?;
1184    files_created.push(format!(
1185        "{}/ln_ctrl/kernel/ir.json.tera",
1186        config.templates_dir
1187    ));
1188
1189    Ok(())
1190}
1191
1192fn generate_ln_ctrl_scripts(
1193    base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
1194    files_created: &mut Vec<String>,
1195) -> clap_noun_verb::Result<()> {
1196    let scripts_dir = base_path.join("scripts");
1197
1198    // Generate validate.sh — runs quality gates
1199    let validate_sh = format!(
1200        "#!/usr/bin/env bash\n\
1201        set -euo pipefail\n\
1202        # Quality gate validation for {}\n\
1203        echo \"Running quality gates for {}...\"\n\
1204        cargo make check && cargo make lint && cargo make test\n\
1205        echo \"All gates passed\"\n",
1206        config.metadata.name, config.metadata.name
1207    );
1208    tx.write_file(scripts_dir.join("validate.sh"), &validate_sh)
1209        .map_err(|e| {
1210            clap_noun_verb::NounVerbError::execution_error(format!(
1211                "Failed to write validate.sh: {}",
1212                e
1213            ))
1214        })?;
1215    files_created.push("scripts/validate.sh".to_string());
1216
1217    // Generate ci.sh — CI pipeline stub
1218    let ci_sh = format!(
1219        "#!/usr/bin/env bash\n\
1220        set -euo pipefail\n\
1221        # CI pipeline for {}\n\
1222        cargo make pre-commit\n\
1223        cargo make audit\n",
1224        config.metadata.name
1225    );
1226    tx.write_file(scripts_dir.join("ci.sh"), &ci_sh)
1227        .map_err(|e| {
1228            clap_noun_verb::NounVerbError::execution_error(format!("Failed to write ci.sh: {}", e))
1229        })?;
1230    files_created.push("scripts/ci.sh".to_string());
1231
1232    Ok(())
1233}
1234
1235fn generate_mcp_a2a_configs(
1236    base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
1237    files_created: &mut Vec<String>,
1238) -> clap_noun_verb::Result<()> {
1239    // Constants for config file names
1240    const PROJECT_MCP_CONFIG: &str = ".mcp.json";
1241    const PROJECT_A2A_CONFIG: &str = "a2a.toml";
1242
1243    // Generate .mcp.json configuration
1244    let mcp_json = format!(
1245        r#"{{
1246  "mcpServers": {{
1247    "ggen": {{
1248      "command": "ggen",
1249      "args": ["mcp", "start-server", "--transport", "stdio"],
1250      "env": {{
1251        "GGEN_LOG_LEVEL": "info"
1252      }}
1253    }}
1254  }},
1255  "description": "MCP servers for ggen project",
1256  "version": "1.0.0",
1257  "metadata": {{
1258    "project": "{}",
1259    "profile": "mcp-a2a"
1260  }}
1261}}
1262"#,
1263        config.metadata.name
1264    );
1265
1266    let mcp_path = base_path.join(PROJECT_MCP_CONFIG);
1267    tx.write_file(&mcp_path, &mcp_json).map_err(|e| {
1268        clap_noun_verb::NounVerbError::execution_error(format!("Failed to write .mcp.json: {}", e))
1269    })?;
1270    files_created.push(PROJECT_MCP_CONFIG.to_string());
1271
1272    // Generate a2a.toml configuration
1273    let a2a_toml = format!(
1274        r#"[server]
1275host = "127.0.0.1"
1276port = 8080
1277timeout = 30
1278max_connections = 100
1279
1280[metadata]
1281version = "{}"
1282environment = "development"
1283
1284[[agents]]
1285name = "{}"
1286agent_type = "mcp-bridge"
1287enabled = true
1288description = "{}"
1289
1290[workflows]
1291# Define your A2A workflows here
1292"#,
1293        config.metadata.version, config.metadata.name, config.metadata.description
1294    );
1295
1296    let a2a_path = base_path.join(PROJECT_A2A_CONFIG);
1297    tx.write_file(&a2a_path, &a2a_toml).map_err(|e| {
1298        clap_noun_verb::NounVerbError::execution_error(format!("Failed to write a2a.toml: {}", e))
1299    })?;
1300    files_created.push(PROJECT_A2A_CONFIG.to_string());
1301
1302    Ok(())
1303}
1304
1305fn generate_readme(config: &WizardConfig) -> String {
1306    format!(
1307        r#"# {}
1308
1309{}
1310
1311Generated by `ggen wizard` with profile: **{}**
1312
1313## Quick Start
1314
1315```bash
1316# Generate all outputs
1317ggen sync
1318
1319# Validate outputs
1320node world.verify.mjs
1321
1322# View world manifest
1323cat world.manifest.json
1324```
1325
1326## Project Structure
1327
1328```
1329.
1330ā”œā”€ā”€ ggen.toml                           # ggen configuration
1331ā”œā”€ā”€ README.md                            # This file
1332ā”œā”€ā”€ {}/                    # RDF specifications
1333│   └── project.ttl                     # Project metadata
1334ā”œā”€ā”€ {}/                # RDF ontologies
1335│   ā”œā”€ā”€ main.ttl                        # Main ontology
1336│   ā”œā”€ā”€ receipts.ttl                    # Receipt schemas
1337│   └── world.ttl                       # World manifest definition
1338ā”œā”€ā”€ {}/                         # SPARQL queries
1339│   ā”œā”€ā”€ world/
1340│   │   └── outputs.sparql              # Query for world outputs
1341│   └── receipts/
1342│       └── receipt_contract.sparql     # Query for receipt contracts
1343ā”œā”€ā”€ {}/                     # Tera templates
1344│   ā”œā”€ā”€ world-manifest.tera             # World manifest template
1345│   ā”œā”€ā”€ world-verify.tera               # World verifier template
1346│   └── receipts/
1347│       ā”œā”€ā”€ receipt.schema.tera         # Receipt schema template
1348│       └── verdict.schema.tera         # Verdict schema template
1349└── {}/                       # Generated outputs
1350    ā”œā”€ā”€ world.manifest.json             # World manifest
1351    ā”œā”€ā”€ world.verify.mjs                # World verifier
1352    └── receipts/
1353        ā”œā”€ā”€ receipt.schema.json         # Receipt schema
1354        └── verdict.schema.json         # Verdict schema
1355```
1356
1357## Commands
1358
1359```bash
1360# Generate code from ontology
1361ggen sync
1362
1363# Dry-run: preview changes without writing
1364ggen sync --dry-run
1365
1366# Watch mode: regenerate on file changes
1367ggen sync --watch
1368
1369# Validate without generating
1370ggen sync --validate-only
1371```
1372
1373## Determinism
1374
1375This project is configured for deterministic output:
1376
1377- āœ… Stable ordering enforced in SPARQL queries
1378- āœ… Canonical JSON/YAML rendering
1379- āœ… Strict template variables (no silent missing)
1380- āœ… SHACL validation enabled
1381- āœ… World manifest tracks all outputs with hashes
1382
1383## Learn More
1384
1385- [ggen Documentation](https://docs.ggen.io)
1386- [RDF/Turtle Syntax](https://www.w3.org/TR/turtle/)
1387- [SPARQL Query Language](https://www.w3.org/TR/sparql11-query/)
1388- [Tera Template Language](https://keats.github.io/tera/)
1389
1390---
1391
1392*Profile: {} | Version: {}*
1393"#,
1394        config.metadata.name,
1395        config.metadata.description,
1396        config.profile.as_str(),
1397        config.specs_dir,
1398        config.ontologies_dir,
1399        config.sparql_dir,
1400        config.templates_dir,
1401        config.output_dir,
1402        config.profile.as_str(),
1403        config.metadata.version,
1404    )
1405}
1406
1407// ============================================================================
1408// Tests
1409// ============================================================================
1410
1411#[cfg(test)]
1412mod tests {
1413    use super::*;
1414    use tempfile::tempdir;
1415
1416    #[test]
1417    fn test_wizard_profile_parsing() {
1418        assert_eq!(
1419            WizardProfile::from_str("receipts-first").unwrap(),
1420            WizardProfile::ReceiptsFirst
1421        );
1422        assert_eq!(
1423            WizardProfile::from_str("c4-diagrams").unwrap(),
1424            WizardProfile::C4Diagrams
1425        );
1426        assert_eq!(
1427            WizardProfile::from_str("ln-ctrl").unwrap(),
1428            WizardProfile::LnCtrl
1429        );
1430        assert!(WizardProfile::from_str("invalid").is_err());
1431    }
1432
1433    #[test]
1434    fn test_wizard_default_config() {
1435        let config = WizardConfig::default();
1436        assert_eq!(config.profile, WizardProfile::ReceiptsFirst);
1437        assert!(config.deterministic_output);
1438        assert!(config.strict_template_variables);
1439        assert!(config.shacl_validation);
1440        assert!(config.generate_world_manifest);
1441        assert!(config.generate_world_verifier);
1442    }
1443
1444    #[test]
1445    fn test_wizard_scaffold_creation() {
1446        let temp_dir = tempdir().expect("Failed to create temp dir");
1447        let project_path = temp_dir.path().to_str().expect("Invalid path");
1448
1449        let config = WizardConfig::default();
1450        let result = perform_wizard(project_path, config, true).expect("Wizard should succeed");
1451
1452        assert_eq!(result.status, "success");
1453        assert_eq!(result.profile, "receipts-first");
1454        assert!(!result.files_created.is_empty());
1455        assert!(!result.directories_created.is_empty());
1456        assert!(result.error.is_none());
1457
1458        // Verify files exist
1459        let base = temp_dir.path();
1460        assert!(base.join("ggen.toml").exists());
1461        assert!(base.join("README.md").exists());
1462        assert!(base.join(".specify/specs/project.ttl").exists());
1463    }
1464
1465    #[test]
1466    fn test_generate_ggen_toml() {
1467        let config = WizardConfig::default();
1468        let toml = generate_ggen_toml(&config);
1469
1470        assert!(toml.contains("[project]"));
1471        assert!(toml.contains("name = \"my-ggen-project\""));
1472        assert!(toml.contains("world-manifest"));
1473        assert!(toml.contains("receipt-schema"));
1474    }
1475
1476    #[test]
1477    fn test_generate_project_ttl() {
1478        let config = WizardConfig::default();
1479        let ttl = generate_project_ttl(&config);
1480
1481        assert!(ttl.contains("@prefix ggen:"));
1482        assert!(ttl.contains("ggen:Project"));
1483        assert!(ttl.contains("receipts-first"));
1484    }
1485
1486    #[test]
1487    fn test_wizard_output_serialization() {
1488        let output = WizardOutput {
1489            status: "success".to_string(),
1490            project_dir: "/tmp/test".to_string(),
1491            profile: "receipts-first".to_string(),
1492            files_created: vec!["ggen.toml".to_string()],
1493            directories_created: vec![".specify".to_string()],
1494            error: None,
1495            next_steps: vec!["Run ggen sync".to_string()],
1496        };
1497
1498        let json = serde_json::to_string(&output).expect("Should serialize");
1499        assert!(json.contains("\"status\":\"success\""));
1500        assert!(json.contains("\"profile\":\"receipts-first\""));
1501    }
1502
1503    #[test]
1504    fn test_ln_ctrl_profile_parsing() {
1505        assert_eq!(
1506            WizardProfile::from_str("ln-ctrl").unwrap(),
1507            WizardProfile::LnCtrl
1508        );
1509        assert_eq!(WizardProfile::LnCtrl.as_str(), "ln-ctrl");
1510        assert!(WizardProfile::LnCtrl.description().contains("λn execution"));
1511    }
1512
1513    #[test]
1514    fn test_ln_ctrl_scaffold_creation() {
1515        let temp_dir = tempdir().expect("Failed to create temp dir");
1516        let project_path = temp_dir.path().to_str().expect("Invalid path");
1517
1518        let config = WizardConfig {
1519            profile: WizardProfile::LnCtrl,
1520            ..Default::default()
1521        };
1522        let result = perform_wizard(project_path, config, true).expect("Wizard should succeed");
1523
1524        assert_eq!(result.status, "success");
1525        assert_eq!(result.profile, "ln-ctrl");
1526        assert!(!result.files_created.is_empty());
1527        assert!(result.error.is_none());
1528
1529        // Verify ln_ctrl-specific files exist
1530        let base = temp_dir.path();
1531        assert!(base.join("ggen.toml").exists());
1532        assert!(base
1533            .join(".specify/ontologies/ln_ctrl_receipts.ttl")
1534            .exists());
1535    }
1536
1537    #[test]
1538    fn test_ln_ctrl_ggen_toml_generation() {
1539        let config = WizardConfig {
1540            profile: WizardProfile::LnCtrl,
1541            ..Default::default()
1542        };
1543        let toml = generate_ggen_toml(&config);
1544
1545        assert!(toml.contains("[project]"));
1546        assert!(toml.contains("ln-ctrl-receipt-schema"));
1547        assert!(toml.contains("ln-ctrl-golden-tests"));
1548        assert!(toml.contains("ln-ctrl-docs"));
1549        assert!(toml.contains("ln-ctrl-kernel-ir"));
1550    }
1551
1552    #[test]
1553    fn test_mcp_a2a_profile_parsing() {
1554        assert_eq!(
1555            WizardProfile::from_str("mcp-a2a").unwrap(),
1556            WizardProfile::McpA2a
1557        );
1558        assert_eq!(WizardProfile::McpA2a.as_str(), "mcp-a2a");
1559        assert!(WizardProfile::McpA2a.description().contains("MCP"));
1560        assert!(WizardProfile::McpA2a.description().contains("A2A"));
1561    }
1562
1563    #[test]
1564    fn test_mcp_a2a_scaffold_creation() {
1565        let temp_dir = tempdir().expect("Failed to create temp dir");
1566        let project_path = temp_dir.path().to_str().expect("Invalid path");
1567
1568        let config = WizardConfig {
1569            profile: WizardProfile::McpA2a,
1570            ..Default::default()
1571        };
1572        let result = perform_wizard(project_path, config, true).expect("Wizard should succeed");
1573
1574        assert_eq!(result.status, "success");
1575        assert_eq!(result.profile, "mcp-a2a");
1576        assert!(!result.files_created.is_empty());
1577        assert!(result.error.is_none());
1578
1579        // Verify MCP/A2A files exist
1580        let base = temp_dir.path();
1581        assert!(base.join(".mcp.json").exists(), ".mcp.json should exist");
1582        assert!(base.join("a2a.toml").exists(), "a2a.toml should exist");
1583    }
1584}