From c62f7e9878c1108ab268bf0756f407f393ef14f0 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 6 Jul 2026 14:52:20 +0200 Subject: [PATCH] feat(cli): bind the resolved regime stop and stamp it into the manifest (#210 T4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/aura-cli/src/campaign_run.rs | 24 +++- crates/aura-cli/src/main.rs | 59 ++++++---- crates/aura-cli/tests/cli_run.rs | 35 +++--- crates/aura-cli/tests/research_docs.rs | 152 +++++++++++++++++++++++++ 4 files changed, 232 insertions(+), 38 deletions(-) diff --git a/crates/aura-cli/src/campaign_run.rs b/crates/aura-cli/src/campaign_run.rs index 15a823d..9cd99f6 100644 --- a/crates/aura-cli/src/campaign_run.rs +++ b/crates/aura-cli/src/campaign_run.rs @@ -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> = vec![Box::new(source)]; diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 394f6e9..b6a5ff3 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -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)>, tx_r: mpsc::Sender<(Timestamp, Vec)>, tx_req: mpsc::Sender<(Timestamp, Vec)>, - 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 --list-axes`: one `:` 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 — diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index bcf0840..f87b2dd 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -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. diff --git a/crates/aura-cli/tests/research_docs.rs b/crates/aura-cli/tests/research_docs.rs index c8ed0b9..c0d134c 100644 --- a/crates/aura-cli/tests/research_docs.rs +++ b/crates/aura-cli/tests/research_docs.rs @@ -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