feat(cli): dissolve aura walkforward --strategy r-sma --real (#210 T3-T4)

The dispatch split lands: `dispatch_walkforward`'s built-in arm now routes
`--strategy r-sma --real` through the one campaign path (`run_walkforward_sugar`
-> generated campaign doc -> executor -> summary reprint), reproducing the
`{"walkforward":{…}}` line byte-for-byte. Everything else — synthetic (any
strategy) and non-r-sma real — stays on the inline `run_walkforward`, fenced
until #159 (Reading A). The committed anchor
`walkforward_real_e2e_pins_the_exact_current_grade` passes: the dissolved path
reproduces windows=9, stitched_total_pips, oos_r, and the param_stability means
exactly. `--select plateau:*` is refused (exit 2 -> #215, Fork B); a multi-value
stop is refused (Fork A).

Two plan guesses were wrong and the anchor + real-data e2e caught both; the fixes
are documented inline:

- Window resolution: the plan mirrored generalize's `(--from,--to) -> (f,t)`
  shortcut, but the inline `wf_full_span` ALWAYS clips `--from`/`--to` to the
  archive's actual first/last bar (`probe_window`), even when both flags are
  explicit — a holiday/weekend edge shifts every IS/OOS window's calendar
  placement (stitched_total_pips diverged by ~5.16M, param_stability means intact,
  confirming only window boundaries moved). The dissolved path now calls
  `source.full_window(env)` unconditionally, matching the inline behaviour;
  verified byte-for-byte against the still-fenced inline path.
- Reducer axis match: the campaign path records the blueprint axes wrapped
  (`sma_signal.fast.length`) while the stop rides unwrapped via the risk regime, so
  the reducer's exact-name match panicked on real data. Fixed with the existing
  `campaign_run::raw_matches_wrapped` (#203) wrap-convention helper.

Tests: the dissolution-proof e2e `walkforward_dissolves_through_the_campaign_path`
(one process + one campaign + one campaign-run auto-registered, windows=9) plus
front-end refusal coverage (multi-value stop, `--select plateau`, missing knobs,
empty `--real`, unknown `--select`, `--name`/`--trace` exclusivity). Full workspace
suite green; `clippy --all-targets -D warnings` clean.

refs #210
This commit is contained in:
2026-07-06 23:05:08 +02:00
parent a72cd6b115
commit a047af65fb
3 changed files with 373 additions and 10 deletions
+126 -7
View File
@@ -67,6 +67,12 @@ const WF_REAL_IS_NS: i64 = 90 * WF_DAY_NS;
const WF_REAL_OOS_NS: i64 = 30 * WF_DAY_NS; const WF_REAL_OOS_NS: i64 = 30 * WF_DAY_NS;
const WF_REAL_STEP_NS: i64 = 30 * WF_DAY_NS; const WF_REAL_STEP_NS: i64 = 30 * WF_DAY_NS;
/// The winner-selection objective for walk-forward's per-window IS refit — the
/// deflation-aware SQN variant (#144 default). Named once so the three call sites
/// (the inline r-sma roller, the inline blueprint roller, and the dissolved
/// campaign sugar's bridge) cannot drift apart on the token.
const WINNER_SELECTION_METRIC: &str = "sqn_normalized";
/// Fixed RNG seed for the trials-deflation reality-check bootstrap in walk-forward /// Fixed RNG seed for the trials-deflation reality-check bootstrap in walk-forward
/// winner selection. Recorded on each winner's manifest (so `overfit_probability` /// winner selection. Recorded on each winner's manifest (so `overfit_probability`
/// is reproducible by re-run); a CLI flag for it is a deferred refinement. The /// is reproducible by re-run); a CLI flag for it is a deferred refinement. The
@@ -1758,7 +1764,7 @@ fn walkforward_family(
let space = r_sma_space(); let space = r_sma_space();
walk_forward(roller, space, |w: WindowBounds| { walk_forward(roller, space, |w: WindowBounds| {
let (is_family, lattice) = r_sma_sweep_over(w.is.0, w.is.1, data, grid, env); let (is_family, lattice) = r_sma_sweep_over(w.is.0, w.is.1, data, grid, env);
let (best, selection) = match select_winner(&is_family, "sqn_normalized", select, lattice.as_deref()) { let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, lattice.as_deref()) {
Ok(v) => v, Ok(v) => v,
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); } Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
}; };
@@ -1882,16 +1888,18 @@ fn walkforward_summary_json(result: &WalkForwardResult) -> String {
/// `stitched_total_pips` = the per-window `total_pips` summed left-to-right in roll /// `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 /// 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 /// 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 /// r-sma IS-refit axis over the per-window chosen params (read from each report's
/// each report's prefix-stripped `manifest.params`) through the same /// `manifest.params` via [`campaign_run::raw_matches_wrapped`] — the blueprint axes
/// `MetricStats::from_values`; the `oos_r` block pools the per-window `trade_rs` /// `fast.length`/`slow.length` are recorded wrapped as `sma_signal.fast.length` etc.
/// by the strategy's own param space, while `stop_length`/`stop_k` ride the risk
/// regime unwrapped, so an exact-name match would miss the wrapped pair) through the
/// same `MetricStats::from_values`; the `oos_r` block pools the per-window `trade_rs`
/// through `r_metrics_from_rs`. Canonical JSON (C14). /// through `r_metrics_from_rs`. Canonical JSON (C14).
/// ///
/// The axis list + order mirror what `r_sma_space` yields (fast, slow, stop_length, /// 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 /// 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 /// order or axis would fail the committed exact-grade anchor loudly (it pins
/// `param_stability[0].mean` = the fast-MA refit mean), never silently. /// `param_stability[0].mean` = the fast-MA refit mean), never silently.
#[allow(dead_code)]
fn walkforward_summary_json_from_reports(reports: &[RunReport]) -> String { fn walkforward_summary_json_from_reports(reports: &[RunReport]) -> String {
let total: f64 = reports.iter().map(|r| r.metrics.total_pips).sum(); let total: f64 = reports.iter().map(|r| r.metrics.total_pips).sum();
// The four r-sma axis names, in `r_sma_space` order. Coercion to `f64` is // The four r-sma axis names, in `r_sma_space` order. Coercion to `f64` is
@@ -1910,7 +1918,7 @@ fn walkforward_summary_json_from_reports(reports: &[RunReport]) -> String {
.manifest .manifest
.params .params
.iter() .iter()
.find(|(name, _)| name == axis) .find(|(name, _)| campaign_run::raw_matches_wrapped(axis, name))
.expect("each r-sma walk-forward window records its chosen axis"); .expect("each r-sma walk-forward window records its chosen axis");
match v { match v {
Scalar::I64(i) => *i as f64, Scalar::I64(i) => *i as f64,
@@ -3141,7 +3149,7 @@ fn blueprint_walkforward_family(
walk_forward(roller, space.clone(), |w: WindowBounds| { walk_forward(roller, space.clone(), |w: WindowBounds| {
let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env) let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env)
.expect("axes validated in the dispatch-boundary pre-flight"); .expect("axes validated in the dispatch-boundary pre-flight");
let (best, selection) = match select_winner(&is_family, "sqn_normalized", select, Some(&lattice)) { let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) {
Ok(v) => v, Ok(v) => v,
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); } Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
}; };
@@ -4201,6 +4209,61 @@ fn run_args_from(a: &RunCmd) -> Result<RunArgs, String> {
Ok(RunArgs { harness, data, trace: a.trace.clone(), cost, slip_vol_mult, carry_per_cycle }) Ok(RunArgs { harness, data, trace: a.trace.clone(), cost, slip_vol_mult, carry_per_cycle })
} }
/// 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.
/// `--name`/`--trace` are mutually exclusive here too, matching the flag's own
/// documented contract and the inline path's `name_persist` refusal.
#[allow(clippy::type_complexity)]
fn walkforward_args_from(
a: &WalkforwardCmd,
) -> Result<(String, String, Vec<i64>, Vec<i64>, i64, f64, Option<i64>, Option<i64>), String> {
let symbol = match a.real.as_deref() {
None | Some("") => return Err("walkforward --strategy r-sma dissolves only over --real <SYMBOL>".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<i64> = parse_csv_list(a.fast.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
let slow: Vec<i64> = parse_csv_list(a.slow.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
let stop_length: Vec<i64> = parse_csv_list(a.stop_length.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
let stop_k: Vec<f64> = parse_csv_list(a.stop_k.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
if stop_length.len() != 1 || stop_k.len() != 1 {
return Err("walkforward: the stop is a single risk regime; --stop-length and --stop-k take one value each".to_string());
}
let name = match (a.name.as_deref(), a.trace.as_deref()) {
(Some(_), Some(_)) => {
return Err("walkforward: --name and --trace are mutually exclusive".to_string());
}
(Some(n), None) => n.to_string(),
(None, Some(t)) => t.to_string(),
(None, None) => "walkforward".to_string(),
};
Ok((name, symbol, fast, slow, stop_length[0], stop_k[0], a.from, a.to))
}
/// Convert `GeneralizeCmd` into the resolved argument shape the generalize sugar /// Convert `GeneralizeCmd` into the resolved argument shape the generalize sugar
/// consumes. The candidate is a single cell (clap already types `--fast`/etc. as /// consumes. The candidate is a single cell (clap already types `--fast`/etc. as
/// one `i64`/`f64`), all four knobs required, `--real` a `>=2`-distinct comma /// one `i64`/`f64`), all four knobs required, `--real` a `>=2`-distinct comma
@@ -4749,6 +4812,62 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
eprintln!("aura: {}", usage()); eprintln!("aura: {}", usage());
std::process::exit(2); 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 let Some(s) = a.select.as_deref()
&& parse_select(s).is_err()
{
eprintln!("aura: {}", usage());
std::process::exit(2);
}
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);
});
// Unlike `dispatch_generalize` (a single run per instrument, insensitive
// to a day's edge shift), 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 (never the
// literal ms request): 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 OOS pips 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_walkforward_sugar(
&fast, &slow, stop_length, stop_k, WINNER_SELECTION_METRIC,
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;
}
let strategy = strategy_from(a.strategy.as_deref(), &usage).unwrap_or_else(|m| { let strategy = strategy_from(a.strategy.as_deref(), &usage).unwrap_or_else(|m| {
eprintln!("aura: {m}"); eprintln!("aura: {m}");
std::process::exit(2); std::process::exit(2);
+51 -3
View File
@@ -352,7 +352,6 @@ pub(crate) fn run_generalize_sugar(
} }
/// The two generated documents of one dissolved `walkforward` invocation. /// The two generated documents of one dissolved `walkforward` invocation.
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct GeneratedWalkforward { pub(crate) struct GeneratedWalkforward {
pub process: ProcessDoc, pub process: ProcessDoc,
@@ -368,7 +367,6 @@ pub(crate) struct GeneratedWalkforward {
/// (ms). `blueprint_canonical` is the bare `sma_signal` blueprint already stored by /// (ms). `blueprint_canonical` is the bare `sma_signal` blueprint already stored by
/// topology hash; its content id is the strategy ref. /// topology hash; its content id is the strategy ref.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
pub(crate) fn translate_walkforward( pub(crate) fn translate_walkforward(
fast: &[i64], fast: &[i64],
slow: &[i64], slow: &[i64],
@@ -442,7 +440,6 @@ pub(crate) fn translate_walkforward(
/// Auto-register both generated walkforward documents (content-addressed, /// Auto-register both generated walkforward documents (content-addressed,
/// idempotent). Returns `(process_id, campaign_id)`. /// idempotent). Returns `(process_id, campaign_id)`.
#[allow(dead_code)]
pub(crate) fn register_generated_wf( pub(crate) fn register_generated_wf(
reg: &aura_registry::Registry, reg: &aura_registry::Registry,
generated: &GeneratedWalkforward, generated: &GeneratedWalkforward,
@@ -454,6 +451,57 @@ pub(crate) fn register_generated_wf(
Ok((process_id, campaign_id)) Ok((process_id, campaign_id))
} }
/// 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(&reg, &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(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+196
View File
@@ -3695,6 +3695,202 @@ fn walkforward_real_e2e_pins_the_exact_current_grade() {
assert_eq!(ps[1]["mean"].as_f64(), Some(17.333333333333332), "slow-MA refit mean: {grade_line}"); assert_eq!(ps[1]["mean"].as_f64(), Some(17.333333333333332), "slow-MA refit mean: {grade_line}");
} }
/// 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}");
}
/// `walkforward_args_from` requires all four grid knobs on the dissolved r-sma-real
/// path (unlike the inline path, which defaults an absent flag via `RGrid::default()`)
/// — a missing knob is a usage refusal (exit 2), not a silent default.
#[test]
fn walkforward_dissolved_refuses_missing_knobs() {
let cwd = temp_cwd("walkforward-missing-knobs");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", "--strategy", "r-sma", "--real", "GER40", "--fast", "3", "--slow", "12"])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "missing --stop-length/--stop-k is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("requires --fast --slow --stop-length --stop-k"), "missing-knob refusal: {stderr}");
}
/// An empty `--real ""` still satisfies clap's `Option::is_some()` dispatch guard, so
/// `walkforward_args_from`'s own empty-string check is what refuses it (exit 2) —
/// distinct from omitting `--real` entirely, which never enters the dissolved branch.
#[test]
fn walkforward_dissolved_refuses_an_empty_real_symbol() {
let cwd = temp_cwd("walkforward-empty-real");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", "--strategy", "r-sma", "--real", "",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "empty --real is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("dissolves only over --real"), "empty-real refusal: {stderr}");
}
/// Front-end parity: an unknown `--select` token is refused (exit 2) on the
/// dissolved path exactly as it is on the inline path — it must not be silently
/// swallowed by `select_is_plateau`'s plateau-only check and fall through to argmax.
#[test]
fn walkforward_dissolved_refuses_an_unknown_select_token() {
let cwd = temp_cwd("walkforward-bad-select");
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", "bogus",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "unknown --select token is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage: aura walkforward"), "unknown-select refusal: {stderr}");
}
/// `--trace`'s own help text ("mutually exclusive with --name") is a contract the
/// inline path already enforces via `name_persist`; the dissolved r-sma-real path
/// must refuse the same combination (exit 2) rather than silently letting --name
/// win over --trace.
#[test]
fn walkforward_dissolved_refuses_name_and_trace_together() {
let cwd = temp_cwd("walkforward-name-and-trace");
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",
"--name", "a", "--trace", "b",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "--name and --trace together is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("mutually exclusive"), "name/trace refusal: {stderr}");
}
/// Property (#210 T3, dispatch split): a successful `aura walkforward --strategy
/// r-sma --real` durably auto-registers exactly one generated process document, one
/// generated campaign document (carrying the `--name` handle and the stop as a
/// non-empty single risk regime), and one campaign-run record — mirroring the
/// generalize dissolution's audit trail. The persisted family set is exactly one
/// `Sweep` family (the pipeline's leading `std::sweep(argmax)` stage) plus one
/// `WalkForward` family (the per-window OOS reports) — the observable proof that
/// `dispatch_walkforward`'s r-sma-real branch now runs through the one campaign
/// executor rather than the deleted inline roller. Gated on the shared 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", "--slow", "12", "--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 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\":\"walkforward\""),
"the generated campaign document carries the --name handle: {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}"
);
}
/// Property: `aura generalize` is now thin sugar over the one campaign path — a /// Property: `aura generalize` is now thin sugar over the one campaign path — a
/// successful run durably auto-registers exactly one generated process document, /// successful run durably auto-registers exactly one generated process document,
/// one generated campaign document (carrying the `--name` handle and the stop as /// one generated campaign document (carrying the `--name` handle and the stop as