plan: 0075 Monte-Carlo R-bootstrap (iteration 2)

Iteration 2 of cycle 0075: the engine primitive r_bootstrap (moving-block
bootstrap over a flat R series, reusing MetricStats/SplitMix64); the CLI
parse_mc_args/McArgs split (synthetic seed-resweep preserved byte-for-byte;
the --strategy stage1-r real-R path reuses iter-1's walkforward_family +
RMetrics.trade_rs); run_mc_r_bootstrap prints one mc_r_bootstrap line. The
mc real-R path accepts only --strategy stage1-r, so a bare --real stays a
usage error (mc_rejects_real_flag_with_usage_exit_2 preserved).

refs #139
This commit is contained in:
2026-06-26 11:31:43 +02:00
parent f286bb85f7
commit b3505c7609
@@ -0,0 +1,487 @@
# Monte-Carlo R-bootstrap (cycle 0075, iteration 2) — Implementation Plan
> **Parent spec:** `docs/specs/0075-walkforward-mc-r-validation.md`
>
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
> this plan. Steps use `- [ ]` checkboxes for tracking.
**Goal:** `aura mc --strategy stage1-r [--real <SYM>]` runs the stage1-r
walk-forward, pools its OOS per-trade R series, and reports a moving-block
bootstrap `E[R]` distribution / confidence interval (`mc_r_bootstrap` line) — while
the bare synthetic `aura mc` path stays byte-identical.
**Architecture:** A new deterministic engine primitive `r_bootstrap` (moving-block
bootstrap over a flat R series, reusing `MetricStats`/`SplitMix64`), then a CLI
`parse_mc_args`/`McArgs` split routing the bare/`--name`/`--trace` forms to today's
`run_mc` (unchanged) and the `--strategy stage1-r` form to a new
`run_mc_r_bootstrap` that reuses iteration 1's `walkforward_family` +
`RMetrics.trade_rs`. The mc real-R path accepts only `--strategy stage1-r` (the
sole R-reporting walk-forward strategy) — a bare `--real` without it stays a usage
error.
**Tech Stack:** Rust workspace — `aura-engine` (`mc.rs`, `lib.rs`), `aura-cli`
(`main.rs`, `tests/cli_run.rs`). Test command: `cargo test --workspace` (one
positional filter per invocation).
---
**Files this plan creates or modifies:**
- Modify: `crates/aura-engine/src/mc.rs:111` — add `pub struct RBootstrap` +
`pub fn r_bootstrap`; `use crate::harness::SplitMix64;` at the module head.
- Modify: `crates/aura-engine/src/lib.rs:72` — re-export `RBootstrap`, `r_bootstrap`
from the `pub use mc::{..}` block.
- Modify: `crates/aura-cli/src/main.rs:2811-2813` — replace the three literal `mc`
dispatch arms with one `["mc", rest @ ..]` arm.
- Modify: `crates/aura-cli/src/main.rs` (new fns near the mc cluster ~2045) —
`parse_mc_args`, `enum McArgs`, `run_mc_r_bootstrap`, `mc_r_bootstrap_json`.
- Modify: `crates/aura-cli/src/main.rs:2756-2757` — USAGE mc fragment.
- Test: `crates/aura-engine/src/mc.rs` (#cfg test ~155) — `r_bootstrap` goldens.
- Test: `crates/aura-cli/src/main.rs` (#cfg test) — `parse_mc_args` units.
- Test: `crates/aura-cli/tests/cli_run.rs``aura mc --strategy stage1-r` E2E; the
`mc_rejects_real_flag_with_usage_exit_2` doc-comment refresh (assertions stay).
---
### Task 1: `r_bootstrap` — moving-block bootstrap over a flat R series
**Files:**
- Modify: `crates/aura-engine/src/mc.rs:1-13` (module-head `use`), `:111` (new
struct + fn)
- Modify: `crates/aura-engine/src/lib.rs:72`
- Test: `crates/aura-engine/src/mc.rs` (#cfg test, ~155)
- [ ] **Step 1: Write the failing tests** — append to the `mod tests` in
`crates/aura-engine/src/mc.rs` (`use super::*` is already in scope):
```rust
#[test]
fn r_bootstrap_empty_series_is_all_zero() {
let b = r_bootstrap(&[], 100, 1, 7);
assert_eq!(b.n_trades, 0);
assert_eq!(b.e_r.mean, 0.0);
assert_eq!(b.prob_le_zero, 0.0);
}
#[test]
fn r_bootstrap_single_block_equals_full_series_mean() {
// block_len == n: the only valid start is 0, so every resample IS the full
// series -> every resample mean == the series mean -> zero spread.
let rs = [1.0, -2.0, 3.0]; // mean = 2/3
let b = r_bootstrap(&rs, 500, rs.len(), 7);
let mean = 2.0 / 3.0;
assert!((b.e_r.mean - mean).abs() < 1e-12);
assert!((b.e_r.p5 - mean).abs() < 1e-12);
assert!((b.e_r.p95 - mean).abs() < 1e-12);
assert_eq!(b.prob_le_zero, 0.0); // mean > 0
assert_eq!(b.n_trades, 3);
assert_eq!(b.block_len, 3);
assert_eq!(b.n_resamples, 500);
}
#[test]
fn r_bootstrap_is_deterministic_given_seed() {
let rs = [0.5, -1.0, 2.0, -0.5, 1.5, -2.0, 0.25];
let a = r_bootstrap(&rs, 1000, 1, 42);
let b = r_bootstrap(&rs, 1000, 1, 42);
assert_eq!(a, b, "same rs+seed+params -> identical RBootstrap (C1)");
let c = r_bootstrap(&rs, 1000, 1, 43);
assert_ne!(a.e_r.p5, c.e_r.p5, "a different seed reshuffles differently");
}
#[test]
fn r_bootstrap_block_len_is_clamped_to_series_len() {
// block_len > n clamps to n -> single-block behaviour (no panic, no OOB).
let rs = [1.0, 2.0];
let b = r_bootstrap(&rs, 10, 99, 1);
assert_eq!(b.block_len, 2);
assert!((b.e_r.mean - 1.5).abs() < 1e-12);
}
```
- [ ] **Step 2: Run to verify it fails**
Run: `cargo test --workspace r_bootstrap_single_block_equals_full_series_mean`
Expected: FAIL — `cannot find function `r_bootstrap` in this scope`.
- [ ] **Step 3: Add the module-head import** — in `crates/aura-engine/src/mc.rs`,
after the existing `use crate::{RunMetrics, RunReport, Scalar};` (line 13):
```rust
use crate::harness::SplitMix64;
```
- [ ] **Step 4: Add `RBootstrap` + `r_bootstrap`** — in
`crates/aura-engine/src/mc.rs`, after the `quantile` fn (line 110), before
`#[cfg(test)] mod tests`:
```rust
/// Distribution of E[R] under a moving-block bootstrap of an OOS per-trade R series
/// (#139). `block_len == 1` is the i.i.d. trade-shuffle; `block_len > 1` resamples
/// contiguous runs, preserving the serial correlation of sequential trades a pure
/// shuffle would erase. Deterministic (C1) given `seed`.
#[derive(Clone, Debug, PartialEq)]
pub struct RBootstrap {
pub e_r: MetricStats, // mean + p5/p25/p50/p75/p95 of the resampled E[R]
pub prob_le_zero: f64, // fraction of resamples whose mean R <= 0
pub n_trades: usize,
pub block_len: usize,
pub n_resamples: usize,
}
/// Moving-block bootstrap of `rs` (non-circular; the final block of each resample is
/// truncated so the resample has exactly `n` values). `block_len` is clamped to
/// `[1, n]`. Empty `rs` or zero resamples -> an all-zero `RBootstrap`. Pure given
/// `seed` (drives the existing `SplitMix64`).
pub fn r_bootstrap(rs: &[f64], n_resamples: usize, block_len: usize, seed: u64) -> RBootstrap {
let n = rs.len();
if n == 0 || n_resamples == 0 {
return RBootstrap {
e_r: MetricStats { mean: 0.0, p5: 0.0, p25: 0.0, p50: 0.0, p75: 0.0, p95: 0.0 },
prob_le_zero: 0.0,
n_trades: n,
block_len: block_len.clamp(1, n.max(1)),
n_resamples,
};
}
let block_len = block_len.clamp(1, n);
let mut rng = SplitMix64::new(seed);
let mut means: Vec<f64> = Vec::with_capacity(n_resamples);
for _ in 0..n_resamples {
let mut sample: Vec<f64> = Vec::with_capacity(n);
while sample.len() < n {
let start = (rng.next_u64() % (n - block_len + 1) as u64) as usize;
let take = block_len.min(n - sample.len());
sample.extend_from_slice(&rs[start..start + take]);
}
means.push(sample.iter().sum::<f64>() / n as f64);
}
let e_r = MetricStats::from_values(&means);
let prob_le_zero = means.iter().filter(|&&m| m <= 0.0).count() as f64 / n_resamples as f64;
RBootstrap { e_r, prob_le_zero, n_trades: n, block_len, n_resamples }
}
```
- [ ] **Step 5: Re-export from the crate root** — in
`crates/aura-engine/src/lib.rs:72`, add `RBootstrap` and `r_bootstrap` to the
`pub use mc::{..}` list:
```rust
pub use mc::{monte_carlo, r_bootstrap, McAggregate, McDraw, McFamily, MetricStats, RBootstrap};
```
- [ ] **Step 6: Run to verify they pass**
Run: `cargo test --workspace r_bootstrap_single_block_equals_full_series_mean`
Expected: PASS
Run: `cargo test --workspace r_bootstrap_is_deterministic_given_seed`
Expected: PASS
Run: `cargo test --workspace r_bootstrap_empty_series_is_all_zero`
Expected: PASS
Run: `cargo test --workspace r_bootstrap_block_len_is_clamped_to_series_len`
Expected: PASS
### Task 2: `parse_mc_args` + `McArgs` + `run_mc_r_bootstrap` + dispatch
**Files:**
- Modify: `crates/aura-cli/src/main.rs` — new `enum McArgs`, `parse_mc_args`,
`run_mc_r_bootstrap`, `mc_r_bootstrap_json` (near the mc cluster); dispatch arm
(2811-2813); USAGE (2757).
- Test: `crates/aura-cli/src/main.rs` (#cfg test) — `parse_mc_args` units.
- [ ] **Step 1: Write the failing parse tests** — append to the `mod tests` in
`crates/aura-cli/src/main.rs`:
```rust
#[test]
fn parse_mc_args_bare_and_name_route_to_synthetic() {
assert_eq!(parse_mc_args(&[]), Ok(McArgs::Synthetic { name: "mc".to_string(), persist: false }));
assert_eq!(parse_mc_args(&["--name", "m"]), Ok(McArgs::Synthetic { name: "m".to_string(), persist: false }));
assert_eq!(parse_mc_args(&["--trace", "m"]), Ok(McArgs::Synthetic { name: "m".to_string(), persist: true }));
}
#[test]
fn parse_mc_args_stage1_r_routes_to_real_r_with_defaults_and_overrides() {
assert_eq!(
parse_mc_args(&["--strategy", "stage1-r"]),
Ok(McArgs::RealR {
choice: DataChoice::Synthetic, grid: Stage1RGrid::default(),
block_len: 1, n_resamples: 1000, seed: 1,
})
);
let r = parse_mc_args(&["--strategy", "stage1-r", "--real", "USDJPY",
"--block-len", "5", "--resamples", "200", "--seed", "9", "--fast", "5,10"]);
let McArgs::RealR { choice, grid, block_len, n_resamples, seed } = r.expect("valid real-R mc args")
else { panic!("expected RealR") };
assert_eq!(choice, DataChoice::Real { symbol: "USDJPY".to_string(), from_ms: None, to_ms: None });
assert_eq!((block_len, n_resamples, seed), (5, 200, 9));
assert_eq!(grid.fast, vec![5, 10]);
}
#[test]
fn parse_mc_args_real_without_stage1_r_is_usage_error() {
// a bare --real (no R candidate) is still rejected — the synthetic seed-resweep
// is undefined over real bars; the R path needs --strategy stage1-r.
assert!(parse_mc_args(&["--real", "EURUSD"]).is_err());
assert!(parse_mc_args(&["--strategy", "sma"]).is_err());
assert!(parse_mc_args(&["--strategy", "stage1-r", "--name", "x"]).is_err()); // name invalid on R path
}
```
- [ ] **Step 2: Run to verify it fails**
Run: `cargo test --workspace parse_mc_args_bare_and_name_route_to_synthetic`
Expected: FAIL — `cannot find function `parse_mc_args` / type `McArgs``.
- [ ] **Step 3: Add `McArgs` + `parse_mc_args`** — in `crates/aura-cli/src/main.rs`,
after `run_mc` (~line 2045):
```rust
/// Parsed form of the `mc` tail: either the synthetic seed-resweep (today's path,
/// preserved byte-for-byte) or the real-candidate R-bootstrap. The R path accepts
/// ONLY `--strategy stage1-r` (the sole R-reporting walk-forward strategy); a bare
/// `--real` without it is a usage error (the synthetic seed-resweep is undefined
/// over real bars).
#[derive(Clone, Debug, PartialEq)]
enum McArgs {
Synthetic { name: String, persist: bool },
RealR { choice: DataChoice, grid: Stage1RGrid, block_len: usize, n_resamples: usize, seed: u64 },
}
fn parse_mc_args(rest: &[&str]) -> Result<McArgs, String> {
let usage = || "mc [--name <n>|--trace <n>] | mc --strategy stage1-r [--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();
let mut strategy: Option<Strategy> = None;
let mut name: Option<(String, bool)> = None;
let mut real = RealWindowGrammar::default();
let mut grid = Stage1RGrid::default();
let mut block_len: usize = 1;
let mut n_resamples: usize = 1000;
let mut seed: u64 = 1;
let mut r_path = false;
let mut tail = rest;
while let Some((flag, t)) = tail.split_first() {
let (value, t) = t.split_first().ok_or_else(usage)?;
if real.accept(flag, value, &usage)? {
r_path = true;
tail = t;
continue;
}
match *flag {
"--strategy" => {
strategy = Some(match *value {
"stage1-r" => Strategy::Stage1R,
_ => return Err(usage()),
});
r_path = true;
}
"--name" if name.is_none() => name = Some(((*value).to_string(), false)),
"--trace" if name.is_none() => name = Some(((*value).to_string(), true)),
"--fast" => { grid.fast = parse_csv_list(value).map_err(|()| usage())?; r_path = true; }
"--slow" => { grid.slow = parse_csv_list(value).map_err(|()| usage())?; r_path = true; }
"--stop-length" => { grid.stop_length = parse_csv_list(value).map_err(|()| usage())?; r_path = true; }
"--stop-k" => { grid.stop_k = parse_csv_list(value).map_err(|()| usage())?; r_path = true; }
"--block-len" => { block_len = value.parse().map_err(|_| usage())?; r_path = true; }
"--resamples" => { n_resamples = value.parse().map_err(|_| usage())?; r_path = true; }
"--seed" => { seed = value.parse().map_err(|_| usage())?; r_path = true; }
_ => return Err(usage()),
}
tail = t;
}
if r_path {
if strategy != Some(Strategy::Stage1R) {
return Err(usage());
}
if name.is_some() {
return Err(usage());
}
let choice = real.finish(&usage)?;
Ok(McArgs::RealR { choice, grid, block_len, n_resamples, seed })
} else {
let (name, persist) = name.unwrap_or_else(|| ("mc".to_string(), false));
Ok(McArgs::Synthetic { name, persist })
}
}
```
- [ ] **Step 4: Add `run_mc_r_bootstrap` + `mc_r_bootstrap_json`** — after
`parse_mc_args`:
```rust
/// `aura mc --strategy stage1-r [--real <SYM>]`: run the stage1-r walk-forward, pool
/// every OOS window's per-trade R series in roll order, and print one moving-block
/// bootstrap `mc_r_bootstrap` line (E[R] distribution + P(E[R] <= 0)). Frictionless
/// Stage-1 (no costs); deterministic given `seed` (C1).
fn run_mc_r_bootstrap(data: DataSource, grid: &Stage1RGrid, block_len: usize, n_resamples: usize, seed: u64) {
let result = walkforward_family(Strategy::Stage1R, None, &data, grid);
let pooled: Vec<f64> = result
.windows
.iter()
.flat_map(|w| w.run.oos_report.metrics.r.as_ref().map(|r| r.trade_rs.clone()).unwrap_or_default())
.collect();
let boot = r_bootstrap(&pooled, n_resamples, block_len, seed);
println!("{}", mc_r_bootstrap_json(&boot));
}
/// Render an `RBootstrap` as one canonical JSON line (`MetricStats` serializes; the
/// scalar fields are spliced in), mirroring `mc_aggregate_json`.
fn mc_r_bootstrap_json(b: &RBootstrap) -> String {
serde_json::json!({
"mc_r_bootstrap": {
"n_trades": b.n_trades,
"block_len": b.block_len,
"n_resamples": b.n_resamples,
"e_r": b.e_r,
"prob_le_zero": b.prob_le_zero,
}
})
.to_string()
}
```
(Add `r_bootstrap`, `RBootstrap` to the `use aura_engine::{..}` import block at the
top of `main.rs`, alongside the existing `monte_carlo` / `McFamily` imports.)
- [ ] **Step 5: Replace the dispatch arms** — in `crates/aura-cli/src/main.rs`,
replace the three literal arms (2811-2813):
```rust
["mc", rest @ ..] => match parse_mc_args(rest) {
Ok(McArgs::Synthetic { name, persist }) => run_mc(&name, persist),
Ok(McArgs::RealR { choice, grid, block_len, n_resamples, seed }) => {
run_mc_r_bootstrap(DataSource::from_choice(choice), &grid, block_len, n_resamples, seed)
}
Err(msg) => {
eprintln!("aura: {msg}");
std::process::exit(2);
}
},
```
- [ ] **Step 6: Extend USAGE** — in `crates/aura-cli/src/main.rs:2757`, replace the
`aura mc [--name <n>|--trace <n>]` fragment with:
```
aura mc [--name <n>|--trace <n>] | aura mc --strategy stage1-r [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--block-len <n>] [--resamples <n>] [--seed <n>]
```
- [ ] **Step 7: Build gate**
Run: `cargo build --workspace`
Expected: 0 errors.
- [ ] **Step 8: Run the parse units + the preserved synthetic mc goldens**
Run: `cargo test --workspace parse_mc_args_bare_and_name_route_to_synthetic`
Expected: PASS
Run: `cargo test --workspace parse_mc_args_stage1_r_routes_to_real_r_with_defaults_and_overrides`
Expected: PASS
Run: `cargo test --workspace parse_mc_args_real_without_stage1_r_is_usage_error`
Expected: PASS
Run: `cargo test --workspace mc_runs_persists_a_monte_carlo_family_and_lists_it`
Expected: PASS (bare `aura mc` unchanged — routed to `run_mc` byte-for-byte)
Run: `cargo test --workspace mc_rejects_real_flag_with_usage_exit_2`
Expected: PASS (a bare `--real` without `--strategy stage1-r` still exits 2 + usage)
### Task 3: stage1-r mc E2E + doc-comment refresh
**Files:**
- Test: `crates/aura-cli/tests/cli_run.rs` — new `aura mc --strategy stage1-r` E2E;
refresh the `mc_rejects_real_flag_with_usage_exit_2` doc-comment (1298-1304).
- [ ] **Step 1: Write the failing E2E test** — append to
`crates/aura-cli/tests/cli_run.rs` (uses the established `Command::new(BIN)` +
`temp_cwd` harness):
```rust
/// Property: `aura mc --strategy stage1-r` runs the stage1-r walk-forward on
/// synthetic data, bootstraps the pooled OOS per-trade R series, and prints exactly
/// one canonical `mc_r_bootstrap` line carrying an `e_r` distribution block and
/// `prob_le_zero`. Deterministic: a second run with the same (default) seed is
/// byte-identical (C1). Frictionless Stage-1 — no costs, no `runs/` family write.
#[test]
fn mc_strategy_stage1_r_prints_a_bootstrap_line_deterministically() {
let dir = temp_cwd("mc_stage1r_boot");
let run = || {
Command::new(BIN)
.args(["mc", "--strategy", "stage1-r", "--resamples", "200"])
.current_dir(&dir)
.output()
.expect("spawn aura mc --strategy stage1-r")
};
let out = run();
assert!(out.status.success(), "exit: {:?} stderr: {}", out.status, String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert_eq!(stdout.lines().count(), 1, "exactly one mc_r_bootstrap line: {stdout:?}");
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("canonical JSON");
assert!(v["mc_r_bootstrap"]["e_r"]["mean"].is_number(), "e_r block present: {stdout}");
assert!(v["mc_r_bootstrap"]["prob_le_zero"].is_number(), "prob_le_zero present: {stdout}");
assert_eq!(v["mc_r_bootstrap"]["n_resamples"], 200);
let out2 = run();
assert_eq!(out.stdout, out2.stdout, "same seed -> byte-identical line (C1)");
let _ = std::fs::remove_dir_all(&dir);
}
```
- [ ] **Step 2: Run to verify it fails**
(Without Task 2 this would fail; with Tasks 1-2 already applied in the same iter,
this verifies the wired path.)
Run: `cargo test --workspace mc_strategy_stage1_r_prints_a_bootstrap_line_deterministically`
Expected: PASS once Tasks 1-2 are in the tree (the implementer applies tasks in
order; if run before, FAIL with exit 2 / no `mc_r_bootstrap` line).
- [ ] **Step 3: Refresh the stale doc-comment** — the
`mc_rejects_real_flag_with_usage_exit_2` assertions stay (a bare `--real` is still
rejected), but its rationale prose (1298-1304) overclaims that mc carves out the
real path entirely. Replace that doc-comment block with:
```rust
/// The synthetic seed-resweep `mc` is undefined over real bars (one realization ->
/// identical members), so a bare `aura mc --real EURUSD` — with no R candidate — is
/// REJECTED (usage on stderr, exit 2) at the binary boundary. The real path is
/// reachable ONLY via `--strategy stage1-r` (the R-bootstrap over the pooled OOS R
/// series, `mc_strategy_stage1_r_prints_a_bootstrap_line_deterministically`); a
/// `--real` without it stays a usage error. NOT gated — the refusal precedes any
/// data access, so it is CI-safe on every machine.
```
- [ ] **Step 4: Full-suite regression gate**
Run: `cargo test --workspace`
Expected: PASS — the new engine + parse + E2E tests green; all pre-existing goldens
(bare `aura mc`, `aura walkforward` SMA, stage1-r single-run, C18 round-trips)
green.
Run: `cargo clippy --workspace --all-targets -- -D warnings`
Expected: 0 warnings.
---
## Self-review
- **Spec coverage:** spec Change 2 (`RBootstrap`/`r_bootstrap` — Task 1) and Change 4
(`parse_mc_args`/`McArgs`/`run_mc_r_bootstrap`/dispatch/USAGE — Task 2; E2E +
doc-refresh — Task 3). Both in-scope sections covered. ✓
- **Placeholder scan:** no TBD/TODO/"similar to". ✓
- **Type/name consistency:** `RBootstrap`, `r_bootstrap`, `McArgs`,
`parse_mc_args`, `run_mc_r_bootstrap`, `mc_r_bootstrap_json`, `Strategy::Stage1R`,
`Stage1RGrid`, `DataChoice` used identically across tasks; the pooled field path
is `w.run.oos_report.metrics.r.trade_rs` (per recon, not the spec's abbreviated
`w.oos_report...`). ✓
- **Step granularity:** each step is one edit or one command. ✓
- **No commit steps.** ✓
- **Compile-gate ordering (rule 7):** Task 1 is purely additive (new symbol + its
re-export + tests) — no caller breakage. Task 2 replaces the dispatch arms and
adds `parse_mc_args`/`McArgs`/`run_mc_r_bootstrap` together (the arm references
both, so they land in one task) before its build gate; the only `run_mc` caller
becomes the `McArgs::Synthetic` branch, threaded in the same task. ✓
- **Filter strings (rule 8):** every `cargo test` filter names a real test —
pre-existing (`mc_runs_persists_a_monte_carlo_family_and_lists_it`,
`mc_rejects_real_flag_with_usage_exit_2`) or introduced in the same task; the
final gate is the unfiltered `cargo test --workspace`. ✓
- **mc-R restriction (derived, recorded on #139):** the R path accepts only
`--strategy stage1-r`; a bare `--real` stays rejected, so
`mc_rejects_real_flag_with_usage_exit_2`'s assertions are preserved (only its
prose is refreshed). ✓