diff --git a/crates/aura-campaign/src/exec.rs b/crates/aura-campaign/src/exec.rs index 5379ec6..72a3ecf 100644 --- a/crates/aura-campaign/src/exec.rs +++ b/crates/aura-campaign/src/exec.rs @@ -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, }, )) diff --git a/crates/aura-cli/src/campaign_run.rs b/crates/aura-cli/src/campaign_run.rs index f1cd3b0..5771ef0 100644 --- a/crates/aura-cli/src/campaign_run.rs +++ b/crates/aura-cli/src/campaign_run.rs @@ -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_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 { /// `traces//` — 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> = 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] diff --git a/crates/aura-registry/src/lib.rs b/crates/aura-registry/src/lib.rs index 6752174..c42c4ab 100644 --- a/crates/aura-registry/src/lib.rs +++ b/crates/aura-registry/src/lib.rs @@ -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, diff --git a/crates/aura-registry/src/lineage.rs b/crates/aura-registry/src/lineage.rs index f337e8e..d51469a 100644 --- a/crates/aura-registry/src/lineage.rs +++ b/crates/aura-registry/src/lineage.rs @@ -108,6 +108,29 @@ pub struct CellRealization { /// Inclusive epoch-ms window. pub window_ms: (i64, i64), pub stages: Vec, + /// 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, + /// 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 diff --git a/crates/aura-registry/tests/campaign_run_store_e2e.rs b/crates/aura-registry/tests/campaign_run_store_e2e.rs index d655228..02f2476 100644 --- a/crates/aura-registry/tests/campaign_run_store_e2e.rs +++ b/crates/aura-registry/tests/campaign_run_store_e2e.rs @@ -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")),