diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index a121dee..9d339b1 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -2075,6 +2075,51 @@ fn run_oos_blueprint( (Vec::new(), report) } +/// A constant, zero-compute `RunReport` for [`validate_axis_grid`]'s sweep-terminal +/// probe. `SweepBinder::sweep_with_lattice`'s own `resolve_axes`/arity/kind checks all +/// run BEFORE this closure is invoked per grid point, so its body never influences the +/// validation outcome — only its signature (`Fn(&[Cell]) -> RunReport`) needs to +/// satisfy the terminal, at O(1) cost per point instead of a full member run. +fn axis_grid_probe_report() -> RunReport { + RunReport { + manifest: RunManifest { + commit: String::new(), + params: Vec::new(), + window: (Timestamp(0), Timestamp(0)), + seed: 0, + broker: "wf-axis-preflight-placeholder".to_string(), + selection: None, + instrument: None, + topology_hash: None, + project: None, + }, + metrics: RunMetrics { total_pips: 0.0, max_drawdown: 0.0, bias_sign_flips: 0, r: None }, + } +} + +/// Validate the `--axis` grid against `doc`'s wrapped param space WITHOUT running any +/// member (#253). Reuses the SAME strict, erroring axis-name check +/// `blueprint_sweep_over` performs (`override_paths` — single-sourced, so the two +/// paths cannot drift to differently-worded rejections) to derive the #246 override +/// set, then drives the sweep terminal's own `resolve_axes`/arity/kind checks +/// (`SweepBinder::sweep_with_lattice`) with [`axis_grid_probe_report`] standing in for +/// the run closure — no data access, no sim engine tick. Axis resolution is +/// window-agnostic, so the caller derives or passes no window at all. +fn validate_axis_grid( + doc: &str, axes: &[(String, Vec)], raw_space: &[ParamSpec], probe_signal: &Composite, + env: &project::Env, +) -> Result<(), BindError> { + let overrides = override_paths(axes, raw_space, probe_signal).map_err(BindError::UnknownKnob)?; + let probe = blueprint_axis_probe_reopened(doc, env, &overrides); + let mut iter = axes.iter(); + let (first_name, first_vals) = iter.next().expect("a blueprint walk-forward declares >= 1 axis"); + let mut binder = probe.axis(first_name, first_vals.clone()); + for (n, vals) in iter { + binder = binder.axis(n, vals.clone()); + } + binder.sweep_with_lattice(|_| axis_grid_probe_report()).map(|_| ()) +} + /// The loaded-blueprint IS-refit walk-forward: per IS window, re-optimize the /// blueprint over the user `--axis` grid, select by `sqn_normalized`, run the /// winner OOS, reusing the generic `walk_forward` driver + `select_winner`; @@ -2124,15 +2169,10 @@ fn blueprint_walkforward_family( // (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, env, &binding) { + // exit lands — a racy, duplicated rejection (#177). Axis resolution is window-agnostic + // (#253): `validate_axis_grid` resolves the SAME grid the sweep terminal would, without + // running a single member — no IS window (or a second roller) is needed here at all. + if let Err(e) = validate_axis_grid(doc, axes, &raw_space, &probe_signal, env) { eprintln!("aura: {e:?}"); std::process::exit(2); }