feat(campaign,cli): permit --select plateau in the walk_forward stage (#215)

The campaign walk_forward stage picked its per-window in-sample winner by
optimize_deflated (argmax) unconditionally, dropping the stage's `select`
field, and both the preflight and the `walkforward` verb's `--real` campaign
sub-branch refused plateau:* with a forward-pointer here (the #210 decision-4
deferral; the synthetic non-`--real` sub-branch has accepted plateau since
#173). Honour plateau selection when no gate precedes the wf stage.

- Preflight: the plateau-in-wf refusal is narrowed to gate-preceded stages.
  With no preceding gate the survivors are the full sweep grid, so the
  parameter lattice (grid.axis_lens) is exact and optimize_plateau is sound; a
  gate filters survivors below the grid, breaking the lattice, so plateau after
  any gate stays refused. The refusal is static — document validity must be
  data-independent (a gate's filtering depends on the data) — and keeps the
  existing "a gated survivor subset has no parameter lattice" message, which now
  describes exactly the only case it fires.
- Executor: run_walk_forward_stage threads the per-cell grid.axis_lens and picks
  each window's in-sample winner via select_sweep_winner — the same dispatch the
  sweep stage uses. (Argmax, deflate=true) is byte-identical to the old
  optimize_deflated call, so the argmax grade and content-id pins are unchanged;
  the plateau arms read axis_lens and ignore deflate.
- CLI: the walkforward verb threads --select through translate_walkforward /
  run_walkforward_sugar (a walkforward-local param, not the shared four-verb
  SugarInvocation), mapping the parsed Selection to the research SelectRule via
  select_rule_of; the refusal and its select_is_plateau helper are deleted.
  Scope is walkforward-only — the mc verb's wf-stage select is untouched.

The plateau:worst grade anchor (stitched_total_pips = -9683776.67) is distinct
from the argmax anchor (-10398606.67), proving the plateau path genuinely
changes winner selection rather than falling back to argmax. A gate-free plateau
process validates clean while a gate-preceded one is refused (a new
campaign-validate e2e). Argmax defaults stay byte-identical.

closes #215
This commit is contained in:
2026-07-10 02:59:33 +02:00
parent 35b996e3bf
commit 94cdbf90cc
6 changed files with 335 additions and 67 deletions
+10 -7
View File
@@ -375,6 +375,7 @@ pub(crate) fn translate_walkforward(
inv: &SugarInvocation,
metric: &str,
w: WfWindows,
select: SelectRule,
) -> Result<GeneratedWalkforward, String> {
let process = ProcessDoc {
format_version: FORMAT_VERSION,
@@ -395,7 +396,7 @@ pub(crate) fn translate_walkforward(
step_ms: w.step_ms,
mode: WfMode::Rolling,
metric: metric.to_string(),
select: SelectRule::Argmax,
select,
},
],
};
@@ -445,9 +446,10 @@ pub(crate) fn run_walkforward_sugar(
inv: &SugarInvocation,
metric: &str,
w: WfWindows,
select: SelectRule,
env: &crate::project::Env,
) -> Result<(), String> {
let generated = translate_walkforward(inv, metric, w)?;
let generated = translate_walkforward(inv, metric, w, select)?;
let probe_params = probe_params_from(inv.axes);
validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?;
let reg = env.registry();
@@ -817,8 +819,8 @@ mod tests {
fn translate_walkforward_is_deterministic_and_carries_the_regime() {
let ax = wf_axes();
let i = inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}");
let a = translate_walkforward(&i, "sqn_normalized", WF_W).unwrap();
let b = translate_walkforward(&i, "sqn_normalized", WF_W).unwrap();
let a = translate_walkforward(&i, "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
let b = translate_walkforward(&i, "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
assert_eq!(
content_id_of(&campaign_to_json(&a.campaign)),
content_id_of(&campaign_to_json(&b.campaign)),
@@ -866,9 +868,9 @@ mod tests {
#[test]
fn translate_walkforward_diverging_regimes_do_not_collide_content_ids() {
let ax = wf_axes();
let base = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
let different_k = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
let different_length = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 20, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
let base = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
let different_k = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
let different_length = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 20, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
let base_id = content_id_of(&campaign_to_json(&base.campaign));
let k_id = content_id_of(&campaign_to_json(&different_k.campaign));
let length_id = content_id_of(&campaign_to_json(&different_length.campaign));
@@ -893,6 +895,7 @@ mod tests {
&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"),
"sqn_normalized",
WF_W,
SelectRule::Argmax,
)
.unwrap();
let (process_id, campaign_id) = register_generated_wf(&reg, &generated).unwrap();