fix(cli): list valid taps on bad --tap, make --help uniform across subcommands

Two CLI-UX defects the Stage-1 R fieldtest surfaced (#131):

1. Tap discovery. `aura chart <name> --tap <bad>` rejected an unknown tap but
   the error omitted the valid names. filter_to_tap now enumerates the run's
   available taps in the message ("... (available: equity, exposure)"), so the
   user can correct a typo instead of guessing -- refuse, but help.

2. Uniform --help. `aura chart --help` errored with "unexpected chart argument
   '--help'" (exit 2) and `aura run --help` leaked usage to stderr via the
   error path, while bare `aura --help` printed to stdout (exit 0). main() now
   intercepts `--help`/`-h` anywhere in the invocation, prints usage to stdout,
   and exits 0 -- uniform for every subcommand. The now-dead bare-flag match arm
   is removed (the early intercept covers it).

RED-first: the existing chart_tap_nonexistent test gains an available-taps
assertion (was: message had no names), and a new per_subcommand_help test pins
`aura <sub> --help` -> stdout usage + exit 0 for run/chart/sweep/walkforward/
mc/graph/runs (was: chart --help exited 2). Unknown-subcommand stays exit 2.

Verified: clippy --all-targets -D warnings clean; full suite 514 passed / 0
failed (one new test); live `aura chart --help` prints usage to stdout, exit 0.

closes #131
This commit is contained in:
2026-06-24 14:35:51 +02:00
parent 0288878ad2
commit 5c6de45b6e
2 changed files with 48 additions and 4 deletions
+12 -4
View File
@@ -467,10 +467,12 @@ fn build_comparison_chart_data(
/// run has no such tap (refuse-don't-guess). Used by the `--tap` flag on the
/// single-run chart path; without `--tap` the single-run page is unchanged.
fn filter_to_tap(data: ChartData, tap: &str) -> Result<ChartData, String> {
let series: Vec<Series> = data.series.into_iter().filter(|s| s.name == tap).collect();
if series.is_empty() {
return Err(format!("run has no tap named '{tap}'"));
if !data.series.iter().any(|s| s.name == tap) {
// List the valid taps so the user can correct a typo (#131) — refuse, but help.
let available: Vec<&str> = data.series.iter().map(|s| s.name.as_str()).collect();
return Err(format!("run has no tap named '{tap}' (available: {})", available.join(", ")));
}
let series: Vec<Series> = data.series.into_iter().filter(|s| s.name == tap).collect();
let mut meta = data.meta;
meta.taps = vec![tap.to_string()];
Ok(ChartData { xs: data.xs, series, meta })
@@ -1966,6 +1968,13 @@ fn main() {
// so an unexpected trailing token falls through to the usage-error path rather
// than masquerading as a successful run (#16 strict reading).
let args: Vec<String> = std::env::args().skip(1).collect();
// A `--help` / `-h` anywhere prints usage to stdout and exits 0, uniformly across
// subcommands (#131): `aura chart --help` shows usage instead of erroring
// "unexpected argument", and the bare `aura --help` is the same affordance.
if args.iter().any(|a| a == "--help" || a == "-h") {
println!("{USAGE}");
return;
}
match args.iter().map(String::as_str).collect::<Vec<_>>().as_slice() {
["run", rest @ ..] => match parse_run_args(rest) {
Ok(args) => match run_dispatch(args) {
@@ -2012,7 +2021,6 @@ fn main() {
["runs", "families"] => runs_families(),
["runs", "family", id] => runs_family(id, None),
["runs", "family", id, "rank", metric] => runs_family(id, Some(metric)),
["--help"] | ["-h"] => println!("{USAGE}"),
_ => {
eprintln!("aura: {USAGE}");
std::process::exit(2);
+36
View File
@@ -489,6 +489,12 @@ fn chart_tap_nonexistent_on_run_refuses_with_exit_2() {
stderr.contains("run has no tap named 'nope'"),
"expected the no-such-tap refusal message, got: {stderr}"
);
// #131: the refusal lists the valid taps so the user can correct the typo
// (the demo run's taps are `equity` and `exposure`).
assert!(
stderr.contains("available:") && stderr.contains("equity") && stderr.contains("exposure"),
"the refusal should enumerate the available taps, got: {stderr}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
@@ -557,6 +563,36 @@ fn help_flag_prints_usage_to_stdout_and_exits_zero() {
);
}
/// Property (#131): `--help`/`-h` is UNIFORM across subcommands — `aura <sub> --help`
/// prints usage to stdout and exits 0 for every subcommand, not only the bare `aura
/// --help`. Pre-fix, `aura chart --help` errored with "unexpected chart argument"
/// (exit 2) while `aura run --help` leaked usage to stderr; this pins the uniform
/// success-path help affordance for all subcommands.
#[test]
fn per_subcommand_help_is_uniform_stdout_exit_zero() {
for sub in ["run", "chart", "sweep", "walkforward", "mc", "graph", "runs"] {
for flag in ["--help", "-h"] {
let out = Command::new(BIN).args([sub, flag]).output().expect("spawn aura <sub> --help");
assert_eq!(
out.status.code(),
Some(0),
"`aura {sub} {flag}` should exit 0, got: {:?}",
out.status
);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(
stdout.contains("usage"),
"`aura {sub} {flag}` should print usage to stdout, got: {stdout:?}"
);
assert!(
out.stderr.is_empty(),
"`aura {sub} {flag}` should not write to stderr, got: {:?}",
String::from_utf8_lossy(&out.stderr)
);
}
}
}
/// Property: `aura graph` emits a single self-contained HTML page — the
/// interactive pin-graph viewer with the deterministic model inlined and the
/// vendored Graphviz-WASM + pan-zoom inlined (no remote fetch). Driven through