From 860b89ad937c0cc49aed779eead137fc95aa896d Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 1 Jul 2026 12:07:52 +0200 Subject: [PATCH] plan: 0095 monte-carlo over a loaded blueprint 4 tasks: parse_mc_blueprint_args, blueprint_mc_family (closed-blueprint guard + seed-driven synthetic walks), the reproduce MonteCarlo realization branch, and run_blueprint_mc + the .json dispatch arm + E2E. All in aura-cli/main.rs (engine/registry untouched). --- docs/plans/0095-mc-over-a-loaded-blueprint.md | 565 ++++++++++++++++++ 1 file changed, 565 insertions(+) create mode 100644 docs/plans/0095-mc-over-a-loaded-blueprint.md diff --git a/docs/plans/0095-mc-over-a-loaded-blueprint.md b/docs/plans/0095-mc-over-a-loaded-blueprint.md new file mode 100644 index 0000000..427ad13 --- /dev/null +++ b/docs/plans/0095-mc-over-a-loaded-blueprint.md @@ -0,0 +1,565 @@ +# Monte-Carlo over a Loaded Blueprint — Implementation Plan + +> **Parent spec:** `docs/specs/0095-mc-over-a-loaded-blueprint.md` +> +> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run +> this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** Add `aura mc --seeds N` — a Monte-Carlo family from a +loaded CLOSED blueprint, each seed drawing a distinct synthetic walk — persisted as +`FamilyKind::MonteCarlo` with the content-addressed store hook, `aura reproduce` +bit-identical. + +**Architecture:** All code lands in `crates/aura-cli/src/main.rs` (engine + registry +untouched — invariants 8/9). A new `.json`-discriminator on the `["mc", ..]` dispatch +arm (mirroring the 0093 sweep arm) routes to `blueprint_mc_family` (builds an +`McFamily` by running the fixed closed blueprint over N seed-driven synthetic walks +through the shared `run_blueprint_member` path) → `run_blueprint_mc` (the persist seam: +`put_blueprint` + `append_family(MonteCarlo)` + print). `reproduce_family_in` gains a +`MonteCarlo` realization branch that reconstructs each member's seed-driven walk. + +**Tech Stack:** Rust; `SyntheticSpec::source(seed)` (aura-engine), `McFamily`/`McDraw`/ +`McAggregate` (aura-engine), `run_blueprint_member` (unchanged), `Registry::put_blueprint` ++ `append_family` (aura-registry), `topology_hash`/`content_id` (local). + +--- + +**Files this plan creates or modifies:** + +- Modify: `crates/aura-cli/src/main.rs` — new `parse_mc_blueprint_args` + `McBlueprintArgs` + (beside `parse_sweep_blueprint_args` ~3797-3835); new `synthetic_walk_sources` + + `blueprint_mc_family` + `run_blueprint_mc` (beside `blueprint_sweep_family`/`run_blueprint_sweep` + ~3154-3246); the `MonteCarlo` branch in `reproduce_family_in` (~2503-2574, the run at ~2561); + the `["mc", rest @ ..]` dispatch arm (~4006-4015). +- Test: `crates/aura-cli/src/main.rs` — unit tests beside the sweep units (~5144-5230): + `parse_mc_blueprint_args_grammar`, `blueprint_mc_family_seeds_differ`, + `reproduce_family_re_derives_every_mc_member_bit_identically`. +- Test: `crates/aura-cli/tests/cli_run.rs` — E2E beside the sweep/reproduce E2Es (~3296-3489): + `aura_mc_over_a_blueprint_reproduces_bit_identically`, `aura_mc_rejects_an_open_blueprint`. + +Facts pinned by plan-recon (line refs are current; re-anchor by symbol if drifted): +- `run_blueprint_member(signal: Composite, point: &[Cell], space: &[ParamSpec], sources: + Vec>, window: (Timestamp,Timestamp), seed: u64, pip: f64, + topo: &str) -> RunReport` — **unchanged**; stamps `manifest.seed`/`manifest.topology_hash`. +- `McDraw { pub seed: u64, pub report: RunReport }`, `McFamily { pub draws, pub aggregate }`, + `McAggregate::from_draws(&[McDraw]) -> McAggregate` (all pub; `from_draws` needs draws non-empty). +- `SyntheticSpec { start: f64, len: usize, step: i64 }`; `.source(seed) -> impl Source`; + `window_of(&[Box]) -> Option<(Timestamp,Timestamp)>`. +- `mc_member_line(id, seed, report) -> String`; `mc_aggregate_json(&McAggregate) -> String`; + `mc_member_reports(&McFamily) -> Vec`. +- `Family { pub id, pub kind: FamilyKind, pub members: Vec }`; + `FamilyKind::MonteCarlo` is a variant. `stage1_signal(fast: Option, slow: Option) + -> Composite` (Some binds → closed). `STAGE1_R_STOP_LENGTH = 3`, fixture SMA slow=4 → the + graph warms in ~4 bars, so a len-60 walk yields many trades that differ across seeds. +- All needed imports (`Cell`, `ParamSpec`, `Composite`, `McFamily`, `McDraw`, `McAggregate`, + `SyntheticSpec`, `window_of`, `blueprint_from_json`, `blueprint_to_json`, `FamilyKind`, + `mc_member_reports`, `std_vocabulary`) are already in `main.rs` — no `use` edits. + +--- + +### Task 1: `parse_mc_blueprint_args` + `McBlueprintArgs` + +**Files:** +- Modify: `crates/aura-cli/src/main.rs` (beside `parse_sweep_blueprint_args` ~3797-3835) +- Test: `crates/aura-cli/src/main.rs` (beside the sweep-arg unit `parse_sweep_blueprint_args_grammar` ~5107) + +- [ ] **Step 1: Write the failing test** + +Add to the `#[cfg(test)] mod tests` block, beside `parse_sweep_blueprint_args_grammar`: + +```rust +#[test] +fn parse_mc_blueprint_args_grammar() { + // --seeds required and parsed; default name "mc"; seeds >= 1; unknown/real flags rejected. + let ok = parse_mc_blueprint_args(&["--seeds", "8", "--name", "mc1"]).expect("valid"); + assert_eq!(ok.n_seeds, 8); + assert_eq!(ok.name, "mc1"); + let def = parse_mc_blueprint_args(&["--seeds", "3"]).expect("default name"); + assert_eq!(def.name, "mc"); + assert!(parse_mc_blueprint_args(&[]).is_err(), "--seeds is required"); + assert!(parse_mc_blueprint_args(&["--seeds", "0"]).is_err(), "seeds must be >= 1"); + assert!(parse_mc_blueprint_args(&["--seeds", "x"]).is_err(), "seeds must be a number"); + assert!( + parse_mc_blueprint_args(&["--seeds", "4", "--real", "EURUSD"]).is_err(), + "synthetic-only this cycle: --real is rejected (real-data MC is #172)" + ); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p aura-cli --bin aura parse_mc_blueprint_args_grammar` +Expected: FAIL — compile error `cannot find function parse_mc_blueprint_args` (the fn/struct do not exist yet). + +- [ ] **Step 3: Write minimal implementation** + +Add beside `parse_sweep_blueprint_args` (~3797): + +```rust +/// Parsed tail of `aura mc …`: the seed count + the family name. +/// Synthetic-only this cycle (real-data MC rides #172), so `--real`/`--from`/`--to` +/// are rejected as unknown flags. Mirrors `parse_sweep_blueprint_args`' shape. +struct McBlueprintArgs { + n_seeds: u64, + name: String, +} + +fn parse_mc_blueprint_args(rest: &[&str]) -> Result { + let usage = || "usage: mc --seeds [--name ]".to_string(); + let mut n_seeds: Option = None; + let mut name: Option = None; + let mut tail = rest; + while let Some((flag, t)) = tail.split_first() { + let (value, t) = t.split_first().ok_or_else(usage)?; + match *flag { + "--seeds" => { + let n: u64 = value.parse().map_err(|_| usage())?; + if n == 0 { + return Err(usage()); + } + n_seeds = Some(n); + } + "--name" if name.is_none() => name = Some((*value).to_string()), + _ => return Err(usage()), + } + tail = t; + } + let n_seeds = n_seeds.ok_or_else(usage)?; // --seeds is required + Ok(McBlueprintArgs { n_seeds, name: name.unwrap_or_else(|| "mc".to_string()) }) +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p aura-cli --bin aura parse_mc_blueprint_args_grammar` +Expected: PASS. + +--- + +### Task 2: `synthetic_walk_sources` + `blueprint_mc_family` (closed-blueprint guard) + +**Files:** +- Modify: `crates/aura-cli/src/main.rs` (beside `blueprint_sweep_family` ~3154; `mc_family` pattern ~2210) +- Test: `crates/aura-cli/src/main.rs` (beside `blueprint_sweep_member_equals_single_run_and_shares_topology_hash` ~5151) + +- [ ] **Step 1: Write the failing test** + +```rust +#[test] +fn blueprint_mc_family_seeds_differ() { + // MC over a CLOSED signal (both SMA knobs bound): 3 seeds -> 3 draws, one shared + // topology_hash, and DIFFERING metrics — the seed reaches the DATA (a distinct + // synthetic walk per draw), not just the manifest label. The anti-degenerate guard: + // a regression to seed-as-label-only would make the three draws identical. + let closed = stage1_signal(Some(2), Some(4)); + let doc = blueprint_to_json(&closed).expect("serializes"); + let family = blueprint_mc_family(&doc, 3, &DataSource::Synthetic); + + assert_eq!(family.draws.len(), 3, "one draw per seed"); + assert_eq!(family.draws.iter().map(|d| d.seed).collect::>(), vec![1, 2, 3]); + let topo = family.draws[0].report.manifest.topology_hash.clone(); + assert!(topo.is_some(), "members carry a topology_hash"); + assert!( + family.draws.iter().all(|d| d.report.manifest.topology_hash == topo), + "all members share one topology_hash" + ); + let m: Vec<_> = family.draws.iter().map(|d| &d.report.metrics).collect(); + assert!(m[0] != m[1] || m[1] != m[2], "seeds must yield differing realizations"); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p aura-cli --bin aura blueprint_mc_family_seeds_differ` +Expected: FAIL — compile error `cannot find function blueprint_mc_family` (fn does not exist yet). + +- [ ] **Step 3: Write minimal implementation** + +Add beside `blueprint_sweep_family` (~3154): + +```rust +/// A fresh seeded synthetic price walk for one Monte-Carlo draw — the `mc_family` +/// pattern (a distinct realization per seed). A FIXED `SyntheticSpec` shared by the +/// `aura mc ` persist path AND the reproduce MonteCarlo branch, so the +/// seed->walk reconstruction is bit-exact (C1). Length 60 comfortably warms the loaded +/// stage1-r graph (SMA slow=4 + the len-3 vol stop) so draws produce differing trades. +fn synthetic_walk_sources(seed: u64) -> Vec> { + let spec = SyntheticSpec { start: 1.0, len: 60, step: 1 }; + vec![Box::new(spec.source(seed))] +} + +/// Build a Monte-Carlo family from a loaded CLOSED signal blueprint: run the fixed +/// blueprint across `n_seeds` seeds, each seed drawing a distinct synthetic walk. The +/// blueprint must be CLOSED (empty wrapped `param_space`) — MC binds no axis, so a free +/// knob has no binder; an OPEN blueprint is rejected with a named error (exit 2) before +/// any run, pre-empting the `compile_with_params` arity panic. Each draw runs the shared +/// reduce-mode member path (`run_blueprint_member`, the same fn reproduce re-runs), so +/// reproduction is bit-identical (C1); every member carries the shared `topology_hash`. +fn blueprint_mc_family(doc: &str, n_seeds: u64, data: &DataSource) -> McFamily { + let reload = |d: &str| { + blueprint_from_json(d, &|t| std_vocabulary(t)) + .expect("doc parse-validated at the dispatch boundary; reload is infallible") + }; + let topo = topology_hash(&reload(doc)); + let pip = data.pip_size(); + // probe the wrapped param_space (as blueprint_sweep_family does); MC needs it empty. + let (tx_eq, _) = mpsc::channel(); + let (tx_ex, _) = mpsc::channel(); + let (tx_r, _) = mpsc::channel(); + let (tx_req, _) = mpsc::channel(); + let space = + wrap_stage1r(reload(doc), tx_eq, tx_ex, tx_r, tx_req, false, true, None).param_space(); + if !space.is_empty() { + eprintln!( + "aura: mc requires a closed blueprint (no free parameters); {} free knob(s) — \ + bind them or use `aura sweep --axis`", + space.len() + ); + std::process::exit(2); + } + let point: Vec = Vec::new(); // closed -> empty point (as `aura run `) + let draws: Vec = (1..=n_seeds) + .map(|seed| { + let sources = synthetic_walk_sources(seed); + let window = window_of(&sources).expect("non-empty synthetic walk"); + let report = + run_blueprint_member(reload(doc), &point, &space, sources, window, seed, pip, &topo); + McDraw { seed, report } + }) + .collect(); + let aggregate = McAggregate::from_draws(&draws); + McFamily { draws, aggregate } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p aura-cli --bin aura blueprint_mc_family_seeds_differ` +Expected: PASS. (If the three draws come back identical, the walk did not warm the +stop — increase `SyntheticSpec.len`; 60 should suffice for slow=4 + stop-len 3.) + +--- + +### Task 3: `reproduce_family_in` MonteCarlo realization branch + +**Files:** +- Modify: `crates/aura-cli/src/main.rs` (`reproduce_family_in` ~2503-2574; the run at ~2561-2570) +- Test: `crates/aura-cli/src/main.rs` (beside `reproduce_family_re_derives_every_member_bit_identically` ~5194) + +- [ ] **Step 1: Write the failing test** + +```rust +#[test] +fn reproduce_family_re_derives_every_mc_member_bit_identically() { + let dir = std::env::temp_dir().join(format!("aura-repro-mc-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("temp dir"); + let reg = Registry::open(dir.join("runs.jsonl")); + + // a CLOSED signal (both SMA knobs bound) — MC binds no axis. + let closed = stage1_signal(Some(2), Some(4)); + let doc = blueprint_to_json(&closed).expect("serializes"); + let data = DataSource::Synthetic; + let family = blueprint_mc_family(&doc, 3, &data); + + // persist exactly as run_blueprint_mc does: store the blueprint, append the MC family. + let topo = family.draws[0].report.manifest.topology_hash.clone().expect("topo"); + let canonical = + blueprint_to_json(&blueprint_from_json(&doc, &|t| std_vocabulary(t)).unwrap()).unwrap(); + reg.put_blueprint(&topo, &canonical).expect("store blueprint"); + let id = reg + .append_family("mcrepro", FamilyKind::MonteCarlo, &mc_member_reports(&family)) + .expect("append"); + + // reproduce: every MC member re-derives bit-identically (its seed-driven walk is + // reconstructed from manifest.seed — the realization branch). + let rep = reproduce_family_in(®, &id, &data); + assert_eq!(rep.outcomes.len(), 3, "three MC members reproduced"); + assert!( + rep.outcomes.iter().all(|(_, ok)| *ok), + "every MC member re-derives bit-identically: {:?}", + rep.outcomes + ); + let _ = std::fs::remove_dir_all(&dir); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test -p aura-cli --bin aura reproduce_family_re_derives_every_mc_member_bit_identically` +Expected: FAIL — the assertion `every MC member re-derives bit-identically` fails: without +the branch, reproduce re-runs MC members over `data.run_sources()` (the showcase series), +not the seed-driven walk, so `rerun.metrics != stored.metrics` for every member. + +- [ ] **Step 3: Write minimal implementation** + +In `reproduce_family_in`, replace the single member run (currently ~2561-2570): + +```rust + let rerun = run_blueprint_member( + reload(), + &point, + &space, + data.run_sources(), + window, + stored.manifest.seed, + pip, + &hash, + ); +``` + +with a realization-aware version that reconstructs the member's data from the facet its +family kind recorded: + +```rust + // Realization-aware: a MonteCarlo member ran over a seed-driven synthetic walk, + // not the showcase — reconstruct it from manifest.seed so the re-run matches (C1). + let (sources, member_window, seed) = match family.kind { + FamilyKind::MonteCarlo => { + let s = synthetic_walk_sources(stored.manifest.seed); + let w = window_of(&s).expect("non-empty synthetic walk"); + (s, w, stored.manifest.seed) + } + _ => (data.run_sources(), window, stored.manifest.seed), + }; + let rerun = run_blueprint_member( + reload(), + &point, + &space, + sources, + member_window, + seed, + pip, + &hash, + ); +``` + +(`window` is the pre-loop `data.full_window()` at ~2515, used unchanged for the `_` arm; +`family.kind` is on the grouped `Family` the loop iterates.) + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cargo test -p aura-cli --bin aura reproduce_family_re_derives_every_mc_member_bit_identically` +Expected: PASS. + +- [ ] **Step 5: Verify the Sweep reproduce path stays green** + +Run: `cargo test -p aura-cli --bin aura reproduce_family_re_derives_every_member_bit_identically` +Expected: PASS (the `_` arm is byte-unchanged for Sweep members). + +--- + +### Task 4: `run_blueprint_mc` + the `["mc", ..]` dispatch arm + E2E + +**Files:** +- Modify: `crates/aura-cli/src/main.rs` (`run_blueprint_mc` beside `run_blueprint_sweep` ~3210; the + `["mc", rest @ ..]` dispatch arm ~4006-4015) +- Test: `crates/aura-cli/tests/cli_run.rs` (beside `aura_reproduce_re_derives_a_persisted_sweep_bit_identically` ~3456) + +- [ ] **Step 1: Write the failing E2E tests** + +Add to `crates/aura-cli/tests/cli_run.rs`: + +```rust +/// E2E (acc 1+3+4): `aura mc --seeds N` builds a MonteCarlo +/// family (one member per seed), writes exactly one content-addressed blueprint, and +/// `aura reproduce ` re-derives every member bit-identically (exit 0). +#[test] +fn aura_mc_over_a_blueprint_reproduces_bit_identically() { + let cwd = temp_cwd("mc-blueprint-reproduce"); + let fixture = format!("{}/tests/fixtures/stage1_signal.json", env!("CARGO_MANIFEST_DIR")); + + let mc = Command::new(BIN) + .args(["mc", &fixture, "--seeds", "4", "--name", "mcx"]) + .current_dir(&cwd) + .output() + .expect("spawn aura mc blueprint"); + assert!(mc.status.success(), "mc exit: {:?} stderr={}", mc.status, String::from_utf8_lossy(&mc.stderr)); + let mc_out = String::from_utf8(mc.stdout).expect("utf-8 stdout"); + // one member line per seed (each carries "seed":n) + one aggregate line. + assert_eq!( + mc_out.lines().filter(|l| l.contains("\"seed\":")).count(), + 4, + "one member line per seed: {mc_out}" + ); + assert!(mc_out.contains("mc_aggregate"), "prints the aggregate line: {mc_out}"); + + // exactly one blueprint stored for the whole family (one topology per family). + let store = cwd.join("runs/blueprints"); + let entries: Vec<_> = std::fs::read_dir(&store) + .unwrap_or_else(|e| panic!("mc must create the blueprint store {store:?}: {e}")) + .map(|e| e.expect("dir entry").path()) + .collect(); + assert_eq!(entries.len(), 1, "one stored topology per family: {entries:?}"); + + let repro = Command::new(BIN) + .args(["reproduce", "mcx-0"]) + .current_dir(&cwd) + .output() + .expect("spawn aura reproduce"); + assert!(repro.status.success(), "reproduce exit: {:?} stderr={}", repro.status, String::from_utf8_lossy(&repro.stderr)); + let repro_out = String::from_utf8(repro.stdout).expect("utf-8 stdout"); + assert!( + repro_out.contains("reproduced 4/4 members bit-identically"), + "every MC member re-derives: {repro_out}" + ); + let _ = std::fs::remove_dir_all(&cwd); +} + +/// E2E (acc 2): `aura mc ` is rejected with a named error + exit 2 +/// (NOT a panic / exit 101) — MC binds no axis, so a free knob has no binder. +#[test] +fn aura_mc_rejects_an_open_blueprint() { + let cwd = temp_cwd("mc-blueprint-open-reject"); + let fixture = format!("{}/tests/fixtures/stage1_signal_open.json", env!("CARGO_MANIFEST_DIR")); + let out = Command::new(BIN) + .args(["mc", &fixture, "--seeds", "4"]) + .current_dir(&cwd) + .output() + .expect("spawn aura mc open-blueprint"); + assert_eq!(out.status.code(), Some(2), "an open blueprint fails clean (exit 2, not a panic): stderr={}", String::from_utf8_lossy(&out.stderr)); + let stderr = String::from_utf8(out.stderr).expect("utf-8"); + assert!(stderr.contains("closed blueprint"), "names the closed-blueprint requirement: {stderr}"); + let _ = std::fs::remove_dir_all(&cwd); +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cargo test -p aura-cli --test cli_run aura_mc_over_a_blueprint_reproduces_bit_identically` +Expected: FAIL — `aura mc .json` currently falls into `parse_mc_args`, which does +not accept a `.json` first positional, so the command errors (no `.json` discriminator yet). + +- [ ] **Step 3: Write `run_blueprint_mc`** + +Add beside `run_blueprint_sweep` (~3210): + +```rust +/// `aura mc --seeds N`: build a Monte-Carlo family from a loaded CLOSED +/// blueprint (the World/C21 verb), store the canonical blueprint ONCE keyed by the shared +/// `topology_hash` (the 0094 hook, so `aura reproduce` re-derives it), record it as a +/// `FamilyKind::MonteCarlo` family (C18/C21 lineage), and print each draw's member line +/// (carrying the seed) plus the aggregate — mirroring `run_mc` / `run_blueprint_sweep`. +fn run_blueprint_mc(doc: &str, n_seeds: u64, name: &str, data: DataSource) { + let family = blueprint_mc_family(doc, n_seeds, &data); + let reg = default_registry(); + // Store the canonical blueprint ONCE, keyed by the family's shared topology_hash. + let topo = family.draws[0] + .report + .manifest + .topology_hash + .clone() + .expect("a blueprint mc stamps every member's topology_hash"); + let canonical = blueprint_to_json( + &blueprint_from_json(doc, &|t| std_vocabulary(t)) + .expect("doc parse-validated at the dispatch boundary"), + ) + .expect("a loaded blueprint re-serializes"); + reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| { + eprintln!("aura: {e}"); + std::process::exit(2); + }); + let id = match reg.append_family(name, FamilyKind::MonteCarlo, &mc_member_reports(&family)) { + Ok(id) => id, + Err(e) => { + eprintln!("aura: {e}"); + std::process::exit(2); + } + }; + for draw in &family.draws { + println!("{}", mc_member_line(&id, draw.seed, &draw.report)); + } + println!("{}", mc_aggregate_json(&family.aggregate)); +} +``` + +- [ ] **Step 4: Grow the `["mc", ..]` dispatch arm** + +Replace the current arm (~4006-4015): + +```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); + } + }, +``` + +with the `.json`-discriminator arm (mirroring the `["sweep", ..]` arm ~3952): + +```rust + ["mc", rest @ ..] => { + // A `.json`-file first argument selects the loaded-blueprint Monte-Carlo (the + // World/C21 verb), mirroring the `["sweep", ..]` discriminator; a bare + // `--strategy`/`--name` token never ends in `.json`, so the paths stay disjoint. + if let Some(path) = + rest.first().filter(|a| a.ends_with(".json") && std::path::Path::new(a).is_file()) + { + let doc = std::fs::read_to_string(path).unwrap_or_else(|e| { + eprintln!("aura: {path}: {e}"); + std::process::exit(2); + }); + if let Err(e) = blueprint_from_json(&doc, &|t| std_vocabulary(t)) { + eprintln!("aura: {path}: {e:?}"); + std::process::exit(2); + } + let McBlueprintArgs { n_seeds, name } = + parse_mc_blueprint_args(&rest[1..]).unwrap_or_else(|msg| { + eprintln!("aura: {msg}"); + std::process::exit(2); + }); + run_blueprint_mc(&doc, n_seeds, &name, DataSource::Synthetic); + return; + } + 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 5: Run the E2E tests to verify they pass** + +Run: `cargo test -p aura-cli --test cli_run aura_mc_over_a_blueprint_reproduces_bit_identically` +Expected: PASS. + +Run: `cargo test -p aura-cli --test cli_run aura_mc_rejects_an_open_blueprint` +Expected: PASS. + +- [ ] **Step 6: Verify the hard-wired `aura mc` path is undisturbed** + +Run: `cargo test -p aura-cli --test cli_run mc` +Expected: PASS (the hard-wired-`mc` E2Es still green — the `.json` discriminator keeps the +non-`.json` path falling through to `parse_mc_args`). + +--- + +### Task 5: Full-workspace gate + +**Files:** none (verification only) + +- [ ] **Step 1: Build the workspace** + +Run: `cargo build --workspace` +Expected: builds clean, 0 errors. + +- [ ] **Step 2: Run the full test suite** + +Run: `cargo test --workspace` +Expected: all suites pass (the 3 new units + 2 new E2Es green; no regressions). + +- [ ] **Step 3: Lint** + +Run: `cargo clippy --workspace --all-targets -- -D warnings` +Expected: 0 warnings.