Merge main into the clap-adoption cycle — re-integrate the two parallel CLI fixes

main advanced with two `fix(cli)` commits (dec0780, 6a775f7) landed by a parallel
session while the clap adoption (cycles 0098/0099) was built from 780d823. Both
touch behaviour the clap migration rewrote or sits beside; this merge preserves
them rather than letting the rewrite silently drop them:

- dec0780 (open-blueprint guard, #176): `aura run <open blueprint.json>` must
  refuse with a clean exit 2, never a `compile_with_params` arity panic (exit
  101). The fix lived in the old hand-rolled `run` .json-arm, which the clap
  migration replaced — re-integrated into `dispatch_run`'s .json branch (the same
  `blueprint_axis_probe().param_space()` check + "closed blueprint" diagnostic).
- 6a775f7 (walkforward axis-once, #177): axis validation hoisted to a single
  dispatch-boundary pre-flight, in the execution fn `blueprint_walkforward_family`
  — which the migration did not touch, so it auto-merged unchanged.

Both fixes are verified by their own RED tests
(`aura_run_rejects_an_open_blueprint_without_panicking`,
`aura_walkforward_emits_an_unknown_axis_rejection_once`), and both are consistent
with this cycle's iteration-2 exit-code classification: an open blueprint and an
unknown axis are argument faults, so usage errors → exit 2.

The conflict was confined to `fn main` in crates/aura-cli/src/main.rs (the clap
`Cli::parse()` dispatch vs the old argv `match`); resolved to the clap side plus
the re-integrated guard. cli_run.rs auto-merged (their two new tests + this
cycle's renegotiated pins).

Verified green (orchestrator, this session): cargo build --workspace, cargo clippy
--workspace --all-targets -- -D warnings, and cargo test --workspace all clean.
This commit is contained in:
2026-07-02 00:53:54 +02:00
2 changed files with 95 additions and 1 deletions
+29 -1
View File
@@ -3002,9 +3002,25 @@ fn blueprint_walkforward_family(
let space = blueprint_axis_probe(doc).param_space();
let topo = topology_hash(&blueprint_from_json(doc, &|t| std_vocabulary(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible"));
// Validate the `--axis` grid ONCE at the dispatch boundary, mirroring `aura sweep`
// (which resolves its axes a single time before any member runs). `walk_forward` fans
// the per-window closure out across the windows in parallel, so a `BindError` raised
// *inside* the closure would `eprintln!`+`exit(2)` from several windows before any one
// exit lands — a racy, duplicated rejection (#177). Axis resolution is window-agnostic,
// so pre-flighting the first IS window here surfaces the error exactly once (it fails in
// `resolve_axes`, before any member runs); a per-window resolve then cannot re-raise.
let first_is = WindowRoller::new(span, is_len, oos_len, step, RollMode::Rolling)
.expect("roller config validated just above")
.next()
.expect("roller yields >= 1 window (validated above)")
.is;
if let Err(e) = blueprint_sweep_over(doc, axes, first_is.0, first_is.1, data) {
eprintln!("aura: {e:?}");
std::process::exit(2);
}
walk_forward(roller, space.clone(), |w: WindowBounds| {
let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data)
.unwrap_or_else(|e| { eprintln!("aura: {e:?}"); std::process::exit(2); });
.expect("axes validated in the dispatch-boundary pre-flight");
let (best, selection) = match select_winner(&is_family, "sqn_normalized", select, Some(&lattice)) {
Ok(v) => v,
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
@@ -4129,6 +4145,18 @@ fn dispatch_run(a: RunCmd) {
eprintln!("aura: {path}: {e:?}");
std::process::exit(2);
});
// Refuse an open (free-knob) blueprint at the dispatch boundary, mirroring
// `blueprint_mc_family`'s closed-guard: `run` bootstraps over the EMPTY point, so a
// free knob would panic in `compile_with_params` — reject it clean instead (#176).
let free = blueprint_axis_probe(&doc).param_space();
if !free.is_empty() {
eprintln!(
"aura: run requires a closed blueprint (no free parameters); {} free knob(s) — \
bind them or use `aura sweep --axis`",
free.len()
);
std::process::exit(2);
}
let params = match a.params.as_deref() {
Some(j) => parse_param_cells(j).unwrap_or_else(|m| {
eprintln!("aura: {m}");