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:
@@ -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)");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user