# Walkforward Dissolution — Implementation Plan > **Parent spec:** `docs/specs/walkforward-dissolution.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run > this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Route `aura walkforward --strategy r-sma --real` through the one campaign executor as thin sugar (a generated, content-addressed campaign document), reproducing the `{"walkforward":{…}}` summary line byte-for-byte, while retaining the inline `run_walkforward` for the fenced synthetic / non-r-sma branches. **Architecture:** Pure sugar — NO executor change. `dispatch_walkforward`'s built-in arm splits on `--strategy r-sma && --real`; that branch synthesizes a bare `sma_signal` blueprint (exactly as generalize), builds a `[std::sweep(argmax), std::walk_forward]` campaign (multi-value fast/slow axes, one Vol risk regime carrying the stop), runs it via `run_campaign_returning`, and reprints the summary from the recorded `WalkForward` `StageFamily.reports`. The summary reducer reconstructs `stitched_total_pips = Σ per-window total_pips`, pooled `oos_r` from the reports' in-memory `trade_rs`, and `param_stability` via `r_sma_space` + `MetricStats::from_values` — all byte-verified against the committed anchor. **Tech Stack:** `crates/aura-cli/src/verb_sugar.rs` (translator + sugar runner), `crates/aura-cli/src/main.rs` (args parser, reducer, dispatch split), `crates/aura-cli/tests/cli_run.rs` (front-end + dissolution-proof tests). The committed anchor `walkforward_real_e2e_pins_the_exact_current_grade` is the gate. **Files this plan creates or modifies:** - Modify: `crates/aura-cli/src/verb_sugar.rs` — `GeneratedWalkforward`, `translate_walkforward`, `register_generated_wf`, `run_walkforward_sugar`; add `WfMode` to the `aura_research` import. - Modify: `crates/aura-cli/src/main.rs` — `walkforward_summary_json_from_reports` reducer, `walkforward_args_from`, `wf_ms_sizes`, `select_is_plateau`, the `dispatch_walkforward` built-in-arm split (retain the existing body). - Test: `crates/aura-cli/src/verb_sugar.rs` (mod tests) — translator determinism, pipeline shape, regime, diverging-stop content ids, registry resolution. - Test: `crates/aura-cli/src/main.rs` (mod tests) — reducer characterization. - Test: `crates/aura-cli/tests/cli_run.rs` — front-end refusals + dissolution-proof e2e; the committed anchor stays green. --- ## Task 1: `translate_walkforward` + `register_generated_wf` (verb_sugar.rs) **Files:** - Modify: `crates/aura-cli/src/verb_sugar.rs:13-17` (import), after `:352` (`run_generalize_sugar` end) - Test: `crates/aura-cli/src/verb_sugar.rs` (mod tests, after `:528`) - [ ] **Step 1: Add `WfMode` to the `aura_research` import** In the `use aura_research::{…}` block (lines 13-17), add `WfMode` (alphabetically near `Window`). The others (`StageBlock`, `SelectRule`, `RiskRegime`, `SweepSelection`, `Axis`, `CampaignDoc`, `ProcessDoc`, `DataSection`, `Window`, `StrategyEntry`, `DocRef`, `ProcessRef`, `Presentation`, `DocKind`, `FORMAT_VERSION`, `content_id_of`, `process_to_json`) are already present. Replace `RiskRegime, SelectRule, StageBlock,` (line 15) with `RiskRegime, SelectRule, StageBlock, WfMode,` — verify `WfMode` is a real re-export first: Run: `cargo build -p aura-cli 2>&1 | tail -3` Expected: clean (or an unused-import warning on `WfMode`, resolved by Step 3). - [ ] **Step 2: Add `GeneratedWalkforward` + `translate_walkforward` + `register_generated_wf`** Append after `run_generalize_sugar` (line 352), mirroring `GeneratedGeneralize`/`translate_generalize`/`register_generated_g`: ```rust /// The two generated documents of one dissolved `walkforward` invocation. #[derive(Debug)] pub(crate) struct GeneratedWalkforward { pub process: ProcessDoc, pub campaign: CampaignDoc, } /// Translate one `aura walkforward --strategy r-sma --real` invocation into its two /// generated documents: a `[std::sweep(argmax), std::walk_forward]` process (the /// sweep only enumerates the IS-refit survivor grid for the wf stage; its recorded /// argmax selection is not part of the summary) and a campaign running the fast/slow /// grid over one instrument under a single risk regime that carries the stop. The /// roller sizes (`in_sample_ms`/`out_of_sample_ms`/`step_ms`) come from the caller /// (ms). `blueprint_canonical` is the bare `sma_signal` blueprint already stored by /// topology hash; its content id is the strategy ref. #[allow(clippy::too_many_arguments)] pub(crate) fn translate_walkforward( fast: &[i64], slow: &[i64], stop_length: i64, stop_k: f64, metric: &str, in_sample_ms: u64, out_of_sample_ms: u64, step_ms: 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: "walkforward".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, }, ], }; 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: 0, presentation: Presentation { persist_taps: vec![], emit: vec!["family_table".to_string()], }, }; Ok(GeneratedWalkforward { process, campaign }) } /// Auto-register both generated walkforward documents (content-addressed, /// idempotent). Returns `(process_id, campaign_id)`. pub(crate) fn register_generated_wf( reg: &aura_registry::Registry, generated: &GeneratedWalkforward, ) -> 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)) } ``` Note: `campaign_to_json` is already imported (line 14, used by `register_generated_g`). - [ ] **Step 3: Build to confirm it compiles (dead-code allowed until Task 3 wires it)** `translate_walkforward`/`register_generated_wf`/`GeneratedWalkforward` have no caller yet. To keep `clippy -D warnings` green this task, add `#[allow(dead_code)]` directly above `pub(crate) struct GeneratedWalkforward`, above `pub(crate) fn translate_walkforward`, and above `pub(crate) fn register_generated_wf`. Task 3 removes all three once the dispatch wires them (mirrors the generalize cycle's dead-code bridge). Run: `cargo build -p aura-cli 2>&1 | tail -3` Expected: clean build (no errors). - [ ] **Step 4: Add translator unit tests** Append inside the `mod tests` block (after line 528, before the closing `}` at 529), mirroring `translate_generalize_is_deterministic_and_carries_the_regime` and `translate_generalize_diverging_regimes_do_not_collide_content_ids`: ```rust #[test] fn translate_walkforward_is_deterministic_and_carries_the_regime() { let a = translate_walkforward( &[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", 7_776_000_000, 2_592_000_000, 2_592_000_000, "walkforward", "GER40", 100, 200, "{\"bp\":1}", ) .unwrap(); let b = translate_walkforward( &[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", 7_776_000_000, 2_592_000_000, 2_592_000_000, "walkforward", "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) 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, }, ] ); 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); // multi-value IS-refit grid axes 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, "walkforward"); 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_walkforward_diverging_regimes_do_not_collide_content_ids() { let base = translate_walkforward( &[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", 7_776_000_000, 2_592_000_000, 2_592_000_000, "walkforward", "GER40", 100, 200, "{\"bp\":1}", ) .unwrap(); let different_k = translate_walkforward( &[3, 5], &[12, 20], 14, 3.0, "sqn_normalized", 7_776_000_000, 2_592_000_000, 2_592_000_000, "walkforward", "GER40", 100, 200, "{\"bp\":1}", ) .unwrap(); assert_ne!( content_id_of(&campaign_to_json(&base.campaign)), content_id_of(&campaign_to_json(&different_k.campaign)), "a different stop_k must not collide onto the same campaign id" ); } #[test] fn register_generated_wf_stores_documents_the_campaign_can_actually_resolve() { let dir = std::env::temp_dir().join(format!( "aura-verb-sugar-walkforward-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_walkforward( &[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", 7_776_000_000, 2_592_000_000, 2_592_000_000, "walkforward", "GER40", 100, 200, "{\"bp\":1}", ) .unwrap(); let (process_id, campaign_id) = register_generated_wf(®, &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!("walkforward'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 5: Run the translator tests** Run: `cargo test -p aura-cli --bin aura translate_walkforward 2>&1 | tail -8` Expected: PASS (`translate_walkforward_is_deterministic_and_carries_the_regime`, `translate_walkforward_diverging_regimes_do_not_collide_content_ids` run). Run: `cargo test -p aura-cli --bin aura register_generated_wf 2>&1 | tail -6` Expected: PASS (`register_generated_wf_stores_documents_the_campaign_can_actually_resolve`). --- ## Task 2: `walkforward_summary_json_from_reports` reducer (main.rs) **Files:** - Modify: `crates/aura-cli/src/main.rs` — new fn after `walkforward_summary_json` (`:1877`) - Test: `crates/aura-cli/src/main.rs` (mod tests) - [ ] **Step 1: Write the pure-sugar reducer** Add after `walkforward_summary_json` (ends `:1877`). It reconstructs the exact summary from the recorded per-window OOS reports, reusing the same reductions the inline path uses (`MetricStats::from_values` via `r_sma_space`, `r_metrics_from_rs`), so its output is byte-identical to `walkforward_summary_json`: ```rust /// The walk-forward summary line reconstructed from the recorded per-window OOS /// reports (the campaign path's `WalkForward` `StageFamily.reports`) rather than a /// live `WalkForwardResult`. Byte-identical to `walkforward_summary_json`: /// `stitched_total_pips` = the per-window `total_pips` summed left-to-right in roll /// order (the engine `stitch` folds each segment's final cumulative value, and a /// window's OOS segment ends at its `total_pips`); `param_stability` reduces each /// r-sma IS-refit axis over the per-window chosen params (read by exact name from /// each report's prefix-stripped `manifest.params`) through the same /// `MetricStats::from_values`; the `oos_r` block pools the per-window `trade_rs` /// through `r_metrics_from_rs`. Canonical JSON (C14). /// /// The axis list + order mirror what `r_sma_space` yields (fast, slow, stop_length, /// stop_k) — the same order + values the inline `param_stability` reduces. A wrong /// order or axis would fail the committed exact-grade anchor loudly (it pins /// `param_stability[0].mean` = the fast-MA refit mean), never silently. fn walkforward_summary_json_from_reports(reports: &[RunReport]) -> String { let total: f64 = reports.iter().map(|r| r.metrics.total_pips).sum(); // (name, is the axis an f64 knob?) — i64 axes coerce value-as-f64, the f64 axis // directly, exactly as `param_stability`'s per-`ScalarKind` coerce does. const AXES: [&str; 4] = ["fast.length", "slow.length", "stop_length", "stop_k"]; let param_stability: Vec = AXES .iter() .map(|axis| { let vals: Vec = reports .iter() .map(|r| { let (_, v) = r .manifest .params .iter() .find(|(name, _)| name == axis) .expect("each r-sma walk-forward window records its chosen axis"); match v { Scalar::I64(i) => *i as f64, Scalar::F64(f) => *f, Scalar::Bool(b) => *b as i64 as f64, Scalar::Timestamp(_) => { unreachable!("a timestamp is a structural axis, never a param knob") } } }) .collect(); MetricStats::from_values(&vals) }) .collect(); let pooled_rs: Vec = reports .iter() .flat_map(|r| r.metrics.r.as_ref().map(|m| m.trade_rs.clone()).unwrap_or_default()) .collect(); let mut obj = serde_json::json!({ "windows": reports.len(), "stitched_total_pips": total, "param_stability": param_stability, }); if reports.iter().any(|r| r.metrics.r.is_some()) { obj["oos_r"] = serde_json::to_value(r_metrics_from_rs(&pooled_rs)) .expect("RMetrics serializes"); } serde_json::json!({ "walkforward": obj }).to_string() } ``` Confirm `MetricStats` is in scope (imported from `aura_engine`; `param_stability` already returns `Vec`, so the type is reachable — if the bare name is not imported, qualify as `aura_engine::MetricStats`). `r_metrics_from_rs`, `r_sma_space`, `Scalar`, `RunReport` are already in scope. - [ ] **Step 2: Build** Run: `cargo build -p aura-cli 2>&1 | tail -3` Expected: clean (a dead-code warning on `walkforward_summary_json_from_reports` is acceptable this task — Task 3 wires it; add `#[allow(dead_code)]` above the fn to keep clippy green, removed in Task 3). - [ ] **Step 3: Write a characterization test for the reducer** Add to the `main.rs` `mod tests` block a hand-built two-window fixture and assert the exact reconstructed JSON fields. Build the reports with `total_pips` and `r.trade_rs` set, and `manifest.params` carrying the four r-sma slots (stripped names) so `param_stability` reconstructs: ```rust #[test] fn walkforward_summary_from_reports_reconstructs_the_summary() { // Two windows, chosen params (3,12,14,2.0) then (5,20,14,2.0). let mk = |fast: i64, slow: i64, pips: f64, rs: Vec| -> RunReport { let mut rep = RunReport { manifest: RunManifest { commit: "t".into(), params: vec![ ("fast.length".into(), Scalar::I64(fast)), ("slow.length".into(), Scalar::I64(slow)), ("stop_length".into(), Scalar::I64(14)), ("stop_k".into(), Scalar::F64(2.0)), ("bias_scale".into(), Scalar::F64(0.5)), ], window: (Timestamp(0), Timestamp(0)), seed: 0, broker: "t".into(), selection: None, instrument: None, topology_hash: None, project: None, }, metrics: summarize(&[], &[]), }; rep.metrics.total_pips = pips; rep.metrics.r = Some(r_metrics_from_rs(&rs)); rep }; let reports = vec![mk(3, 12, 10.0, vec![1.0, -0.5]), mk(5, 20, -4.0, vec![0.25])]; let line = walkforward_summary_json_from_reports(&reports); let v: serde_json::Value = serde_json::from_str(&line).unwrap(); let wf = &v["walkforward"]; assert_eq!(wf["windows"].as_u64(), Some(2)); assert_eq!(wf["stitched_total_pips"].as_f64(), Some(6.0)); // 10.0 + -4.0 let ps = wf["param_stability"].as_array().unwrap(); assert_eq!(ps.len(), 4, "four r-sma slots"); assert_eq!(ps[0]["mean"].as_f64(), Some(4.0)); // fast: (3+5)/2 assert_eq!(ps[1]["mean"].as_f64(), Some(16.0)); // slow: (12+20)/2 assert!(wf["oos_r"].is_object(), "pooled oos_r present when any window has r"); assert_eq!(wf["oos_r"]["n_trades"].as_u64(), Some(3)); // 2 + 1 } ``` Confirm `RunManifest`, `summarize`, `Timestamp` are importable in the test module (the `walkforward.rs` engine tests use exactly this `RunManifest{…}` shape — mirror their field set; if a field name differs, read `aura-engine/src/report.rs` and match it). - [ ] **Step 4: Run the reducer test** Run: `cargo test -p aura-cli --bin aura walkforward_summary_from_reports 2>&1 | tail -6` Expected: PASS. --- ## Task 3: `walkforward_args_from` + `wf_ms_sizes` + dispatch split (main.rs, verb_sugar.rs) **Files:** - Modify: `crates/aura-cli/src/main.rs` — `walkforward_args_from`, `wf_ms_sizes`, `select_is_plateau`, `dispatch_walkforward:4681-4714`; remove the Task-2 `#[allow(dead_code)]` on the reducer. - Modify: `crates/aura-cli/src/verb_sugar.rs` — `run_walkforward_sugar`; remove the Task-1 `#[allow(dead_code)]` bridges. - Test: `crates/aura-cli/tests/cli_run.rs` — front-end refusals. - [ ] **Step 1: Add `run_walkforward_sugar` to verb_sugar.rs (and drop the Task-1 dead-code allows)** Append after `register_generated_wf`, and delete the three `#[allow(dead_code)]` lines added in Task 1: ```rust /// Run one dissolved `walkforward` invocation end-to-end: register the generated /// documents, run through the one campaign path, then reprint the per-window member /// lines + the summary line from the recorded `WalkForward` family. The stdout /// summary line is byte-identical to the inline path (the committed exact-grade /// anchor is the gate). #[allow(clippy::too_many_arguments)] pub(crate) fn run_walkforward_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, name: &str, symbol: &str, from_ms: i64, to_ms: i64, blueprint_canonical: &str, env: &crate::project::Env, ) -> Result<(), String> { let generated = translate_walkforward( fast, slow, stop_length, stop_k, metric, in_sample_ms, out_of_sample_ms, step_ms, 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_wf(®, &generated)?; let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?; // The single cell's walk_forward StageFamily. let wf = run .outcome .cells .iter() .flat_map(|c| c.families.iter()) .find(|f| f.block == "std::walk_forward") .ok_or("walkforward produced no walk_forward family")?; for report in &wf.reports { println!("{}", crate::family_member_line(&wf.family_id, report)); } println!("{}", crate::walkforward_summary_json_from_reports(&wf.reports)); Ok(()) } ``` Also delete the `#[allow(dead_code)]` line above `walkforward_summary_json_from_reports` in main.rs (it now has a caller). Ordering note: after this step `run_walkforward_sugar` itself has no caller until Step 3 wires the dispatch split, and Steps 2-3 add the helpers it and the dispatch need. The workspace build + clippy gate is Step 4 (and Task 4), AFTER the wiring — do not run `clippy -D warnings` between Steps 1 and 3, where `run_walkforward_sugar` is transiently unused. - [ ] **Step 2: Add `wf_ms_sizes` + `select_is_plateau` + `walkforward_args_from` to main.rs** Add near `generalize_args_from` (`:4144-4183`). `wf_ms_sizes` converts the ns roller constants to ms; `select_is_plateau` detects the plateau modes via the existing `parse_select`; `walkforward_args_from` mirrors `generalize_args_from` but for a single instrument, multi-value fast/slow, single-value stop: ```rust /// The real walk-forward roller sizes in ms (the campaign wf stage works in ms: /// span `cell.window_ms`, sizes the `StageBlock` `_ms` fields). `WF_REAL_*_NS` are /// nanoseconds; divide by 1e6. The ms sizes over the doc window reproduce the same /// calendar windows the inline ns roller produces (anchor-gated). fn wf_ms_sizes() -> (u64, u64, u64) { ( (WF_REAL_IS_NS / 1_000_000) as u64, (WF_REAL_OOS_NS / 1_000_000) as u64, (WF_REAL_STEP_NS / 1_000_000) as u64, ) } /// True iff `--select` names a plateau mode (refused on the dissolved path, Fork B, /// until #215 ships the wf-stage plateau relaxation). Reuses `parse_select` so the /// vocabulary stays single-sourced. fn select_is_plateau(select: Option<&str>) -> bool { matches!(select.map(parse_select), Some(Ok(Selection::Plateau(_)))) } /// Convert `WalkforwardCmd` into the resolved argument shape the walkforward 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 knobs required. `parse_csv_list` /// (`:1586`) is generic over `T: FromStr`, so the same helper parses the i64 grids /// and the f64 stop; the local `knobs` closure (captureless → `Copy`, mirroring /// `generalize_args_from`'s idiom) is reused across the four `ok_or_else` calls. #[allow(clippy::type_complexity)] fn walkforward_args_from( a: &WalkforwardCmd, ) -> Result<(String, String, Vec, Vec, i64, f64, Option, Option), String> { let symbol = match a.real.as_deref() { None | Some("") => return Err("walkforward --strategy r-sma dissolves only over --real ".to_string()), Some(s) => s.to_string(), }; let knobs = || "walkforward 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("walkforward: the stop is a single risk regime; --stop-length and --stop-k take one value each".to_string()); } let name = a.name.clone().or_else(|| a.trace.clone()).unwrap_or_else(|| "walkforward".to_string()); Ok((name, symbol, fast, slow, stop_length[0], stop_k[0], a.from, a.to)) } ``` - [ ] **Step 3: Split the `dispatch_walkforward` built-in arm** In `dispatch_walkforward`'s `None` arm (`:4681-4714`), insert the dissolution split at the TOP of the arm body (after the existing `if a.blueprint.is_some() || !a.axis.is_empty()` guard, before `strategy_from`). Keep the entire existing body as the fenced `else`: ```rust None => { let usage = || "Usage: aura walkforward [--strategy ] [--real [--from ] [--to ]] [--name | --trace ] [--fast ] [--slow ] [--stop-length ] [--stop-k ] [--select ]".to_string(); if a.blueprint.is_some() || !a.axis.is_empty() { eprintln!("aura: {}", usage()); std::process::exit(2); } // Dissolution split (#210, Reading A): only the real-archive r-sma // execution routes through the campaign path. Everything else — synthetic // (any strategy) and non-r-sma real — stays on the inline handler below, // fenced until #159. if a.strategy.as_deref() == Some("r-sma") && a.real.is_some() { if select_is_plateau(a.select.as_deref()) { eprintln!("aura: --select plateau is not yet available on the campaign path; see #215"); std::process::exit(2); } let (name, symbol, fast, slow, stop_length, stop_k, from, to) = walkforward_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); }); let (from_ms, to_ms) = match (from, to) { (Some(f), Some(t)) => (f, t), _ => { 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); ( 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_walkforward_sugar( &fast, &slow, stop_length, stop_k, "sqn_normalized", is_ms, oos_ms, step_ms, &name, &symbol, from_ms, to_ms, &canonical, env, ) .unwrap_or_else(|m| { eprintln!("aura: {m}"); std::process::exit(1); }); return; } // …EXISTING fenced body, unchanged: strategy_from / RGrid parse / // parse_select / name_persist / data_choice_from / run_walkforward(...)… ``` Leave everything from `let strategy = strategy_from(...)` (`:4687`) to the end of the arm exactly as it is (the fenced path). The window-resolution match mirrors `dispatch_generalize:4375-4388` verbatim (confirm `DataChoice::Real`, `DataSource::from_choice`, `source.full_window`, `aura_ingest::epoch_ns_to_unix_ms` are the same symbols dispatch_generalize uses). - [ ] **Step 4: Build the workspace** Run: `cargo build --workspace 2>&1 | tail -5` Expected: clean build (0 errors). No dead-code warnings (all bridges removed, all new fns wired). - [ ] **Step 5: The committed anchor must stay green (the acceptance gate)** Run: `cargo test -p aura-cli --test cli_run walkforward_real_e2e_pins_the_exact_current_grade 2>&1 | tail -8` Expected: PASS — the dissolved `--strategy r-sma --real` path reproduces windows=9, stitched_total_pips=-10398606.666650848, oos_r, param_stability means byte-for-byte. (Skips clean only if the GER40 archive is absent; on this box it runs.) - [ ] **Step 6: Add front-end refusal tests** Append to `crates/aura-cli/tests/cli_run.rs`: ```rust /// Fork A: the dissolved walkforward binds the stop as a single risk regime, so a /// multi-value --stop-length/--stop-k is refused (exit 2) before any run. #[test] fn walkforward_dissolved_refuses_a_multi_value_stop() { let cwd = temp_cwd("walkforward-multi-stop"); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", "--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}"); } /// Fork B: --select plateau is refused on the dissolved path (exit 2) with a /// pointer to #215 until the wf-stage plateau relaxation ships. #[test] fn walkforward_dissolved_refuses_select_plateau() { let cwd = temp_cwd("walkforward-plateau"); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", "--strategy", "r-sma", "--real", "GER40", "--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0", "--select", "plateau:worst", ]) .current_dir(&cwd) .output() .expect("spawn aura"); assert_eq!(out.status.code(), Some(2), "plateau is refused on the dissolved path"); let stderr = String::from_utf8_lossy(&out.stderr); assert!(stderr.contains("#215"), "forward pointer to the plateau follow-up: {stderr}"); } ``` - [ ] **Step 7: Run the refusal tests** Run: `cargo test -p aura-cli --test cli_run walkforward_dissolved_refuses_a_multi_value_stop 2>&1 | tail -6` Expected: PASS. Run: `cargo test -p aura-cli --test cli_run walkforward_dissolved_refuses_select_plateau 2>&1 | tail -6` Expected: PASS. --- ## Task 4: dissolution-proof e2e (cli_run.rs) **Files:** - Test: `crates/aura-cli/tests/cli_run.rs` - [ ] **Step 1: Add the dissolution-proof e2e** Mirror `generalize_dissolves_through_the_campaign_path` (`:3706`): a successful real run auto-registers exactly one process + one campaign + one campaign-run record, and the walk_forward family is present. ```rust /// Property: `aura walkforward --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 stop as a single /// risk regime), and one campaign-run record, and persists a WalkForward family. /// The observable proof the r-sma-real path runs through the executor. Gated on the /// GER40 archive; skips cleanly on a data refusal. #[test] fn walkforward_dissolves_through_the_campaign_path() { const FROM_MS: &str = "1735689600000"; const TO_MS: &str = "1767225599000"; let cwd = temp_cwd("walkforward-dissolves"); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ "walkforward", "--strategy", "r-sma", "--real", "GER40", "--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0", "--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); 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 written"); assert_eq!(runs_log.lines().count(), 1, "exactly one campaign-run record"); let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); assert!(stdout.contains("\"walkforward\":"), "summary line present: {stdout}"); assert!(stdout.contains("\"windows\":9"), "nine windows: {stdout}"); } ``` Confirm the `runs/processes` + `runs/campaigns` + `runs/campaign_runs.jsonl` layout matches what `generalize_dissolves_through_the_campaign_path` asserts (read `:3706-3730` and mirror the exact directory/file names it uses). - [ ] **Step 2: Run the e2e** Run: `cargo test -p aura-cli --test cli_run walkforward_dissolves_through_the_campaign_path 2>&1 | tail -8` Expected: PASS (or clean skip on a data refusal). - [ ] **Step 3: Full suite + lint gate** Run: `cargo test --workspace 2>&1 | tail -15` Expected: all green (no regressions; the fenced synthetic/inline walkforward tests `walkforward_bare_sma_summary_has_no_oos_r` / `walkforward_strategy_r_sma_reports_oos_r` still pass unchanged). Run: `cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -8` Expected: clean (no dead-code, no too_many_arguments — the `#[allow]`s cover the new translator/runner).