diff --git a/crates/aura-campaign/src/exec.rs b/crates/aura-campaign/src/exec.rs index 2e46410..6cf3c75 100644 --- a/crates/aura-campaign/src/exec.rs +++ b/crates/aura-campaign/src/exec.rs @@ -13,7 +13,7 @@ use aura_analysis::{FamilySelection, SelectionMode}; use aura_core::{zip_params, Cell, ParamSpec, Scalar, Timestamp}; use aura_engine::{ sweep, walk_forward, GridSpace, ListSpace, RollMode, RunManifest, RunMetrics, RunReport, - Space, SweepFamily, SweepPoint, WindowBounds, WindowRoller, WindowRun, + Space, SweepFamily, SweepPoint, WalkForwardError, WindowBounds, WindowRoller, WindowRun, }; use aura_registry::{ optimize, optimize_deflated, optimize_plateau, sweep_member_reports, @@ -508,7 +508,24 @@ fn run_walk_forward_stage( *step_ms as i64, roll_mode, ) - .map_err(|e| ExecFault::Window { stage, detail: format!("{e:?}") })?; + .map_err(|e| ExecFault::Window { + stage, + // The detail string reaches user-facing seams verbatim — phrase both + // roller refusals in the doc's ms unit, never a Debug form. + detail: match e { + WalkForwardError::SpanTooShort { .. } => format!( + "in_sample_ms + out_of_sample_ms = {} ms exceeds the campaign window [{}, {}] ({} ms)", + in_sample_ms + out_of_sample_ms, + cell.window_ms.0, + cell.window_ms.1, + cell.window_ms.1 - cell.window_ms.0 + 1, + ), + // Zero lengths are doc-tier faults (preflight); phrased anyway. + WalkForwardError::NonPositiveLength { field, value } => { + format!("walk_forward {field} must be > 0 (got {value})") + } + }, + })?; // A second identical roller enumerates the bounds up front: the engine's // walk_forward consumes its roller, and the parallel closure needs each // window's roll index for deterministic fault attribution. diff --git a/crates/aura-campaign/src/lib.rs b/crates/aura-campaign/src/lib.rs index 050ae9d..d31c289 100644 --- a/crates/aura-campaign/src/lib.rs +++ b/crates/aura-campaign/src/lib.rs @@ -861,8 +861,13 @@ mod wf_tests { ExecFault::Window { stage, detail } => { assert_eq!(stage, 1, "walk_forward is pipeline stage 1 here"); assert!( - detail.contains("SpanTooShort"), - "detail carries the roller refusal: {detail}" + detail.contains("in_sample_ms + out_of_sample_ms = 60 ms") + && detail.contains("(50 ms)"), + "detail phrases the refusal in doc-unit ms prose: {detail}" + ); + assert!( + !detail.contains("SpanTooShort") && !detail.contains("Timestamp("), + "no Debug form leaks through the detail seam: {detail}" ); } other => panic!("expected ExecFault::Window, got {other:?}"),