fix(cli): reproduce re-derives the stop regime from the member manifest

reproduce_family_in hardcoded the default StopRule::Vol{3,2.0} at both
the param-space probe and the member re-run, so a family minted under a
non-default vol-stop (campaign risk-regime cell, or wf/mc/generalize
with --stop-length/--stop-k) spuriously reported DIVERGED.

New stop_rule_from_params re-derives StopRule::Vol from the manifest's
stamped stop_length/stop_k, falling back to the defaults when the keys
are absent (pre-stamp members) — the same one-directional widening
point_from_params applies to missing manifest params, mirroring
campaign_run's stop_rule_for_regime None arm. The stop rides outside
the wrapped param space, so point_from_params could never recover it.

Verified: RED test green, full workspace suite green (independent
mini-verify), clippy -D warnings clean.

closes #233
This commit is contained in:
2026-07-10 18:53:01 +02:00
parent ab14eeed3c
commit fb145f5a15
+19 -3
View File
@@ -1031,6 +1031,22 @@ fn point_from_params(space: &[ParamSpec], params: &[(String, Scalar)]) -> Vec<Ce
.collect()
}
/// Re-derive the `StopRule` a member was minted under from its manifest params
/// (`stop_length`/`stop_k`, stamped by `run_blueprint_member` — the stop rides the
/// risk regime OUTSIDE the wrapped param_space, so `point_from_params` cannot
/// recover it). Falls back to the default vol-stop
/// regime when the manifest carries no stop knobs (pre-#233 members), mirroring
/// `stop_rule_for_regime`'s `None` arm (campaign_run.rs) for the same one-directional
/// widening `point_from_params` already applies to missing manifest params.
fn stop_rule_from_params(params: &[(String, Scalar)]) -> StopRule {
let length = params.iter().find(|(n, _)| n == "stop_length").map(|(_, s)| s.as_i64());
let k = params.iter().find(|(n, _)| n == "stop_k").map(|(_, s)| s.as_f64());
match (length, k) {
(Some(length), Some(k)) => StopRule::Vol { length, k },
_ => StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K },
}
}
/// Look up a persisted family by id, or exit 1 (unknown id / registry load failure) —
/// the single place `reproduce_family` and `reproduce_family_in` resolve a family, so
/// the two exit-1 error phrasings can't drift out of sync between the call sites.
@@ -1092,8 +1108,8 @@ fn reproduce_family_in(
let (tx_ex, _) = mpsc::channel();
let (tx_r, _) = mpsc::channel();
let (tx_req, _) = mpsc::channel();
let space =
wrap_r(reload(), tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, true, SYNTHETIC_PIP_SIZE).param_space();
let stop = stop_rule_from_params(&stored.manifest.params);
let space = wrap_r(reload(), tx_eq, tx_ex, tx_r, tx_req, stop, true, SYNTHETIC_PIP_SIZE).param_space();
let point = point_from_params(&space, &stored.manifest.params);
// A MonteCarlo member carries no tuning params (the params-join is empty), so its
// reproduce line would print a BLANK member label; the seed IS its realization
@@ -1153,7 +1169,7 @@ fn reproduce_family_in(
pip,
&hash,
env,
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K },
stop,
);
outcomes.push((label, rerun.metrics == stored.metrics));
}