refactor(cli): retire the r-breakout demo — topology now lives only as data (#159 cut 2)
Cut 2 of the hard-wired demo retirement, applying the r-sma template one level
down. The r-breakout signal leg was fused into r_breakout_graph (which also built
the whole pip/R harness inline), so this cut first carves the signal leg out as a
pure price->bias Composite, ships it as a proven blueprint example, then deletes
the fused builder and the built-in --strategy r-breakout surface. The r-breakout
topology now exists only as data (crates/aura-cli/examples/r_breakout{,_open}.json).
Carved: r_breakout_signal(channel) -> Composite, a verbatim lift of the fused
builder's signal leg (Delay/RollingMax/RollingMin/Gt/Latch/Sub) exposing the raw
exposure as "bias"; the generic wrap_r/run_signal_r path (unchanged) supplies the
pip/R harness. It is #[cfg(test)] — its only role is regenerating the examples and
pinning them to a faithful serialisation; production loads the shipped JSON, never
this builder (so no dead-code residue, no production topology constructor — #159's
invariant).
Proof: before the fused builder was deleted, an equivalence test proved the carved
signal, wrapped and run, reproduces the retiring r_breakout_graph grade byte-for-byte
on the synthetic stream at channel=3 with the shared default R-stop (metric
computation is identical on both paths). That gate is deleted with the builder it
references; the durable anchors that survive are the byte-identity of the shipped
examples to the carve, the loaded-example-grades-identically-to-the-carve proof, and
the closed-ness / open-axis introspect anchors (channel_hi.length, channel_lo.length
per-node — ganging is the separate #61).
Deleted: r_breakout_graph, r_breakout_sweep_family (aura sweep --strategy r-breakout),
Strategy::RBreakout + all four match arms, the r-breakout token from the --strategy
usage strings and docs, and the now-dead --channel flag + RGrid.channel field (its
sole reader was r_breakout_sweep_family). The data successors: aura run
examples/r_breakout.json, and aura sweep examples/r_breakout_open.json --axis
channel_hi.length --axis channel_lo.length.
Test surface: the three built-in-surface sweep tests drop; the walkforward
valid-but-unsupported-token test is re-pointed to momentum (the property is generic
and stays live until Cut 4); and a new negative pins that --strategy r-breakout on
both sweep and walkforward now falls into the generic usage error, not a
special-cased message — catching a half-finished retirement that left the token
parseable.
Verification (own, not the workflow's report): full `cargo test --workspace` green
(0 failed across every binary); `cargo clippy --workspace --all-targets -- -D warnings`
clean, no dead-code residue; the r-sma introspect + grade anchors and the
dissolved-verb real goldens stay green. The implement-loop ran both plan tasks in one
pass (task_range was not honoured); the tree was verified by hand — grep-confirmed the
retired surface is gone and the carve is #[cfg(test)]-scoped.
refs #159
This commit is contained in:
@@ -2107,101 +2107,6 @@ fn metrics_object(line: &str) -> &str {
|
||||
panic!("unbalanced metrics object: {line}")
|
||||
}
|
||||
|
||||
/// The breakout strategy reaches the CLI seam: `aura sweep --strategy r-breakout`
|
||||
/// emits an R-bearing member, and the folded no-trace path equals the raw --trace path
|
||||
/// byte-for-byte (parity with the r-sma fold-vs-raw guard). channel 3 warms up on
|
||||
/// the synthetic stream; one channel value × the single default stop = one member.
|
||||
#[test]
|
||||
fn sweep_strategy_r_breakout_folded_no_trace_metrics_equal_raw_trace_metrics() {
|
||||
let cwd = temp_cwd("sweep-r-breakout-fold-vs-raw");
|
||||
|
||||
let folded = Command::new(BIN)
|
||||
.args(["sweep", "--strategy", "r-breakout", "--channel", "3"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn folded (no-trace) r-breakout sweep");
|
||||
assert!(
|
||||
folded.status.success(),
|
||||
"folded no-trace breakout 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-breakout", "--channel", "3", "--trace", "b1"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn raw (--trace) r-breakout sweep");
|
||||
assert!(
|
||||
raw.status.success(),
|
||||
"raw --trace breakout 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 breakout 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 breakout member {i} must carry an r block: {fm}");
|
||||
assert_eq!(
|
||||
fm, rm,
|
||||
"folded vs raw breakout metrics diverge at member {i}\n folded: {fm}\n raw: {rm}"
|
||||
);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// Property: the breakout family is the cartesian product of its grid axes — a
|
||||
/// multi-value `--channel` list yields exactly one member per channel value, each
|
||||
/// member carrying its own bound channel length in the manifest params. Guards the
|
||||
/// manual cartesian-product loop in `r_breakout_sweep_family`: a regression that
|
||||
/// collapsed the loop (sweeping only the last value, or ganging the two channels)
|
||||
/// would silently drop members here. Channels 2 and 3 both warm up on the 18-bar
|
||||
/// synthetic stream, so two members must appear, in grid order, distinctly.
|
||||
#[test]
|
||||
fn sweep_strategy_r_breakout_grids_one_member_per_channel() {
|
||||
let cwd = temp_cwd("sweep-r-breakout-channel-grid");
|
||||
let out = Command::new(BIN)
|
||||
.args(["sweep", "--strategy", "r-breakout", "--channel", "2,3"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn 2-channel r-breakout sweep");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"channel-grid breakout 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-channel grid must print 2 members: {stdout:?}");
|
||||
// each member carries its own bound channel length (grid order: 2 then 3).
|
||||
assert!(
|
||||
lines[0].contains("[\"channel\",{\"I64\":2}]"),
|
||||
"first member must bind channel=2: {}",
|
||||
lines[0]
|
||||
);
|
||||
assert!(
|
||||
lines[1].contains("[\"channel\",{\"I64\":3}]"),
|
||||
"second member must bind channel=3: {}",
|
||||
lines[1]
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -2328,31 +2233,87 @@ fn walkforward_bare_sma_summary_has_no_oos_r() {
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// Property: a strategy that parses but has no walk-forward form yet (e.g.
|
||||
/// `r-breakout`) is rejected with exit 2 and a stderr message that names the
|
||||
/// Property: a strategy that parses but has no walk-forward form (e.g.
|
||||
/// `momentum`) is rejected with exit 2 and a stderr message that names the
|
||||
/// OFFENDING strategy by its CLI token — not silently run as SMA, not crashed.
|
||||
/// The dispatch's catch-all arm routes through `Strategy::cli_token()`, so the
|
||||
/// diagnostic must echo the token the user typed (`r-breakout`), proving the
|
||||
/// diagnostic must echo the token the user typed (`momentum`), proving the
|
||||
/// inverse map is wired. Distinct from a bad `--strategy bogus` (a usage parse
|
||||
/// error): here the token is a valid strategy with no walk-forward arm.
|
||||
#[test]
|
||||
fn walkforward_unsupported_strategy_exits_2_naming_the_token() {
|
||||
let cwd = temp_cwd("wf-unsupported");
|
||||
let out = Command::new(BIN)
|
||||
.args(["walkforward", "--strategy", "r-breakout"])
|
||||
.args(["walkforward", "--strategy", "momentum"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura walkforward --strategy r-breakout");
|
||||
.expect("spawn aura walkforward --strategy momentum");
|
||||
assert_eq!(out.status.code(), Some(2), "unsupported walkforward strategy must exit 2");
|
||||
let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr");
|
||||
assert!(
|
||||
stderr.contains("r-breakout"),
|
||||
stderr.contains("momentum"),
|
||||
"stderr must name the offending strategy token: {stderr:?}"
|
||||
);
|
||||
assert!(out.stdout.is_empty(), "no family summary on the rejected path: {:?}", out.stdout);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// Property (#159 cut 2): `--strategy r-breakout` is a fully retired CLI token, on
|
||||
/// BOTH `sweep` and `walkforward` — indistinguishable from an unrecognized token
|
||||
/// (`bogus`), i.e. it falls through `strategy_from`'s catch-all into the plain
|
||||
/// usage error, not into a "valid-but-unsupported" branch (contrast
|
||||
/// `walkforward_unsupported_strategy_exits_2_naming_the_token`, which pins that
|
||||
/// shape for `momentum`). A regression that left `Strategy::RBreakout` parseable
|
||||
/// while only deleting its family builder would make this test's stderr start
|
||||
/// naming "r-breakout" specially instead of reading the generic `Usage: aura
|
||||
/// <verb> …` line — this is the negative that catches a half-finished retirement.
|
||||
#[test]
|
||||
fn sweep_and_walkforward_reject_retired_r_breakout_token_as_unrecognized() {
|
||||
for (args, verb) in [
|
||||
(&["sweep", "--strategy", "r-breakout"][..], "sweep"),
|
||||
(&["walkforward", "--strategy", "r-breakout"][..], "walkforward"),
|
||||
] {
|
||||
let cwd = temp_cwd("retired-r-breakout-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),
|
||||
"`aura {args:?}` must fall into the generic usage error, not a special-cased \
|
||||
message: {stderr:?}"
|
||||
);
|
||||
assert!(out.stdout.is_empty(), "no family 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
|
||||
/// on `SweepCmd` while deleting only its plumbing would parse successfully here and
|
||||
/// this test would fail on the exit code.
|
||||
#[test]
|
||||
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"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura sweep --channel");
|
||||
assert_eq!(out.status.code(), Some(2), "--channel must be an unrecognized clap argument: {:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("--channel") || stderr.to_lowercase().contains("unexpected argument"),
|
||||
"clap must name the unknown flag: {stderr:?}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// Property: `aura generalize` grades a single r-sma candidate across two
|
||||
/// instruments — its stdout carries the per-instrument breakdown + the worst-case
|
||||
/// floor + the sign-agreement, then the persisted CrossInstrument family handle.
|
||||
|
||||
@@ -731,3 +731,28 @@ fn shipped_r_sma_open_example_lists_its_axis_namespace() {
|
||||
"the raw open params, in order, from the public gallery copy"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#159 cut 2): the shipped closed example (`examples/r_breakout.json`) is
|
||||
/// genuinely closed — `graph introspect --params` reports zero unbound params.
|
||||
#[test]
|
||||
fn shipped_r_breakout_example_is_genuinely_closed() {
|
||||
let dir = temp_cwd("r-breakout-example-closed-params");
|
||||
let (stdout, stderr, code) =
|
||||
run_in(&dir, &["graph", "introspect", "--params", &example("r_breakout.json")]);
|
||||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||||
assert_eq!(stdout, "", "the closed example must leave zero params unbound");
|
||||
}
|
||||
|
||||
/// Property (#159 cut 2): the shipped open example (`examples/r_breakout_open.json`)
|
||||
/// lists its two channel axes, in lowering order (per-node exposure; ganging is #61).
|
||||
#[test]
|
||||
fn shipped_r_breakout_open_example_lists_its_axis_namespace() {
|
||||
let dir = temp_cwd("r-breakout-example-open-params");
|
||||
let (stdout, stderr, code) =
|
||||
run_in(&dir, &["graph", "introspect", "--params", &example("r_breakout_open.json")]);
|
||||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||||
assert_eq!(
|
||||
stdout, "channel_hi.length:I64\nchannel_lo.length:I64\n",
|
||||
"the raw open params, in order, from the public gallery copy"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user