Files
Aura/docs/plans/mc-dissolution.md
T
Brummel 764dc2adbf docs(specs,plans): mc dissolution — spec + plan (#210)
The FOURTH and LAST verb dissolution. `aura mc --strategy r-sma --real`
dissolves into thin sugar over the one campaign executor — a generated
[std::sweep(argmax), std::walk_forward, std::monte_carlo] campaign
document whose terminal monte_carlo stage reproduces the current
`mc_r_bootstrap` grade byte-for-byte (the committed anchor 3fc491a gates
it). Pure sugar, no executor change; mirrors the walkforward dissolution.

The one new load-bearing decision is the seed mapping: the campaign
carries the mc `--seed` (the wf winners are argmax hence
deflation-seed-independent, proven by the shipped walkforward anchor).
Fork C retention as walkforward: the built-in arm splits, run_mc_r_bootstrap
/ run_mc stay inline for the fenced synthetic/non-real paths (until #159).

Spec grounding-check PASS, auto-signed on #210 (2026-07-06).

refs #210
2026-07-06 23:45:50 +02:00

30 KiB

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.rsmc_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):

/// 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<GeneratedMc, String> {
    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<String, Axis> = BTreeMap::new();
    doc_axes.insert(
        "fast.length".to_string(),
        axis_from_values("fast.length", &fast.iter().map(|&v| Scalar::i64(v)).collect::<Vec<_>>())?,
    );
    doc_axes.insert(
        "slow.length".to_string(),
        axis_from_values("slow.length", &slow.iter().map(|&v| Scalar::i64(v)).collect::<Vec<_>>())?,
    );
    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:

/// 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(&reg, &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):

    #[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(&reg, &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):

/// 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<i64>, Vec<i64>, i64, f64, u32, u32, u64, Option<i64>, Option<i64>), String> {
    let symbol = match a.real.as_deref() {
        None | Some("") => return Err("mc --strategy r-sma dissolves only over --real <SYMBOL>".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<i64> = parse_csv_list(a.fast.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
    let slow: Vec<i64> = parse_csv_list(a.slow.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
    let stop_length: Vec<i64> = parse_csv_list(a.stop_length.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
    let stop_k: Vec<f64> = 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):

        None => {
            let usage = || "Usage: aura mc [--name <n>|--trace <n>] | aura mc --strategy r-sma [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--block-len <n>] [--resamples <n>] [--seed <n>]".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:

            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):

/// 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.