#![allow(clippy::unused_unit)]
use clap_noun_verb_macros::verb;
use ggen_core::codegen::executor::{SyncExecutor, SyncOptions};
use ggen_core::codegen::FileTransaction;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::{self, Write};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WizardProfile {
#[serde(rename = "receipts-first")]
ReceiptsFirst,
#[serde(rename = "c4-diagrams")]
C4Diagrams,
#[serde(rename = "openapi-contracts")]
OpenAPIContracts,
#[serde(rename = "infra-k8s-gcp")]
InfraK8sGcp,
#[serde(rename = "lnctrl-output-contracts")]
LnCtrlOutputContracts,
#[serde(rename = "ln-ctrl")]
LnCtrl,
#[serde(rename = "mcp-a2a")]
McpA2a,
#[serde(rename = "custom")]
Custom,
}
impl WizardProfile {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self, String> {
match s {
"receipts-first" => Ok(Self::ReceiptsFirst),
"c4-diagrams" => Ok(Self::C4Diagrams),
"openapi-contracts" => Ok(Self::OpenAPIContracts),
"infra-k8s-gcp" => Ok(Self::InfraK8sGcp),
"lnctrl-output-contracts" => Ok(Self::LnCtrlOutputContracts),
"ln-ctrl" => Ok(Self::LnCtrl),
"mcp-a2a" => Ok(Self::McpA2a),
"custom" => Ok(Self::Custom),
_ => Err(format!("Unknown profile: {}", s)),
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::ReceiptsFirst => "receipts-first",
Self::C4Diagrams => "c4-diagrams",
Self::OpenAPIContracts => "openapi-contracts",
Self::InfraK8sGcp => "infra-k8s-gcp",
Self::LnCtrlOutputContracts => "lnctrl-output-contracts",
Self::LnCtrl => "ln-ctrl",
Self::McpA2a => "mcp-a2a",
Self::Custom => "custom",
}
}
pub fn description(&self) -> &'static str {
match self {
Self::ReceiptsFirst => "World manifest + receipt schemas + audit trail (default)",
Self::C4Diagrams => "C4 L1-L4 Mermaid diagram generation",
Self::OpenAPIContracts => "OpenAPI specification generation",
Self::InfraK8sGcp => "Kubernetes + GCP infrastructure manifests",
Self::LnCtrlOutputContracts => "LN_CTRL output contract schemas",
Self::LnCtrl => {
"LN_CTRL full profile: λn execution traces with causal chaining receipts"
}
Self::McpA2a => "MCP (Model Context Protocol) + A2A (Agent-to-Agent) configuration",
Self::Custom => "Custom configuration (advanced)",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectMetadata {
pub name: String,
pub version: String,
pub description: String,
pub license: String,
pub authors: Vec<String>,
}
impl Default for ProjectMetadata {
fn default() -> Self {
Self {
name: "my-ggen-project".to_string(),
version: "0.1.0".to_string(),
description: "A ggen project initialized with wizard".to_string(),
license: "MIT".to_string(),
authors: vec!["ggen wizard".to_string()],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct WizardConfig {
pub profile: WizardProfile,
pub metadata: ProjectMetadata,
pub deterministic_output: bool,
pub strict_template_variables: bool,
pub shacl_validation: bool,
pub syntax_validation: bool,
pub audit_trail: bool,
pub generate_world_manifest: bool,
pub generate_world_verifier: bool,
pub specs_dir: String,
pub ontologies_dir: String,
pub templates_dir: String,
pub output_dir: String,
pub sparql_dir: String,
}
impl Default for WizardConfig {
fn default() -> Self {
Self {
profile: WizardProfile::ReceiptsFirst,
metadata: ProjectMetadata::default(),
deterministic_output: true,
strict_template_variables: true,
shacl_validation: true,
syntax_validation: true,
audit_trail: true,
generate_world_manifest: true,
generate_world_verifier: true,
specs_dir: ".specify/specs".to_string(),
ontologies_dir: ".specify/ontologies".to_string(),
templates_dir: "templates".to_string(),
output_dir: ".".to_string(),
sparql_dir: "sparql".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct WizardOutput {
pub status: String,
pub project_dir: String,
pub profile: String,
pub files_created: Vec<String>,
pub directories_created: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub next_steps: Vec<String>,
}
fn apply_metadata_overrides(
config: &mut WizardConfig, name: Option<String>, version: Option<String>,
description: Option<String>,
) {
if let Some(n) = name {
config.metadata.name = n;
}
if let Some(v) = version {
config.metadata.version = v;
}
if let Some(d) = description {
config.metadata.description = d;
}
}
#[verb("wizard", "root")]
pub fn wizard(
profile: Option<String>, output_dir: Option<String>, yes: Option<bool>, no_sync: Option<bool>,
name: Option<String>, version: Option<String>, description: Option<String>,
) -> clap_noun_verb::Result<WizardOutput> {
let output_path = output_dir.unwrap_or_else(|| ".".to_string());
let accept_defaults = yes.unwrap_or(false);
let skip_sync = no_sync.unwrap_or(false);
let selected_profile = if let Some(profile_str) = profile {
WizardProfile::from_str(&profile_str).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Invalid profile: {}", e))
})?
} else if accept_defaults {
WizardProfile::ReceiptsFirst
} else {
select_profile_interactive()?
};
let mut config = if accept_defaults {
WizardConfig {
profile: selected_profile,
..Default::default()
}
} else {
configure_interactive(selected_profile)?
};
apply_metadata_overrides(&mut config, name, version, description);
perform_wizard(&output_path, config, skip_sync)
}
fn select_profile_interactive() -> clap_noun_verb::Result<WizardProfile> {
println!("\n🧙 ggen wizard - Bootstrap your project");
println!("\nWizard creates a deterministic factory; outputs are disposable projections.\n");
println!("Select a profile:\n");
println!(
" 1. receipts-first (default) - {}",
WizardProfile::ReceiptsFirst.description()
);
println!(
" 2. c4-diagrams - {}",
WizardProfile::C4Diagrams.description()
);
println!(
" 3. openapi-contracts - {}",
WizardProfile::OpenAPIContracts.description()
);
println!(
" 4. infra-k8s-gcp - {}",
WizardProfile::InfraK8sGcp.description()
);
println!(
" 5. lnctrl-output-contracts - {}",
WizardProfile::LnCtrlOutputContracts.description()
);
println!(" 6. ln-ctrl - {}", WizardProfile::LnCtrl.description());
println!(" 7. mcp-a2a - {}", WizardProfile::McpA2a.description());
println!(" 8. custom - {}", WizardProfile::Custom.description());
print!("\nEnter choice (1-8) [1]: ");
io::stdout().flush().map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Failed to flush stdout: {}", e))
})?;
let mut input = String::new();
io::stdin().read_line(&mut input).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Failed to read input: {}", e))
})?;
let choice = input.trim();
let profile = match choice {
"" | "1" => WizardProfile::ReceiptsFirst,
"2" => WizardProfile::C4Diagrams,
"3" => WizardProfile::OpenAPIContracts,
"4" => WizardProfile::InfraK8sGcp,
"5" => WizardProfile::LnCtrlOutputContracts,
"6" => WizardProfile::LnCtrl,
"7" => WizardProfile::McpA2a,
"8" => WizardProfile::Custom,
_ => {
return Err(clap_noun_verb::NounVerbError::execution_error(
"Invalid choice".to_string(),
))
}
};
println!("\n✓ Selected profile: {}", profile.as_str());
Ok(profile)
}
fn configure_interactive(profile: WizardProfile) -> clap_noun_verb::Result<WizardConfig> {
let mut config = WizardConfig {
profile,
..Default::default()
};
println!("\n📋 Project Metadata");
print!("Project name [{}]: ", config.metadata.name);
io::stdout().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input).ok();
if !input.trim().is_empty() {
config.metadata.name = input.trim().to_string();
}
print!("Version [{}]: ", config.metadata.version);
io::stdout().flush().ok();
input.clear();
io::stdin().read_line(&mut input).ok();
if !input.trim().is_empty() {
config.metadata.version = input.trim().to_string();
}
print!("Description [{}]: ", config.metadata.description);
io::stdout().flush().ok();
input.clear();
io::stdin().read_line(&mut input).ok();
if !input.trim().is_empty() {
config.metadata.description = input.trim().to_string();
}
println!("\n✓ Configuration complete");
Ok(config)
}
fn perform_wizard(
project_dir: &str, config: WizardConfig, skip_sync: bool,
) -> clap_noun_verb::Result<WizardOutput> {
let base_path = Path::new(project_dir);
fs::create_dir_all(base_path).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to create project directory: {}",
e
))
})?;
let mut tx = FileTransaction::new().map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to initialize file transaction: {}",
e
))
})?;
let mut directories_created = Vec::new();
let mut files_created = Vec::new();
let sparql_world = format!("{}/world", config.sparql_dir);
let sparql_receipts = format!("{}/receipts", config.sparql_dir);
let templates_receipts = format!("{}/receipts", config.templates_dir);
let dirs = vec![
&config.specs_dir,
&config.ontologies_dir,
&sparql_world,
&sparql_receipts,
&templates_receipts,
&config.output_dir,
];
for dir in &dirs {
let dir_path = base_path.join(dir);
if !dir_path.exists() {
fs::create_dir_all(&dir_path).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to create directory {}: {}",
dir, e
))
})?;
directories_created.push(dir.to_string());
}
}
generate_scaffold(base_path, &config, &mut tx, &mut files_created)?;
let _receipt = tx.commit().map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to commit file transaction: {}",
e
))
})?;
let mut next_steps = vec!["Run 'ggen sync' to generate initial outputs".to_string()];
let mut sync_error: Option<String> = None;
if !skip_sync {
println!("\n⚙️ Running initial sync...");
match run_initial_sync(base_path) {
Ok(()) => {
next_steps.insert(0, "Initial sync completed".to_string());
}
Err(e) => {
next_steps.insert(
0,
"Initial sync did not run; run 'ggen sync' to generate outputs".to_string(),
);
sync_error = Some(e);
}
}
}
next_steps.push("Edit .specify/specs/project.ttl to customize your project".to_string());
next_steps.push("Review world.manifest.json to see all outputs".to_string());
next_steps.push("Run world.verify.mjs to validate outputs".to_string());
let status = if sync_error.is_some() {
"error".to_string()
} else {
"success".to_string()
};
Ok(WizardOutput {
status,
project_dir: project_dir.to_string(),
profile: config.profile.as_str().to_string(),
files_created,
directories_created,
error: sync_error,
next_steps,
})
}
fn run_initial_sync(base_path: &Path) -> std::result::Result<(), String> {
let manifest_path = base_path.join("ggen.toml");
let options = SyncOptions {
manifest_path,
..SyncOptions::default()
};
let result = SyncExecutor::new(options)
.execute()
.map_err(|e| e.to_string())?;
if result.status == "success" {
Ok(())
} else {
Err(result
.error
.unwrap_or_else(|| format!("sync reported status '{}'", result.status)))
}
}
fn generate_scaffold(
base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
files_created: &mut Vec<String>,
) -> clap_noun_verb::Result<()> {
let ggen_toml = generate_ggen_toml(config);
let toml_path = base_path.join("ggen.toml");
tx.write_file(&toml_path, &ggen_toml).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Failed to write ggen.toml: {}", e))
})?;
files_created.push("ggen.toml".to_string());
let project_ttl = generate_project_ttl(config);
let project_path = base_path.join(&config.specs_dir).join("project.ttl");
tx.write_file(&project_path, &project_ttl).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write project.ttl: {}",
e
))
})?;
files_created.push(format!("{}/project.ttl", config.specs_dir));
match config.profile {
WizardProfile::LnCtrl | WizardProfile::LnCtrlOutputContracts => {
generate_ln_ctrl_ontologies(base_path, config, tx, files_created)?;
generate_ln_ctrl_sparql(base_path, config, tx, files_created)?;
generate_ln_ctrl_templates(base_path, config, tx, files_created)?;
generate_ln_ctrl_scripts(base_path, config, tx, files_created)?;
}
WizardProfile::McpA2a => {
generate_mcp_a2a_configs(base_path, config, tx, files_created)?;
generate_ontologies(base_path, config, tx, files_created)?;
generate_sparql_queries(base_path, config, tx, files_created)?;
generate_tera_templates(base_path, config, tx, files_created)?;
}
_ => {
generate_ontologies(base_path, config, tx, files_created)?;
generate_sparql_queries(base_path, config, tx, files_created)?;
generate_tera_templates(base_path, config, tx, files_created)?;
}
}
let readme = generate_readme(config);
let readme_path = base_path.join("README.md");
tx.write_file(&readme_path, &readme).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Failed to write README.md: {}", e))
})?;
files_created.push("README.md".to_string());
Ok(())
}
fn generate_ggen_toml(config: &WizardConfig) -> String {
let base_config = format!(
r#"[project]
name = "{}"
version = "{}"
description = "{}"
authors = ["{}"]
license = "{}"
[ontology]
source = "{}/main.ttl"
[generation]
output_dir = "{}"
# World manifest generation
[[generation.rules]]
name = "world-manifest"
query = {{ file = "{}/world/outputs.sparql" }}
template = {{ file = "{}/world-manifest.tera" }}
output_file = "{}/world.manifest.json"
mode = "Overwrite"
# World verifier generation
[[generation.rules]]
name = "world-verifier"
query = {{ file = "{}/world/outputs.sparql" }}
template = {{ file = "{}/world-verify.tera" }}
output_file = "{}/world.verify.mjs"
mode = "Overwrite"
# Receipt schema generation
[[generation.rules]]
name = "receipt-schema"
query = {{ file = "{}/receipts/receipt_contract.sparql" }}
template = {{ file = "{}/receipts/receipt.schema.tera" }}
output_file = "{}/receipts/receipt.schema.json"
mode = "Overwrite"
# Verdict schema generation
[[generation.rules]]
name = "verdict-schema"
query = {{ file = "{}/receipts/receipt_contract.sparql" }}
template = {{ file = "{}/receipts/verdict.schema.tera" }}
output_file = "{}/receipts/verdict.schema.json"
mode = "Overwrite"
"#,
config.metadata.name,
config.metadata.version,
config.metadata.description,
config
.metadata
.authors
.first()
.unwrap_or(&"ggen wizard".to_string()),
config.metadata.license,
config.ontologies_dir,
config.output_dir,
config.sparql_dir,
config.templates_dir,
config.output_dir,
config.sparql_dir,
config.templates_dir,
config.output_dir,
config.sparql_dir,
config.templates_dir,
config.output_dir,
config.sparql_dir,
config.templates_dir,
config.output_dir,
);
let ln_ctrl_rules = if matches!(config.profile, WizardProfile::LnCtrl) {
format!(
r#"
# ln_ctrl Receipt Schema Generation
[[generation.rules]]
name = "ln-ctrl-receipt-schema"
query = {{ file = "{}/ln_ctrl/receipt_trace.sparql" }}
template = {{ file = "{}/ln_ctrl/schemas/receipt.schema.json.tera" }}
output_file = "{}/ln_ctrl/schemas/receipt.schema.json"
mode = "Overwrite"
# ln_ctrl Golden Tests
[[generation.rules]]
name = "ln-ctrl-golden-tests"
query = {{ file = "{}/ln_ctrl/golden_tests.sparql" }}
template = {{ file = "{}/ln_ctrl/goldens/test.golden.json.tera" }}
output_file = "{}/ln_ctrl/goldens/{{{{ workflow_id }}}}.golden.json"
mode = "Overwrite"
# ln_ctrl Documentation
[[generation.rules]]
name = "ln-ctrl-docs"
query = {{ file = "{}/ln_ctrl/docs.sparql" }}
template = {{ file = "{}/ln_ctrl/docs/ln_ctrl.md.tera" }}
output_file = "{}/ln_ctrl/docs/ln_ctrl_guide.md"
mode = "Overwrite"
# ln_ctrl Kernel IR
[[generation.rules]]
name = "ln-ctrl-kernel-ir"
query = {{ file = "{}/ln_ctrl/kernel_ir.sparql" }}
template = {{ file = "{}/ln_ctrl/kernel/ir.json.tera" }}
output_file = "{}/ln_ctrl/kernel/execution.ir.json"
mode = "Overwrite"
"#,
config.sparql_dir,
config.templates_dir,
config.output_dir,
config.sparql_dir,
config.templates_dir,
config.output_dir,
config.sparql_dir,
config.templates_dir,
config.output_dir,
config.sparql_dir,
config.templates_dir,
config.output_dir,
)
} else {
String::new()
};
let footer = format!(
r#"
[sync]
enabled = true
on_change = "manual"
validate_after = true
conflict_mode = "fail"
[rdf]
formats = ["turtle"]
default_format = "turtle"
strict_validation = {}
[templates]
enable_caching = true
auto_reload = true
[output]
formatting = "default"
deterministic = {}
line_length = 100
indent = 2
"#,
config.shacl_validation, config.deterministic_output,
);
format!("{}{}{}", base_config, ln_ctrl_rules, footer)
}
fn generate_project_ttl(config: &WizardConfig) -> String {
format!(
r#"@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ggen: <https://ggen.io/marketplace/> .
ggen:Project a rdfs:Class ;
rdfs:label "{}" ;
rdfs:comment "{}" ;
ggen:version "{}" ;
ggen:profile "{}" .
"#,
config.metadata.name,
config.metadata.description,
config.metadata.version,
config.profile.as_str(),
)
}
fn generate_ontologies(
base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
files_created: &mut Vec<String>,
) -> clap_noun_verb::Result<()> {
let main_ttl = include_str!("../../templates/wizard/ontologies/main.ttl");
let main_path = base_path.join(&config.ontologies_dir).join("main.ttl");
tx.write_file(&main_path, main_ttl).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Failed to write main.ttl: {}", e))
})?;
files_created.push(format!("{}/main.ttl", config.ontologies_dir));
let receipts_ttl = include_str!("../../templates/wizard/ontologies/receipts.ttl");
let receipts_path = base_path.join(&config.ontologies_dir).join("receipts.ttl");
tx.write_file(&receipts_path, receipts_ttl).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write receipts.ttl: {}",
e
))
})?;
files_created.push(format!("{}/receipts.ttl", config.ontologies_dir));
let world_ttl = include_str!("../../templates/wizard/ontologies/world.ttl");
let world_path = base_path.join(&config.ontologies_dir).join("world.ttl");
tx.write_file(&world_path, world_ttl).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Failed to write world.ttl: {}", e))
})?;
files_created.push(format!("{}/world.ttl", config.ontologies_dir));
Ok(())
}
fn generate_sparql_queries(
base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
files_created: &mut Vec<String>,
) -> clap_noun_verb::Result<()> {
let outputs_sparql = include_str!("../../templates/wizard/sparql/world/outputs.sparql");
let outputs_path = base_path
.join(&config.sparql_dir)
.join("world")
.join("outputs.sparql");
tx.write_file(&outputs_path, outputs_sparql).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write outputs.sparql: {}",
e
))
})?;
files_created.push(format!("{}/world/outputs.sparql", config.sparql_dir));
let receipt_sparql =
include_str!("../../templates/wizard/sparql/receipts/receipt_contract.sparql");
let receipt_path = base_path
.join(&config.sparql_dir)
.join("receipts")
.join("receipt_contract.sparql");
tx.write_file(&receipt_path, receipt_sparql).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write receipt_contract.sparql: {}",
e
))
})?;
files_created.push(format!(
"{}/receipts/receipt_contract.sparql",
config.sparql_dir
));
Ok(())
}
fn generate_tera_templates(
base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
files_created: &mut Vec<String>,
) -> clap_noun_verb::Result<()> {
let manifest_tera = include_str!("../../templates/wizard/tera/world-manifest.tera");
let manifest_path = base_path
.join(&config.templates_dir)
.join("world-manifest.tera");
tx.write_file(&manifest_path, manifest_tera).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write world-manifest.tera: {}",
e
))
})?;
files_created.push(format!("{}/world-manifest.tera", config.templates_dir));
let verifier_tera = include_str!("../../templates/wizard/tera/world-verify.tera");
let verifier_path = base_path
.join(&config.templates_dir)
.join("world-verify.tera");
tx.write_file(&verifier_path, verifier_tera).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write world-verify.tera: {}",
e
))
})?;
files_created.push(format!("{}/world-verify.tera", config.templates_dir));
let receipt_schema_tera =
include_str!("../../templates/wizard/tera/receipts/receipt.schema.tera");
let receipt_schema_path = base_path
.join(&config.templates_dir)
.join("receipts")
.join("receipt.schema.tera");
tx.write_file(&receipt_schema_path, receipt_schema_tera)
.map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write receipt.schema.tera: {}",
e
))
})?;
files_created.push(format!(
"{}/receipts/receipt.schema.tera",
config.templates_dir
));
let verdict_schema_tera =
include_str!("../../templates/wizard/tera/receipts/verdict.schema.tera");
let verdict_schema_path = base_path
.join(&config.templates_dir)
.join("receipts")
.join("verdict.schema.tera");
tx.write_file(&verdict_schema_path, verdict_schema_tera)
.map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write verdict.schema.tera: {}",
e
))
})?;
files_created.push(format!(
"{}/receipts/verdict.schema.tera",
config.templates_dir
));
Ok(())
}
fn generate_ln_ctrl_ontologies(
base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
files_created: &mut Vec<String>,
) -> clap_noun_verb::Result<()> {
let ln_ctrl_receipts_ttl =
include_str!("../../templates/wizard/ln_ctrl/ontologies/ln_ctrl_receipts.ttl");
let receipts_path = base_path
.join(&config.ontologies_dir)
.join("ln_ctrl_receipts.ttl");
tx.write_file(&receipts_path, ln_ctrl_receipts_ttl)
.map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write ln_ctrl_receipts.ttl: {}",
e
))
})?;
files_created.push(format!("{}/ln_ctrl_receipts.ttl", config.ontologies_dir));
Ok(())
}
fn generate_ln_ctrl_sparql(
base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
files_created: &mut Vec<String>,
) -> clap_noun_verb::Result<()> {
let ln_ctrl_sparql_dir = base_path.join(&config.sparql_dir).join("ln_ctrl");
fs::create_dir_all(&ln_ctrl_sparql_dir).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to create ln_ctrl SPARQL directory: {}",
e
))
})?;
let receipt_trace_sparql = r"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX ln_ctrl: <https://ggen.io/ontology/ln_ctrl#>
SELECT ?receipt ?timestamp ?operation ?workflow_id ?step_index
?causal_parent ?hash_chain ?redex_type ?redex_expression
?frontier_size ?frontier_hash ?budget_steps ?budget_memory
WHERE {
?receipt a ln_ctrl:Receipt ;
ln_ctrl:timestamp ?timestamp ;
ln_ctrl:operation ?operation ;
ln_ctrl:workflow_id ?workflow_id ;
ln_ctrl:step_index ?step_index ;
ln_ctrl:hash_chain ?hash_chain .
OPTIONAL { ?receipt ln_ctrl:causal_parent ?causal_parent }
?receipt ln_ctrl:redex_executed ?redex .
?redex ln_ctrl:redex_type ?redex_type ;
ln_ctrl:redex_expression ?redex_expression .
?receipt ln_ctrl:frontier_after ?frontier .
?frontier ln_ctrl:frontier_size ?frontier_size ;
ln_ctrl:frontier_hash ?frontier_hash .
?receipt ln_ctrl:budget_remaining ?budget .
?budget ln_ctrl:budget_steps ?budget_steps ;
ln_ctrl:budget_memory ?budget_memory .
}
ORDER BY ?workflow_id ?step_index
";
let receipt_trace_path = ln_ctrl_sparql_dir.join("receipt_trace.sparql");
tx.write_file(&receipt_trace_path, receipt_trace_sparql)
.map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write receipt_trace.sparql: {}",
e
))
})?;
files_created.push(format!(
"{}/ln_ctrl/receipt_trace.sparql",
config.sparql_dir
));
let golden_tests_sparql = r"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX ln_ctrl: <https://ggen.io/ontology/ln_ctrl#>
SELECT ?workflow_id ?receipts_count ?total_steps ?final_frontier_hash
WHERE {
{
SELECT ?workflow_id (COUNT(?receipt) AS ?receipts_count) (MAX(?step_index) AS ?total_steps)
WHERE {
?receipt a ln_ctrl:Receipt ;
ln_ctrl:workflow_id ?workflow_id ;
ln_ctrl:step_index ?step_index .
}
GROUP BY ?workflow_id
}
?final_receipt a ln_ctrl:Receipt ;
ln_ctrl:workflow_id ?workflow_id ;
ln_ctrl:step_index ?total_steps ;
ln_ctrl:frontier_after ?frontier .
?frontier ln_ctrl:frontier_hash ?final_frontier_hash .
}
ORDER BY ?workflow_id
";
let golden_tests_path = ln_ctrl_sparql_dir.join("golden_tests.sparql");
tx.write_file(&golden_tests_path, golden_tests_sparql)
.map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write golden_tests.sparql: {}",
e
))
})?;
files_created.push(format!("{}/ln_ctrl/golden_tests.sparql", config.sparql_dir));
let docs_sparql = r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX ln_ctrl: <https://ggen.io/ontology/ln_ctrl#>
SELECT ?class ?label ?comment ?property ?property_label ?property_comment
WHERE {
{
?class a rdfs:Class ;
rdfs:label ?label ;
rdfs:comment ?comment .
FILTER(STRSTARTS(STR(?class), "https://ggen.io/ontology/ln_ctrl#"))
}
UNION
{
?property a rdf:Property ;
rdfs:label ?property_label ;
rdfs:comment ?property_comment .
FILTER(STRSTARTS(STR(?property), "https://ggen.io/ontology/ln_ctrl#"))
}
}
ORDER BY ?class ?property
"#;
let docs_path = ln_ctrl_sparql_dir.join("docs.sparql");
tx.write_file(&docs_path, docs_sparql).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write docs.sparql: {}",
e
))
})?;
files_created.push(format!("{}/ln_ctrl/docs.sparql", config.sparql_dir));
let kernel_ir_sparql = r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX ln_ctrl: <https://ggen.io/ontology/ln_ctrl#>
SELECT ?receipt ?timestamp ?operation ?redex_type ?redex_expression
?frontier_terms ?effects ?budget_steps
WHERE {
?receipt a ln_ctrl:Receipt ;
ln_ctrl:timestamp ?timestamp ;
ln_ctrl:operation ?operation ;
ln_ctrl:redex_executed ?redex ;
ln_ctrl:frontier_after ?frontier ;
ln_ctrl:budget_remaining ?budget .
?redex ln_ctrl:redex_type ?redex_type ;
ln_ctrl:redex_expression ?redex_expression .
?frontier ln_ctrl:frontier_terms ?frontier_terms .
?budget ln_ctrl:budget_steps ?budget_steps .
OPTIONAL {
?receipt ln_ctrl:effects_performed ?effect .
?effect ln_ctrl:effect_type ?effect_type ;
ln_ctrl:effect_data ?effect_data .
BIND(CONCAT(?effect_type, ":", ?effect_data) AS ?effects)
}
}
ORDER BY ?timestamp
"#;
let kernel_ir_path = ln_ctrl_sparql_dir.join("kernel_ir.sparql");
tx.write_file(&kernel_ir_path, kernel_ir_sparql)
.map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write kernel_ir.sparql: {}",
e
))
})?;
files_created.push(format!("{}/ln_ctrl/kernel_ir.sparql", config.sparql_dir));
Ok(())
}
fn generate_ln_ctrl_templates(
base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
files_created: &mut Vec<String>,
) -> clap_noun_verb::Result<()> {
let ln_ctrl_schemas_dir = base_path
.join(&config.templates_dir)
.join("ln_ctrl")
.join("schemas");
fs::create_dir_all(&ln_ctrl_schemas_dir).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to create ln_ctrl schemas directory: {}",
e
))
})?;
let ln_ctrl_goldens_dir = base_path
.join(&config.templates_dir)
.join("ln_ctrl")
.join("goldens");
fs::create_dir_all(&ln_ctrl_goldens_dir).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to create ln_ctrl goldens directory: {}",
e
))
})?;
let ln_ctrl_docs_dir = base_path
.join(&config.templates_dir)
.join("ln_ctrl")
.join("docs");
fs::create_dir_all(&ln_ctrl_docs_dir).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to create ln_ctrl docs directory: {}",
e
))
})?;
let ln_ctrl_kernel_dir = base_path
.join(&config.templates_dir)
.join("ln_ctrl")
.join("kernel");
fs::create_dir_all(&ln_ctrl_kernel_dir).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to create ln_ctrl kernel directory: {}",
e
))
})?;
let receipt_schema_tera =
include_str!("../../templates/wizard/ln_ctrl/templates/schemas/receipt.schema.json.tera");
let receipt_schema_path = ln_ctrl_schemas_dir.join("receipt.schema.json.tera");
tx.write_file(&receipt_schema_path, receipt_schema_tera)
.map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write receipt.schema.json.tera: {}",
e
))
})?;
files_created.push(format!(
"{}/ln_ctrl/schemas/receipt.schema.json.tera",
config.templates_dir
));
let golden_test_tera = r#"{
"workflow_id": "{{ workflow_id }}",
"total_receipts": {{ receipts_count }},
"total_steps": {{ total_steps }},
"final_frontier_hash": "{{ final_frontier_hash }}",
"generated_at": "{{ now() | date(format='%Y-%m-%dT%H:%M:%SZ') }}",
"deterministic": true
}
"#;
let golden_test_path = ln_ctrl_goldens_dir.join("test.golden.json.tera");
tx.write_file(&golden_test_path, golden_test_tera)
.map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write test.golden.json.tera: {}",
e
))
})?;
files_created.push(format!(
"{}/ln_ctrl/goldens/test.golden.json.tera",
config.templates_dir
));
let docs_tera = r"# ln_ctrl Ontology Documentation
Generated: {{ now() | date(format='%Y-%m-%d %H:%M:%S UTC') }}
## Classes
{% for result in results -%}
{% if result.class -%}
### {{ result.label }}
**URI**: `{{ result.class }}`
{{ result.comment }}
{% endif -%}
{% endfor %}
## Properties
{% for result in results -%}
{% if result.property -%}
### {{ result.property_label }}
**URI**: `{{ result.property }}`
{{ result.property_comment }}
{% endif -%}
{% endfor %}
---
*This documentation is generated from the ln_ctrl RDF ontology.*
";
let docs_path = ln_ctrl_docs_dir.join("ln_ctrl.md.tera");
tx.write_file(&docs_path, docs_tera).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write ln_ctrl.md.tera: {}",
e
))
})?;
files_created.push(format!(
"{}/ln_ctrl/docs/ln_ctrl.md.tera",
config.templates_dir
));
let kernel_ir_tera = r#"{
"version": "1.0",
"generated_at": "{{ now() | date(format='%Y-%m-%dT%H:%M:%SZ') }}",
"execution_trace": [
{% for result in results -%}
{
"receipt_id": "{{ result.receipt }}",
"timestamp": "{{ result.timestamp }}",
"operation": "{{ result.operation }}",
"redex": {
"type": "{{ result.redex_type }}",
"expression": "{{ result.redex_expression }}"
},
"frontier_terms": {{ result.frontier_terms | json_encode }},
"effects": [{{ result.effects | default(value="") }}],
"budget_remaining": {
"steps": {{ result.budget_steps }}
}
}{% if not loop.last %},{% endif %}
{% endfor -%}
]
}
"#;
let kernel_ir_path = ln_ctrl_kernel_dir.join("ir.json.tera");
tx.write_file(&kernel_ir_path, kernel_ir_tera)
.map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write ir.json.tera: {}",
e
))
})?;
files_created.push(format!(
"{}/ln_ctrl/kernel/ir.json.tera",
config.templates_dir
));
Ok(())
}
fn generate_ln_ctrl_scripts(
base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
files_created: &mut Vec<String>,
) -> clap_noun_verb::Result<()> {
let scripts_dir = base_path.join("scripts");
let validate_sh = format!(
"#!/usr/bin/env bash\n\
set -euo pipefail\n\
# Quality gate validation for {}\n\
echo \"Running quality gates for {}...\"\n\
cargo make check && cargo make lint && cargo make test\n\
echo \"All gates passed\"\n",
config.metadata.name, config.metadata.name
);
tx.write_file(scripts_dir.join("validate.sh"), &validate_sh)
.map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!(
"Failed to write validate.sh: {}",
e
))
})?;
files_created.push("scripts/validate.sh".to_string());
let ci_sh = format!(
"#!/usr/bin/env bash\n\
set -euo pipefail\n\
# CI pipeline for {}\n\
cargo make pre-commit\n\
cargo make audit\n",
config.metadata.name
);
tx.write_file(scripts_dir.join("ci.sh"), &ci_sh)
.map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Failed to write ci.sh: {}", e))
})?;
files_created.push("scripts/ci.sh".to_string());
Ok(())
}
fn generate_mcp_a2a_configs(
base_path: &Path, config: &WizardConfig, tx: &mut FileTransaction,
files_created: &mut Vec<String>,
) -> clap_noun_verb::Result<()> {
const PROJECT_MCP_CONFIG: &str = ".mcp.json";
const PROJECT_A2A_CONFIG: &str = "a2a.toml";
let mcp_json = format!(
r#"{{
"mcpServers": {{
"ggen": {{
"command": "ggen",
"args": ["mcp", "start-server", "--transport", "stdio"],
"env": {{
"GGEN_LOG_LEVEL": "info"
}}
}}
}},
"description": "MCP servers for ggen project",
"version": "1.0.0",
"metadata": {{
"project": "{}",
"profile": "mcp-a2a"
}}
}}
"#,
config.metadata.name
);
let mcp_path = base_path.join(PROJECT_MCP_CONFIG);
tx.write_file(&mcp_path, &mcp_json).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Failed to write .mcp.json: {}", e))
})?;
files_created.push(PROJECT_MCP_CONFIG.to_string());
let a2a_toml = format!(
r#"[server]
host = "127.0.0.1"
port = 8080
timeout = 30
max_connections = 100
[metadata]
version = "{}"
environment = "development"
[[agents]]
name = "{}"
agent_type = "mcp-bridge"
enabled = true
description = "{}"
[workflows]
# Define your A2A workflows here
"#,
config.metadata.version, config.metadata.name, config.metadata.description
);
let a2a_path = base_path.join(PROJECT_A2A_CONFIG);
tx.write_file(&a2a_path, &a2a_toml).map_err(|e| {
clap_noun_verb::NounVerbError::execution_error(format!("Failed to write a2a.toml: {}", e))
})?;
files_created.push(PROJECT_A2A_CONFIG.to_string());
Ok(())
}
fn generate_readme(config: &WizardConfig) -> String {
format!(
r"# {}
{}
Generated by `ggen wizard` with profile: **{}**
## Quick Start
```bash
# Generate all outputs
ggen sync
# Validate outputs
node world.verify.mjs
# View world manifest
cat world.manifest.json
```
## Project Structure
```
.
├── ggen.toml # ggen configuration
├── README.md # This file
├── {}/ # RDF specifications
│ └── project.ttl # Project metadata
├── {}/ # RDF ontologies
│ ├── main.ttl # Main ontology
│ ├── receipts.ttl # Receipt schemas
│ └── world.ttl # World manifest definition
├── {}/ # SPARQL queries
│ ├── world/
│ │ └── outputs.sparql # Query for world outputs
│ └── receipts/
│ └── receipt_contract.sparql # Query for receipt contracts
├── {}/ # Tera templates
│ ├── world-manifest.tera # World manifest template
│ ├── world-verify.tera # World verifier template
│ └── receipts/
│ ├── receipt.schema.tera # Receipt schema template
│ └── verdict.schema.tera # Verdict schema template
└── {}/ # Generated outputs
├── world.manifest.json # World manifest
├── world.verify.mjs # World verifier
└── receipts/
├── receipt.schema.json # Receipt schema
└── verdict.schema.json # Verdict schema
```
## Commands
```bash
# Generate code from ontology
ggen sync
# Dry-run: preview changes without writing
ggen sync --dry-run
# Watch mode: regenerate on file changes
ggen sync --watch
# Validate without generating
ggen sync --validate-only
```
## Determinism
This project is configured for deterministic output:
- ✅ Stable ordering enforced in SPARQL queries
- ✅ Canonical JSON/YAML rendering
- ✅ Strict template variables (no silent missing)
- ✅ SHACL validation enabled
- ✅ World manifest tracks all outputs with hashes
## Learn More
- [ggen Documentation](https://docs.ggen.io)
- [RDF/Turtle Syntax](https://www.w3.org/TR/turtle/)
- [SPARQL Query Language](https://www.w3.org/TR/sparql11-query/)
- [Tera Template Language](https://keats.github.io/tera/)
---
*Profile: {} | Version: {}*
",
config.metadata.name,
config.metadata.description,
config.profile.as_str(),
config.specs_dir,
config.ontologies_dir,
config.sparql_dir,
config.templates_dir,
config.output_dir,
config.profile.as_str(),
config.metadata.version,
)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_wizard_profile_parsing() {
assert_eq!(
WizardProfile::from_str("receipts-first").unwrap(),
WizardProfile::ReceiptsFirst
);
assert_eq!(
WizardProfile::from_str("c4-diagrams").unwrap(),
WizardProfile::C4Diagrams
);
assert_eq!(
WizardProfile::from_str("ln-ctrl").unwrap(),
WizardProfile::LnCtrl
);
assert!(WizardProfile::from_str("invalid").is_err());
}
#[test]
fn test_wizard_default_config() {
let config = WizardConfig::default();
assert_eq!(config.profile, WizardProfile::ReceiptsFirst);
assert!(config.deterministic_output);
assert!(config.strict_template_variables);
assert!(config.shacl_validation);
assert!(config.generate_world_manifest);
assert!(config.generate_world_verifier);
}
#[test]
fn test_wizard_scaffold_creation() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let project_path = temp_dir.path().to_str().expect("Invalid path");
let config = WizardConfig::default();
let result = perform_wizard(project_path, config, true).expect("Wizard should succeed");
assert_eq!(result.status, "success");
assert_eq!(result.profile, "receipts-first");
assert!(!result.files_created.is_empty());
assert!(!result.directories_created.is_empty());
assert!(result.error.is_none());
let base = temp_dir.path();
assert!(base.join("ggen.toml").exists());
assert!(base.join("README.md").exists());
assert!(base.join(".specify/specs/project.ttl").exists());
}
#[test]
fn test_generate_ggen_toml() {
let config = WizardConfig::default();
let toml = generate_ggen_toml(&config);
assert!(toml.contains("[project]"));
assert!(toml.contains("name = \"my-ggen-project\""));
assert!(toml.contains("world-manifest"));
assert!(toml.contains("receipt-schema"));
}
#[test]
fn test_generate_project_ttl() {
let config = WizardConfig::default();
let ttl = generate_project_ttl(&config);
assert!(ttl.contains("@prefix ggen:"));
assert!(ttl.contains("ggen:Project"));
assert!(ttl.contains("receipts-first"));
}
#[test]
fn test_wizard_output_serialization() {
let output = WizardOutput {
status: "success".to_string(),
project_dir: "/tmp/test".to_string(),
profile: "receipts-first".to_string(),
files_created: vec!["ggen.toml".to_string()],
directories_created: vec![".specify".to_string()],
error: None,
next_steps: vec!["Run ggen sync".to_string()],
};
let json = serde_json::to_string(&output).expect("Should serialize");
assert!(json.contains("\"status\":\"success\""));
assert!(json.contains("\"profile\":\"receipts-first\""));
}
#[test]
fn test_ln_ctrl_profile_parsing() {
assert_eq!(
WizardProfile::from_str("ln-ctrl").unwrap(),
WizardProfile::LnCtrl
);
assert_eq!(WizardProfile::LnCtrl.as_str(), "ln-ctrl");
assert!(WizardProfile::LnCtrl.description().contains("λn execution"));
}
#[test]
fn test_ln_ctrl_scaffold_creation() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let project_path = temp_dir.path().to_str().expect("Invalid path");
let config = WizardConfig {
profile: WizardProfile::LnCtrl,
..Default::default()
};
let result = perform_wizard(project_path, config, true).expect("Wizard should succeed");
assert_eq!(result.status, "success");
assert_eq!(result.profile, "ln-ctrl");
assert!(!result.files_created.is_empty());
assert!(result.error.is_none());
let base = temp_dir.path();
assert!(base.join("ggen.toml").exists());
assert!(base
.join(".specify/ontologies/ln_ctrl_receipts.ttl")
.exists());
}
#[test]
fn test_ln_ctrl_ggen_toml_generation() {
let config = WizardConfig {
profile: WizardProfile::LnCtrl,
..Default::default()
};
let toml = generate_ggen_toml(&config);
assert!(toml.contains("[project]"));
assert!(toml.contains("ln-ctrl-receipt-schema"));
assert!(toml.contains("ln-ctrl-golden-tests"));
assert!(toml.contains("ln-ctrl-docs"));
assert!(toml.contains("ln-ctrl-kernel-ir"));
}
#[test]
fn test_mcp_a2a_profile_parsing() {
assert_eq!(
WizardProfile::from_str("mcp-a2a").unwrap(),
WizardProfile::McpA2a
);
assert_eq!(WizardProfile::McpA2a.as_str(), "mcp-a2a");
assert!(WizardProfile::McpA2a.description().contains("MCP"));
assert!(WizardProfile::McpA2a.description().contains("A2A"));
}
#[test]
fn test_wizard_initial_sync_produces_real_output() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let project_path = temp_dir.path().to_str().expect("Invalid path");
let config = WizardConfig::default();
let result =
perform_wizard(project_path, config, false).expect("Wizard should not hard-error");
let base = temp_dir.path();
let claimed_completed = result
.next_steps
.iter()
.any(|s| s == "Initial sync completed");
let manifest_output_exists = base.join("world.manifest.json").exists();
if claimed_completed {
assert_eq!(
result.status, "success",
"claimed sync completion but status was not success"
);
assert!(
manifest_output_exists,
"claimed 'Initial sync completed' but world.manifest.json was never generated — \
this is the decorative-completion lie"
);
assert!(result.error.is_none(), "success must carry no error");
} else {
assert_eq!(
result.status, "error",
"sync did not complete yet status was not 'error'"
);
assert!(
result.error.is_some(),
"sync did not complete but no error was surfaced"
);
}
}
#[test]
fn test_wizard_skip_sync_never_claims_completion() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let project_path = temp_dir.path().to_str().expect("Invalid path");
let config = WizardConfig::default();
let result = perform_wizard(project_path, config, true).expect("Wizard should succeed");
let base = temp_dir.path();
assert!(
!base.join("world.manifest.json").exists(),
"no sync was requested yet a sync artifact appeared"
);
assert!(
!result.next_steps.iter().any(|s| s.contains("completed")),
"skip_sync was set but output falsely claims a sync 'completed': {:?}",
result.next_steps
);
}
#[test]
fn test_mcp_a2a_scaffold_creation() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let project_path = temp_dir.path().to_str().expect("Invalid path");
let config = WizardConfig {
profile: WizardProfile::McpA2a,
..Default::default()
};
let result = perform_wizard(project_path, config, true).expect("Wizard should succeed");
assert_eq!(result.status, "success");
assert_eq!(result.profile, "mcp-a2a");
assert!(!result.files_created.is_empty());
assert!(result.error.is_none());
let base = temp_dir.path();
assert!(base.join(".mcp.json").exists(), ".mcp.json should exist");
assert!(base.join("a2a.toml").exists(), "a2a.toml should exist");
}
}