From cb6211c888e1f6fd62aa66a8f829bc79c051c43e Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 18 Jul 2026 01:14:29 +0200 Subject: [PATCH] fix(cli): aura run refuses an unrunnable blueprint cleanly, not by panic (#282) Two user-authorable blueprint shapes crashed `aura run` with an internal .expect instead of the CLI's clean aura: refusal register: - a tap wire pointing out of range: run_signal_r's compile now handles CompileError (TapWireOutOfRange) as an aura: refusal + exit 1, naming that the blueprint does not compile to a runnable harness. - an exposed output not named bias: a pre-check in dispatch_run (the single loaded-blueprint call site, before the free-knob axis probe that welds the hardcoded bias port and would itself panic) refuses with a message telling the user aura run expects a strategy with a bias output and listing what the blueprint exposes. The sweep path (run_blueprint_member) is untouched. Surfaced by the measurement-milestone fieldtest as the sharp edge of raw-index tap authoring (#284 tracks the name-addressed tap op that removes the miscount class at the source). closes #282 --- crates/aura-cli/src/main.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 785115a..f7a3ed9 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -1748,9 +1748,10 @@ fn run_signal_r( let (tx_req, _rx_req) = mpsc::channel(); let (sources, window, pip_size) = resolve_run_data(&data, env, &binding); let wrapped = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, false, pip_size, &binding, None); - let mut flat = wrapped - .compile_with_params(params) - .expect("signal binds + wraps to a valid harness"); + let mut flat = wrapped.compile_with_params(params).unwrap_or_else(|e| { + eprintln!("aura: this blueprint does not compile to a runnable harness: {e:?}"); + std::process::exit(1); + }); // Bind each declared tap to a fresh `Recorder` + channel, before bootstrap // — `flat.taps` already carries the signal's declared taps hoisted to the // root. Dedup is the caller's per `TapBindError::DuplicateBind`'s doc (the @@ -3585,6 +3586,25 @@ fn dispatch_run(a: RunCmd, env: &project::Env) { eprintln!("aura: {path}: {msg}"); std::process::exit(2); }); + // `wrap_r` (below, via `run_signal_r` AND the axis probe right here) welds + // the loaded signal by its hardcoded `"bias"` output port (the R-evaluator + // contract). A signal exposing any other output shape (a measurement-only + // blueprint) would otherwise only surface as `blueprint_axis_probe`'s + // `wrap_r` call panicking on `BuildError::UnknownOutPort` a few lines + // down — refuse here instead, before that probe runs. + if !signal.output().iter().any(|o| o.name == "bias") { + let exposed: Vec<&str> = signal.output().iter().map(|o| o.name.as_str()).collect(); + eprintln!( + "aura: `aura run` expects a strategy with a bias output; this blueprint \ + exposes {}", + if exposed.is_empty() { + "no outputs".to_string() + } else { + exposed.join(", ") + } + ); + std::process::exit(1); + } // Refuse an open (free-knob) blueprint at the dispatch boundary, mirroring // `blueprint_mc_family`'s closed-guard: `run` bootstraps over the EMPTY point, so a // free knob would panic in `compile_with_params` — reject it clean instead (#176).