feat(cli): default the stop regime on walkforward/mc/generalize

GREEN side of the #217 executable-spec: --stop-length/--stop-k are now
optional on the three dissolved verbs; each missing flag independently
defaults to the single-sourced regime constants (length 3, k 2.0) at
the argv-boundary extractors — the same regime the campaign member
runner resolves for an unbound regime. The generated campaign document
still binds the regime explicitly, so the default spelling is
content-identical to an explicit --stop-length 3 --stop-k 2.0 (pinned
by registry dedup in the headline test). The multi-value stop refusal
survives byte-identically; sweep is untouched.

Sanctioned contract migrations (decision on the issue, 2026-07-09):
walkforward_dissolved_refuses_missing_knobs now pins the surviving
multi-value refusal; mc_args_from_refuses_missing_knobs pins the
default path. Usage strings name the defaults.

Held quality nit (deferred to cycle close, minimal-slice constraint):
the None=>default / Some=>parse stop block recurs 4x across the
walkforward/mc extractors.

closes #217
This commit is contained in:
2026-07-09 16:38:05 +02:00
parent af8564d20b
commit 210230596e
2 changed files with 104 additions and 45 deletions
+26 -7
View File
@@ -1654,23 +1654,42 @@ fn walkforward_dissolved_refuses_select_plateau() {
assert!(stderr.contains("#215"), "forward pointer to the plateau follow-up: {stderr}");
}
/// `walkforward_args_from` requires both stop knobs on the campaign path — a
/// missing knob is a usage refusal (exit 2), not a silent default.
/// Property (#217): `walkforward_args_from` no longer requires both stop knobs
/// together on the campaign path — each defaults independently to the
/// single-sourced regime. Omitting only `--stop-k` while keeping `--stop-length`
/// explicit still succeeds, proving the defaulting is per-flag, not
/// all-or-nothing (the sibling
/// `walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2`
/// pins the both-omitted case). Gated on the shared GER40 archive; skips
/// cleanly on a data refusal.
#[test]
fn walkforward_dissolved_refuses_missing_knobs() {
let cwd = temp_cwd("walkforward-missing-knobs");
fn walkforward_dissolved_defaults_a_single_omitted_knob() {
let cwd = temp_cwd("walkforward-single-knob-default");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "3",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "missing --stop-length/--stop-k is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("requires --stop-length --stop-k"), "missing-knob refusal: {stderr}");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(
out.status.code(),
Some(0),
"omitting only --stop-k must still succeed (it defaults independently): {}",
String::from_utf8_lossy(&out.stderr)
);
}
/// An empty `--real ""` still satisfies clap's `Option::is_some()` dispatch guard, so