1#![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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47pub enum WizardProfile {
48 #[serde(rename = "receipts-first")]
50 ReceiptsFirst,
51 #[serde(rename = "c4-diagrams")]
53 C4Diagrams,
54 #[serde(rename = "openapi-contracts")]
56 OpenAPIContracts,
57 #[serde(rename = "infra-k8s-gcp")]
59 InfraK8sGcp,
60 #[serde(rename = "lnctrl-output-contracts")]
62 LnCtrlOutputContracts,
63 #[serde(rename = "ln-ctrl")]
65 LnCtrl,
66 #[serde(rename = "mcp-a2a")]
68 McpA2a,
69 #[serde(rename = "custom")]
71 Custom,
72}
73
74impl WizardProfile {
75 #[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 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 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#[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#[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#[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#[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 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 select_profile_interactive()?
241 };
242
243 let config = if accept_defaults {
245 WizardConfig {
246 profile: selected_profile,
247 ..Default::default()
248 }
249 } else {
250 configure_interactive(selected_profile)?
252 };
253
254 perform_wizard(&output_path, config, skip_sync)
256}
257
258fn 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 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
358fn 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 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 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 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(base_path, &config, &mut tx, &mut files_created)?;
415
416 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 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 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 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 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 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 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_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 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
506fn 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 const PROJECT_MCP_CONFIG: &str = ".mcp.json";
1241 const PROJECT_A2A_CONFIG: &str = "a2a.toml";
1242
1243 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 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#[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 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 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 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}