diff --git a/docs/plans/mc-dissolution.md b/docs/plans/mc-dissolution.md deleted file mode 100644 index d33b0a4..0000000 --- a/docs/plans/mc-dissolution.md +++ /dev/null @@ -1,664 +0,0 @@ -# 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. diff --git a/docs/specs/fieldtest-sweep-dissolution.md b/docs/specs/fieldtest-sweep-dissolution.md deleted file mode 100644 index 9164022..0000000 --- a/docs/specs/fieldtest-sweep-dissolution.md +++ /dev/null @@ -1,245 +0,0 @@ -# Fieldtest — cycle 0110 (sweep dissolution) — 2026-07-04 - -**Status:** Draft — awaiting orchestrator triage -**Author:** fieldtester (dispatched by fieldtest skill) -**Binary exercised:** `target/debug/aura` (workspace built from HEAD -`2c72996`; project cdylib `liblab.so` built from the scaffolded `lab/` crate). - -## Scope -Cycle 0110 is cycle 1 of the "verb dissolution" milestone (#210). Two -user-visible changes: (1) the `std::sweep` process block's selection triple -(`metric`+`select`+`deflate`) became an optional, all-or-nothing selection -group — omit it for a **selection-free sweep**, permitted only as the -pipeline's terminal stage; (2) `aura sweep --real SYM --from ---to --axis k=v,…` is now thin sugar over the campaign engine: it -auto-registers a generated selection-free process document + a campaign -document (content-addressed) and appends a campaign-run record, printing the -same member lines as before (member manifests now carry an `instrument` -stamp). This fieldtest exercises both, plus the two consumer paths the -selection-free sweep exists for (refusal edges, rank-later). - -## Examples -All fixtures under `fieldtests/cycle-0110-sweep-dissolution/`; the scaffolded -project is `lab/` (its `runs/` store is kept as evidence, `target/` removed). -Real data: GER40, Sep 2024 (`--from 1725148800000 --to 1727740799999`). - -### c0110_1_explore_sweep.json — selection-free single-stage process document -- A bare `{"block":"std::sweep"}` pipeline authored straight from - authoring-guide §2. -- Fits scope: exercises the new optional-selection-group surface at its - simplest (the omitted group). -- Outcome: `process validate` → `valid (intrinsic): 1 pipeline blocks`; - `process register` → id `78f2d8d7…`, re-register dedupes (one file); - stored canonical form is byte-exactly `…"pipeline":[{"block":"std::sweep"}]`. - `process introspect --block std::sweep` matches the guide verbatim - (metric/select/deflate all "optional"). **Ran, matched expected.** - -### c0110_2_smacross_open_op.json (+ built blueprint) — the dissolved verb -- Unbound SMA-crossover op-script → `graph build` → blueprint with three open - axes; `aura sweep --real GER40 --from … --to … --axis - graph.fast.length=2,4 --axis graph.slow.length=8,16 --axis - graph.bias.scale=0.5 --name c0110-explore`. -- Fits scope: this *is* the dissolved verb; a realistic ad-hoc explore-sweep. -- Outcome: prints 4 GER40-stamped member lines; auto-registers a generated - process (`9227c300`, name "sweep", selection-free) + campaign (`46e4f7ef`, - raw axis names, GER40, ms sub-range window) and appends one - `campaign_runs.jsonl` record; identical re-run **dedupes both documents** - (no new files, same ids) while appending a second realization (family - suffix `s0-0`→`s0-1`, metrics bit-identical — C1); `campaign runs` lists - both. **Ran, matched expected.** (`c0110_2_smacross_op.json`, the *bound* - variant, is kept as evidence for finding F3.) - -### c0110_3a_metric_only.json / c0110_3a_select_only.json / c0110_3b_* — refusal edges -- Half-populated selection group (metric-only, select-only); a selection-free - sweep followed by a `std::gate` stage, driven to the executor via - `c0110_3b_campaign_nonterminal.json`. -- Fits scope: the two new refusals (all-or-nothing group; terminal-only). -- Outcome: half-group → clean intrinsic refusal naming the missing slot; - non-terminal free sweep → passes intrinsic, refused at the executable tier - and at `campaign run` with clear prose, no realization appended. **Ran, - matched expected.** - -### (Task 4) rank-later over the persisted family — `aura runs family … rank …` -- No new fixture; consumes the `46e4f7ef-0-GER40-w0-s0-0` family persisted by - example 2. -- Fits scope: the rank-later pattern the selection-free sweep exists for. -- Outcome: `rank sqn` / `rank expectancy_r` return the 4 members best-first; - ranking respects metric directionality (max_drawdown ascending, sqn - descending); the rank roster equals the `rankable` metric roster - (`win_rate`/nonexistent refused with the known list). **Ran, matched - expected.** - -## Findings - -### [working] Selection-free process document round-trips cleanly -- Example 1. -- `process validate` → `process document valid (intrinsic): 1 pipeline - blocks, 0 gate predicates`; register id `78f2d8d7…`, idempotent on - re-register; stored form is the bare block; `introspect --block std::sweep` - output is byte-for-byte the guide's transcription. -- Working: the new optional group is reachable, authorable from the doc alone, - and the introspection self-description matches the ledger/guide. -- Action: carry-on. - -### [working] The dissolved sweep verb generates + dedupes a selection-free campaign -- Example 2. -- One invocation produced the generated process `9227c300` - (`{"name":"sweep","pipeline":[{"block":"std::sweep"}]}`), campaign - `46e4f7ef` (raw axes `fast.length`/`slow.length`/`bias.scale`, `instrument: - GER40`, window `[1725236099999,1727726220000]` — a ms sub-range of the - request), and one `campaign_runs.jsonl` line. Identical re-run: **no new - documents** (same content ids), a second realization (`run 1`, family suffix - `s0-1`), member metrics bit-identical. `campaign runs` lists both runs. -- Working: the sugar→campaign parity, content-addressed dedup, deterministic - accumulation (C1), and the additive `instrument` stamp all hold end to end. -- Action: carry-on. - -### [working] All-or-nothing + terminal-only refusals are clear and guide-matching -- Examples 3a/3b. -- metric-only → `block std::sweep: slot "metric" without "select" — the - selection group is all-or-nothing`; select-only symmetric; non-terminal free - sweep → `campaign is not executable: process stage 0: a sweep without a - selection group (metric + select) must be the last stage of its process` - (same prose at `campaign run`, no realization appended). -- Working: both new refusals fire at the right tier with comprehensible prose - that matches authoring-guide §2. -- Action: carry-on. - -### [working] Rank-later over the persisted family, best-first, roster-enforced -- Task 4. -- `rank sqn` orders 1.054 > 0.849 > 0.052 > −0.545; `rank max_drawdown` orders - 4.22M < 6.16M < 6.57M < 9.14M (ascending — lower drawdown ranked better); - `rank win_rate` → `unknown metric 'win_rate' (known: total_pips, - max_drawdown, bias_sign_flips, sqn, sqn_normalized, expectancy_r, - net_expectancy_r)`. -- Working: the selection-free sweep's reason to exist (defer selection to a - rank-later step) works, and the rank roster equals the documented `rankable` - set. (Direction convention noted as F-min below.) -- Action: carry-on. - -### [spec_gap] `aura sweep --axis` requires the `graph.`-wrapped name; the docs teach the raw form -- Example 2 (works with `graph.fast.length`) vs probe P2 (fails with - `fast.length`). -- `graph introspect --params` prints `fast.length` and the authoring guide - (§1) calls that "the raw param-space namespace (what a campaign's axes bind - against)". The stored campaign doc also uses the raw form. But the dissolved - `aura sweep --axis` accepts **only** the wrapped `graph.` form — - discoverable solely via `aura sweep --list-axes`, which the authoring - guide never mentions (the guide does not document `aura sweep` at all; the - glossary and `--help` say only `--axis name=`). -- Why spec_gap: the public interface documents the raw namespace as canonical - and is silent on the wrapper; a consumer following it picks the wrong form. - The wrapper leaks the root-composite instance name (`graph`) into the user's - axis vocabulary. -- Recommended action: document (glossary `sweep` + a note in authoring-guide) - that `aura sweep --axis` uses the `--list-axes` (wrapped) names, **or** make - the dispatch accept the raw `--params` names too. → tighten the design - ledger / docs (+ plan if the surface is to accept raw). - -### [friction] The wrapped-name refusal mangles the axis the user typed -- Probe P2: `aura sweep … --axis fast.length=2,4 --axis slow.length=8,16 - --axis bias.scale=0.5` → - `strategy 597d…: axis "length" is not in the param space` / - `axis "scale" is not in the param space` / - `open param "fast.length" is bound by no campaign axis` … -- The `#203` first-segment strip consumes the leading `fast`/`slow`/`bias` - segment, so the diagnostic names `length`/`scale` — fragments the user never - typed as a whole axis — while separately reporting the real params as - "bound by no axis". The two same-stripped legs (`fast.length`,`slow.length`) - even collapse to a single `length` key. The message never says "a leading - segment was stripped as the graph name". -- Why friction: the task can be completed (use the wrapped name), but the - refusal actively misdirects — it names a mangled fragment instead of echoing - the input and explaining the wrapper convention. (Same root cause as the - spec_gap above; split per the one-class-per-finding rule.) -- Recommended action: echo the typed axis and/or name the expected wrapper in - the refusal. → plan (tidy iteration). - -### [spec_gap] Authoring-guide §1 inline SMA-crossover binds `length`, contradicting every id / `--params` / §3 axis it prints -- Encountered building the sweep blueprint (Task 2); evidence - `c0110_2_smacross_op.json`. -- The guide's §1 worked op-script binds `fast.length=2` and `slow.length=4` - and claims (line 72) it matches the on-disk corpus - `mra_1_strategy_smacross.json`. It does not: the on-disk file leaves both - **unbound**. The bound inline form hashes to `e2be81b2…` with `--params` = - `bias.scale` only; the unbound corpus form hashes to `597d719b…` (the id the - guide later prints at §1/§3 and the id every campaign example references) - with `--params` = `fast.length`,`slow.length`,`bias.scale`. A reader copying - the inline example gets a blueprint whose id matches nothing in the guide and - whose `fast.length`/`slow.length` are closed — so authoring-guide §3's - campaign (axes `fast.length`,`slow.length`) would refuse against it. -- Why spec_gap: the guide is internally inconsistent, and the underlying rule — - that an op-script `bind` *pins a param out of the sweepable space* (not a - sweep-overridable default) — is nowhere stated. Note the 0110 audit commit - claimed "worked example verified against the live binary"; this §1 example - (authored earlier, #197/#208) was not. -- Recommended action: drop the `bind` from the §1 inline example (or state - that binding closes the axis) and document `bind`-vs-axis interaction. → - tighten docs. - -### [friction] `aura sweep ` rejects the op-script array with a raw serde error -- Probe: `aura sweep c0110_2_smacross_open_op.json --list-axes` → - `aura: …_op.json: Json(Error("invalid type: map, expected u32", line: 2, - column: 2))`. -- `graph introspect --content-id/--params/register` auto-discriminate the - op-script array vs the built envelope (guide §1), but `aura sweep`'s - blueprint slot accepts only the built envelope, and the mismatch surfaces as - a leaked serde `Json(Error(...))` rather than a house-style - "expected a built blueprint; run `aura graph build` first". The guide never - says `aura sweep` needs a pre-built blueprint. -- Why friction: the path completes once you build first, but the diagnostic is - an internal-type leak (contrast #162's Display-free-error convention seen - elsewhere) and the required build step is undocumented for `aura sweep`. -- Recommended action: shape-discriminate (as `graph introspect` does) or emit - a house-style hint. → plan. - -### [friction] Refused dissolved sweeps still register their generated campaign document (store litter, some malformed) -- Probes P2 (raw names) and P3 (subset axes) both **refused** at the - referential tier, yet each left a generated campaign doc in - `runs/campaigns/` (`92c66b18…` "c0110-rawname", `b0e3f90b…` "c0110-subset"). - `campaign_runs.jsonl` correctly stayed at 2 (no realization). The rawname - doc is *malformed*: its axes are the stripped/collapsed `{"length":[8,16], - "scale":[0.5]}` — a document the tool itself immediately rejects, now - permanent in the content-addressed store. -- Why friction: the sugar registers the generated process+campaign documents - *before* referential/executable validation, so a fat-fingered axis pollutes - the store with dead (occasionally malformed) documents. Content-addressing - bounds the growth (identical bad invocations dedupe), and no realization is - recorded, so nothing downstream breaks — hence friction, not bug. - (`campaign run ` registering before an executable refusal — the Task-3 - `d10ed054…` doc — is by contrast *documented* register-then-run behaviour and - is not flagged.) -- Recommended action: in the sweep sugar, validate the generated documents - (referential/executable) before `register_generated`, registering only on a - clean preflight. → plan. - -### [friction] `process`/`campaign` intrinsic validate needs the built project cdylib when inside a project -- Task 1 setup: inside the fresh `lab/` project, `process validate - c0110_1_explore_sweep.json` (and register/introspect) first failed with - `aura: cargo metadata failed: no cdylib target …`, then after `cargo build` - with `aura: project dylib not found … run cargo build`. The very same - `process validate` runs **dylib-free outside any project** (probe P1 → - `valid (intrinsic)`). -- Why friction: a std-only process document references no project nodes, so - the intrinsic (and referential-over-std) check needs no compiled dylib; the - CLI nonetheless loads the project env eagerly for any `process`/`campaign` - subcommand once an `Aura.toml` is found. A newcomer following the guide - inside a fresh project hits a build wall on a pure-shape check. (Likely - pre-existing, not introduced by 0110; the error is actionable.) -- Recommended action: let the intrinsic tier run before/without the dylib load - inside a project, or document that any in-project `process`/`campaign` call - requires a built cdylib. → plan or ratify (decide the intended coupling). - -## Recommendation summary -| Finding | Class | Action | -|---|---|---| -| Selection-free process doc round-trips | working | carry-on | -| Dissolved sweep → deduped selection-free campaign | working | carry-on | -| All-or-nothing + terminal-only refusals clear | working | carry-on | -| Rank-later best-first, roster-enforced | working | carry-on | -| `aura sweep --axis` needs wrapped name, docs teach raw | spec_gap | tighten docs (+ plan if raw to be accepted) | -| Wrapped-name refusal mangles the typed axis | friction | plan | -| Guide §1 inline binds `length`, inconsistent with its ids/axes | spec_gap | tighten docs | -| `aura sweep ` leaks a serde error | friction | plan | -| Refused sweeps still register their generated campaign doc | friction | plan | -| In-project intrinsic validate needs the built cdylib | friction | plan / ratify | diff --git a/docs/specs/mc-dissolution.md b/docs/specs/mc-dissolution.md deleted file mode 100644 index 4c4293d..0000000 --- a/docs/specs/mc-dissolution.md +++ /dev/null @@ -1,247 +0,0 @@ -# mc R-bootstrap dissolution — Design Spec - -**Date:** 2026-07-06 -**Status:** Draft — awaiting user spec review -**Authors:** orchestrator + Claude - -> Milestone #210, the FOURTH and LAST verb dissolution. Reference issue: Gitea -> Brummel/Aura #210. Mirrors the just-shipped walkforward dissolution; the only new -> load-bearing decision is the seed mapping (recorded as a #210 comment, -> 2026-07-06). Scope reading: decision 2 / Reading A (Fork C retention). - -## Goal - -Dissolve `aura mc --strategy r-sma --real`'s inline execution into thin sugar over -the one campaign executor (`aura_campaign::execute`), completing the verb set -(sweep, generalize, walkforward, mc). The `dispatch_mc` built-in arm gains an -r-sma-real branch that runs through a generated, content-addressed campaign -document with a `[std::sweep(argmax), std::walk_forward, std::monte_carlo]` process, -reproducing the `{"mc_r_bootstrap":{…}}` line byte-for-byte. The committed anchor -`mc_r_bootstrap_real_e2e_pins_the_exact_current_grade` (3fc491a) is the acceptance -gate. - -`run_mc_r_bootstrap`, `run_mc`, and their helpers are NOT deleted (Fork C, as -walkforward): the built-in arm splits, and everything the dissolution predicate -does not match stays inline, fenced until #159 — the synthetic-r-sma path -(`--strategy r-sma` without `--real`), the synthetic seed-resweep (`aura mc`), and -the blueprint mc path. - -## Architecture - -Pure sugar — NO executor change (`aura-campaign`/`aura-engine`/`aura-research` -diff-free). The campaign monte_carlo stage already does the identical work: -`crates/aura-campaign/src/exec.rs:400-446`, `StageBlock::MonteCarlo` after a -`std::walk_forward` stage produces `StageBootstrap::PooledOos(r_bootstrap( -&pooled_trade_rs(&fam.reports), resamples, block_len, seed))`, seeded from the -campaign seed, landing in `StageRealization.bootstrap` -(`crates/aura-registry/src/lineage.rs:130`). It is reachable in-memory at -`run.outcome.record.cells[0].stages[].bootstrap`. So the sugar reads that -`PooledOos(RBootstrap)` and renders the existing `mc_r_bootstrap_json(&RBootstrap)`. -mc prints ONLY the one summary line (no per-window member lines, unlike walkforward). - -The inline mc R-bootstrap is `run_mc_r_bootstrap` → `mc_r_bootstrap_report` → -`walkforward_family` → `pooled_oos_trade_rs` → `r_bootstrap` → `mc_r_bootstrap_json` -(main.rs ~2106-2133). The dissolution reroutes the same rolling walk-forward + -pooled-OOS bootstrap through the campaign path. - -**The seed mapping (the one new load-bearing detail).** The inline path uses TWO -seeds: a fixed `DEFLATION_SEED` for the per-window wf winner selection, and the mc -`--seed` for the terminal `r_bootstrap`. The campaign path has ONE `campaign.seed`, -used for both. This reconciles because the wf winners are **argmax** — the deflation -seed only annotates the recorded selection provenance, never changes the pick — so -the winners, and the pooled OOS series, are **seed-independent**. The shipped -walkforward anchor already proved this (the campaign path at `campaign.seed = 0` -reproduced the inline `DEFLATION_SEED` winners byte-for-byte). So `translate_mc` -sets `campaign.seed = the mc --seed`: the pooled series is unchanged (n_trades = -20681, matching the walkforward multi-grid anchor — mc pools the same series) and -the `r_bootstrap` at that seed reproduces the inline bootstrap. `block_len` / -`resamples` map to `StageBlock::MonteCarlo{resamples, block_len}`. - -Fork A (stop): a single `RiskRegime::Vol{stop_length, stop_k}` (multi-value stop -refused); fast/slow are the multi-value IS-refit grid. - -## Concrete code shapes - -### The user-facing program (unchanged surface, dissolved execution) - -```console -$ aura mc --strategy r-sma --real GER40 \ - --from 1735689600000 --to 1767225599000 \ - --fast 3,5 --slow 12,20 --stop-length 14 --stop-k 2.0 \ - --block-len 5 --resamples 1000 --seed 42 -{"mc_r_bootstrap":{"block_len":5,"e_r":{"mean":-0.0025753095301594307,…}, - "n_resamples":1000,"n_trades":20681,"prob_le_zero":0.558}} -``` - -Byte-identical to today's inline output (the acceptance gate). The audience — a -researcher stress-testing a candidate's E[R] distribution — runs the identical -command; the execution becomes a durable, content-addressed campaign document (C18, -reproducible-from-manifest). It reintroduces no look-ahead or determinism failure: -the same deterministic engine `r_bootstrap` over the same pooled OOS series (C1), -no new streamed non-scalar (invariant 4). All mc-specific logic stays in the CLI -translator/reader (invariant 10: no verb-special engine logic); the World runs it -through the one campaign path (invariant 12). - -### Sugar translator — mirror of `translate_walkforward` (verb_sugar.rs), + the monte_carlo stage - -```rust -// crates/aura-cli/src/verb_sugar.rs — new, mirror of translate_walkforward -pub(crate) struct GeneratedMc { pub process: ProcessDoc, pub campaign: CampaignDoc } - -#[allow(clippy::too_many_arguments)] // #214: bundle deferred until after this last verb -pub(crate) fn translate_mc( - fast: &[i64], slow: &[i64], - stop_length: i64, stop_k: f64, - metric: &str, // "sqn_normalized" (the wf objective) - in_sample_ms: u64, out_of_sample_ms: u64, step_ms: u64, - resamples: u32, block_len: u32, seed: u64, // seed = the mc --seed - 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 }, - ], - }; - // …doc_axes for fast.length / slow.length exactly as translate_walkforward… - let campaign = CampaignDoc { - // …instruments: [symbol], windows: [{from_ms,to_ms}], one StrategyEntry with the axes… - risk: vec![RiskRegime::Vol { length: stop_length, k: stop_k }], - seed, // <-- the mc --seed (the wf winners are seed-independent) - // …presentation: persist_taps [], emit ["family_table"]… - // (process/data/strategies fields identical in shape to translate_walkforward) - }; - Ok(GeneratedMc { process, campaign }) -} -``` - -`register_generated_mc` is the verbatim `register_generated_wf` shape (put_process + -put_campaign). - -### Sugar runner — read the bootstrap from the record, print one line - -```rust -// crates/aura-cli/src/verb_sugar.rs — new, mirror of run_walkforward_sugar -#[allow(clippy::too_many_arguments)] -pub(crate) fn run_mc_sugar(/* translate_mc args + env */) -> Result<(), String> { - let generated = translate_mc(/* … */)?; - let probe_params = 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 (_p, campaign_id) = register_generated_mc(®, &generated)?; - let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?; - - // The single cell's monte_carlo StageRealization carries the pooled-OOS 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(()) -} -``` - -### Dispatch split — mirror of the walkforward split; `run_mc_r_bootstrap`/`run_mc` STAY - -```rust -// crates/aura-cli/src/main.rs — dispatch_mc, the None (built-in) arm -None => { - // …existing usage / blueprint+seeds guards, unchanged… - if a.strategy.as_deref() == Some("r-sma") && a.real.is_some() { - // r-sma --real: the dissolved sugar path. - let (name, symbol, fast, slow, stop_length, stop_k, block_len, resamples, seed, from, to) = - mc_args_from(&a) /* unwrap_or_else: eprintln + exit 2 */; - let signal = sma_signal(None, None); - let canonical = blueprint_to_json(&signal).expect("a bare sma_signal serializes"); - let reg = env.registry(); - reg.put_blueprint(&topology_hash(&signal), &canonical) /* exit 1 on err */; - let (from_ms, to_ms) = resolve_window(&symbol, from, to, env); // full_window unconditionally - let (is_ms, oos_ms, step_ms) = wf_ms_sizes(); - verb_sugar::run_mc_sugar(&fast, &slow, stop_length, stop_k, "sqn_normalized", - is_ms, oos_ms, step_ms, resamples, block_len, seed, &name, &symbol, from_ms, to_ms, &canonical, env) - /* unwrap_or_else: eprintln + exit 1 */; - return; - } - // …EXISTING fenced body, verbatim: the r_path / McArgs::RealR / McArgs::Synthetic - // logic → run_mc_r_bootstrap / run_mc… -} -``` - -`mc_args_from` mirrors `walkforward_args_from` plus `block_len` (default 1), -`resamples` (default 1000), `seed` (default 1) — single-value stop (multi-value -refused, Fork A), fast/slow CSV grids, `--real` required. `run_mc_r_bootstrap`, -`run_mc`, `mc_r_bootstrap_report`, `mc_r_bootstrap_json`, `pooled_oos_trade_rs` are -all RETAINED (the fenced paths + `mc_r_bootstrap_json` is reused by the sugar). - -## Components - -- **`crates/aura-cli/src/verb_sugar.rs`** — `GeneratedMc`, `translate_mc`, - `register_generated_mc`, `run_mc_sugar`. Reuses `axis_from_values`, - `validate_before_register`, `run_campaign_returning`. -- **`crates/aura-cli/src/main.rs`** — `mc_args_from`, the `dispatch_mc` built-in-arm - split (add the r-sma-real sugar branch; retain the existing body). Reuses - `wf_ms_sizes`, `resolve_window` (the walkforward helper if it was extracted, else - the same inline match), `sma_signal`, `mc_r_bootstrap_json`. -- **No executor change.** - -## Data flow - -argv → `mc_args_from` → `dispatch_mc` synthesizes + stores the `sma_signal` -blueprint, resolves the window, ms roller sizes → `run_mc_sugar` → `translate_mc` -builds `[sweep(argmax), walk_forward, monte_carlo]` + a campaign (fast/slow axes, -one instrument, one Vol regime, seed = mc_seed) → `validate_before_register` → -`register_generated_mc` → `run_campaign_returning` (sweep → wf refit per window → -monte_carlo pools the OOS R + bootstraps at the campaign seed) → the sugar reads -`StageBootstrap::PooledOos(RBootstrap)` from the record's stages and prints -`mc_r_bootstrap_json`. - -## Error handling - -- Non-`r-sma`, missing knobs, empty `--real`: the fenced arm's / `mc_args_from`'s - refusals (exit 2), reusing message strings where shared. -- Multi-value `--stop-length`/`--stop-k`: exit 2 (Fork A), same message shape as - walkforward. -- Referential faults: `validate_before_register` refuses before any store write. -- Data refusal (no archive): the member runner's `no_real_data` path (exit 1). - -## Testing strategy - -- **The committed anchor** `mc_r_bootstrap_real_e2e_pins_the_exact_current_grade` - (3fc491a) is the equivalence gate: it must stay green through the dissolution - (block_len, n_resamples, n_trades, e_r.mean, e_r.p50, prob_le_zero all exact). -- **`translate_mc` unit tests** (mirror the walkforward translator tests): - deterministic content ids; the pipeline is `[sweep(argmax), walk_forward, - monte_carlo]` with the right roller/mc fields + seed; the regime carries the stop; - diverging seeds/stops do not collide content ids; `register_generated_mc` resolves. -- **Dissolution-proof e2e** (mirror `walkforward_dissolves_through_the_campaign_path`): - a successful real run auto-registers one process + one campaign + one campaign-run, - and the output is the single `mc_r_bootstrap` line. -- **Front-end refusal tests**: multi-value stop (exit 2), non-r-sma real handled by - the fenced path (still works). - -## Acceptance criteria - -1. `aura mc --strategy r-sma --real …` runs through the campaign path; the - anchor stays green (the `mc_r_bootstrap` line byte-identical). -2. `--strategy r-sma --real` no longer flows through `run_mc_r_bootstrap`; it runs - the sugar path. `run_mc_r_bootstrap` / `run_mc` are RETAINED for the fenced - branches. -3. The fenced branches are behaviour-unchanged (Reading A): synthetic-r-sma - (`--strategy r-sma` no `--real`), the synthetic seed-resweep (`aura mc`), and the - blueprint mc path all run inline exactly as before. -4. Multi-value `--stop-length`/`--stop-k` is refused (Fork A). -5. No executor change (`aura-campaign`/`aura-engine`/`aura-research` diff-free). -6. Full workspace suite green; `clippy --all-targets -D warnings` clean.