fix(cli): walkforward validates its --axis grid once, not once per window

`aura walkforward <bp> --axis <name>=<v>` emitted a rejected axis's
diagnostic more than once (a racy 1-3x) when the grid did not resolve.
`blueprint_walkforward_family` resolved the axes *inside* the per-window
closure, and `walk_forward` fans that closure out across the windows in
parallel — so several windows each `eprintln!` the same `BindError` and
`exit(2)` before any one exit tears the process down.

Axis resolution is window-independent, so it is hoisted to a single
dispatch-boundary pre-flight (mirroring `aura sweep`, which validates its
axes once before any member runs): the first in-sample window is resolved
up front, surfacing the `BindError` exactly once; the per-window closure
then resolves already-validated axes and cannot re-raise (`expect`).

RED-first: cli_run.rs `aura_walkforward_emits_an_unknown_axis_rejection_once`
drives the verb 20x and asserts the rejection is single every run (the emit
count was racy, so a single invocation would be a flaky assertion). Surfaced
by the World/C21 milestone fieldtest.

closes #177
refs #170
This commit is contained in:
2026-07-01 17:41:09 +02:00
parent dec07809df
commit 6a775f700f
2 changed files with 52 additions and 1 deletions
+17 -1
View File
@@ -3306,9 +3306,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); }