Spec auto-signed on a grounding-check PASS: the risk regime becomes a structural campaign axis (a peer of instruments/windows, kept-separate in the executor, each member's resolved stop stamped into its manifest; regimes are compared, never argmax-selected across — a cross-regime E[R] argmax would mix R units, C10). The 5-task plan threads it through aura-research (RiskRegime + CampaignDoc.risk), aura-campaign + aura-registry (matrix expansion + regime_ordinal), and aura-cli (StopArm + wrap_r + the manifest stamp). Two bounded, documented descopes: the descriptive CAMPAIGN_SECTIONS introspection entry and multi-regime trace-dir isolation. refs #210
34 KiB
Risk regime as a structural campaign axis — Implementation Plan
Parent spec:
docs/specs/risk-regime-axis.mdFor agentic workers: REQUIRED SUB-SKILL: use the
implementskill to run this plan. Steps use- [ ]checkboxes for tracking.
Goal: Make the protective-stop regime a first-class structural axis of the
campaign matrix — a peer of instruments and windows — so a campaign runs every
(strategy, instrument, window, regime) cell, stamps each member's resolved
stop into its manifest, and compares regimes at presentation without ever
argmax-selecting across them.
Architecture: Three tiers. aura-research gains the serializable
RiskRegime vocabulary and an optional CampaignDoc.risk list (absent = one
implicit default regime, absent-serializes for content-id parity).
aura-campaign (+ the aura-registry record it writes) expands the matrix with
the regime as a kept-separate axis (peer of window; the nominee key gains
the regime ordinal, so generalize aggregates across instruments within a
regime, never across regimes). aura-cli maps RiskRegime → StopRule at the
member-runner boundary, threads it through wrap_r's bound-stop arm, and stamps
it into the manifest.
Tech Stack: Rust workspace crates aura-research, aura-registry,
aura-campaign, aura-cli; serde; the wrap_r R-scaffolding; the campaign
executor + registry.
Two bounded, documented descopes (non-load-bearing details of the spec's Components/Family-naming sections, deferred as clean follow-ups):
- Descriptive
CAMPAIGN_SECTIONSintrospection entry forrisk.riskis a top-level array, not a slot-bearing section likedata/strategy, so it does not fit the currentBlockSchemaslot model. The load-bearing validation (a bad regime is refused) still lands, in Task 2. Introspection completeness for the regime list is a follow-up. - Multi-regime trace-directory isolation (
campaign_cell_key). The load-bearing registry family-collision fix is the-r{regime_ordinal}discriminator in thefamily_name(Task 3, exec.rs) — the family store keys on that.campaign_cell_keynames trace dirs and runs only whenpersist_tapsis non-empty; threading a regime through it would drag in aCellRealization.regime_ordinalstored-format change. This cycle's E2E usespersist_taps: [], so multi-regime trace-dir isolation is deferred.
Files this plan creates or modifies:
- Modify:
crates/aura-research/src/lib.rs—RiskRegimeenum,CampaignDoc.riskfield,DocFault::BadRegime,validate_campaignregime check, in-file tests. - Modify:
crates/aura-cli/src/research_docs.rs:141— CLI phrasing forDocFault::BadRegime. - Modify:
crates/aura-registry/src/lineage.rs:158—CampaignGeneralization.regime_ordinal. - Modify:
crates/aura-registry/src/lib.rs— theCampaignGeneralizationtest literal. - Modify:
crates/aura-campaign/src/lib.rs:26—CellSpec.regime+regime_ordinal. - Modify:
crates/aura-campaign/src/exec.rs— regime expansion, 3-tuple nominee key, generalizeregime_ordinal, family-name discriminator. - Modify:
crates/aura-campaign/tests/member_seam_e2e.rs:18—CellSpecliteral. - Modify:
crates/aura-campaign/tests/execute.rs— family-id pins + new two-regime test. - Modify:
crates/aura-cli/src/main.rs—StopArm,wrap_rsignature,r_sma_graphmap,run_blueprint_memberstop param + manifest stamp + its 6 callers. - Modify:
crates/aura-cli/src/campaign_run.rs—CliMemberRunner::run_memberregime→stop map. - Modify:
crates/aura-cli/tests/cli_run.rs— flip the characterization pin, extend the dissolved-contract pin, add the two-regime E2E. - Modify:
crates/aura-cli/src/verb_sugar.rs:76—risk: vec![]in the generated doc. - Modify:
docs/design/INDEX.md— the C10/C20 amendment note.
Task 1: RiskRegime vocabulary + CampaignDoc.risk field (aura-research)
Files:
-
Modify:
crates/aura-research/src/lib.rs -
Step 1: Write the failing tests
Add to the #[cfg(test)] mod in crates/aura-research/src/lib.rs (the module that already holds CAMPAIGN_FIXTURE):
#[test]
fn risk_regime_round_trips_as_externally_tagged_vol() {
let r = RiskRegime::Vol { length: 3, k: 2.0 };
let j = serde_json::to_string(&r).unwrap();
assert_eq!(j, r#"{"vol":{"length":3,"k":2.0}}"#);
let back: RiskRegime = serde_json::from_str(&j).unwrap();
assert_eq!(back, r);
}
#[test]
fn campaign_absent_and_empty_risk_omit_from_canonical_bytes() {
let doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
assert!(doc.risk.is_empty(), "an absent risk key deserializes to empty");
let absent = campaign_to_json(&doc);
assert!(!absent.contains("\"risk\""), "absent risk omits from serialization: {absent}");
let mut empty = doc.clone();
empty.risk = vec![];
assert_eq!(
content_id_of(&campaign_to_json(&empty)),
content_id_of(&absent),
"an explicit empty risk hashes identically to an absent one"
);
}
#[test]
fn campaign_with_risk_serializes_and_round_trips_the_regimes() {
let mut doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
doc.risk = vec![
RiskRegime::Vol { length: 3, k: 1.5 },
RiskRegime::Vol { length: 3, k: 3.0 },
];
let out = campaign_to_json(&doc);
assert!(out.contains(r#""risk":[{"vol":{"length":3,"k":1.5}},{"vol":{"length":3,"k":3.0}}]"#),
"risk serializes in place as a tagged list: {out}");
let back: CampaignDoc = serde_json::from_str(&out).unwrap();
assert_eq!(back.risk, doc.risk);
}
- Step 2: Run the tests to verify they fail
Run: cargo test -p aura-research risk_regime 2>&1 | tail -20
Expected: compile error — cannot find type RiskRegime / no field risk on CampaignDoc.
- Step 3: Add the
RiskRegimeenum
In crates/aura-research/src/lib.rs, immediately after the Axis type block (after the pub struct Axis { pub kind: ScalarKind, pub values: Vec<Scalar> } region near line 471), add:
/// A protective-stop regime: a serializable, content-addressable mirror of the
/// runtime `StopRule` structural axis (C10/C20). The sole implemented variant is
/// the vol-stop; the shipped fixed-stop rule is admitted as a future additive
/// variant (it runs as a composite today but is not yet campaign-reachable).
/// Externally tagged so adding `Fixed` is additive — no content-id churn on
/// stored `Vol` regimes.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "snake_case")]
pub enum RiskRegime {
Vol { length: i64, k: f64 },
}
- Step 4: Add the
CampaignDoc.riskfield
In crates/aura-research/src/lib.rs, in pub struct CampaignDoc (line 425-436), insert the risk field immediately after pub data: DataSection,:
pub data: DataSection,
/// Structural risk-execution axis: the finite list of stop regimes the
/// matrix runs each cell under. Absent or empty = one implicit default
/// regime (the runtime's baked constants). Absent-serializes → content-id
/// parity for every risk-less document (the `description` precedent).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub risk: Vec<RiskRegime>,
pub strategies: Vec<StrategyEntry>,
- Step 5: Run the tests to verify they pass
(One positional filter per invocation — a multi-filter cargo test X Y prints nothing in this environment.)
Run: cargo test -p aura-research risk_regime_round_trips 2>&1 | tail -5
Expected: PASS (1 test).
Run: cargo test -p aura-research campaign_absent_and_empty_risk 2>&1 | tail -5
Expected: PASS (1 test).
Run: cargo test -p aura-research campaign_with_risk_serializes 2>&1 | tail -5
Expected: PASS (1 test).
Task 2: validate_campaign regime refusal + CLI phrasing (aura-research + aura-cli)
Files:
-
Modify:
crates/aura-research/src/lib.rs -
Modify:
crates/aura-cli/src/research_docs.rs:141 -
Step 1: Write the failing test
Add to the #[cfg(test)] mod in crates/aura-research/src/lib.rs:
#[test]
fn validate_campaign_flags_each_non_positive_regime() {
let mut doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
doc.risk = vec![
RiskRegime::Vol { length: 3, k: 2.0 }, // 0: valid
RiskRegime::Vol { length: 0, k: 2.0 }, // 1: length < 1
RiskRegime::Vol { length: 3, k: 0.0 }, // 2: k <= 0
];
let faults = validate_campaign(&doc);
assert!(faults.contains(&DocFault::BadRegime { index: 1 }), "{faults:?}");
assert!(faults.contains(&DocFault::BadRegime { index: 2 }), "{faults:?}");
assert!(!faults.contains(&DocFault::BadRegime { index: 0 }), "valid regime not flagged: {faults:?}");
}
#[test]
fn validate_campaign_accepts_a_valid_risk_section() {
let mut doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
doc.risk = vec![RiskRegime::Vol { length: 3, k: 1.5 }];
assert!(validate_campaign(&doc).is_empty(), "{:?}", validate_campaign(&doc));
}
- Step 2: Run the test to verify it fails
Run: cargo test -p aura-research validate_campaign_flags 2>&1 | tail -20
Expected: compile error — no variant BadRegime on DocFault.
- Step 3: Add the
DocFault::BadRegimevariant
In crates/aura-research/src/lib.rs, in pub enum DocFault, insert on the campaign side, immediately after BadWindow { index: usize }, (line 658):
BadWindow { index: usize },
BadRegime { index: usize },
- Step 4: Add the regime check to
validate_campaign
In crates/aura-research/src/lib.rs, in pub fn validate_campaign, insert after the windows loop closes (after line 747, the } that ends the for (i, w) in doc.data.windows... loop):
for (i, r) in doc.risk.iter().enumerate() {
let RiskRegime::Vol { length, k } = r;
// length < 1 catches 0 and negatives; !(k > 0.0) catches k <= 0 AND NaN.
if *length < 1 || !(*k > 0.0) {
faults.push(DocFault::BadRegime { index: i });
}
}
- Step 5: Add the CLI phrasing arm (cross-crate compile break — same task)
In crates/aura-cli/src/research_docs.rs, in the DocFault match, insert immediately after the DocFault::BadWindow { index } => { ... } arm (line 141):
DocFault::BadRegime { index } => {
format!("risk regime {index} is invalid: stop length must be >= 1 and k must be > 0")
}
- Step 6: Run the tests + the workspace build gate
Run: cargo build -p aura-research -p aura-cli 2>&1 | tail -5
Expected: Finished — 0 errors (the DocFault match is exhaustive again).
Run: cargo test -p aura-research validate_campaign_flags 2>&1 | tail -5
Expected: PASS (1 test).
Run: cargo test -p aura-research validate_campaign_accepts_a_valid_risk 2>&1 | tail -5
Expected: PASS (1 test).
Task 3: matrix regime expansion (aura-campaign + aura-registry)
Files:
-
Modify:
crates/aura-registry/src/lineage.rs:158 -
Modify:
crates/aura-registry/src/lib.rs— theCampaignGeneralizationtest literal (~1852) -
Modify:
crates/aura-campaign/src/lib.rs:26 -
Modify:
crates/aura-campaign/src/exec.rs -
Modify:
crates/aura-campaign/tests/member_seam_e2e.rs:18 -
Modify:
crates/aura-campaign/tests/execute.rs -
Step 1: Write the failing test
Add to crates/aura-campaign/tests/execute.rs (modeled on execute_sweep_only_records_family_and_selection):
#[test]
fn execute_runs_one_family_per_regime_with_distinct_ids() {
let reg = temp_registry("two_regimes");
let mut doc = campaign(&["EURUSD"]);
doc.risk = vec![
aura_research::RiskRegime::Vol { length: 3, k: 1.5 },
aura_research::RiskRegime::Vol { length: 3, k: 2.0 },
];
let proc_doc = process(vec![sweep_stage(false)]);
let out = execute(CAMPAIGN_ID, &doc, &proc_doc, &strategies(), &FakeRunner::clean(), ®)
.expect("two-regime campaign executes");
// one cell per regime, each with its own sweep family.
assert_eq!(out.cells.len(), 2, "one cell per regime");
let p = &CAMPAIGN_ID[..8];
assert_eq!(out.cells[0].families[0].family_id, format!("{p}-0-EURUSD-w0-r0-s0-0"));
assert_eq!(out.cells[1].families[0].family_id, format!("{p}-0-EURUSD-w0-r1-s0-0"));
}
- Step 2: Run the test to verify it fails
Run: cargo test -p aura-campaign execute_runs_one_family_per_regime 2>&1 | tail -20
Expected: compile error — no field risk visible on the test doc path is fine (Task 1 shipped it), so the real failure is the assertion: family_id is ...-w0-s0-0 (no -r0) and out.cells.len() == 1 (regimes not expanded).
- Step 3: Add
regime_ordinaltoCampaignGeneralization(aura-registry)
In crates/aura-registry/src/lineage.rs, in pub struct CampaignGeneralization (line 158), insert after pub window_ordinal: usize,:
pub window_ordinal: usize,
/// The regime ordinal (0 for the default regime) — the third structural
/// coordinate, so a generalize grade is unambiguous per (strategy, window,
/// regime). `#[serde(default)]` keeps pre-feature stored records parsing.
#[serde(default)]
pub regime_ordinal: usize,
- Step 4: Update the
CampaignGeneralizationtest literal (aura-registry)
In crates/aura-registry/src/lib.rs, find the CampaignGeneralization { ... } literal in the tests (near line 1852) and add regime_ordinal: 0, after its window_ordinal: field.
Run: git grep -nE 'CampaignGeneralization \{' -- crates/aura-registry/src/lib.rs
(then edit the one literal — the pub struct def is in lineage.rs, not lib.rs).
- Step 5: Add the regime fields to
CellSpec(aura-campaign)
In crates/aura-campaign/src/lib.rs, in pub struct CellSpec (line 26-37), insert after pub window_ms: (i64, i64),:
pub window_ms: (i64, i64),
/// The cell's risk regime. `None` = the member runner's baked default (the
/// absent/empty-`risk` case); `Some` = an explicit document regime.
pub regime: Option<aura_research::RiskRegime>,
/// The regime's ordinal in the resolved regime list (0 for the default) —
/// keys the generalize unit and discriminates family names.
pub regime_ordinal: usize,
(If aura_research::RiskRegime is not yet imported in this file, add it to the existing use aura_research::{...} line.)
- Step 6: Expand the matrix over regimes in
exec.rs
In crates/aura-campaign/src/exec.rs, change the nominee map type (line 124):
// before
let mut nominees: BTreeMap<(usize, usize), Vec<(String, Nominee)>> = BTreeMap::new();
// after
let mut nominees: BTreeMap<(usize, usize, usize), Vec<(String, Nominee)>> = BTreeMap::new();
Immediately before the for (strategy_ordinal, ...) loop (line 125), resolve the regime list:
// Absent/empty risk = a single default cell (regime None, ordinal 0); a
// non-empty list maps each regime to Some. The default's value is the member
// runner's — the doc is never mutated (its content id hashes with risk absent).
let regimes: Vec<(usize, Option<aura_research::RiskRegime>)> = if campaign.risk.is_empty() {
vec![(0, None)]
} else {
campaign.risk.iter().copied().enumerate().map(|(i, r)| (i, Some(r))).collect()
};
Wrap the cell build in a regime loop (innermost, inside the window loop at line 129) and thread the ordinal into the cell + nominee key:
for (window_ordinal, window) in campaign.data.windows.iter().enumerate() {
for (regime_ordinal, regime) in ®imes {
let cell = CellSpec {
strategy_ordinal,
strategy_id: strategy_id.clone(),
blueprint_json: blueprint_json.clone(),
axes: entry.axes.clone(),
instrument: instrument.clone(),
window_ms: (window.from_ms, window.to_ms),
regime: *regime,
regime_ordinal: *regime_ordinal,
};
let (outcome, realization) = run_cell(
&cell, process, campaign_prefix, window_ordinal, campaign.seed, runner, registry,
)?;
nominees
.entry((strategy_ordinal, window_ordinal, *regime_ordinal))
.or_default()
.push((instrument.clone(), outcome.nominee.clone()));
cells_out.push(outcome);
cells_rec.push(realization);
}
}
- Step 7: Thread the regime ordinal through generalize
In crates/aura-campaign/src/exec.rs, the generalize loop (line 166) now iterates the 3-tuple key; update the destructuring and the CampaignGeneralization construction (line 187):
// before
for ((strategy_ordinal, window_ordinal), cells) in &nominees {
// after
for ((strategy_ordinal, window_ordinal, regime_ordinal), cells) in &nominees {
and in the CampaignGeneralization { ... } literal add regime_ordinal: *regime_ordinal, after window_ordinal: *window_ordinal,.
- Step 8: Add the
-r{regime_ordinal}family-name discriminator
In crates/aura-campaign/src/exec.rs, at BOTH family-name format sites (line 250 and line 352), change:
// before
"{campaign_prefix}-{}-{}-w{window_ordinal}-s{stage_ordinal}",
cell.strategy_ordinal, cell.instrument,
// after
"{campaign_prefix}-{}-{}-w{window_ordinal}-r{}-s{stage_ordinal}",
cell.strategy_ordinal, cell.instrument, cell.regime_ordinal,
- Step 9: Update the
CellSpecliteral inmember_seam_e2e.rs
In crates/aura-campaign/tests/member_seam_e2e.rs, in fn cell() (line 18-26), add after instrument: "EURUSD".to_string(), / window_ms: (0, 20),:
window_ms: (0, 20),
regime: None,
regime_ordinal: 0,
- Step 10: Update the family-id pins in
execute.rs
In crates/aura-campaign/tests/execute.rs, the existing tests use campaign() (no risk → one default regime, ordinal 0), so every family id gains exactly -r0. Update all three sites:
// line 238 and line 301: before
let expected_family_id = format!("{}-0-EURUSD-w0-s0-0", &CAMPAIGN_ID[..8]);
// after
let expected_family_id = format!("{}-0-EURUSD-w0-r0-s0-0", &CAMPAIGN_ID[..8]);
// line 381: before
let base = format!("{}-0-EURUSD-w0-s0", &CAMPAIGN_ID[..8]);
// after
let base = format!("{}-0-EURUSD-w0-r0-s0", &CAMPAIGN_ID[..8]);
- Step 11: Build + test gate (compile-coherent unit)
Run: cargo build -p aura-registry -p aura-campaign 2>&1 | tail -5
Expected: Finished — 0 errors (all CellSpec / CampaignGeneralization literals threaded).
Run: cargo test -p aura-campaign 2>&1 | tail -15
Expected: PASS, including execute_runs_one_family_per_regime_with_distinct_ids and the updated pins.
Task 4: the runtime binding — StopArm, stamp, regime→stop map (aura-cli)
Files:
-
Modify:
crates/aura-cli/src/main.rs -
Modify:
crates/aura-cli/src/campaign_run.rs -
Modify:
crates/aura-cli/tests/cli_run.rs -
Step 1: Flip the characterization pin + extend the dissolved-contract pin (the RED side)
In crates/aura-cli/tests/cli_run.rs, in sweep_real_blueprint_member_manifest_carries_no_stop_param_today, replace the assertion body of the for line in &lines loop so it now REQUIRES the default stop (the ratified additive stamp):
for line in &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(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}");
}
Rename the test to sweep_real_blueprint_member_manifest_stamps_the_default_stop and update its doc comment's first sentence to: the dissolved real-data sweep member manifest stamps its resolved stop (stop_length/stop_k) — the default regime here.
In sweep_real_blueprint_member_lines_pin_the_dissolved_contract, inside its for i in 0..4 loop, after the instrument-stamp assertion (line 1471-1475), add:
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}"
);
- Step 2: Run the pins to verify they fail
Run: cargo test -p aura-cli --test cli_run sweep_real_blueprint_member_manifest_stamps_the_default_stop 2>&1 | tail -20
Expected: FAIL — the manifest carries no stop_length/stop_k yet (assertion Some(3) gets None).
- Step 3: Add the
StopArmenum
In crates/aura-cli/src/main.rs, immediately before fn wrap_r( (line 2652, above its doc comment at 2645), add:
/// 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),
}
- Step 4: Change
wrap_r's signature and stop arm
In crates/aura-cli/src/main.rs, in fn wrap_r (line 2652-2665), change the 6th parameter:
// before
stop_open: bool,
// after
stop: StopArm,
and the stop-arm body (line 2691-2695):
// before
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)
});
// after
let exec = g.add(match stop {
StopArm::Open => risk_executor_vol_open(1.0),
StopArm::Bound(rule) => risk_executor(rule, 1.0),
});
- Step 5: Map
r_sma_graph's bool at itswrap_rcall
In crates/aura-cli/src/main.rs, in fn r_sma_graph, the wrap_r(...) call (line 2633-2642) passes stop_open as the 6th arg; replace that arg with the mapped StopArm:
wrap_r(
sma_signal(fast_len, slow_len),
tx_eq,
tx_ex,
tx_r,
tx_req,
if stop_open {
StopArm::Open
} else {
StopArm::Bound(StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K })
},
reduce,
cost,
)
- Step 6: Give
run_blueprint_memberastopparam + the manifest stamp
In crates/aura-cli/src/main.rs, in fn run_blueprint_member (line 2877-2886), add a trailing parameter:
topo: &str,
env: &project::Env,
stop: StopRule,
) -> RunReport {
Change its wrap_r call (line 2892), passing the bound stop:
// before
let mut h = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, false, true, None)
// after
let mut h = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopArm::Bound(stop), true, None)
Stamp the resolved stop into the manifest params — after let named = zip_params(space, point); (line 2896):
let named = zip_params(space, point); // by-name params for the manifest record
let named = {
let mut p = named;
// `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 {
p.push(("stop_length".to_string(), Scalar::i64(length)));
p.push(("stop_k".to_string(), Scalar::f64(k)));
}
p
};
- Step 7: Thread the remaining
wrap_rcall sites (bound default)
In crates/aura-cli/src/main.rs, the three other wrap_r sites pass false as the 6th arg — replace each with StopArm::Bound(StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }):
-
line 2288:
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(); -
line 2849:
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); -
line 2932:
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) -
Step 8: Thread the 5 default
run_blueprint_membercall sites (main.rs)
In crates/aura-cli/src/main.rs, append StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K } as the new last argument at each of these run_blueprint_member(...) calls: line 2326 (the let rerun = run_blueprint_member( multi-line call — add as the final arg after env), 3005, 3038, 3056, 3163. Each was run_blueprint_member(reload(doc), point, &space, ..., pip, &topo, env) → ..., &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }).
- Step 9: Map the regime → stop in the campaign member runner
In crates/aura-cli/src/campaign_run.rs, in impl MemberRunner for CliMemberRunner, run_member (line 269-281), map the cell regime and pass it into the run_blueprint_member call:
// before the run_blueprint_member call (after the `let signal = ...` at line 269):
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,
&space,
vec![Box::new(source)],
(from, to),
0,
geo.pip_size,
&cell.strategy_id,
self.env,
stop,
);
The full paths aura_composites::StopRule / aura_research::RiskRegime resolve without a new use (both crates are aura-cli deps). Also in crates/aura-cli/src/main.rs, mark the two stop constants crate-visible so campaign_run.rs can read them:
// before
const R_SMA_STOP_LENGTH: i64 = 3;
const R_SMA_STOP_K: f64 = 2.0;
// after
pub(crate) const R_SMA_STOP_LENGTH: i64 = 3;
pub(crate) const R_SMA_STOP_K: f64 = 2.0;
- Step 10: Build + run the gate (aura-cli, real-data pins)
Run: cargo build -p aura-cli 2>&1 | tail -8
Expected: Finished — 0 errors (all wrap_r + run_blueprint_member callers threaded).
Run: cargo test -p aura-cli --test cli_run sweep_real_blueprint_member_manifest_stamps_the_default_stop 2>&1 | tail -10
Expected: PASS (the flipped characterization pin now sees the stamped default stop).
Run: cargo test -p aura-cli --test cli_run sweep_real_blueprint_member_lines_pin_the_dissolved_contract 2>&1 | tail -10
Expected: PASS (the extended dissolved-contract pin sees the stop keys present).
Run: cargo test -p aura-cli 2>&1 | tail -20
Expected: PASS across aura-cli. NOTE: the stamp lives in run_blueprint_member, so the five single-blueprint run paths (main.rs callers at 2326/3005/3038/3056/3163) now stamp the default stop too — the same ratified C18 extension. Any aura-cli test asserting a stop-less blueprint-run OR campaign-member manifest param set is updated here for the additive stop_length/stop_k keys (an exact-set assertion, not a find, is what surfaces it). This is expected drift, not a regression.
Task 5: sugar parity, two-regime E2E, ledger note (aura-cli + docs)
Files:
-
Modify:
crates/aura-cli/src/verb_sugar.rs:76 -
Modify:
crates/aura-cli/tests/research_docs.rs— the two-regime E2E (its helpers live here) -
Modify:
docs/design/INDEX.md -
Step 1: Write the failing two-regime E2E
Add to crates/aura-cli/tests/research_docs.rs (its helpers seed_blueprint / SWEEP_ONLY_PROCESS_DOC / run_code_in / ScratchGuard live here). The campaign document is written inline (the campaign_doc_json helper has no risk parameter), carrying a two-regime risk section; real-data-gated with the same skip idiom as the sibling e2e:
/// Property (risk-regime axis, #210): a campaign document with two vol regimes
/// runs one sweep family per regime; the families' ids differ by the
/// `-r{ordinal}` discriminator, and every member's manifest stamps ITS regime's
/// resolved stop (k=1.5 for regime 0, k=2.5 for regime 1). Gated on the local
/// GER40 archive.
#[test]
fn campaign_two_regimes_stamp_distinct_stops_and_family_ids() {
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("regimes.process.json")),
ScratchPath::File(dir.join("regimes.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "two-regimes-seed");
let proc_id = register_process_doc(dir, "regimes.process.json", SWEEP_ONLY_PROCESS_DOC);
let doc = format!(
r#"{{
"format_version": 1,
"kind": "campaign",
"name": "two-regimes",
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1725148800000, "to_ms": 1727740799999 }} ] }},
"risk": [ {{ "vol": {{ "length": 3, "k": 1.5 }} }}, {{ "vol": {{ "length": 3, "k": 2.5 }} }} ],
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 4] }},
"slow.length": {{ "kind": "I64", "values": [8, 16] }} }} }} ],
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
"seed": 7,
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
}}"#
);
write_doc(dir, "regimes.campaign.json", &doc);
let (out, code) = run_code_in(dir, &["campaign", "run", "regimes.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 two-regime campaign e2e");
return;
}
assert_eq!(code, Some(0), "stdout/stderr: {out}");
// Group member lines by regime discriminator; each stamps its own stop_k.
let mut r0_k: Vec<f64> = vec![];
let mut r1_k: Vec<f64> = vec![];
for line in out.lines().filter(|l| l.starts_with("{\"family_id\":")) {
let v: serde_json::Value = serde_json::from_str(line).expect("member line JSON");
let fid = v["family_id"].as_str().expect("family_id");
let params = v["report"]["manifest"]["params"].as_array().expect("params");
let k = params
.iter()
.find(|p| p[0].as_str() == Some("stop_k"))
.and_then(|p| p[1]["F64"].as_f64())
.expect("the member manifest stamps stop_k");
if fid.contains("-r0-") {
r0_k.push(k);
} else if fid.contains("-r1-") {
r1_k.push(k);
}
}
assert!(!r0_k.is_empty() && !r1_k.is_empty(), "both regime families emitted members: {out}");
assert!(r0_k.iter().all(|&k| k == 1.5), "regime 0 stamps k=1.5: {r0_k:?}");
assert!(r1_k.iter().all(|&k| k == 2.5), "regime 1 stamps k=2.5: {r1_k:?}");
}
Run: cargo test -p aura-cli --test research_docs campaign_two_regimes 2>&1 | tail -20
Expected: at RED (before Tasks 3-4 land) this cannot even reach the assertions — but it is authored in Task 5 after Tasks 1-4 are green, so its first run here is the GREEN gate: PASS (or a clean skip on a data-less machine).
- Step 2: Add
risk: vec![]to the generated sweep doc (parity)
In crates/aura-cli/src/verb_sugar.rs, in translate_sweep's CampaignDoc { ... } literal (line 76-98), add after the data: DataSection { ... }, block (line 84):
data: DataSection {
instruments: vec![symbol.to_string()],
windows: vec![Window { from_ms, to_ms }],
},
risk: vec![],
strategies: vec![StrategyEntry {
- Step 3: Verify sugar parity (generated doc unchanged)
Run: cargo test -p aura-cli translate_sweep_is_deterministic 2>&1 | tail
Expected: PASS — risk: vec![] absent-serializes, so the generated campaign doc's content id is byte-unchanged.
- Step 4: Add the design-ledger amendment note
In docs/design/INDEX.md, in the C10 (or C20) section, append this paragraph (find the C10 block near line 492 / the C20 structural-axis discussion near line 1622; place it as a dated realization note under whichever the maintainer keeps the structural-axis list):
**Realization (2026-07-06 — the risk regime as a structural campaign axis,
#210).** The protective-stop regime is a first-class structural axis of the
campaign document (`CampaignDoc.risk: [RiskRegime]`), a peer of instruments and
windows — kept-separate in the executor (the nominee key carries the regime
ordinal), so generalize aggregates across instruments *within* a regime, never
across regimes. Regimes are compared at presentation, never argmax-selected
across: a cross-regime E[R] argmax would compare R-multiples in different R units
(the stop defines 1R), so the verbs' `--stop-length`/`--stop-k` joint stop grid
is retired as a campaign methodology. Each member manifest stamps its resolved
stop (default included), closing the C18 gap. Absent `risk` = one implicit
default regime, absent-serializing for content-id parity.
- Step 5: Full workspace gate
Run: cargo build --workspace 2>&1 | tail -3
Expected: Finished — 0 errors.
Run: cargo test --workspace 2>&1 | tail -8
Expected: PASS across the workspace (the new regime tests green, all prior pins updated).
Run: cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -3
Expected: no warnings.