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
This commit is contained in:
2026-07-18 01:14:29 +02:00
parent e768072ed0
commit cb6211c888
+23 -3
View File
@@ -1748,9 +1748,10 @@ fn run_signal_r(
let (tx_req, _rx_req) = mpsc::channel(); let (tx_req, _rx_req) = mpsc::channel();
let (sources, window, pip_size) = resolve_run_data(&data, env, &binding); 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 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 let mut flat = wrapped.compile_with_params(params).unwrap_or_else(|e| {
.compile_with_params(params) eprintln!("aura: this blueprint does not compile to a runnable harness: {e:?}");
.expect("signal binds + wraps to a valid harness"); std::process::exit(1);
});
// Bind each declared tap to a fresh `Recorder` + channel, before bootstrap // Bind each declared tap to a fresh `Recorder` + channel, before bootstrap
// — `flat.taps` already carries the signal's declared taps hoisted to the // — `flat.taps` already carries the signal's declared taps hoisted to the
// root. Dedup is the caller's per `TapBindError::DuplicateBind`'s doc (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}"); eprintln!("aura: {path}: {msg}");
std::process::exit(2); 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 // 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 // `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). // free knob would panic in `compile_with_params` — reject it clean instead (#176).