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:
2026-07-07 19:25:13 +02:00
parent f7c809e5ce
commit 5856cadadd
5 changed files with 175 additions and 266 deletions
+62 -101
View File
@@ -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.