diff --git a/crates/aura-runner/src/family.rs b/crates/aura-runner/src/family.rs index d77a823..adf3936 100644 --- a/crates/aura-runner/src/family.rs +++ b/crates/aura-runner/src/family.rs @@ -354,6 +354,20 @@ fn lowest_point_fault(family: &SweepFamily, faults: Mutex, String .find_map(|pt| captured.iter().find(|(p, _)| *p == pt.params).map(|(_, m)| m.clone())) } +/// The LOWEST seed's captured member-panic message (#278), found by walking +/// `family.draws` — `monte_carlo`'s own contract guarantees seed-**input** +/// order regardless of thread completion (C1) — for the first draw whose +/// seed matches a captured fault. Thread-order-independent by construction, +/// mirroring [`lowest_point_fault`]'s and `lowest_window_fault`'s identical +/// convention one level over (per-point sweep / per-window walk-forward). +fn lowest_seed_fault(family: &McFamily, faults: Mutex>) -> Option { + let captured = faults.into_inner().expect("fault capture lock"); + family + .draws + .iter() + .find_map(|d| captured.iter().find(|(s, _)| *s == d.seed).map(|(_, m)| m.clone())) +} + /// Render + exit(3) on a contained member panic (#278: the deliberate /// failed-cells exit, C14, never the raw 101 an uncontained panic would /// yield) — reuses `aura_campaign::member_fault_prose`'s established @@ -772,11 +786,35 @@ pub fn blueprint_mc_family( // re-runs the shared reduce-mode member path over its own seeded synthetic walk. let seeds: Vec = (1..=n_seeds).collect(); let base_point: Vec = Vec::new(); + // #278: `run_blueprint_member` ran bare here, the one family builder without + // the #272 fault boundary its sweep/walk-forward siblings gained in 51096a3 + // — a member-compile panic (e.g. an `Sma::new` length assert) would otherwise + // unwind straight through `monte_carlo`'s `run_indexed` to an uncaught exit + // 101. Contained the same way: `catch_member_panic` + a per-seed capture, + // resolved to the LOWEST seed's message after the join (thread-order- + // independent, C1) via `lowest_seed_fault`. + let faults: Mutex> = Mutex::new(Vec::new()); let family = monte_carlo(&base_point, &seeds, |seed, _base| { let sources = synthetic_walk_sources(seed); let window = window_of(&sources).expect("non-empty synthetic walk"); - run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT) + match catch_member_panic(|| { + run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT) + }) { + Ok(report) => report, + Err(msg) => { + faults.lock().expect("fault capture lock").push((seed, msg)); + axis_grid_probe_report() + } + } }); + // The captured fault must win BEFORE the vacuous-mc guard below: a faulted + // draw's placeholder report is metrics-identical across every faulted seed, + // so a run with >= 2 faulted seeds (or one faulted + one real draw sharing + // its placeholder's zero metrics) could otherwise trip the vacuous refusal + // instead of surfacing the real member fault. + if let Some(msg) = lowest_seed_fault(&family, faults) { + exit_on_member_panic(&msg); + } // Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's // metrics are bit-identical to the first, no seed reached a distinguishable realization — // the strategy never warmed over the fixed synthetic walk (e.g. a lookback as deep as the