feat(stage1-r): operate the R layer from the CLI (iter 3)

`aura run --harness stage1-r [--real <SYM>] [--trace <n>]` now runs a Stage-1 R
backtest and prints its signal-quality metrics (the R block) in the RunReport
JSON - one shell call, the research loop's primary verb, ergonomic for Claude
to drive.

What shipped:
- vol_stop + risk_executor promoted from test fixtures to public aura-engine
  composite-builders, with a StopRule{Fixed,Vol} axis (C11). aura-std becomes a
  normal dependency of aura-engine (the composites are the engine's convenience
  layer over the standard nodes - the sibling tier of summarize_r; the graph
  stays acyclic: aura-engine -> aura-std -> aura-core). Both fixtures call the
  public fns; a Vol-backed RED test pins the new match arm.
- stage1_r_blueprint fans one bias into BOTH a SimBroker (pip) and the
  RiskExecutor (R), so one report carries both yardsticks honestly. run_stage1_r
  folds summarize (pip) + summarize_r (R, round_trip_cost 0.0 - Stage-1 is
  frictionless signal quality) and sets RunMetrics.r = Some(..). An r_equity tap
  (cum_realized_r + unrealized_r via LinComb) persists through a separate 3-tap
  persist_traces_r and renders via the existing `aura chart --tap r_equity`.
- `aura run --harness <sma|macd|stage1-r>`: a parse_run_args tokenizer replaces
  the five `run` literal-slice arms so --harness/--real/--from/--to/--trace
  compose in any order; --macd kept as a back-compat alias; --harness sma the
  default. A compile-time match over Rust-authored built-ins - not a runtime node
  registry, not a DSL (C9/C17): the CLI runs an authored harness, it does not
  wire one.

Back-compat: --harness sma/macd leave RunMetrics.r = None, so skip_serializing_if
keeps pip-only and legacy runs.jsonl JSON byte-unchanged; the plain-run tap list
stays ["equity","exposure"].

Refactor beyond the plan (verified behavior-equivalent): the old standalone
parse_real_args was orphaned by the new tokenizer, so it and its now-redundant
unit test were removed and run's --real parsing inlined into parse_run_args. The
--real strictness (empty-symbol, non-i64 ms, repeated-flag, window-requires-real
-> exit 2) is preserved and now pinned by
parse_run_args_rejects_malformed_real_and_repeated_flags (added to restore the
deleted coverage). All five run_real integration tests stay green.

Verification: cargo build/test --workspace green (every crate, 0 failed); clippy
--workspace --all-targets -D warnings clean. The full diff was adversarially
reviewed (run-arg refactor equivalence against the removed function, harness
wiring, C1/C2/C9/C17, the serde back-compat pin) - verdict SOUND; the one flagged
coverage gap is closed here.

Follow-on (noted, not done): the stage1-r manifest reuses the
"sim-optimal(pip_size=...)" broker label; a dedicated "risk-executor" label would
read more honestly for a dual-tap harness that runs a RiskExecutor branch.

closes #129
refs #117 #128
This commit is contained in:
2026-06-24 12:09:58 +02:00
parent 13086b8590
commit a6fc48daa7
7 changed files with 638 additions and 144 deletions
+111
View File
@@ -1451,3 +1451,114 @@ fn chart_family_serves_member_count_and_spanning_window_in_meta() {
let _ = std::fs::remove_dir_all(&cwd);
}
// --- `aura run --harness <name>` selector (iter-3 Task 3) --------------------
/// Property: `aura run --harness stage1-r` runs the R-scored harness — its stdout
/// `RunReport` carries the nested `r` block (the R yardstick), and that block carries
/// the `sqn` field. Pins the selector wiring stage1-r → `run_stage1_r` at the
/// built-binary boundary; a miswiring to the pip-only SMA arm would drop the `r` key.
#[test]
fn run_harness_stage1_r_prints_an_r_block() {
let out = std::process::Command::new(BIN).args(["run", "--harness", "stage1-r"]).output().unwrap();
assert!(out.status.success());
let s = String::from_utf8(out.stdout).unwrap();
assert!(s.contains("\"r\":{"), "stage1-r run must carry an r block: {s}");
assert!(s.contains("\"sqn\""), "the r block must carry sqn: {s}");
}
/// Property: `aura run --harness sma` is the pip-only default — its `RunReport`
/// OMITS the `r` key entirely (the `skip_serializing_if` keeps a pip run byte-clean).
/// The companion of the stage1-r test: it proves the selector switches the surface,
/// not that every run sprouts an `r` block.
#[test]
fn run_harness_sma_is_pip_only_no_r_block() {
let out = std::process::Command::new(BIN).args(["run", "--harness", "sma"]).output().unwrap();
assert!(out.status.success());
let s = String::from_utf8(out.stdout).unwrap();
assert!(!s.contains("\"r\":"), "a pip run must omit the r key: {s}");
}
/// Property: an unknown `--harness` name is a usage error at the binary boundary —
/// exit 2 (never a silent default run), the `Err` arm wiring through to `exit(2)`.
#[test]
fn run_unknown_harness_exits_two() {
let out = std::process::Command::new(BIN).args(["run", "--harness", "nope"]).output().unwrap();
assert_eq!(out.status.code(), Some(2));
}
/// Property: `--trace` on the stage1-r harness persists the THIRD `r_equity` tap
/// beside equity/exposure, and that tap round-trips through `aura chart --tap
/// r_equity` into a rendered series. Pins the full author→persist→chart loop for the
/// R-equity series end to end (the pip harnesses write only the two taps).
#[test]
fn stage1_r_trace_persists_r_equity_and_charts_it() {
// `temp_cwd` (cli_run.rs:13) gives a unique CWD with no external tempfile dep — the
// pattern the existing trace tests use; it returns a `PathBuf`, so `.join` directly.
let dir = temp_cwd("stage1-r-trace");
let run = Command::new(BIN)
.current_dir(&dir)
.args(["run", "--harness", "stage1-r", "--trace", "q1"])
.output()
.unwrap();
assert!(run.status.success());
// the third tap is persisted beside equity/exposure
assert!(dir.join("runs/traces/q1/r_equity.json").exists(), "r_equity tap must persist");
let chart = Command::new(BIN)
.current_dir(&dir)
.args(["chart", "q1", "--tap", "r_equity"])
.output()
.unwrap();
assert!(chart.status.success());
let html = String::from_utf8(chart.stdout).unwrap();
assert!(!html.is_empty() && html.contains("r_equity"), "chart must render the r_equity series");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (the spec's dual-yardstick headline): `aura run --harness stage1-r` scores
/// ONE signal in BOTH yardsticks at once — the single emitted `RunReport` carries the pip
/// `metrics.total_pips` (off the SimBroker branch) AND the nested `r` block (off the
/// RiskExecutor branch), because one bias is fanned into both. The shipped selector test
/// pins only that the `r` block appears; this pins that the pip branch SURVIVES alongside
/// it. A regression that swapped the pip branch out for the R branch (dropping the dual
/// tap) would still print an `r` block — and pass that test — while silently losing the
/// pip yardstick; this fails it. Asserts on the structural co-presence of both keys, never
/// the volatile metric floats, so it stays deterministic.
#[test]
fn run_harness_stage1_r_carries_both_pip_and_r_yardsticks() {
let out = std::process::Command::new(BIN)
.args(["run", "--harness", "stage1-r"])
.output()
.expect("spawn aura run --harness stage1-r");
assert!(out.status.success(), "exit: {:?}", out.status);
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
// exactly one report line.
assert_eq!(s.lines().count(), 1, "stdout was: {s:?}");
// the pip yardstick (SimBroker branch) is still present in the same report...
assert!(s.contains("\"total_pips\":"), "stage1-r must keep the pip yardstick: {s}");
// ...and the R yardstick (RiskExecutor branch) is folded in alongside it.
assert!(s.contains("\"r\":{"), "stage1-r must carry the R yardstick too: {s}");
}
/// Property (C1, the foundational determinism invariant at the new harness boundary):
/// `aura run --harness stage1-r` is byte-deterministic — running it twice over the
/// fixed synthetic stream yields BYTE-IDENTICAL stdout (modulo the build-constant git
/// commit, which is equal within one build). The whole engine rests on a backtest being
/// reproducible (C1); the sibling sweep/walkforward paths each have a determinism E2E,
/// but the brand-new dual-tap stage1-r run-loop (the SMA→Bias fan into SimBroker + the
/// RiskExecutor + the summarize_r fold) had none. A non-determinism in the new R branch
/// (e.g. an unstable channel drain order or an allocator-ordered fold) would surface as a
/// drift here. Compares the full stdout body, so it pins the metric floats too.
#[test]
fn run_harness_stage1_r_is_byte_deterministic_across_runs() {
let run = || {
let out = std::process::Command::new(BIN)
.args(["run", "--harness", "stage1-r"])
.output()
.expect("spawn aura run --harness stage1-r");
assert!(out.status.success(), "exit: {:?}", out.status);
String::from_utf8(out.stdout).expect("utf-8 stdout")
};
assert_eq!(run(), run(), "the stage1-r run must be byte-identical across runs (C1)");
}