fix(aura-runner, aura-cli): milestone-fieldtest bugs — library exposes-neither guard, override class, doc truth
RED-first fixes for the two code bugs the safe-to-embed milestone fieldtest caught: (1) run_signal_r panicked (exit 101) on a blueprint exposing neither bias nor a declared tap — the CLI pre-validated but the library seam, the very surface the milestone promises is kill-free, did not; the guard now refuses class 2 with the CLI's prose family before wrap_r (run_measurement has no wrap seam and no hole). (2) The override unknown-param refusal exited 1 through the one inline site the #297 adjudication missed; it and its wrapped-retired sibling are class 2 now, three existing pins re-pinned with the adjudication comment. Doc truth from the same fieldtest: the guide's campaign no-data class corrected (contained cell faults exit 3, not 1), TapPlanError's rustdoc no longer advertises the retired exit-1 register, and C30 records that rev + the embedding's committed lockfile are the reproducible-build contract (branch-referenced engine deps resolve at lock time). refs #296
This commit is contained in:
@@ -1277,13 +1277,15 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au
|
||||
raw node.param namespace — did you mean \"{raw}\"? see \
|
||||
`aura graph introspect --params <bp>`"
|
||||
);
|
||||
std::process::exit(1);
|
||||
// C14 class 2: fault in argv-named content (#297 adjudication, fieldtest follow-up)
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
let axes: Vec<(String, Vec<Scalar>)> = ov.iter().map(|(p, v)| (p.clone(), vec![*v])).collect();
|
||||
let paths = override_paths(&axes, &raw_space, &signal).unwrap_or_else(|e| {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(1);
|
||||
// C14 class 2: fault in argv-named content (#297 adjudication, fieldtest follow-up)
|
||||
std::process::exit(2);
|
||||
});
|
||||
// Refuse an open (free-knob) blueprint at the dispatch boundary — this
|
||||
// leg's own closed-blueprint guard now (#176; inherited from the
|
||||
|
||||
@@ -690,10 +690,11 @@ fn exec_override_non_literal_value_names_the_value_not_a_grammar_fault() {
|
||||
/// with `override_paths`' own prose (`member.rs:768-771`), pointing at the
|
||||
/// live axis-discovery verb (`aura graph introspect --params`) — the retired
|
||||
/// `aura sweep --list-axes` no longer exists.
|
||||
// C14 class 2: fault in argv-named content (#297 adjudication, fieldtest follow-up)
|
||||
#[test]
|
||||
fn exec_override_unknown_path_refuses_with_the_override_prose() {
|
||||
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "nope.knob=1"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert_eq!(code, Some(2), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("names no param of this blueprint (open or bound)"),
|
||||
"stdout/stderr: {out}"
|
||||
@@ -704,6 +705,34 @@ fn exec_override_unknown_path_refuses_with_the_override_prose() {
|
||||
);
|
||||
}
|
||||
|
||||
/// C14's FORM-vs-VALUE boundary, the unknown-param corner (fieldtest
|
||||
/// milestone-safe-to-embed, finding B2): "a form fault the surface detects
|
||||
/// before the node ever runs (kind mismatch, unknown param, compile error)
|
||||
/// stays class 2 argv-content" (`c14-headless-two-faces.md`, and the guide
|
||||
/// verbatim). An `--override` path naming no param of the blueprint (open or
|
||||
/// bound) is exactly that form fault, so it must exit 2 like its
|
||||
/// malformed-token, non-literal-value and kind-mismatch siblings — never 1,
|
||||
/// which a CI wrapper branching on the class reads as "the environment is
|
||||
/// missing something". The refusal prose itself is pinned as the bytes that
|
||||
/// exist today (`override_paths`, member.rs).
|
||||
#[test]
|
||||
fn exec_override_unknown_param_refuses_as_argv_content_class_2() {
|
||||
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "nope.length=3"]);
|
||||
assert_eq!(
|
||||
code,
|
||||
Some(2),
|
||||
"C14: unknown param is a form fault → argv-content class 2; stdout/stderr: {out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("names no param of this blueprint (open or bound)"),
|
||||
"the existing refusal prose must survive the re-class: {out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("aura graph introspect --params"),
|
||||
"the discovery pointer must survive the re-class: {out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A repeated `--override` path (the same NODE.PARAM named twice) refuses as
|
||||
/// a usage error naming the duplicated path — it must NOT fall through to
|
||||
/// `reopen_all`'s `NoSuchBoundParam` panic (the second reopen of an
|
||||
@@ -813,12 +842,13 @@ fn exec_override_kind_mismatch_refuses_with_prose_not_the_debug_struct() {
|
||||
/// missing it. Refuse before the zip, mirroring the campaign leg's own
|
||||
/// did-you-mean refusal for the same retired form
|
||||
/// (`RefFault::AxisNotInParamSpace`).
|
||||
// C14 class 2: fault in argv-named content (#297 adjudication, fieldtest follow-up)
|
||||
#[test]
|
||||
fn exec_override_wrapped_form_path_refuses_not_panics() {
|
||||
let (out, code) =
|
||||
run_code(&["exec", "examples/r_sma.json", "--override", "sma_signal.fast.length=1"]);
|
||||
assert_ne!(code, Some(101), "must not panic: {out}");
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert_eq!(code, Some(2), "stdout/stderr: {out}");
|
||||
assert!(!out.contains("panicked at"), "no raw panic report reaches the consumer: {out}");
|
||||
assert!(
|
||||
out.contains("did you mean") && out.contains("fast.length"),
|
||||
@@ -1688,11 +1718,14 @@ fn exec_well_formed_but_unregistered_content_id_refuses_at_runtime() {
|
||||
fn exec_exit_codes_partition_usage_two_from_runtime_one() {
|
||||
let (usage_out, usage_code) = run_code(&["exec", "examples/r_sma.json", "--bogus"]);
|
||||
assert_eq!(usage_code, Some(2), "unknown flag is a usage error -> 2: {usage_out}");
|
||||
// C14 class 2: fault in argv-named content (#297 adjudication, fieldtest
|
||||
// follow-up) — an unresolvable `--override` path is a form fault the
|
||||
// surface detects before the node ever runs, not a runtime failure.
|
||||
let (runtime_out, runtime_code) =
|
||||
run_code(&["exec", "examples/r_sma.json", "--override", "nope.knob=1"]);
|
||||
assert_eq!(
|
||||
runtime_code, Some(1),
|
||||
"a well-formed command with an unresolvable override is a runtime failure -> 1: {runtime_out}"
|
||||
runtime_code, Some(2),
|
||||
"a well-formed command with an unresolvable override is argv-content class 2: {runtime_out}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -554,6 +554,23 @@ pub fn run_signal_r(
|
||||
signal: Composite, params: &[Scalar], data: RunData, seed: u64, env: &Env,
|
||||
plan: TapPlan, topo: Option<&str>,
|
||||
) -> Result<(RunReport, Vec<String>), RunnerError> {
|
||||
// #297 fieldtest finding B1 (milestone-safe-to-embed): mirror the CLI's
|
||||
// own `exec_blueprint_leg` exposes-neither guard (`aura-cli/src/main.rs`,
|
||||
// `has_bias`/`has_tap`) IN the library, before `wrap_r` below wires a
|
||||
// "bias" output that does not exist — an embedding host driving
|
||||
// `run_signal_r` directly (never through the CLI shell) must see this
|
||||
// same typed refusal instead of `wrap_r`'s wiring `.expect` panicking
|
||||
// with `UnknownOutPort { name: "bias" }` and killing the host. Same
|
||||
// detection (`output()`/`taps()`), same prose family as the CLI's,
|
||||
// minus its own "aura: " print prefix — C14 class 2 (argv-named content).
|
||||
if !signal.output().iter().any(|o| o.name == "bias") && signal.taps().is_empty() {
|
||||
return Err(RunnerError {
|
||||
exit_code: 2,
|
||||
message: "exec needs either a `bias` output (a strategy) or ≥1 declared tap \
|
||||
(a measurement); this blueprint exposes neither"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
// topology_hash's own two-line body, inlined: `content_id_of` over the
|
||||
// canonical (#164) blueprint JSON — the CLI shell's `topology_hash`
|
||||
// helper is the same primitive, kept single-sourced at `aura_research`.
|
||||
@@ -1974,4 +1991,30 @@ mod tests {
|
||||
assert_eq!(err.exit_code, 2);
|
||||
assert!(err.message.contains("unknown fold"), "{}", err.message);
|
||||
}
|
||||
|
||||
/// The #297 promise at the exposes-neither seam (fieldtest
|
||||
/// milestone-safe-to-embed, finding B1): a blueprint exposing NEITHER a
|
||||
/// `bias` output NOR a declared tap is not runnable on this leg, and the
|
||||
/// library must say so as a returned `RunnerError` — C14 class 2
|
||||
/// (argv-named content, the CLI guard's own prose family) — never via
|
||||
/// `wrap_r`'s wiring `.expect` (`UnknownOutPort { name: "bias" }`,
|
||||
/// member.rs:443) panicking and killing the embedding host. The CLI's
|
||||
/// shell-side pre-validation (`exec_blueprint_leg`) must not be the only
|
||||
/// guard on the seam the milestone promises is kill-free.
|
||||
#[test]
|
||||
fn run_signal_r_refuses_an_exposes_neither_blueprint_in_process() {
|
||||
let (env, _root) = temp_project_env("exposes-neither");
|
||||
// Minimal exposes-neither signal: one role, one node, no exposed
|
||||
// output, no declared tap.
|
||||
let mut g = GraphBuilder::new("blind_signal");
|
||||
let s = g.add(Sub::builder());
|
||||
let price = g.source_role("price", ScalarKind::F64);
|
||||
g.feed(price, [s.input("lhs"), s.input("rhs")]);
|
||||
let blind = g.build().expect("an exposes-neither composite still builds");
|
||||
let err = run_signal_r(blind, &[], RunData::Synthetic, 0, &env, TapPlan::empty(), None)
|
||||
.unwrap_err();
|
||||
assert_eq!(err.exit_code, 2, "argv-named content (C14): {}", err.message);
|
||||
assert!(err.message.contains("bias"), "{}", err.message);
|
||||
assert!(err.message.contains("exposes neither"), "{}", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,8 +217,9 @@ impl FoldRegistry {
|
||||
}
|
||||
|
||||
/// A typed tap-plan fault — the pre-bootstrap refusals plus the terminal
|
||||
/// store fault. Entry points map every variant to the established
|
||||
/// `aura: ` + exit-1 refusal register via `Display`.
|
||||
/// store fault. Entry points wrap every variant into a returned
|
||||
/// `RunnerError` (#297) — prose via `Display`, class via `exit_class`
|
||||
/// (content faults 2, store I/O 1, the C14 partition).
|
||||
pub enum TapPlanError {
|
||||
/// The plan names a tap the blueprint does not declare.
|
||||
UnknownTap { name: String, declared: Vec<String> },
|
||||
|
||||
@@ -855,9 +855,11 @@ mismatch, a compile error, a tap-bind content fault (unknown fold/tap,
|
||||
duplicate) — are the content of the argv-named file and exit 2 like a usage
|
||||
error; only its environment refusals (a tap-trace store write failure)
|
||||
exit 1. The no-data family (no local data, no data in the requested window,
|
||||
no recorded geometry) belongs to the real-data paths — `reproduce` and the
|
||||
campaign legs — where it likewise exits 1 (#297; the full partition lives
|
||||
in [C14](design/contracts/c14-headless-two-faces.md)). `aura
|
||||
no recorded geometry) belongs to the real-data paths: on `reproduce` it
|
||||
exits 1, while inside a campaign the same fault is contained per cell
|
||||
(#272) — the run completes and exits 3, naming the failed cells (#297;
|
||||
the full partition lives in
|
||||
[C14](design/contracts/c14-headless-two-faces.md)). `aura
|
||||
reproduce`'s identity/pip guard (a stored member's recorded instrument or
|
||||
broker/pip label contradicting the source it re-derives against) is
|
||||
likewise class 1 on the plain verb — its source is derived from the
|
||||
|
||||
@@ -25,9 +25,13 @@ hand-maintained version.
|
||||
refused at load (point 3), never trusted.
|
||||
- An **external embedding** (a World program in its own repo)
|
||||
consumes the workspace crates as a cargo git dependency pinned to a
|
||||
rev. The pin is the whole contract: updating it is a deliberate act
|
||||
that accepts whatever moved, with the design ledger and commit
|
||||
bodies as the changelog.
|
||||
rev. The pin names the engine; the embedding's own committed
|
||||
`Cargo.lock` freezes the rest of the graph — dependencies the
|
||||
engine itself takes as git-branch references (e.g. the data-server)
|
||||
resolve at lock time, so rev + lockfile together are the
|
||||
reproducible-build contract (fieldtest finding, 2026-07-26).
|
||||
Updating either is a deliberate act that accepts whatever moved,
|
||||
with the design ledger and commit bodies as the changelog.
|
||||
|
||||
3. **The load handshake refuses non-identical builds.** The cdylib
|
||||
handshake already refuses a rustc mismatch (`RUSTC_VERSION`, emitted
|
||||
|
||||
Reference in New Issue
Block a user