feat(cli): bind the resolved regime stop and stamp it into the manifest (#210 T4)

The runtime-binding slice — the regime's stop now actually reaches the graph and
the record:

- `StopArm { Open, Bound(StopRule) }` replaces `wrap_r`'s `stop_open: bool` so a
  bound stop carries its params; all call sites threaded (the r-sma sweep path
  stays Open, every bound site passes the resolved rule).
- `run_blueprint_member` takes a `stop: StopRule` and stamps `stop_length` /
  `stop_k` into the member manifest (the ratified C18 extension — the stop is no
  longer invisible on the campaign/blueprint path); its callers thread the
  default, and `CliMemberRunner` maps `cell.regime` (None -> baked default,
  Some(Vol) -> that vol stop).
- The characterization pin flips from asserting the stop's ABSENCE to requiring
  the stamped default; a real two-regime e2e pins that each regime stamps its
  own k.

Known deferred gap (concern, documented descope): `persist_campaign_traces`
re-runs the nominee to persist its traces and — since `CellRealization` carries
no regime — binds that re-run to the DEFAULT stop, so a non-default-regime
nominee's persisted trace would reflect the wrong stop. Only the persist path
(non-empty `persist_taps`) is affected; the core run/stamp/generalize path is
correct. Filed as a follow-up. Full workspace suite green.

refs #210
This commit is contained in:
2026-07-06 14:52:20 +02:00
parent a0d8aec3f7
commit c62f7e9878
4 changed files with 232 additions and 38 deletions
+23 -1
View File
@@ -268,6 +268,15 @@ impl MemberRunner for CliMemberRunner<'_> {
// id, project provenance stamped inside the helper.
let signal = blueprint_from_json(&cell.blueprint_json, &|t| self.env.resolve(t))
.expect("stored blueprint passed the referential gate; reload is infallible");
let stop = match cell.regime {
None => aura_composites::StopRule::Vol {
length: crate::R_SMA_STOP_LENGTH,
k: crate::R_SMA_STOP_K,
},
Some(aura_research::RiskRegime::Vol { length, k }) => {
aura_composites::StopRule::Vol { length, k }
}
};
let mut report = crate::run_blueprint_member(
signal,
&point,
@@ -278,6 +287,7 @@ impl MemberRunner for CliMemberRunner<'_> {
geo.pip_size,
&cell.strategy_id,
self.env,
stop,
);
report.manifest.instrument = Some(cell.instrument.clone());
Ok(report)
@@ -679,7 +689,19 @@ fn persist_campaign_traces(
let (tx_ex, rx_ex) = mpsc::channel();
let (tx_r, rx_r) = mpsc::channel();
let (tx_req, rx_req) = mpsc::channel();
let mut h = crate::wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, false, false, None)
let mut h = crate::wrap_r(
signal,
tx_eq,
tx_ex,
tx_r,
tx_req,
crate::StopArm::Bound(aura_composites::StopRule::Vol {
length: crate::R_SMA_STOP_LENGTH,
k: crate::R_SMA_STOP_K,
}),
false,
None,
)
.bootstrap_with_cells(&point)
.expect("the nominee's point re-bootstraps (it already ran this realization)");
let sources: Vec<Box<dyn aura_engine::Source>> = vec![Box::new(source)];
+40 -19
View File
@@ -2285,7 +2285,7 @@ fn reproduce_family_in(
let (tx_r, _) = mpsc::channel();
let (tx_req, _) = mpsc::channel();
let space =
wrap_r(reload(), tx_eq, tx_ex, tx_r, tx_req, false, true, None).param_space();
wrap_r(reload(), tx_eq, tx_ex, tx_r, tx_req, StopArm::Bound(StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }), true, None).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
@@ -2333,6 +2333,7 @@ fn reproduce_family_in(
pip,
&hash,
env,
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K },
);
outcomes.push((label, rerun.metrics == stored.metrics));
}
@@ -2488,11 +2489,11 @@ fn run_macd(trace: Option<&str>, env: &project::Env) -> RunReport {
/// The r-sma vol-stop EWMA length (cycles). Single source for the `StopRule::Vol`
/// the blueprint embeds and the `stop` param the manifest records — kept honest by one
/// constant instead of a hand-synced literal across the function boundary.
const R_SMA_STOP_LENGTH: i64 = 3;
pub(crate) const R_SMA_STOP_LENGTH: i64 = 3;
/// The r-sma vol-stop multiplier (1R = `k`·σ). Single source for the embedded
/// `StopRule::Vol` and its manifest record, like [`R_SMA_STOP_LENGTH`].
const R_SMA_STOP_K: f64 = 2.0;
pub(crate) const R_SMA_STOP_K: f64 = 2.0;
/// The r-sma demo signal SMA lengths (fast/slow), bound at single-run build time.
/// Single source for the graph that runs (`r_sma_graph`), the signal that is hashed
@@ -2636,12 +2637,24 @@ fn r_sma_graph(
tx_ex,
tx_r,
tx_req,
stop_open,
if stop_open {
StopArm::Open
} else {
StopArm::Bound(StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K })
},
reduce,
cost,
)
}
/// The vol-stop arm `wrap_r` embeds: an OPEN stop whose two knobs land in
/// `param_space` as sweep axes (the r-sma verb's gridded-stop path), or a BOUND
/// stop with fixed params (a single run, a blueprint/campaign member).
enum StopArm {
Open,
Bound(StopRule),
}
/// Wrap a `signal` composite (a `price`→`bias` leg) in the R run
/// scaffolding: pip broker, the per-tap recorders, the vol-stop RiskExecutor, the
/// r_equity / cost legs. The signal is nested and its `price`/`bias` boundary is
@@ -2655,7 +2668,7 @@ fn wrap_r(
tx_ex: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
tx_r: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
tx_req: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
stop_open: bool,
stop: StopArm,
reduce: bool,
cost: Option<(
CostConfig,
@@ -2668,8 +2681,9 @@ fn wrap_r(
let sig = g.add(BlueprintNode::Composite(signal));
// pip branch (verbatim from sample_blueprint_with_sinks).
let broker = g.add(SimBroker::builder(SYNTHETIC_PIP_SIZE));
// R branch: bias + price → RiskExecutor(vol_stop) → dense R-record. The stop knobs
// are bound to the pinned constants (default) or left open as sweep axes (`stop_open`).
// R branch: bias + price → RiskExecutor(vol_stop) → dense R-record. The stop is either
// left OPEN as sweep axes (`StopArm::Open`) or BOUND to a fixed `StopRule` — the pinned
// default constants, or an arbitrary per-regime rule the caller resolved (`StopArm::Bound`).
// In `reduce` mode the per-cycle taps fold online: SeriesReducer folds the eq/ex
// f64 series to one summary row, GatedRecorder retains only the gated R rows — the
// O(cycles)→O(trades) memory win. The raw `Recorder`s (and the r_equity tap) are the
@@ -2688,10 +2702,9 @@ fn wrap_r(
} else {
g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex))
};
let exec = g.add(if stop_open {
risk_executor_vol_open(1.0)
} else {
risk_executor(StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, 1.0)
let exec = g.add(match stop {
StopArm::Open => risk_executor_vol_open(1.0),
StopArm::Bound(rule) => risk_executor(rule, 1.0),
});
let rrec = if reduce {
g.add(GatedRecorder::builder(PM_RECORD_KINDS.to_vec(), gate_col, Firing::Any, tx_r))
@@ -2846,7 +2859,7 @@ fn run_signal_r(
// The req tap (r_equity recorder) is wired but not persisted on this path; keep the
// receiver alive so the sink's sends do not fail, but do not drain it.
let (tx_req, _rx_req) = mpsc::channel();
let wrapped = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, false, false, None);
let wrapped = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopArm::Bound(StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }), false, None);
let flat = wrapped
.compile_with_params(params)
.expect("signal binds + wraps to a valid harness");
@@ -2884,16 +2897,24 @@ fn run_blueprint_member(
pip: f64,
topo: &str,
env: &project::Env,
stop: StopRule,
) -> RunReport {
let (tx_eq, rx_eq) = mpsc::channel();
let (tx_ex, rx_ex) = mpsc::channel();
let (tx_r, rx_r) = mpsc::channel();
let (tx_req, _rx_req) = mpsc::channel();
let mut h = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, false, true, None)
let mut h = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopArm::Bound(stop), true, None)
.bootstrap_with_cells(point)
.expect("member bootstraps (point kind-checked against param_space)");
h.run(sources);
let named = zip_params(space, point); // by-name params for the manifest record
let mut named = zip_params(space, point); // by-name params for the manifest record
// `if let` (not an irrefutable `let`): `StopRule` also has a `Fixed` variant,
// which stamps no vol knobs. The campaign/single-run paths only pass `Vol`,
// so the `Fixed` arm is inert here.
if let StopRule::Vol { length, k } = stop {
named.push(("stop_length".to_string(), Scalar::i64(length)));
named.push(("stop_k".to_string(), Scalar::f64(k)));
}
let mut manifest = sim_optimal_manifest(named, window, seed, pip);
manifest.broker = r_sma_broker_label(pip);
manifest.topology_hash = Some(topo.to_string());
@@ -2929,7 +2950,7 @@ fn blueprint_axis_probe(doc: &str, env: &project::Env) -> Composite {
let (tx_ex, _) = mpsc::channel();
let (tx_r, _) = mpsc::channel();
let (tx_req, _) = mpsc::channel();
wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, false, true, None)
wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopArm::Bound(StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }), true, None)
}
/// `aura sweep <blueprint.json> --list-axes`: one `<name>:<kind>` line per open
@@ -3002,7 +3023,7 @@ fn blueprint_sweep_family(
.sweep(|point| {
// fresh per-member graph (Composite is !Clone, reload per member) run through
// the shared reduce-mode member path — the same fn reproduction re-runs.
run_blueprint_member(reload(doc), point, &space, data.run_sources(env), window, 0, pip, &topo, env)
run_blueprint_member(reload(doc), point, &space, data.run_sources(env), window, 0, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K })
})
// render the sweep terminal's BindError to a message (the fn's String error contract),
// so `UnknownKnob("nope")` still surfaces verbatim at the IO wrapper.
@@ -3035,7 +3056,7 @@ fn blueprint_sweep_over(
binder.sweep_with_lattice(|point| {
let sources = data.windowed_sources(from, to, env);
let window = window_of(&sources).expect("non-empty in-sample window");
run_blueprint_member(reload(doc), point, &space, sources, window, 0, pip, &topo, env)
run_blueprint_member(reload(doc), point, &space, sources, window, 0, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K })
})
}
@@ -3053,7 +3074,7 @@ fn run_oos_blueprint(
let pip = data.pip_size();
let sources = data.windowed_sources(from, to, env);
let window = window_of(&sources).expect("non-empty out-of-sample window");
let report = run_blueprint_member(reload, params, space, sources, window, 0, pip, topo, env);
let report = run_blueprint_member(reload, params, space, sources, window, 0, pip, topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K });
(Vec::new(), report)
}
@@ -3160,7 +3181,7 @@ fn blueprint_mc_family(
let family = monte_carlo(&base_point, &seeds, |seed, _base| {
let sources = synthetic_walk_sources(seed);
let window = window_of(&sources).expect("non-empty synthetic walk");
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env)
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K })
});
// Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's
// metrics are bit-identical to the first, no seed reached a distinguishable realization —
+17 -18
View File
@@ -1404,18 +1404,15 @@ const GER40_SEPT2024_FROM_MS: &str = "1725148800000";
const GER40_SEPT2024_TO_MS: &str = "1727740799999";
/// Characterization pin (grounding-check ratification for the risk-regime-axis
/// spec): the dissolved real-data sweep member manifest carries ONLY the swept
/// signal axes as `params` — NO `stop_length` / `stop_k`. The R-path stop is a
/// `wrap_r` constant baked OUTSIDE the signal `param_space`, so it never reaches
/// a campaign member manifest today; the sibling
/// spec): the dissolved real-data sweep member manifest stamps its resolved stop
/// (`stop_length`/`stop_k`) — the default regime here. The R-path stop knobs are
/// bound via `run_blueprint_member`'s `stop` param and stamped into the manifest
/// alongside the swept signal axes; the sibling
/// `sweep_real_blueprint_member_lines_pin_the_dissolved_contract` locates each
/// binding with `find` and so never pins this ABSENCE. This pins the stop-less
/// baseline the risk-regime cycle deliberately extends: once the resolved regime
/// is stamped into the manifest, this assertion flips to require the stop key —
/// the ratified additive contract extension, exactly like the instrument stamp.
/// Gated on the local GER40 archive; skips cleanly when absent.
/// binding with `find` and so never pins the exact resolved VALUES this test
/// checks. Gated on the local GER40 archive; skips cleanly when absent.
#[test]
fn sweep_real_blueprint_member_manifest_carries_no_stop_param_today() {
fn sweep_real_blueprint_member_manifest_stamps_the_default_stop() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
@@ -1445,14 +1442,11 @@ fn sweep_real_blueprint_member_manifest_carries_no_stop_param_today() {
let params = v["report"]["manifest"]["params"]
.as_array()
.expect("manifest.params is an array");
let names: Vec<&str> = params.iter().filter_map(|p| p[0].as_str()).collect();
// The swept signal axis IS stamped ...
assert!(!names.is_empty(), "the swept signal axis is stamped: {line}");
// ... and no stop param is (the baseline this cycle extends).
assert!(
!names.iter().any(|n| n.contains("stop")),
"no stop param is stamped in the campaign member manifest today: {line}"
);
let get = |name: &str| params.iter().find(|p| p[0].as_str() == Some(name));
assert_eq!(get("stop_length").and_then(|p| p[1]["I64"].as_i64()), Some(3),
"the resolved default stop length is stamped: {line}");
assert_eq!(get("stop_k").and_then(|p| p[1]["F64"].as_f64()), Some(2.0),
"the resolved default stop k is stamped: {line}");
}
}
@@ -1526,6 +1520,11 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
Some("GER40"),
"the dissolved sweep stamps the instrument: {line}"
);
assert!(
params.iter().any(|p| p[0].as_str() == Some("stop_length"))
&& params.iter().any(|p| p[0].as_str() == Some("stop_k")),
"the dissolved sweep stamps the resolved stop: {line}"
);
}
// exactly one new Sweep family persisted, with the grid's four members.
+152
View File
@@ -1745,6 +1745,158 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
);
}
/// Property (#210 T3, Task 4 Step 9 — the regime->stop map): a campaign
/// document's non-default risk regime (`risk: [{ "vol": { length, k } }]`)
/// reaches the real `CliMemberRunner::run_member` path and IS the stop actually
/// stamped into each emitted member's manifest — `stop_length`/`stop_k` equal
/// the regime's own values (5, 3.5), not `R_SMA_STOP_LENGTH`/`R_SMA_STOP_K`
/// (3, 2.0). Every other campaign-run test in this file is regime-less (`risk`
/// absent), so they all only ever exercise the `None` arm of the campaign
/// member runner's regime match and would keep passing even if a regression
/// dropped `cell.regime` on the floor and always stamped the baked default;
/// this is the one test that actually drives the `Some(RiskRegime::Vol { .. })`
/// arm end to end. Gated on the local GER40 archive like its sibling e2e tests
/// above; skips on a data-less host.
#[test]
fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() {
let _fixture = project_lock();
let dir = built_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir.clone()),
ScratchPath::File(dir.join("regime.process.json")),
ScratchPath::File(dir.join("regime.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-regime-seed");
let proc_id = register_process_doc(dir, "regime.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
(1725148800000, 1727740799999),
"",
"\"family_table\"",
);
let with_regime = base.replacen(
"\"seed\": 7,",
"\"seed\": 7,\n \"risk\": [ { \"vol\": { \"length\": 5, \"k\": 3.5 } } ],",
1,
);
assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "regime.campaign.json", &with_regime);
let (out, code) = run_code_in(dir, &["campaign", "run", "regime.campaign.json"]);
if code == Some(1)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
{
eprintln!("skip: no local GER40 data for the campaign regime e2e");
return;
}
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let member_lines: Vec<&str> =
out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect();
assert!(!member_lines.is_empty(), "family_table member lines emitted: {out}");
for line in &member_lines {
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
let params = v["report"]["manifest"]["params"]
.as_array()
.expect("manifest.params is an array");
let get = |name: &str| params.iter().find(|p| p[0].as_str() == Some(name));
assert_eq!(
get("stop_length").and_then(|p| p[1]["I64"].as_i64()),
Some(5),
"the campaign's own regime length is stamped, not the R_SMA default: {line}"
);
assert_eq!(
get("stop_k").and_then(|p| p[1]["F64"].as_f64()),
Some(3.5),
"the campaign's own regime k is stamped, not the R_SMA default: {line}"
);
}
}
/// Property (#210 T3/T4 — per-cell regime resolution, not a shared last-write):
/// a campaign with TWO distinct non-default risk regimes stamps EACH regime's
/// OWN stop into ITS OWN cell's members, never the other regime's values and
/// never both collapsing onto one. The single-regime sibling test above cannot
/// catch a bug where the executor's per-cell loop resolves the WRONG regime for
/// a cell (e.g. an off-by-one on `regime_ordinal`, or a closure that captures
/// the last-seen `regime` instead of the current one) — with only one non-
/// default entry, such a bug is invisible (there is nothing else it could pick).
/// Distinguishes the two cells' member lines by the `-r{regime_ordinal}-`
/// segment `run_cell` bakes into the family name (exec.rs), which is the only
/// externally observable handle on which cell a member line belongs to. Gated
/// on the local GER40 archive like its siblings; skips on a data-less host.
#[test]
fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() {
let _fixture = project_lock();
let dir = built_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir.clone()),
ScratchPath::File(dir.join("tworegimes.process.json")),
ScratchPath::File(dir.join("tworegimes.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-two-regimes-seed");
let proc_id = register_process_doc(dir, "tworegimes.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
(1725148800000, 1727740799999),
"",
"\"family_table\"",
);
let with_regimes = base.replacen(
"\"seed\": 7,",
"\"seed\": 7,\n \"risk\": [ { \"vol\": { \"length\": 5, \"k\": 3.5 } }, \
{ \"vol\": { \"length\": 8, \"k\": 1.2 } } ],",
1,
);
assert_ne!(with_regimes, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "tworegimes.campaign.json", &with_regimes);
let (out, code) = run_code_in(dir, &["campaign", "run", "tworegimes.campaign.json"]);
if code == Some(1)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
{
eprintln!("skip: no local GER40 data for the campaign two-regime e2e");
return;
}
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let member_lines: Vec<&str> =
out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect();
assert!(!member_lines.is_empty(), "family_table member lines emitted: {out}");
let expect_for = |regime_tag: &str, want_length: i64, want_k: f64| {
let matches: Vec<&&str> = member_lines
.iter()
.filter(|line| line.contains(regime_tag))
.collect();
assert!(!matches.is_empty(), "at least one member line tagged {regime_tag}: {out}");
for line in matches {
let v: serde_json::Value =
serde_json::from_str(line).expect("member line parses as JSON");
let params = v["report"]["manifest"]["params"]
.as_array()
.expect("manifest.params is an array");
let get = |name: &str| params.iter().find(|p| p[0].as_str() == Some(name));
assert_eq!(
get("stop_length").and_then(|p| p[1]["I64"].as_i64()),
Some(want_length),
"{regime_tag} stamps its own stop_length, not the other regime's: {line}"
);
assert_eq!(
get("stop_k").and_then(|p| p[1]["F64"].as_f64()),
Some(want_k),
"{regime_tag} stamps its own stop_k, not the other regime's: {line}"
);
}
};
expect_for("-r0-", 5, 3.5);
expect_for("-r1-", 8, 1.2);
}
/// Property: an UNPRODUCIBLE tap requested alongside a producible one degrades
/// gracefully, never a crash or a silently-dropped whole request. `net_r_equity`
/// is in the closed tap vocabulary (`validate_campaign` accepts it) but has no