diff --git a/docs/plans/0066-r-sweep-rank.md b/docs/plans/0066-r-sweep-rank.md new file mode 100644 index 0000000..ae0a68d --- /dev/null +++ b/docs/plans/0066-r-sweep-rank.md @@ -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 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":,"max_drawdown":,"bias_sign_flips":,"r":{"expectancy_r":,...,"conviction_terciles_r":[,,]}}` + +- [ ] **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` 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)>, + tx_ex: mpsc::Sender<(Timestamp, Vec)>, + tx_r: mpsc::Sender<(Timestamp, Vec)>, + tx_req: mpsc::Sender<(Timestamp, Vec)>, + fast_len: Option, + slow_len: Option, +) -> 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 = 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 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 = 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)> = rx_eq.try_iter().collect(); + let ex_rows: Vec<(Timestamp, Vec)> = rx_ex.try_iter().collect(); + let r_rows: Vec<(Timestamp, Vec)> = 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 `` in the sweep usage/doc text (`main.rs:1128`, `:1136` the +`usage` closure, `:1215` doc, `:1974` the top-level usage literal), change to +``. + +- [ ] **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.