From a20b6f6003f5ddacf65acaa8f7dd2702f0abc904 Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 24 Jul 2026 00:57:33 +0200 Subject: [PATCH] fix(runner): contain mc synthetic member panics (GREEN) blueprint_mc_family joins its sweep/walkforward siblings on the 51096a3 machinery: catch_member_panic around the bare member run in the monte_carlo draw closure, per-seed capture, lowest-seed resolution after the join (seed-input order, thread-order-independent, C1), exit via the shared aura: warning: + exit 3 path. Ordering is load-bearing and documented at the site: the captured fault resolves BEFORE the vacuous-mc guard, so metrics-identical placeholder reports from faulted draws cannot masquerade as a vacuous run. All three containment e2es green; clippy clean. refs #278 --- crates/aura-runner/src/family.rs | 40 +++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) 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