plan: 0066 r-sweep-rank — R sweep families + rank-by-SQN

Parent spec docs/specs/0066-r-sweep-rank.md (#133). Four tasks: golden characterization of the stage1-r single run (byte-preservation guard); extract stage1_r_graph helper (float fast/slow for the sweep, single run binds Some(2)/Some(4)); metric_cmp learns sqn/expectancy_r/net_expectancy_r (None sorts worst); stage1_r_sweep_family + --strategy stage1-r + rank-by-sqn integration test.

refs #133
This commit is contained in:
2026-06-24 15:30:53 +02:00
parent 1c8678a6ad
commit d430dbb885
+542
View File
@@ -0,0 +1,542 @@
# R-sweep families + rank-by-SQN — Implementation Plan
> **Parent spec:** `docs/specs/0066-r-sweep-rank.md`
>
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
> this plan. Steps use `- [ ]` checkboxes for tracking.
**Goal:** Make SQN the single-number objective for ranking a Stage-1 sweep family
by R signal quality: `aura sweep --strategy stage1-r` produces an R-bearing family,
and `aura runs family <id> rank sqn` (also `expectancy_r`, `net_expectancy_r`) ranks
it best-first.
**Architecture:** Two independent crates. `aura-registry``metric_cmp` learns
three higher-is-better R metrics that reach into `RunReport.metrics.r`, with a
`None` member sorting to the worst position. `aura-cli``enum Strategy` gains
`Stage1R`; the stage1-r topology is extracted into one helper so the single run
keeps its identical binding (byte-output unchanged, verified by a new golden) while
the sweep floats `fast.length`/`slow.length` over a 4-point grid, folding `r:
Some(..)` per member. The family store, its persistence, and the `runs family …
rank …` routing are unchanged — they already carry full `RunReport`s and route any
metric string through `metric_cmp`.
**Tech Stack:** `aura-registry/src/lib.rs` (Metric/metric_cmp/Display + tests),
`aura-cli/src/main.rs` (Strategy/parse_sweep_args/run_sweep/stage1_r_graph/
stage1_r_sweep_family + unit tests), `aura-cli/tests/cli_run.rs` (golden +
integration test), read-only fixture source `aura-engine/src/report.rs`.
**Files this plan creates or modifies:**
- Modify: `crates/aura-cli/tests/cli_run.rs` — new golden char. test + new sweep+rank integration test
- Modify: `crates/aura-cli/src/main.rs``stage1_r_graph` helper, `stage1_r_sweep_family`, `Strategy::Stage1R`, parse + dispatch, usage strings, unit test
- Modify: `crates/aura-registry/src/lib.rs``Metric` variants, `metric_cmp` arms, `UnknownMetric` Display, R-metric tests
- Read-only: `crates/aura-engine/src/report.rs``RMetrics` struct literal (fixture source)
**Task order (dependencies):** Task 1 (golden, GREEN on current HEAD) → Task 2
(helper extraction; golden + existing stage1-r tests stay GREEN) → Task 3
(registry metric_cmp, self-contained RED→GREEN) → Task 4 (sweep family + CLI
wiring, RED→GREEN; needs Task 2's helper and Task 3's rank-by-sqn).
---
### Task 1: Golden characterization test for the stage1-r single run
Pins the EXACT current `metrics` output so Task 2's refactor is *verified*
byte-preserving (the grounding-check found no such golden exists). Captured against
current HEAD → GREEN immediately; Task 2 must keep it GREEN.
**Files:**
- Modify: `crates/aura-cli/tests/cli_run.rs` (append a test near the stage1-r tests ~1600)
- [ ] **Step 1: Capture the current metrics literal**
Run: `cargo run -q -p aura-cli -- run --harness stage1-r`
This prints one JSON line `{"manifest":{...},"metrics":{...}}`. Copy the substring
from `"metrics":` to the end of the line (the trailing `}` included) — this is the
golden literal for Step 2. Example shape (DO NOT use these numbers — paste the real
captured bytes):
`"metrics":{"total_pips":<f>,"max_drawdown":<f>,"bias_sign_flips":<n>,"r":{"expectancy_r":<f>,...,"conviction_terciles_r":[<f>,<f>,<f>]}}`
- [ ] **Step 2: Add the golden test, pasting the captured literal**
```rust
/// Golden characterization (cycle 0066): pins the EXACT metric output of the
/// stage1-r single run, so the `stage1_r_graph` helper extraction (Task 2) is
/// VERIFIED byte-preserving, not assumed. Captured against HEAD before the
/// refactor; the refactor must keep it identical (a drift is a behaviour change →
/// debug). Commit-agnostic: pins only the `metrics` object, since the manifest
/// carries the volatile build commit.
#[test]
fn stage1_r_single_run_output_golden() {
let out = Command::new(BIN).args(["run", "--harness", "stage1-r"]).output().unwrap();
assert!(out.status.success(), "exit: {:?}", out.status);
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
let key = "\"metrics\":";
let start = s.find(key).expect("stdout has a metrics object");
let metrics = s[start..].trim_end();
assert_eq!(
metrics,
r#"PASTE_THE_CAPTURED_METRICS_LITERAL_FROM_STEP_1"#,
"stage1-r single-run metric output drifted from the golden: {s}"
);
}
```
- [ ] **Step 3: Run the golden against current HEAD — must already pass**
Run: `cargo test -p aura-cli --test cli_run stage1_r_single_run_output_golden`
Expected: PASS (it characterizes the unmodified code). If it FAILS here, the pasted
literal is wrong — re-capture from Step 1.
---
### Task 2: Extract `stage1_r_graph` helper; float fast/slow; single run binds Some(2)/Some(4)
Behaviour-preserving refactor. One wiring source; the single-run binding is
structurally identical, so the Task-1 golden + the existing stage1-r tests stay
GREEN.
**Files:**
- Modify: `crates/aura-cli/src/main.rs:1737-1788` (rename `stage1_r_blueprint``stage1_r_graph`, add the two `Option<i64>` params)
- Modify: `crates/aura-cli/src/main.rs:1806-1808` (`run_stage1_r` calls the helper with `Some(2)`/`Some(4)`)
- [ ] **Step 1: Replace `stage1_r_blueprint` with `stage1_r_graph`**
Replace the whole `fn stage1_r_blueprint(...) -> Composite { ... }` (1737-1788)
with the helper below. Only the `fast`/`slow` builder lines change (made
conditional on the `Option`); every other line is verbatim from the original body.
```rust
/// The Stage-1 R harness topology, shared by the single run and the sweep. The two
/// signal knobs are bound when `Some` (single run — identical to the old
/// build-time bind, output byte-unchanged) and left free when `None` (sweep — they
/// land in `param_space` as `fast.length` / `slow.length`). Four taps: equity
/// (SimBroker), exposure (Bias), the 14-column R-record (→ summarize_r), and
/// r_equity = cum_realized_r + unrealized_r.
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
fn stage1_r_graph(
tx_eq: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
tx_ex: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
tx_r: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
tx_req: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
fast_len: Option<i64>,
slow_len: Option<i64>,
) -> Composite {
let mut g = GraphBuilder::new("stage1_r");
// SMA-cross signal → Bias (the same signal the pip sample uses).
let mut fast_b = Sma::builder().named("fast");
if let Some(l) = fast_len {
fast_b = fast_b.bind("length", Scalar::i64(l));
}
let fast = g.add(fast_b);
let mut slow_b = Sma::builder().named("slow");
if let Some(l) = slow_len {
slow_b = slow_b.bind("length", Scalar::i64(l));
}
let slow = g.add(slow_b);
let spread = g.add(Sub::builder());
let exposure = g.add(Bias::builder().named("bias").bind("scale", Scalar::f64(0.5)));
// pip branch (verbatim from sample_blueprint_with_sinks).
let broker = g.add(SimBroker::builder(SYNTHETIC_PIP_SIZE));
let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq));
let ex = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex));
// R branch: bias + price → RiskExecutor(vol_stop) → dense R-record.
let exec = g.add(risk_executor(
StopRule::Vol { length: STAGE1_R_STOP_LENGTH, k: STAGE1_R_STOP_K },
1.0,
));
let rrec = g.add(Recorder::builder(PM_RECORD_KINDS.to_vec(), Firing::Any, tx_r));
// r_equity = cum_realized_r + unrealized_r — one tapped series for charting.
let r_equity = g.add(
LinComb::builder(2)
.bind("weights[0]", Scalar::f64(1.0))
.bind("weights[1]", Scalar::f64(1.0)),
);
let req = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_req));
let price = g.source_role("price", ScalarKind::F64);
g.feed(
price,
[fast.input("series"), slow.input("series"), broker.input("price"), exec.input("price")],
);
g.connect(fast.output("value"), spread.input("lhs"));
g.connect(slow.output("value"), spread.input("rhs"));
g.connect(spread.output("value"), exposure.input("signal"));
// bias fans to: broker (pip), exposure sink, and the RiskExecutor (R).
g.connect(exposure.output("bias"), broker.input("exposure"));
g.connect(exposure.output("bias"), ex.input("col[0]"));
g.connect(exposure.output("bias"), exec.input("bias"));
g.connect(broker.output("equity"), eq.input("col[0]"));
// tap the dense R-record (all PM fields) for summarize_r.
for (i, field) in PM_FIELD_NAMES.iter().enumerate() {
g.connect(exec.output(field), rrec.input(COL_PORTS[i].as_str()));
}
// r_equity sum + tap.
g.connect(exec.output("cum_realized_r"), r_equity.input("term[0]"));
g.connect(exec.output("unrealized_r"), r_equity.input("term[1]"));
g.connect(r_equity.output("value"), req.input("col[0]"));
g.build().expect("stage1_r wiring resolves")
}
```
- [ ] **Step 2: Update `run_stage1_r` to call the helper**
In `run_stage1_r` (~1806), change the build line:
```rust
let flat = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, Some(2), Some(4))
.compile_with_params(&[])
.expect("valid stage1-r blueprint");
```
(was `stage1_r_blueprint(tx_eq, tx_ex, tx_r, tx_req)`.)
- [ ] **Step 3: Build — confirm no other caller of the old name**
Run: `cargo build -p aura-cli`
Expected: clean (0 errors). A compile error naming `stage1_r_blueprint` means
another caller exists — update it to `stage1_r_graph(.., Some(..), Some(..))`.
- [ ] **Step 4: Run the golden + all stage1-r tests — must stay GREEN (byte-preserved)**
Run: `cargo test -p aura-cli --test cli_run stage1_r`
Expected: PASS — `stage1_r_single_run_output_golden`, `run_harness_stage1_r_*`,
`stage1_r_trace_*` all green. A RED golden here means the extraction was NOT
byte-preserving → STOP and bounce to `debug` (do not adjust the golden to match).
---
### Task 3: `metric_cmp` learns sqn / expectancy_r / net_expectancy_r (aura-registry)
Self-contained RED→GREEN in `aura-registry`. All three higher-is-better, reaching
into `RunReport.metrics.r`; a `None` member sorts to the worst position.
**Files:**
- Modify: `crates/aura-registry/src/lib.rs:100-104` (`enum Metric`), `:116-135` (`metric_cmp`), `:184-187` (`UnknownMetric` Display), `mod tests` (~200, fixture + tests)
- [ ] **Step 1: Add the RED tests + an R fixture helper**
In `mod tests`, extend the `use` and add the helper + four tests:
```rust
// add to the existing test `use aura_engine::{...}` line:
use aura_engine::RMetrics;
/// A family member carrying an R block — mirrors `report_with` but sets
/// `metrics.r = Some(..)`. Only sqn / expectancy_r / net_expectancy_r vary per
/// call (the rank keys); the rest are fixed plausible values.
fn report_with_r(sqn: f64, expectancy_r: f64, net_expectancy_r: f64) -> RunReport {
let mut rep = report_with(0.0, 0.0, 0);
rep.metrics.r = Some(RMetrics {
expectancy_r,
n_trades: 4,
win_rate: 0.5,
avg_win_r: 1.5,
avg_loss_r: -0.5,
profit_factor: 3.0,
max_r_drawdown: 0.5,
n_open_at_end: 0,
sqn,
net_expectancy_r,
conviction_terciles_r: [0.0, 0.0, 0.0],
});
rep
}
#[test]
fn rank_by_sqn_orders_members_descending() {
let reports = vec![
report_with_r(0.5, 0.1, 0.1),
report_with_r(2.0, 0.4, 0.4),
report_with_r(1.0, 0.2, 0.2),
];
let ranked = rank_by(reports, "sqn").expect("rank sqn");
let sqns: Vec<f64> = ranked.iter().map(|r| r.metrics.r.as_ref().unwrap().sqn).collect();
assert_eq!(sqns, vec![2.0, 1.0, 0.5], "sqn must rank highest-first");
}
#[test]
fn rank_by_expectancy_and_net_expectancy_r() {
let reports = vec![report_with_r(1.0, 0.1, 0.05), report_with_r(1.0, 0.3, 0.25)];
let by_exp = rank_by(reports.clone(), "expectancy_r").expect("rank expectancy_r");
assert_eq!(by_exp[0].metrics.r.as_ref().unwrap().expectancy_r, 0.3);
let by_net = rank_by(reports, "net_expectancy_r").expect("rank net_expectancy_r");
assert_eq!(by_net[0].metrics.r.as_ref().unwrap().net_expectancy_r, 0.25);
}
#[test]
fn rank_r_metric_sorts_none_member_last() {
// a pip-only member (r: None) ranks below every member with an R block.
let reports = vec![report_with(1.0, 0.5, 1), report_with_r(0.5, 0.1, 0.1)];
let ranked = rank_by(reports, "sqn").expect("rank sqn with a None member");
assert!(ranked[0].metrics.r.is_some(), "the R member ranks first");
assert!(ranked[1].metrics.r.is_none(), "the None member sorts last");
}
#[test]
fn unknown_metric_message_lists_r_metrics() {
let msg = RegistryError::UnknownMetric("nope".to_string()).to_string();
assert!(msg.contains("sqn"), "msg: {msg}");
assert!(msg.contains("expectancy_r"), "msg: {msg}");
assert!(msg.contains("net_expectancy_r"), "msg: {msg}");
}
```
- [ ] **Step 2: Run the new tests — confirm they FAIL**
Run: `cargo test -p aura-registry rank_by_sqn_orders_members_descending`
Expected: FAIL — `rank_by(.., "sqn")` returns `Err(UnknownMetric("sqn"))`, so
`.expect("rank sqn")` panics. (The other three fail likewise / on the message.)
- [ ] **Step 3: Extend `enum Metric`**
```rust
enum Metric {
TotalPips,
MaxDrawdown,
BiasSignFlips,
Sqn,
ExpectancyR,
NetExpectancyR,
}
```
- [ ] **Step 4: Extend `metric_cmp` (string arms + comparator arms + None→-inf)**
In `metric_cmp`, add the string arms before the `other =>` catch-all:
```rust
"sqn" => Metric::Sqn,
"expectancy_r" => Metric::ExpectancyR,
"net_expectancy_r" => Metric::NetExpectancyR,
```
Add a helper above the returned closure and the three comparator arms (a missing
`r` block sorts to the bottom for these higher-is-better metrics):
```rust
fn r_get(rep: &RunReport, f: impl Fn(&aura_engine::RMetrics) -> f64) -> f64 {
rep.metrics.r.as_ref().map(f).unwrap_or(f64::NEG_INFINITY)
}
Ok(move |a: &RunReport, b: &RunReport| match metric {
// ... existing TotalPips / MaxDrawdown / BiasSignFlips arms unchanged ...
Metric::Sqn => r_get(b, |r| r.sqn).total_cmp(&r_get(a, |r| r.sqn)),
Metric::ExpectancyR => r_get(b, |r| r.expectancy_r).total_cmp(&r_get(a, |r| r.expectancy_r)),
Metric::NetExpectancyR => r_get(b, |r| r.net_expectancy_r).total_cmp(&r_get(a, |r| r.net_expectancy_r)),
})
```
(`total_cmp` so `NEG_INFINITY` orders deterministically and no `NaN` can panic. If
`RMetrics` is not already in scope in `lib.rs`, use the fully-qualified
`aura_engine::RMetrics` as written.)
- [ ] **Step 5: Extend the `UnknownMetric` Display known-list**
At `lib.rs:~186`, change the known-list literal:
`(known: total_pips, max_drawdown, bias_sign_flips)`
`(known: total_pips, max_drawdown, bias_sign_flips, sqn, expectancy_r, net_expectancy_r)`
- [ ] **Step 6: Run the registry suite — all GREEN**
Run: `cargo test -p aura-registry`
Expected: PASS (the four new tests + all existing, including
`rank_by_orders_best_first_per_metric` and `rank_by_unknown_metric_is_an_error`).
---
### Task 4: `stage1_r_sweep_family` + `Strategy::Stage1R` + parse + dispatch
RED→GREEN. Adds the R sweep family and wires `--strategy stage1-r` end-to-end.
Needs Task 2's `stage1_r_graph` and Task 3's `rank sqn`.
**Files:**
- Modify: `crates/aura-cli/tests/cli_run.rs` (new integration test ~after 1033)
- Modify: `crates/aura-cli/src/main.rs:1122-1125` (`enum Strategy`), `:1149` (parse), `:1228-1231` (dispatch), new `stage1_r_sweep_family` (~after 1098), usage strings (1128/1136/1215/1974)
- [ ] **Step 1: Add the RED integration test**
Append to `crates/aura-cli/tests/cli_run.rs`:
```rust
/// Property (#133): `aura sweep --strategy stage1-r` runs the stage1-r harness over
/// the fast×slow signal grid (4 members), each carrying an R block, persisted as a
/// rankable family; `runs family <id> rank sqn` orders them highest-SQN first.
#[test]
fn sweep_strategy_stage1_r_ranks_a_family_by_sqn() {
let cwd = temp_cwd("sweep-stage1-r");
let out = Command::new(BIN)
.args(["sweep", "--strategy", "stage1-r"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy stage1-r");
assert!(
out.status.success(),
"stage1-r 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(), 4, "stage1-r sweep must print 4 member lines: {stdout:?}");
for line in &lines {
assert!(line.contains("\"r\":{"), "member must carry an r block: {line}");
assert!(line.contains("\"sqn\""), "member r block must carry sqn: {line}");
}
// rank the family by sqn — highest first.
let rank = Command::new(BIN)
.args(["runs", "family", "sweep-0", "rank", "sqn"])
.current_dir(&cwd)
.output()
.expect("spawn rank sqn");
assert!(rank.status.success(), "rank sqn exit: {:?}", rank.status);
let rank_out = String::from_utf8(rank.stdout).expect("utf-8");
assert_eq!(rank_out.lines().count(), 4, "rank: {rank_out:?}");
let sqns: Vec<f64> = rank_out
.lines()
.map(|l| {
let key = "\"sqn\":";
let start = l.find(key).expect("line has sqn") + key.len();
let tail = &l[start..];
let end = tail.find([',', '}']).expect("token end");
tail[..end].parse().expect("sqn is an f64")
})
.collect();
assert!(sqns.windows(2).all(|w| w[0] >= w[1]), "rank not descending by sqn: {sqns:?}");
// an unknown metric is a usage error (exit 2).
let bogus = Command::new(BIN)
.args(["runs", "family", "sweep-0", "rank", "bogus"])
.current_dir(&cwd)
.output()
.expect("spawn bogus");
assert_eq!(bogus.status.code(), Some(2), "bogus exit: {:?}", bogus.status);
let _ = std::fs::remove_dir_all(&cwd);
}
```
- [ ] **Step 2: Run it — confirm it FAILS**
Run: `cargo test -p aura-cli --test cli_run sweep_strategy_stage1_r_ranks_a_family_by_sqn`
Expected: FAIL — `--strategy stage1-r` is rejected by `parse_sweep_args` (exit 2),
so `out.status.success()` is false. (Build is clean; this is a runtime failure.)
- [ ] **Step 3: Add `Strategy::Stage1R`**
```rust
enum Strategy {
SmaCross,
Momentum,
Stage1R,
}
```
- [ ] **Step 4: Accept `--strategy stage1-r` in `parse_sweep_args`**
In the `--strategy` value `match *value` (~1149), add before the `_ =>` arm:
```rust
"stage1-r" => Strategy::Stage1R,
```
- [ ] **Step 5: Dispatch in `run_sweep`**
In the `match strategy` (~1228), add the arm:
```rust
Strategy::Stage1R => stage1_r_sweep_family(persist.then_some(name), &data),
```
- [ ] **Step 6: Add `stage1_r_sweep_family` (mirror `momentum_sweep_family`)**
Insert after `momentum_sweep_family` (~1098):
```rust
/// `aura sweep --strategy stage1-r`: sweep the stage1-r harness over a fast×slow
/// SIGNAL grid (the stop + sizing stay fixed — see the cycle-0066 / #133 decision
/// log: risk_budget is R-invariant and bias.scale is sign-only under flat-1R, both
/// degenerate axes; the stop defines the R unit, so varying it would break
/// cross-member SQN comparability). Each member folds the dense R-record via
/// summarize_r, so its RunReport carries `r: Some(..)` and the family is rankable
/// by sqn / expectancy_r / net_expectancy_r. Per-member tracing is a follow-up; the
/// `_trace` slot keeps the dispatch shape uniform with the other sweeps.
fn stage1_r_sweep_family(_trace: Option<&str>, data: &DataSource) -> SweepFamily {
let pip = data.pip_size();
let window = data.full_window();
// a throwaway floated build, only to resolve param_space + the named axes
// (its taps are never drained — the per-point run_one rebuilds with live ones).
let floated = || {
let (tx_eq, _) = mpsc::channel();
let (tx_ex, _) = mpsc::channel();
let (tx_r, _) = mpsc::channel();
let (tx_req, _) = mpsc::channel();
stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None)
};
let space = floated().param_space();
floated()
.axis("fast.length", [2, 3])
.axis("slow.length", [6, 12])
.sweep(|point| {
let (tx_eq, rx_eq) = mpsc::channel();
let (tx_ex, rx_ex) = mpsc::channel();
let (tx_r, rx_r) = mpsc::channel();
let (tx_req, _rx_req) = mpsc::channel();
let mut h = stage1_r_graph(tx_eq, tx_ex, tx_r, tx_req, None, None)
.bootstrap_with_cells(point)
.expect("stage1-r grid points are kind-checked against param_space");
h.run(data.run_sources());
let eq_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_eq.try_iter().collect();
let ex_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_ex.try_iter().collect();
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
let named = zip_params(&space, point);
let manifest = sim_optimal_manifest(named, window, 0, pip);
let mut metrics = summarize(&f64_field(&eq_rows, 0), &f64_field(&ex_rows, 0));
metrics.r = Some(summarize_r(&r_rows, 0.0));
RunReport { manifest, metrics }
})
.expect("the stage1-r named grid matches the stage1-r param-space")
}
```
Note: the `.axis(...)` calls must cover EVERY free knob in `param_space`. With only
`fast`/`slow` floated, `param_space` is exactly `{fast.length, slow.length}`. If
`.sweep(...)` panics on an unbound knob, an extra knob is free — inspect
`floated().param_space()` and either bind it in `stage1_r_graph` or add a
single-value axis. (Expected: exactly the two axes.)
- [ ] **Step 7: Update the sweep usage strings to list stage1-r**
At each `<sma|momentum>` in the sweep usage/doc text (`main.rs:1128`, `:1136` the
`usage` closure, `:1215` doc, `:1974` the top-level usage literal), change to
`<sma|momentum|stage1-r>`.
- [ ] **Step 8: Build + run the integration test — GREEN**
Run: `cargo build -p aura-cli`
Expected: clean (the `run_sweep` match is now exhaustive).
Run: `cargo test -p aura-cli --test cli_run sweep_strategy_stage1_r_ranks_a_family_by_sqn`
Expected: PASS — 4 members each with an `r` block, ranked descending by sqn,
unknown metric exits 2.
- [ ] **Step 9: Full workspace gate**
Run: `cargo test --workspace`
Expected: PASS (no regressions; the new tests green).
Run: `cargo clippy --workspace --all-targets -- -D warnings`
Expected: clean.
---
## Self-review
1. **Spec coverage:** Half A (metric_cmp R metrics + None-worst + extended message)
= Task 3. Half B (stage1-r sweep family + --strategy + dispatch) = Tasks 2+4.
Golden (byte-preservation guard) = Task 1. Acceptance #1#5 all covered.
2. **Placeholder scan:** the only deferred literal is the golden's captured metrics
string (Task 1 Step 1→2) — a characterization value that is *by definition*
captured from the running binary, with an exact capture command + paste slot.
This is the legitimate golden-capture pattern, not a TBD; flagged here so it is
not mistaken for one. No other placeholders.
3. **Type consistency:** `stage1_r_graph` (not `stage1_r_blueprint`) used in both
Task 2 and Task 4; `Strategy::Stage1R`, `report_with_r`, `r_get`, `RMetrics`
consistent across tasks.
4. **Step granularity:** each step is one action (one edit / one run).
5. **No commit steps:** none — the orchestrator commits the iter.
6. **Pin/replacement contiguity:** the golden pins the `metrics` substring captured
from real output (contiguous by construction); the integration test extracts
`"sqn":` tokens (no soft-wrap pin). OK.
7. **Compile-gate ordering:** Task 2's signature change (`stage1_r_blueprint`
`stage1_r_graph` + 2 params) threads its one caller (`run_stage1_r`) in the same
task before the build gate (Step 3). Task 4's `Strategy::Stage1R` threads both
the dispatch arm (compiler-flagged) and the parse arm (silent) before the build
gate (Step 8). No deferred caller past a build gate.
8. **Verification-filter strings resolve:** `stage1_r_single_run_output_golden`,
`rank_by_sqn_orders_members_descending`, `sweep_strategy_stage1_r_ranks_a_family_by_sqn`
are the exact new test names; `stage1_r` / `-p aura-registry` are real targets;
the final gates are unfiltered `--workspace`. One positional filter per `cargo
test` (project memory). OK.