feat(std,cli): add the Scale node, retire the r-meanrev demo to data (#159 cut 3)
Cut 3 of the hard-wired demo retirement. Unlike r-sma/r-breakout, r-meanrev could
not round-trip as data: its band computes band_k*sigma (a constant times a stream)
and the closed 22-node std_vocabulary had no way to express it (EqConst is i64->bool,
Bias is clamp(signal/scale,±1), Mul is binary, LinComb is excluded as a
construction-arg node; no Div/Const/constant-emitter). Standard operators are missing
by chance, not by design, so this adds the one r-meanrev needs.
Scale node (aura-std): out = input * factor — one f64 input, one f64 `factor` param,
stateless, warm-up-filtered (the EqConst/Bias shape). A zero-input `Const` emitter was
rejected: the per-cycle eval loop gates every node through an input-firing test
(harness.rs `fires()`), so a node with no input never fires without an engine-core
change; `Scale` fits the existing model and, by IEEE-754 commutativity, reproduces the
retiring LinComb's band byte-for-byte. Rostered (`"Scale" => Scale`); the two
vocabulary count-pins bump 22 -> 23.
r-meanrev migration: r_meanrev_signal(window, band_k) is carved out of the fused
r_meanrev_graph as a #[cfg(test)] price->bias Composite whose band is `Scale` in place
of `LinComb(1)`; production loads the shipped JSON (examples/r_meanrev{,_open}.json),
never a builder. Before the fused builder was deleted, an equivalence test proved the
carved Scale-band signal reproduces its grade byte-for-byte on the synthetic stream
(window=3, band_k=2). Durable survivors: the byte-identity of the examples to the
carve, the loaded-grades-identically-to-carve proof, the closed/open introspect
anchors (mean_window.length, var_window.length, band.factor — band_k is now a
first-class sweepable param), and — re-pointed onto the carve rather than dropped —
the fades-short-above/long-below behavioural test, which becomes the durable
carve-correctness gate the deleted equivalence test used to be.
Deletions + the dead-code cascade the retirement opened: r_meanrev_graph,
r_meanrev_sweep_family, Strategy::RMeanRev (+ all arms). r-meanrev was the last reader
of run_sweep's grid, so the whole vestigial built-in-sweep grid apparatus retires with
it: run_sweep's `grid` param, the sweep call-site grid build, and SweepCmd's
--fast/--slow/--stop-length/--stop-k/--window/--band-k (sma/momentum sweeps build their
own blueprints and ignored these; the fast/slow/stop-* had been vestigial since r-sma's
cut 1b). RGrid the struct SURVIVES — the dissolved `generalize` verb resolves its
candidate from its fast/slow/stop_length/stop_k via generalize_args_from — so only its
window/band_k fields go; its stale doc-comment (r-sma-sweep / persist_traces_r / CLI
flags, all now gone) is rewritten to its true generalize-only role. Also retired as
transitively dead: persist_traces_r and the metrics_object test helper (their only
callers were the deleted families/tests); Add/Gt/Latch/Mul/Sqrt imports are now
#[cfg(test)] (their last production caller was r_meanrev_graph).
Test surface: the two built-in --strategy r-meanrev sweep tests drop; a new negative
pins that --strategy r-meanrev on both sweep and walkforward now falls into the generic
usage error; the grid-flag stray-positional negative is re-pointed to a surviving flag.
Verification (own, not the workflow's report): full `cargo test --workspace` green
(0 failed across 62 result lines); `cargo clippy --workspace --all-targets -- -D
warnings` clean, no dead-code residue; the r-sma + r-breakout anchors and the
dissolved-verb real goldens (generalize still reads RGrid) stay green. Bundled as one
commit (the Scale node's count-pin and the r-meanrev anchors share graph_construct.rs,
so a clean two-commit split was not path-separable); the implement-loop ran all three
plan tasks in one pass and the tree was verified by hand.
refs #159
This commit is contained in:
@@ -2080,126 +2080,6 @@ fn run_unknown_harness_exits_two() {
|
||||
assert_eq!(out.status.code(), Some(2));
|
||||
}
|
||||
|
||||
/// Extract the balanced `"metrics":{...}` object substring from one stdout member
|
||||
/// line. The `family_id` prefix legitimately differs between a no-trace sweep
|
||||
/// (`sweep-0`) and a `--trace n` sweep (`n-0`), so an equivalence check must
|
||||
/// compare only the `metrics` block (the reduced/raw output under test), not the
|
||||
/// whole line. Brace-balanced scan, not a JSON parser (no serde dep in this test
|
||||
/// crate); the member lines are flat enough for this to be exact.
|
||||
fn metrics_object(line: &str) -> &str {
|
||||
let key = "\"metrics\":";
|
||||
let start = line.find(key).expect("member line has a metrics block") + key.len();
|
||||
let bytes = line.as_bytes();
|
||||
assert_eq!(bytes[start], b'{', "metrics value must be an object: {line}");
|
||||
let mut depth = 0usize;
|
||||
for (i, &b) in bytes[start..].iter().enumerate() {
|
||||
match b {
|
||||
b'{' => depth += 1,
|
||||
b'}' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
return &line[start..start + i + 1];
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
panic!("unbalanced metrics object: {line}")
|
||||
}
|
||||
|
||||
/// The mean-reversion strategy reaches the CLI seam: `aura sweep --strategy
|
||||
/// r-meanrev` emits an R-bearing member, and the folded no-trace path equals
|
||||
/// the raw --trace path byte-for-byte (parity with the r-sma / breakout
|
||||
/// fold-vs-raw guard). window 3 warms up on the synthetic stream; one window ×
|
||||
/// one band_k × the single default stop = one member.
|
||||
#[test]
|
||||
fn sweep_strategy_r_meanrev_folded_no_trace_metrics_equal_raw_trace_metrics() {
|
||||
let cwd = temp_cwd("sweep-r-meanrev-fold-vs-raw");
|
||||
|
||||
let folded = Command::new(BIN)
|
||||
.args(["sweep", "--strategy", "r-meanrev", "--window", "3"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn folded (no-trace) r-meanrev sweep");
|
||||
assert!(
|
||||
folded.status.success(),
|
||||
"folded no-trace meanrev sweep exit: {:?}; stderr: {}",
|
||||
folded.status,
|
||||
String::from_utf8_lossy(&folded.stderr)
|
||||
);
|
||||
let folded_out = String::from_utf8(folded.stdout).expect("utf-8");
|
||||
|
||||
let raw = Command::new(BIN)
|
||||
.args(["sweep", "--strategy", "r-meanrev", "--window", "3", "--trace", "m1"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn raw (--trace) r-meanrev sweep");
|
||||
assert!(
|
||||
raw.status.success(),
|
||||
"raw --trace meanrev sweep exit: {:?}; stderr: {}",
|
||||
raw.status,
|
||||
String::from_utf8_lossy(&raw.stderr)
|
||||
);
|
||||
let raw_out = String::from_utf8(raw.stdout).expect("utf-8");
|
||||
|
||||
let folded_lines: Vec<&str> = folded_out.lines().collect();
|
||||
let raw_lines: Vec<&str> = raw_out.lines().collect();
|
||||
assert_eq!(folded_lines.len(), 1, "folded meanrev sweep must print 1 member: {folded_out:?}");
|
||||
assert_eq!(
|
||||
raw_lines.len(),
|
||||
folded_lines.len(),
|
||||
"member count must match: folded {} vs raw {}",
|
||||
folded_lines.len(),
|
||||
raw_lines.len()
|
||||
);
|
||||
|
||||
for (i, (f, r)) in folded_lines.iter().zip(raw_lines.iter()).enumerate() {
|
||||
let fm = metrics_object(f);
|
||||
let rm = metrics_object(r);
|
||||
assert!(fm.contains("\"r\":{"), "folded meanrev member {i} must carry an r block: {fm}");
|
||||
assert_eq!(
|
||||
fm, rm,
|
||||
"folded vs raw meanrev metrics diverge at member {i}\n folded: {fm}\n raw: {rm}"
|
||||
);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// Property: the meanrev family is the cartesian product of its grid axes — a
|
||||
/// multi-value `--window` list yields exactly one member per window value, each
|
||||
/// carrying its own bound window length in the manifest params. Guards the manual
|
||||
/// cartesian loop in `r_meanrev_sweep_family`. windows 3 and 4 both warm up
|
||||
/// on the 18-bar synthetic stream, so two members must appear, in grid order.
|
||||
#[test]
|
||||
fn sweep_strategy_r_meanrev_grids_one_member_per_window() {
|
||||
let cwd = temp_cwd("sweep-r-meanrev-window-grid");
|
||||
let out = Command::new(BIN)
|
||||
.args(["sweep", "--strategy", "r-meanrev", "--window", "3,4"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn 2-window r-meanrev sweep");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"window-grid meanrev sweep exit: {:?}; stderr: {}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8");
|
||||
let lines: Vec<&str> = stdout.lines().collect();
|
||||
assert_eq!(lines.len(), 2, "2-window grid must print 2 members: {stdout:?}");
|
||||
assert!(
|
||||
lines[0].contains("[\"window\",{\"I64\":3}]"),
|
||||
"first member must bind window=3: {}",
|
||||
lines[0]
|
||||
);
|
||||
assert!(
|
||||
lines[1].contains("[\"window\",{\"I64\":4}]"),
|
||||
"second member must bind window=4: {}",
|
||||
lines[1]
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// Property: the bare `aura walkforward` (default SMA-cross) summary is preserved
|
||||
/// byte-for-byte by the new `--strategy`/grid plumbing — it still carries exactly
|
||||
/// `windows`, `stitched_total_pips`, `param_stability` and NOT the r-sma-only
|
||||
@@ -2292,6 +2172,26 @@ fn sweep_and_walkforward_reject_retired_r_breakout_token_as_unrecognized() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Property (#159 cut 3): `--strategy r-meanrev` is a fully retired token on both
|
||||
/// sweep and walkforward — the generic usage error, no special-casing.
|
||||
#[test]
|
||||
fn sweep_and_walkforward_reject_retired_r_meanrev_token_as_unrecognized() {
|
||||
for (args, verb) in [
|
||||
(&["sweep", "--strategy", "r-meanrev"][..], "sweep"),
|
||||
(&["walkforward", "--strategy", "r-meanrev"][..], "walkforward"),
|
||||
] {
|
||||
let cwd = temp_cwd("retired-r-meanrev-token");
|
||||
let out = Command::new(BIN).args(args).current_dir(&cwd).output()
|
||||
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
|
||||
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(stderr.starts_with(&want), "generic usage, not special-cased: {stderr:?}");
|
||||
assert!(out.stdout.is_empty(), "no summary on the rejected path: {:?}", out.stdout);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
}
|
||||
|
||||
/// Property (#159 cut 2): `--channel` is genuinely removed from the sweep grammar,
|
||||
/// not merely unused — clap rejects it as a structurally unknown argument (exit 2)
|
||||
/// rather than silently accepting and dropping it. A regression that left the flag
|
||||
@@ -2301,7 +2201,7 @@ fn sweep_and_walkforward_reject_retired_r_breakout_token_as_unrecognized() {
|
||||
fn sweep_channel_flag_is_removed_from_the_grammar() {
|
||||
let cwd = temp_cwd("sweep-channel-flag-retired");
|
||||
let out = Command::new(BIN)
|
||||
.args(["sweep", "--strategy", "r-meanrev", "--channel", "3"])
|
||||
.args(["sweep", "--strategy", "sma", "--channel", "3"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura sweep --channel");
|
||||
@@ -3925,7 +3825,7 @@ fn dual_grammar_stray_positional_is_a_usage_error_not_swallowed() {
|
||||
// is_file() discriminator sends every case down the built-in branch.
|
||||
for argv in [
|
||||
&["run", "bogus"][..],
|
||||
&["sweep", "bogus", "--fast", "3"][..],
|
||||
&["sweep", "bogus", "--name", "x"][..],
|
||||
&["walkforward", "bogus", "--strategy", "sma"][..],
|
||||
&["mc", "bogus"][..],
|
||||
] {
|
||||
@@ -4254,3 +4154,37 @@ fn shipped_r_sma_example_reproduces_the_builtin_grade() {
|
||||
assert!(stdout.contains("\"total_pips\":0.34185000000002036"), "{stdout}");
|
||||
assert!(stdout.contains("\"n_trades\":3"), "{stdout}");
|
||||
}
|
||||
|
||||
/// `aura run` on the shipped closed r-meanrev example (#159 cut 3) actually loads
|
||||
/// and grades through the real, separately-linked `aura` binary — not merely
|
||||
/// in-process, as `r_meanrev_example_loaded_runs_identically_to_the_carved_signal`
|
||||
/// (main.rs) and this test's own grade-equivalence check both do by calling
|
||||
/// `run_signal_r`/`blueprint_from_json` inside the *test* binary. This is the
|
||||
/// CLI-seam companion `shipped_r_sma_example_reproduces_the_builtin_grade` has for
|
||||
/// r-sma: it pins that the `Scale`-based band, freshly added to the closed
|
||||
/// vocabulary this iteration, resolves and runs cleanly through the production
|
||||
/// vocabulary lookup + arg-parsing path a real invocation takes, not just through
|
||||
/// the test harness's direct function calls. The synthetic stream never crosses
|
||||
/// the band at this window/k, so the grade is genuinely all-zero (no trade) —
|
||||
/// still a deterministic, meaningful pin: a regression that broke `Scale`'s CLI
|
||||
/// wiring (e.g. a roster/registration mismatch) would surface as a non-zero exit
|
||||
/// or a load error here, not as a metrics drift.
|
||||
#[test]
|
||||
fn shipped_r_meanrev_example_runs_end_to_end_via_aura_run() {
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "examples/r_meanrev.json"])
|
||||
.output()
|
||||
.expect("spawn aura run example");
|
||||
assert_eq!(
|
||||
out.status.code(),
|
||||
Some(0),
|
||||
"exit: {:?} stderr={}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8");
|
||||
assert!(stdout.contains("\"topology_hash\":\""), "manifest carries the topology hash: {stdout}");
|
||||
assert!(stdout.contains("\"expectancy_r\":0.0"), "{stdout}");
|
||||
assert!(stdout.contains("\"n_trades\":0"), "{stdout}");
|
||||
assert!(stdout.contains("\"total_pips\":0.0"), "{stdout}");
|
||||
}
|
||||
|
||||
@@ -121,8 +121,8 @@ fn graph_introspect_vocabulary_lists_exactly_the_closed_roster_count() {
|
||||
let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
|
||||
assert_eq!(
|
||||
lines.len(),
|
||||
22,
|
||||
"the std-only (no project) vocabulary has exactly the roster's 22 entries: {stdout}"
|
||||
23,
|
||||
"the std-only (no project) vocabulary has exactly the roster's 23 entries: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -756,3 +756,28 @@ fn shipped_r_breakout_open_example_lists_its_axis_namespace() {
|
||||
"the raw open params, in order, from the public gallery copy"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#159 cut 3): the shipped closed example (`examples/r_meanrev.json`) is
|
||||
/// genuinely closed — `graph introspect --params` reports zero unbound params.
|
||||
#[test]
|
||||
fn shipped_r_meanrev_example_is_genuinely_closed() {
|
||||
let dir = temp_cwd("r-meanrev-example-closed-params");
|
||||
let (stdout, stderr, code) =
|
||||
run_in(&dir, &["graph", "introspect", "--params", &example("r_meanrev.json")]);
|
||||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||||
assert_eq!(stdout, "", "the closed example must leave zero params unbound");
|
||||
}
|
||||
|
||||
/// Property (#159 cut 3): the shipped open example (`examples/r_meanrev_open.json`)
|
||||
/// lists its raw axis namespace, in lowering (node-add) order.
|
||||
#[test]
|
||||
fn shipped_r_meanrev_open_example_lists_its_axis_namespace() {
|
||||
let dir = temp_cwd("r-meanrev-example-open-params");
|
||||
let (stdout, stderr, code) =
|
||||
run_in(&dir, &["graph", "introspect", "--params", &example("r_meanrev_open.json")]);
|
||||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||||
assert_eq!(
|
||||
stdout, "mean_window.length:I64\nvar_window.length:I64\nband.factor:F64\n",
|
||||
"the raw open params, in lowering order"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user