fix(campaign): thread the risk regime through trace persistence

The persist-side C1 drift alarm re-ran every nominee under the hardcoded
default vol-stop because CellRealization dropped the regime at the record
boundary — a non-default risk regime + persist taps false-failed C1 and
refused to persist a correct trace; two regimes over the same cell would
also have collided in one trace dir.

Fix at the record boundary: CellRealization carries regime + regime_ordinal
(serde-default + skip-serializing keeps pre-existing stored records
byte-stable), populated where the cell is realized; the regime->StopRule
binding is single-sourced in stop_rule_for_regime, shared by the member
runner and the persist re-run so the two sites cannot drift apart again;
campaign_cell_key appends -r{regime_ordinal} for non-default regimes
(default keys unchanged, no stored dir renamed).

closes #219, closes #212
This commit is contained in:
2026-07-09 10:38:03 +02:00
parent 70a045cde9
commit a1551a6097
5 changed files with 91 additions and 28 deletions
+2
View File
@@ -459,6 +459,8 @@ fn run_cell(
strategy: cell.strategy_id.clone(),
instrument: cell.instrument.clone(),
window_ms: cell.window_ms,
regime: cell.regime,
regime_ordinal: cell.regime_ordinal,
stages,
},
))
+58 -28
View File
@@ -43,6 +43,23 @@ pub(crate) fn is_content_id(s: &str) -> bool {
s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
}
/// The one regime->StopRule binding, shared by `CliMemberRunner::run_member`
/// and `persist_campaign_traces`'s drift-alarm re-run (#219): `None` is the
/// default vol-stop, `Some(RiskRegime::Vol { .. })` binds that regime's own
/// params. Single-sourced so the persist-side re-run structurally cannot
/// diverge from the run-side binding again (the #219 divergence class).
fn stop_rule_for_regime(regime: Option<aura_research::RiskRegime>) -> aura_composites::StopRule {
match 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 }
}
}
}
/// Phrase an [`ExecFault`] for stderr — Debug-leak-free, path-addressed
/// (stage index + block id), the `doc_fault_prose`/`ref_fault_prose` register.
pub(crate) fn exec_fault_prose(f: &ExecFault) -> String {
@@ -268,15 +285,7 @@ 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 stop = stop_rule_for_regime(cell.regime);
let mut report = crate::run_blueprint_member(
signal,
&point,
@@ -608,9 +617,22 @@ fn tap_channel(tap: &str) -> Option<TapChannel> {
/// `traces/<trace_name>/` — the `member_key` discipline (content, never a
/// runtime ordinal): strategy content-id prefix + instrument +
/// doc-positional window ordinal, sanitized to one portable path component.
fn campaign_cell_key(strategy: &str, instrument: &str, window_ordinal: usize) -> String {
/// `regime_ordinal` (#219/#212) discriminates two risk regimes over the same
/// (strategy, instrument, window) cell so their traces never collide: the
/// default regime (ordinal 0) keeps the pre-#219 key unchanged (no stored
/// trace dir is renamed by this change), a non-default regime appends
/// `-r{ordinal}`.
fn campaign_cell_key(
strategy: &str,
instrument: &str,
window_ordinal: usize,
regime_ordinal: usize,
) -> String {
let strategy8 = strategy.get(..8).unwrap_or(strategy);
crate::sanitize_component(&format!("{strategy8}-{instrument}-w{window_ordinal}"))
let base = format!("{strategy8}-{instrument}-w{window_ordinal}");
let base =
if regime_ordinal == 0 { base } else { format!("{base}-r{regime_ordinal}") };
crate::sanitize_component(&base)
}
/// Persist the requested `persist_taps` for every nominee cell under
@@ -684,8 +706,12 @@ fn persist_campaign_traces(
cell_rec.window_ms.0, cell_rec.window_ms.1
)
})?;
let cell_key =
campaign_cell_key(&cell_rec.strategy, &cell_rec.instrument, window_ordinal);
let cell_key = campaign_cell_key(
&cell_rec.strategy,
&cell_rec.instrument,
window_ordinal,
cell_rec.regime_ordinal,
);
// The cell's blueprint: the run's own index-aligned resolution, by
// content id (exactly the bytes the executor's members ran).
@@ -728,23 +754,17 @@ fn persist_campaign_traces(
}
let signal = blueprint_from_json(blueprint_json, &|t| env.resolve(t))
.expect("stored blueprint passed the referential gate; reload is infallible");
// The cell's OWN regime (#219/#212), not the hardcoded default: the
// recorded nominee ran under `cell_rec.regime`, bound through the same
// `stop_rule_for_regime` helper `CliMemberRunner::run_member` uses, so
// the two sites cannot drift apart again (the #219 divergence class)
// and the re-run binds the same stop the C1 drift alarm below relies on.
let stop = stop_rule_for_regime(cell_rec.regime);
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 = crate::wrap_r(
signal,
tx_eq,
tx_ex,
tx_r,
tx_req,
aura_composites::StopRule::Vol {
length: crate::R_SMA_STOP_LENGTH,
k: crate::R_SMA_STOP_K,
},
false,
None,
)
let mut h = crate::wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, 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)];
@@ -957,7 +977,7 @@ mod tests {
/// instrument + doc-positional window ordinal, one portable component.
fn campaign_cell_key_is_content_derived() {
let strategy = "bb34aa55".repeat(8); // a 64-hex content id
assert_eq!(campaign_cell_key(&strategy, "GER40", 0), "bb34aa55-GER40-w0");
assert_eq!(campaign_cell_key(&strategy, "GER40", 0, 0), "bb34aa55-GER40-w0");
}
#[test]
@@ -966,7 +986,17 @@ mod tests {
/// path or an invalid directory name.
fn campaign_cell_key_sanitizes_non_portable_bytes() {
let strategy = "0123abcd".repeat(8);
assert_eq!(campaign_cell_key(&strategy, "GER/40", 3), "0123abcd-GER_40-w3");
assert_eq!(campaign_cell_key(&strategy, "GER/40", 3, 0), "0123abcd-GER_40-w3");
}
#[test]
/// A non-default regime_ordinal (#219/#212) appends `-r{ordinal}` so two
/// regimes over the same (strategy, instrument, window) cell land in
/// distinct trace dirs; ordinal 0 (the default regime) is covered above
/// and stays unsuffixed for pre-#219 key stability.
fn campaign_cell_key_appends_regime_suffix_for_non_default_ordinal() {
let strategy = "bb34aa55".repeat(8);
assert_eq!(campaign_cell_key(&strategy, "GER40", 0, 2), "bb34aa55-GER40-w0-r2");
}
#[test]
+6
View File
@@ -1761,6 +1761,8 @@ mod tests {
strategy: "3f9c".to_string(),
instrument: "EURUSD".to_string(),
window_ms: (1_136_073_600_000, 1_154_390_400_000),
regime: None,
regime_ordinal: 0,
stages: vec![
StageRealization {
block: "std::sweep".to_string(),
@@ -1825,6 +1827,8 @@ mod tests {
strategy: "3f9c".to_string(),
instrument: "EURUSD".to_string(),
window_ms: (0, 1000),
regime: None,
regime_ordinal: 0,
stages: vec![StageRealization {
block: "std::monte_carlo".to_string(),
family_id: None,
@@ -1840,6 +1844,8 @@ mod tests {
strategy: "3f9c".to_string(),
instrument: "GER40".to_string(),
window_ms: (0, 1000),
regime: None,
regime_ordinal: 0,
stages: vec![StageRealization {
block: "std::monte_carlo".to_string(),
family_id: None,
+23
View File
@@ -108,6 +108,29 @@ pub struct CellRealization {
/// Inclusive epoch-ms window.
pub window_ms: (i64, i64),
pub stages: Vec<StageRealization>,
/// The cell's risk regime (#219/#212) — mirrors `CellSpec::regime`: `None`
/// is the member runner's baked default stop, `Some` an explicit document
/// regime. Threaded through so a downstream re-run (the trace-persist C1
/// drift alarm) binds the SAME stop the cell recorded under, instead of
/// hardcoding the default. `#[serde(default, skip_serializing_if =
/// "Option::is_none")]` keeps pre-#219 stored records byte-identical on
/// round-trip (they predate the risk axis, so their implicit regime was
/// always the default / absent).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub regime: Option<aura_research::RiskRegime>,
/// The regime's ordinal in the resolved regime list (0 for the default) —
/// mirrors `CellSpec::regime_ordinal`; discriminates trace dirs for two
/// regimes over the same (strategy, instrument, window) cell.
/// `#[serde(default, skip_serializing_if = "is_zero_ordinal")]` for the
/// same pre-#219 byte-identical round-trip reason.
#[serde(default, skip_serializing_if = "is_zero_ordinal")]
pub regime_ordinal: usize,
}
/// `CellRealization::regime_ordinal`'s serialize-skip predicate: the default
/// regime (ordinal 0) is omitted, keeping pre-#219 stored lines byte-stable.
fn is_zero_ordinal(n: &usize) -> bool {
*n == 0
}
/// One realized pipeline stage. `family_id` is set for family-producing stages
@@ -52,6 +52,8 @@ fn campaign_run(campaign: &str) -> CampaignRunRecord {
strategy: "strat-id".to_string(),
instrument: "EURUSD".to_string(),
window_ms: (0, 1000),
regime: None,
regime_ordinal: 0,
stages: vec![StageRealization {
block: "std::sweep".to_string(),
family_id: Some(format!("{campaign}-0-EURUSD-w0-s0-0")),