simular/cli/args.rs
1//! CLI argument parsing.
2//!
3//! The accepted grammar is **declarative**: it is the `Cli` and `Commands`
4//! types below, parsed by clap derive. Nothing in this module inspects `argv`
5//! by hand.
6//!
7//! ## Why declarative
8//!
9//! This module previously hand-rolled a `match args[1]` parser, and every one of
10//! its failure modes was silent — the command exited 0 and did the wrong thing:
11//!
12//! - `--seed notanumber` used `.parse().ok().unwrap_or(default)`, so a typo
13//! became the DEFAULT seed rather than an error. A simulation is only
14//! reproducible if the seed it reports is the seed you asked for.
15//! - `--seed` with no value was discarded by the `else { i += 1 }` arm.
16//! - An unknown flag fell through a `_ => i += 1` catch-all and vanished.
17//! - `verify --runs N` was only honoured when `--runs` sat at exactly `argv[3]`.
18//!
19//! clap rejects all four. The grammar is data, not control flow, so it cannot
20//! drift out of sync with itself the way the hand-rolled arms did. Enforced
21//! repo-wide by `scripts/check_no_hand_rolled_parsers.sh`.
22
23use clap::{Parser, Subcommand, ValueEnum};
24use std::path::PathBuf;
25
26/// CLI arguments container.
27#[derive(Debug, Clone, PartialEq, Parser)]
28#[command(
29 name = "simular",
30 version,
31 about = "Unified Simulation Engine for the Sovereign AI Stack",
32 // `help` and `version` are real subcommands below, so that `simular help`
33 // keeps printing simular's own help text (see `output::print_help`) rather
34 // than clap's auto-generated one. The `-h`/`--help` and `-V`/`--version`
35 // FLAGS are still clap's.
36 disable_help_subcommand = true
37)]
38pub struct Cli {
39 /// The command to execute. `None` means no subcommand was given at all.
40 #[command(subcommand)]
41 pub command: Option<Commands>,
42}
43
44impl Cli {
45 /// The selected subcommand, defaulting to [`Commands::Help`].
46 ///
47 /// An empty `argv` showed the help text before the clap conversion; that
48 /// behaviour (help on stdout, exit 0) is preserved here rather than in
49 /// clap's `arg_required_else_help`, which would exit 2 instead.
50 #[must_use]
51 pub fn into_command(self) -> Commands {
52 self.command.unwrap_or(Commands::Help)
53 }
54}
55
56/// Available CLI commands.
57#[derive(Debug, Clone, PartialEq, Subcommand)]
58pub enum Commands {
59 /// Run an experiment
60 Run {
61 /// Path to the experiment YAML file.
62 experiment_path: PathBuf,
63 /// Optional seed override.
64 #[arg(long = "seed", value_name = "N")]
65 seed_override: Option<u64>,
66 /// Enable verbose output.
67 #[arg(short = 'v', long)]
68 verbose: bool,
69 },
70 /// Render simulation to SVG + keyframes
71 Render {
72 /// Simulation domain (orbit, `bouncing_balls`).
73 #[arg(long, default_value = "orbit")]
74 domain: String,
75 /// Output format: svg-frames or svg-keyframes.
76 #[arg(long, value_enum, default_value = "svg-keyframes")]
77 format: RenderFormat,
78 /// Output directory.
79 #[arg(long, default_value = ".")]
80 output: PathBuf,
81 /// Frames per second.
82 #[arg(long, default_value_t = 60)]
83 fps: u32,
84 /// Simulation duration in seconds.
85 #[arg(long, default_value_t = 10.0)]
86 duration: f64,
87 /// Random seed for deterministic output.
88 #[arg(long, default_value_t = 42)]
89 seed: u64,
90 },
91 /// Validate experiment YAML against EDD v2 schema
92 Validate {
93 /// Path to the experiment YAML file.
94 experiment_path: PathBuf,
95 },
96 /// Verify reproducibility of an experiment
97 Verify {
98 /// Path to the experiment YAML file.
99 experiment_path: PathBuf,
100 /// Number of verification runs.
101 #[arg(long, default_value_t = 3, value_name = "N")]
102 runs: usize,
103 },
104 /// Check EMC compliance
105 EmcCheck {
106 /// Path to the experiment YAML file.
107 experiment_path: PathBuf,
108 },
109 /// Validate an EMC YAML file against EDD v2 EMC schema
110 EmcValidate {
111 /// Path to the EMC file.
112 emc_path: PathBuf,
113 },
114 /// List available EMCs in the library
115 ListEmc,
116 /// Show help
117 Help,
118 /// Show version
119 Version,
120}
121
122/// SVG render output format.
123#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
124pub enum RenderFormat {
125 /// One SVG file per frame.
126 SvgFrames,
127 /// One template SVG + keyframes JSON.
128 SvgKeyframes,
129}