# mc R-bootstrap dissolution — Implementation Plan > **Parent spec:** `docs/specs/mc-dissolution.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to > run this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Dissolve `aura mc --strategy r-sma --real` into thin sugar over the one campaign executor — the FOURTH and LAST verb dissolution (#210) — reproducing the `{"mc_r_bootstrap":{…}}` line byte-for-byte via a generated `[std::sweep(argmax), std::walk_forward, std::monte_carlo]` campaign document. **Architecture:** Pure sugar, NO executor change (`aura-campaign`/`aura-engine`/ `aura-research` diff-free). Mirrors the shipped walkforward dissolution verbatim (commits `96dc783..18060cb`): a new `verb_sugar.rs` trio (`GeneratedMc`, `translate_mc`, `register_generated_mc`, `run_mc_sugar`) + a `dispatch_mc` built-in-arm split gated on `--strategy r-sma && --real`. The single new load-bearing detail is the seed mapping: `translate_mc` sets `campaign.seed = the mc --seed` (the wf winners are argmax hence seed-independent, proven by the shipped walkforward anchor). The campaign `std::monte_carlo` stage already produces `StageBootstrap::PooledOos(RBootstrap)` after a `std::walk_forward` stage; the sugar reads it from `run.outcome.record.cells[].stages[].bootstrap` and renders the existing `mc_r_bootstrap_json`. Everything the dissolution predicate does not match stays inline, fenced until #159 (Fork C retention — `run_mc_r_bootstrap` / `run_mc` NOT deleted). **Tech Stack:** `crates/aura-cli/src/verb_sugar.rs`, `crates/aura-cli/src/main.rs`, `crates/aura-cli/tests/cli_run.rs`. Reuses `run_campaign_returning`, `validate_before_register`, `axis_from_values`, `mc_r_bootstrap_json`, `sma_signal`, `blueprint_to_json`, `topology_hash`, `wf_ms_sizes`, `parse_csv_list`, `WINNER_SELECTION_METRIC`. --- **Files this plan creates or modifies:** - Modify: `crates/aura-cli/src/verb_sugar.rs` — add the mc translator/runner trio (`GeneratedMc`, `translate_mc`, `register_generated_mc`, `run_mc_sugar`) after `run_walkforward_sugar` (ends `:503`), plus their unit tests in the existing `#[cfg(test)] mod tests` (after `:794`). No new imports (the block at `:13-17` already carries `StageBlock`/`WfMode`/`SelectRule`/`RiskRegime`/`SweepSelection`/ `Axis`/`FORMAT_VERSION`/…; `aura_registry::StageBootstrap` is fully qualified). - Modify: `crates/aura-cli/src/main.rs` — add `mc_args_from` (after `walkforward_args_from`, `:4265`); insert the r-sma-real sugar branch at the top of `dispatch_mc`'s `None` arm (after the blueprint/seeds guard `:4949`, before the `r_path` computation `:4952`). The existing arm body (`:4950-4998`) stays verbatim as the fenced fall-through. - Test: `crates/aura-cli/tests/cli_run.rs` — `mc_dissolves_through_the_campaign_path` (dissolution proof) + `mc_dissolved_refuses_a_multi_value_stop` (Fork A refusal), after the mc anchor (`:5280`). - **Already present (do NOT recreate):** the acceptance anchor `mc_r_bootstrap_real_e2e_pins_the_exact_current_grade` (`cli_run.rs:5242`, committed `3fc491a`). It must stay green byte-for-byte through the dissolution. - **No executor change.** `aura-campaign`/`aura-engine`/`aura-research` are diff-free. --- ### Task 1: The mc sugar trio + dispatch split (fully wired) **Files:** - Modify: `crates/aura-cli/src/verb_sugar.rs` - Modify: `crates/aura-cli/src/main.rs` - Test: `crates/aura-cli/src/verb_sugar.rs` (unit tests in the existing `mod tests`) This task ships all production code AND wires the dispatch in one unit, so there is no dead-code window (the whole chain is reachable from `main` the moment it lands). - [ ] **Step 1: Add `GeneratedMc` + `translate_mc` to `verb_sugar.rs`** Insert immediately after `run_walkforward_sugar` (after line 503, before the `#[cfg(test)]` on line 505): ```rust /// The two generated documents of one dissolved `mc` invocation. #[derive(Debug)] pub(crate) struct GeneratedMc { pub process: ProcessDoc, pub campaign: CampaignDoc, } /// Translate one `aura mc --strategy r-sma --real` invocation into its two generated /// documents: a `[std::sweep(argmax), std::walk_forward, std::monte_carlo]` process /// (the sweep enumerates the IS-refit survivor grid for the wf stage; the terminal /// monte_carlo stage pools the per-window OOS trade-R series and r-bootstraps E[R]) /// and a campaign running the fast/slow grid over one instrument under a single risk /// regime that carries the stop. Unlike `translate_walkforward` (which hardcodes /// `seed: 0`), `translate_mc` sets `campaign.seed = seed` (the mc `--seed`): the wf /// winners are argmax hence deflation-seed-independent (the shipped walkforward /// anchor proved this), so remapping the campaign seed leaves the pooled series /// unchanged while making the terminal `r_bootstrap` at that seed reproduce the /// inline bootstrap. The roller sizes (`in_sample_ms`/`out_of_sample_ms`/`step_ms`) /// and `blueprint_canonical` come from the caller, exactly as for walkforward. #[allow(clippy::too_many_arguments)] pub(crate) fn translate_mc( fast: &[i64], slow: &[i64], stop_length: i64, stop_k: f64, metric: &str, in_sample_ms: u64, out_of_sample_ms: u64, step_ms: u64, resamples: u32, block_len: u32, seed: u64, name: &str, symbol: &str, from_ms: i64, to_ms: i64, blueprint_canonical: &str, ) -> Result { let process = ProcessDoc { format_version: FORMAT_VERSION, kind: DocKind::Process, name: "mc".to_string(), description: None, pipeline: vec![ StageBlock::Sweep { selection: Some(SweepSelection { metric: metric.to_string(), select: SelectRule::Argmax, deflate: false, }), }, StageBlock::WalkForward { in_sample_ms, out_of_sample_ms, step_ms, mode: WfMode::Rolling, metric: metric.to_string(), select: SelectRule::Argmax, }, StageBlock::MonteCarlo { resamples, block_len }, ], }; let mut doc_axes: BTreeMap = BTreeMap::new(); doc_axes.insert( "fast.length".to_string(), axis_from_values("fast.length", &fast.iter().map(|&v| Scalar::i64(v)).collect::>())?, ); doc_axes.insert( "slow.length".to_string(), axis_from_values("slow.length", &slow.iter().map(|&v| Scalar::i64(v)).collect::>())?, ); let campaign = CampaignDoc { format_version: FORMAT_VERSION, kind: DocKind::Campaign, name: name.to_string(), description: None, data: DataSection { instruments: vec![symbol.to_string()], windows: vec![Window { from_ms, to_ms }], }, strategies: vec![StrategyEntry { r#ref: DocRef::ContentId(content_id_of(blueprint_canonical)), axes: doc_axes, }], process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) }, risk: vec![RiskRegime::Vol { length: stop_length, k: stop_k }], seed, presentation: Presentation { persist_taps: vec![], emit: vec!["family_table".to_string()], }, }; Ok(GeneratedMc { process, campaign }) } ``` - [ ] **Step 2: Add `register_generated_mc` + `run_mc_sugar` to `verb_sugar.rs`** Insert immediately after `translate_mc`: ```rust /// Auto-register both generated mc documents (content-addressed, idempotent). /// Returns `(process_id, campaign_id)`. pub(crate) fn register_generated_mc( reg: &aura_registry::Registry, generated: &GeneratedMc, ) -> Result<(String, String), String> { let process_id = reg.put_process(&process_to_json(&generated.process)).map_err(|e| e.to_string())?; let campaign_id = reg.put_campaign(&campaign_to_json(&generated.campaign)).map_err(|e| e.to_string())?; Ok((process_id, campaign_id)) } /// Run one dissolved `mc` invocation end-to-end: register the generated documents, /// run through the one campaign path, then reprint the single `mc_r_bootstrap` grade /// line from the recorded terminal `PooledOos` bootstrap. The stdout line is /// byte-identical to the inline path (the committed exact-grade anchor is the gate). /// mc prints ONLY the one summary line — no per-window member lines (unlike /// walkforward): the monte_carlo stage is a terminal annotator, not a family. #[allow(clippy::too_many_arguments)] pub(crate) fn run_mc_sugar( fast: &[i64], slow: &[i64], stop_length: i64, stop_k: f64, metric: &str, in_sample_ms: u64, out_of_sample_ms: u64, step_ms: u64, resamples: u32, block_len: u32, seed: u64, name: &str, symbol: &str, from_ms: i64, to_ms: i64, blueprint_canonical: &str, env: &crate::project::Env, ) -> Result<(), String> { let generated = translate_mc( fast, slow, stop_length, stop_k, metric, in_sample_ms, out_of_sample_ms, step_ms, resamples, block_len, seed, name, symbol, from_ms, to_ms, blueprint_canonical, )?; let probe_params: Vec<(String, Scalar)> = vec![ ("fast.length".to_string(), Scalar::i64(fast[0])), ("slow.length".to_string(), Scalar::i64(slow[0])), ]; validate_before_register(&generated.process, &generated.campaign, blueprint_canonical, &probe_params, env)?; let reg = env.registry(); let (_process_id, campaign_id) = register_generated_mc(®, &generated)?; let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?; // The single cell's terminal monte_carlo StageRealization carries the pooled-OOS // bootstrap (record path: outcome.record.cells[].stages[].bootstrap). let boot = run .outcome .record .cells .iter() .flat_map(|c| c.stages.iter()) .find_map(|s| match &s.bootstrap { Some(aura_registry::StageBootstrap::PooledOos(b)) => Some(b), _ => None, }) .ok_or("mc produced no pooled-OOS bootstrap")?; println!("{}", crate::mc_r_bootstrap_json(boot)); Ok(()) } ``` - [ ] **Step 3: Add the `translate_mc` unit tests to `verb_sugar.rs`** Insert inside `#[cfg(test)] mod tests`, immediately after `register_generated_wf_stores_documents_the_campaign_can_actually_resolve` (after line 794, before the closing `}` of the module on line 795): ```rust #[test] fn translate_mc_is_deterministic_and_carries_the_regime_and_seed() { let a = translate_mc( &[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", 7_776_000_000, 2_592_000_000, 2_592_000_000, 1000, 5, 42, "mc", "GER40", 100, 200, "{\"bp\":1}", ) .unwrap(); let b = translate_mc( &[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", 7_776_000_000, 2_592_000_000, 2_592_000_000, 1000, 5, 42, "mc", "GER40", 100, 200, "{\"bp\":1}", ) .unwrap(); assert_eq!( content_id_of(&campaign_to_json(&a.campaign)), content_id_of(&campaign_to_json(&b.campaign)), "identical invocations must generate identical campaign ids" ); // pipeline: sweep(argmax) then walk_forward(argmax, rolling) then monte_carlo assert_eq!( a.process.pipeline, vec![ StageBlock::Sweep { selection: Some(SweepSelection { metric: "sqn_normalized".to_string(), select: SelectRule::Argmax, deflate: false, }) }, StageBlock::WalkForward { in_sample_ms: 7_776_000_000, out_of_sample_ms: 2_592_000_000, step_ms: 2_592_000_000, mode: WfMode::Rolling, metric: "sqn_normalized".to_string(), select: SelectRule::Argmax, }, StageBlock::MonteCarlo { resamples: 1000, block_len: 5 }, ] ); assert_eq!(a.campaign.seed, 42, "the campaign seed carries the mc --seed"); assert_eq!(a.campaign.risk, vec![RiskRegime::Vol { length: 14, k: 2.0 }]); assert_eq!(a.campaign.data.instruments, vec!["GER40".to_string()]); assert_eq!(a.campaign.strategies.len(), 1); assert_eq!( a.campaign.strategies[0].axes["fast.length"].values, vec![Scalar::i64(3), Scalar::i64(5)] ); assert_eq!( a.campaign.strategies[0].axes["slow.length"].values, vec![Scalar::i64(12), Scalar::i64(20)] ); assert_eq!(a.process.name, "mc"); assert!(aura_research::validate_campaign(&a.campaign).is_empty()); assert!(aura_research::validate_process(&a.process).is_empty()); assert!(aura_campaign::preflight(&a.process, &a.campaign).is_ok()); } #[test] fn translate_mc_diverging_seeds_and_stops_do_not_collide_content_ids() { let base = translate_mc( &[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", 7_776_000_000, 2_592_000_000, 2_592_000_000, 1000, 5, 42, "mc", "GER40", 100, 200, "{\"bp\":1}", ) .unwrap(); let different_seed = translate_mc( &[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", 7_776_000_000, 2_592_000_000, 2_592_000_000, 1000, 5, 7, "mc", "GER40", 100, 200, "{\"bp\":1}", ) .unwrap(); let different_k = translate_mc( &[3, 5], &[12, 20], 14, 3.0, "sqn_normalized", 7_776_000_000, 2_592_000_000, 2_592_000_000, 1000, 5, 42, "mc", "GER40", 100, 200, "{\"bp\":1}", ) .unwrap(); let base_id = content_id_of(&campaign_to_json(&base.campaign)); let seed_id = content_id_of(&campaign_to_json(&different_seed.campaign)); let k_id = content_id_of(&campaign_to_json(&different_k.campaign)); assert_ne!(base_id, seed_id, "a different mc seed must not collide onto the same campaign id"); assert_ne!(base_id, k_id, "a different stop_k must not collide onto the same campaign id"); assert_ne!(seed_id, k_id, "the two diverging documents must not collide with each other"); } #[test] fn register_generated_mc_stores_documents_the_campaign_can_actually_resolve() { let dir = std::env::temp_dir().join(format!( "aura-verb-sugar-mc-registry-test-{}", std::process::id() )); std::fs::create_dir_all(&dir).unwrap(); let reg = aura_registry::Registry::open(dir.join("runs.jsonl")); let generated = translate_mc( &[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", 7_776_000_000, 2_592_000_000, 2_592_000_000, 1000, 5, 42, "mc", "GER40", 100, 200, "{\"bp\":1}", ) .unwrap(); let (process_id, campaign_id) = register_generated_mc(®, &generated).unwrap(); let stored_campaign = reg.get_campaign(&campaign_id).unwrap().expect("campaign stored"); assert_eq!(stored_campaign, campaign_to_json(&generated.campaign)); let DocRef::ContentId(ref_id) = &generated.campaign.process.r#ref else { panic!("mc's process ref is a content id"); }; assert_eq!(ref_id, &process_id, "the stored process id must be the campaign's own ref"); let stored_process = reg.get_process(ref_id).unwrap().expect("process stored"); assert_eq!(stored_process, process_to_json(&generated.process)); let _ = std::fs::remove_dir_all(&dir); } ``` - [ ] **Step 4: Verify the verb_sugar unit tests pass** Run: `cargo test -p aura-cli --bin aura translate_mc` Expected: PASS — `translate_mc_is_deterministic_and_carries_the_regime_and_seed` and `translate_mc_diverging_seeds_and_stops_do_not_collide_content_ids` run and pass. Run: `cargo test -p aura-cli --bin aura register_generated_mc` Expected: PASS — `register_generated_mc_stores_documents_the_campaign_can_actually_resolve` runs and passes. - [ ] **Step 5: Add `mc_args_from` to `main.rs`** Insert immediately after `walkforward_args_from` (after line 4265, before `generalize_args_from`'s doc comment on line 4267): ```rust /// Convert `McCmd` into the resolved argument shape the mc sugar consumes (the /// dissolved `--strategy r-sma --real` branch). Single instrument, multi-value /// fast/slow (the IS-refit grid), single-value stop (Fork A: the stop is a risk /// regime, not a swept axis); all four grid knobs required. `--block-len`/ /// `--resamples`/`--seed` default to `1`/`1000`/`1` — byte-identical to the inline /// `McArgs::RealR` defaults (`main.rs:4981-4983`), so an omitted flag produces the /// same document either way. The usize->u32 conversion for `block_len`/`resamples` /// lands HERE, at the argv boundary where the CLI's `usize` meets the document's /// `u32` vocabulary (`StageBlock::MonteCarlo`), keeping every downstream signature /// in the document's native type. `--name`/`--trace` are rejected: the inline mc /// R-path rejects them too (`main.rs:4964`), so the sugar preserves that (the /// R-bootstrap records without a family name — the campaign name is a constant /// "mc"). #[allow(clippy::type_complexity)] fn mc_args_from( a: &McCmd, ) -> Result<(String, String, Vec, Vec, i64, f64, u32, u32, u64, Option, Option), String> { let symbol = match a.real.as_deref() { None | Some("") => return Err("mc --strategy r-sma dissolves only over --real ".to_string()), Some(s) => s.to_string(), }; if a.name.is_some() || a.trace.is_some() { return Err("mc --strategy r-sma: --name/--trace are not accepted (the R-bootstrap records without a family name)".to_string()); } let knobs = || "mc requires --fast --slow --stop-length --stop-k (fast/slow may be CSV grids; the stop is single-value)".to_string(); let fast: Vec = parse_csv_list(a.fast.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?; let slow: Vec = parse_csv_list(a.slow.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?; let stop_length: Vec = parse_csv_list(a.stop_length.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?; let stop_k: Vec = parse_csv_list(a.stop_k.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?; if stop_length.len() != 1 || stop_k.len() != 1 { return Err("mc: the stop is a single risk regime; --stop-length and --stop-k take one value each".to_string()); } let block_len = a.block_len.map(|v| v as u32).unwrap_or(1); let resamples = a.resamples.map(|v| v as u32).unwrap_or(1000); let seed = a.seed.unwrap_or(1); Ok(("mc".to_string(), symbol, fast, slow, stop_length[0], stop_k[0], block_len, resamples, seed, a.from, a.to)) } ``` - [ ] **Step 6: Insert the dissolution split into `dispatch_mc`'s `None` arm** In `main.rs`, the `None` arm currently reads (lines 4944-4949): ```rust None => { let usage = || "Usage: aura mc [--name |--trace ] | aura mc --strategy r-sma [--real [--from ] [--to ]] [--fast ] [--slow ] [--stop-length ] [--stop-k ] [--block-len ] [--resamples ] [--seed ]".to_string(); if a.blueprint.is_some() || a.seeds.is_some() { eprintln!("aura: {}", usage()); std::process::exit(2); } // The R-bootstrap path is selected by any r-sma knob; otherwise the ``` Insert the sugar branch between the guard's closing `}` (line 4949) and the `// The R-bootstrap path is selected…` comment (line 4950), so the arm becomes: ```rust if a.blueprint.is_some() || a.seeds.is_some() { eprintln!("aura: {}", usage()); std::process::exit(2); } // Dissolution split (#210, Reading A): only the real-archive r-sma // R-bootstrap execution routes through the campaign path. Everything else — // synthetic-r-sma (no --real), the synthetic seed-resweep, the blueprint // path — stays on the inline handler below, fenced until #159. if a.strategy.as_deref() == Some("r-sma") && a.real.is_some() { let (name, symbol, fast, slow, stop_length, stop_k, block_len, resamples, seed, from, to) = mc_args_from(&a).unwrap_or_else(|m| { eprintln!("aura: {m}"); std::process::exit(2); }); let signal = sma_signal(None, None); let canonical = blueprint_to_json(&signal).expect("a bare sma_signal serializes"); let reg = env.registry(); let topo = topology_hash(&signal); reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| { eprintln!("aura: {e}"); std::process::exit(1); }); // The inline `walkforward_family` sources its span from // `DataSource::wf_full_span`, which for Real ALWAYS clips `--from`/`--to` // to the archive's actual first/last bar in range: a holiday/weekend edge // shifts every IS/OOS window's calendar placement, so the roller must clip // identically here too, even when both flags are given, or the per-window // winners and pooled OOS series diverge from the committed exact-grade anchor. let source = DataSource::from_choice( DataChoice::Real { symbol: symbol.clone(), from_ms: from, to_ms: to }, env, ); let (from_ts, to_ts) = source.full_window(env); let (from_ms, to_ms) = ( aura_ingest::epoch_ns_to_unix_ms(from_ts), aura_ingest::epoch_ns_to_unix_ms(to_ts), ); let (is_ms, oos_ms, step_ms) = wf_ms_sizes(); verb_sugar::run_mc_sugar( &fast, &slow, stop_length, stop_k, WINNER_SELECTION_METRIC, is_ms, oos_ms, step_ms, resamples, block_len, seed, &name, &symbol, from_ms, to_ms, &canonical, env, ) .unwrap_or_else(|m| { eprintln!("aura: {m}"); std::process::exit(1); }); return; } // The R-bootstrap path is selected by any r-sma knob; otherwise the ``` Leave the entire existing arm body below (`let r_path = …` through the `match mc_args` block, lines 4950-4998) UNTOUCHED — it is the fenced fall-through for synthetic-r-sma, the seed-resweep, and everything else. - [ ] **Step 7: Build gate + the acceptance anchor stays green** Run: `cargo build --workspace` Expected: clean build, 0 errors. (`run_mc_sugar` is now reachable from `main` via the dispatch — no dead code.) Run: `cargo test -p aura-cli --test cli_run mc_r_bootstrap_real_e2e_pins_the_exact_current_grade` Expected: PASS (or `skip: no local GER40 data` if the archive is absent). If it runs, the `mc_r_bootstrap` line is byte-identical (block_len=5, n_resamples=1000, n_trades=20681, e_r.mean=-0.0025753095301594307, e_r.p50=-0.0023049902245975227, prob_le_zero=0.558). This is the equivalence gate — a failure here means the dissolution changed the grade and the task is not done. --- ### Task 2: Integration coverage — dissolution proof + Fork-A refusal **Files:** - Test: `crates/aura-cli/tests/cli_run.rs` - [ ] **Step 1: Add the dissolution-proof and refusal tests** Insert after the mc anchor test (after line 5280, at the end of the file's mc block): ```rust /// Property: `aura mc --strategy r-sma --real` is now thin sugar over the one campaign /// path — a successful run durably auto-registers exactly one generated process /// document, one generated campaign document (carrying the constant "mc" name and the /// stop as a non-empty single risk regime), and one campaign-run record, and emits the /// single `mc_r_bootstrap` grade line. The pipeline's leading argmax sweep and its /// walk_forward stage each persist one family; the terminal monte_carlo stage is an /// annotator and adds NO family. This is the observable proof the inline path is gone /// and the dissolution runs through the executor. Gated on the GER40 2025 archive; /// skips cleanly on a data refusal. #[test] fn mc_dissolves_through_the_campaign_path() { const FROM_MS: &str = "1735689600000"; const TO_MS: &str = "1767225599000"; let cwd = temp_cwd("mc-dissolves"); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "mc", "--strategy", "r-sma", "--real", "GER40", "--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0", "--block-len", "5", "--resamples", "1000", "--seed", "42", "--from", FROM_MS, "--to", TO_MS, ]) .current_dir(&cwd) .output() .expect("spawn aura"); if out.status.code() == Some(1) { let stderr = String::from_utf8_lossy(&out.stderr); assert!( stderr.contains("no local data") || stderr.contains("no recorded geometry"), "exit 1 must be a data refusal, got: {stderr}" ); eprintln!("skip: no local GER40 data"); return; } assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status); // the single grade line is emitted (and nothing else on stdout). let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); assert_eq!(stdout.lines().count(), 1, "mc prints exactly one grade line: {stdout:?}"); assert!( stdout.lines().next().is_some_and(|l| l.starts_with("{\"mc_r_bootstrap\":")), "the one line is the mc_r_bootstrap grade: {stdout}" ); let count = |sub: &str| { std::fs::read_dir(cwd.join("runs").join(sub)) .map(|d| d.count()) .unwrap_or(0) }; assert_eq!(count("processes"), 1, "one generated process document registered"); assert_eq!(count("campaigns"), 1, "one generated campaign document registered"); let runs_log = std::fs::read_to_string(cwd.join("runs").join("campaign_runs.jsonl")) .expect("campaign_runs.jsonl exists after a sugar run"); assert_eq!(runs_log.lines().count(), 1, "one campaign run recorded"); let campaigns_dir = cwd.join("runs").join("campaigns"); let campaign_doc_path = std::fs::read_dir(&campaigns_dir) .expect("campaigns dir exists") .next() .expect("exactly one campaign document") .expect("readable dir entry") .path(); let campaign_doc_json = std::fs::read_to_string(&campaign_doc_path).expect("read campaign doc"); assert!( campaign_doc_json.contains("\"name\":\"mc\""), "the generated campaign document carries the constant mc name: {campaign_doc_json}" ); assert!( campaign_doc_json.contains("\"risk\":[") && campaign_doc_json.contains("\"length\":14") && campaign_doc_json.contains("\"k\":2.0"), "the stop rides a non-empty risk regime: {campaign_doc_json}" ); let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args(["runs", "families"]) .current_dir(&cwd) .output() .expect("spawn families"); let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned(); assert_eq!( fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(), 1, "one Sweep family from the pipeline's leading argmax stage: {fams_out}" ); assert_eq!( fams_out.lines().filter(|l| l.contains("\"kind\":\"WalkForward\"")).count(), 1, "one WalkForward family holding the per-window OOS reports: {fams_out}" ); assert_eq!( fams_out.lines().filter(|l| l.contains("\"kind\":\"MonteCarlo\"")).count(), 0, "the terminal monte_carlo stage is an annotator, not a family: {fams_out}" ); } /// Fork A: the stop is a single risk regime on the dissolved mc path — a multi-value /// `--stop-length`/`--stop-k` is a usage refusal (exit 2), not a swept axis. Mirrors /// `walkforward_dissolved_refuses_a_multi_value_stop`. NOT gated — the refusal is pure /// argument parsing, before any data access. #[test] fn mc_dissolved_refuses_a_multi_value_stop() { let cwd = temp_cwd("mc-multi-stop"); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "mc", "--strategy", "r-sma", "--real", "GER40", "--fast", "3", "--slow", "12", "--stop-length", "14,20", "--stop-k", "2.0", ]) .current_dir(&cwd) .output() .expect("spawn aura"); assert_eq!(out.status.code(), Some(2), "multi-value stop is a usage refusal"); let stderr = String::from_utf8_lossy(&out.stderr); assert!(stderr.contains("single risk regime"), "stop-regime refusal: {stderr}"); } ``` - [ ] **Step 2: Verify the new integration tests pass** Run: `cargo test -p aura-cli --test cli_run mc_dissolves_through_the_campaign_path` Expected: PASS (or `skip: no local GER40 data`). Run: `cargo test -p aura-cli --test cli_run mc_dissolved_refuses_a_multi_value_stop` Expected: PASS (exit 2, stderr carries "single risk regime"). - [ ] **Step 3: Full-suite + clippy gate** Run: `cargo test --workspace` Expected: PASS — 0 failures. In particular the three synthetic-r-sma tests (`mc_strategy_r_sma_prints_a_bootstrap_line_deterministically`, `mc_r_sma_block_len_is_observable_and_changes_the_distribution`, `mc_r_sma_oversized_block_len_clamps_and_collapses`) stay green: they use no `--real`, so they never enter the sugar branch (`real.is_some()` is false) and run the fenced inline path unchanged. `mc_rejects_real_flag_with_usage_exit_2` also stays green (`--real` without `--strategy r-sma` falls through to the inline exit-2 refusal, no registry write). Run: `cargo clippy --workspace --all-targets -- -D warnings` Expected: clean, 0 warnings.