From dfc01bf837ceb088aba236ec4c75d291caa68a83 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 29 Jun 2026 11:35:03 +0200 Subject: [PATCH] plan: 0086 cost-flag CLI ergonomics Four tasks from spec 0086: (1) named non-negativity diagnostic via a parse_nonneg_rate helper + perturbed-test fix; (2) non-R-harness guard (the spec_gap, reject/exit-2); (3) COST_FLAGS_NOTE appended to the usage + --help surfaces; (4) the C10 ledger note + full-suite/clippy gate. Confined to crates/aura-cli; the eight stage1-r cost goldens stay byte-green. refs #153 --- docs/plans/0086-cost-flag-cli-ergonomics.md | 422 ++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 docs/plans/0086-cost-flag-cli-ergonomics.md diff --git a/docs/plans/0086-cost-flag-cli-ergonomics.md b/docs/plans/0086-cost-flag-cli-ergonomics.md new file mode 100644 index 0000000..d953f69 --- /dev/null +++ b/docs/plans/0086-cost-flag-cli-ergonomics.md @@ -0,0 +1,422 @@ +# Cost-flag CLI ergonomics — Implementation Plan + +> **Parent spec:** `docs/specs/0086-cost-flag-cli-ergonomics.md` +> +> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run +> this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** Make the `aura run` cost-flag surface honest — units discoverable, a +named non-negativity diagnostic, and a refuse-don't-guess rejection of cost flags +on a non-R harness — with one C10 ledger note, changing no cost-model/R math. + +**Architecture:** All code lands in the pure `parse_run_args` grammar (and two +print sites) in `crates/aura-cli/src/main.rs`; every new rejection is a pure +`Result::Err` printed by `main`'s existing `eprintln!("aura: {msg}")` + `exit(2)` +path (main.rs:3273-3279). One realization note is added to contract C10 in the +ledger. The accepted stage1-r cost path and its eight `net_expectancy_r`/bleed +goldens stay byte-identical. + +**Tech Stack:** Rust; `crates/aura-cli/src/main.rs` (the `aura` binary + its +`mod tests`), `crates/aura-cli/tests/cli_run.rs` (integration), `docs/design/INDEX.md`. + +--- + +**Files this plan creates or modifies:** + +- Modify: `crates/aura-cli/src/main.rs` — new private `parse_nonneg_rate` fn + new + `const COST_FLAGS_NOTE`; rewire the three cost-flag arms (:3185-3211); insert the + non-R-harness guard (between :3215 and :3224); append the note to the `usage` + closure (:3137-3140) and the `--help` print (:3265). +- Test: `crates/aura-cli/src/main.rs` `mod tests` (:3338) — pure `parse_run_args` + units (negative-rate, non-R-harness, accepted-path, malformed-regression). +- Modify: `crates/aura-cli/tests/cli_run.rs` — update the perturbed negative-carry + test (:1825-1841); add non-R-harness, negative-cost, and `--help`-note integration + tests beside the cost block (:1631-1888) and the help block (:595-663). +- Modify: `docs/design/INDEX.md` — one C10 realization note at the cost-section tail + (after `sweep-path cost. Decision log: #148.`, :831). + +--- + +### Task 1: Named non-negativity diagnostic (spec item 2) + +**Files:** +- Modify: `crates/aura-cli/src/main.rs` (new `parse_nonneg_rate`; arms :3185-3211) +- Test: `crates/aura-cli/src/main.rs` `mod tests` (:3338) +- Test: `crates/aura-cli/tests/cli_run.rs` (update :1825-1841; add one negative-cost test) + +- [ ] **Step 1: Write the failing unit tests** + +Add to `mod tests` in `crates/aura-cli/src/main.rs`, beside the existing +`parse_run_args_*` units (near :4480): + +```rust +#[test] +fn parse_run_args_negative_cost_rates_are_named_not_usage() { + for flag in ["--cost-per-trade", "--slip-vol-mult", "--carry-per-cycle"] { + let err = parse_run_args(&["--harness", "stage1-r", flag, "-0.5"]) + .expect_err("a negative cost rate must be refused"); + assert!(err.contains("must be non-negative"), "{flag}: {err}"); + assert!(err.contains(flag), "message must name the offending flag: {err}"); + assert!(err.contains("-0.5"), "message must carry the value: {err}"); + } +} + +#[test] +fn parse_run_args_malformed_cost_rate_stays_grammar_usage() { + let err = parse_run_args(&["--harness", "stage1-r", "--cost-per-trade", "x"]) + .expect_err("a malformed rate is a grammar error"); + assert!(err.contains("usage"), "a malformed value stays the usage path: {err}"); +} +``` + +- [ ] **Step 2: Run the unit tests to verify they fail** + +Run: `cargo test -p aura-cli parse_run_args_negative_cost_rates_are_named_not_usage` +Expected: FAIL — the current arms `return Err(usage())` on a negative value, so the +message is the usage string and `err.contains("must be non-negative")` is false. + +- [ ] **Step 3: Add the `parse_nonneg_rate` helper** + +Insert immediately above `fn parse_run_args` (currently `crates/aura-cli/src/main.rs:3136`): + +```rust +/// Parse a cost-rate flag value and enforce non-negativity, naming the offending +/// flag so a sign typo is distinguishable from a mistyped flag name. A *malformed* +/// value stays a grammar usage error (unchanged); only a parsed-but-negative value +/// yields the named message. Pure — no I/O, no exit. +fn parse_nonneg_rate(flag: &str, value: &str, usage: &impl Fn() -> String) -> Result { + let v: f64 = value.parse().map_err(|_| usage())?; + if v < 0.0 { + return Err(format!("{flag} must be non-negative, got {v}")); + } + Ok(v) +} +``` + +- [ ] **Step 4: Rewire the three cost-flag arms to call the helper** + +In `parse_run_args`, replace the `--cost-per-trade` arm (`main.rs:3185-3193`): + +```rust + "--cost-per-trade" if cost.is_none() => { + let (value, t) = t.split_first().ok_or_else(usage)?; + cost = Some(parse_nonneg_rate("--cost-per-trade", value, &usage)?); + tail = t; + } +``` + +the `--slip-vol-mult` arm (`main.rs:3194-3202`): + +```rust + "--slip-vol-mult" if slip_vol_mult.is_none() => { + let (value, t) = t.split_first().ok_or_else(usage)?; + slip_vol_mult = Some(parse_nonneg_rate("--slip-vol-mult", value, &usage)?); + tail = t; + } +``` + +and the `--carry-per-cycle` arm (`main.rs:3203-3211`): + +```rust + "--carry-per-cycle" if carry_per_cycle.is_none() => { + let (value, t) = t.split_first().ok_or_else(usage)?; + carry_per_cycle = Some(parse_nonneg_rate("--carry-per-cycle", value, &usage)?); + tail = t; + } +``` + +- [ ] **Step 5: Run the unit tests to verify they pass** + +Run: `cargo test -p aura-cli parse_run_args_negative_cost_rates_are_named_not_usage` +Expected: PASS. +Run: `cargo test -p aura-cli parse_run_args_malformed_cost_rate_stays_grammar_usage` +Expected: PASS (a malformed value still routes through `usage()`). + +- [ ] **Step 6: Update the now-perturbed integration test** + +The existing `stage1_r_negative_carry_per_cycle_refused_with_usage_exit_2` +(`crates/aura-cli/tests/cli_run.rs:1825-1841`) asserts `stderr.contains("usage")`, +which the new named message no longer satisfies. Update its assertion and name. + +Replace the assertion line (`cli_run.rs:1838`): + +```rust + assert!(stderr.contains("must be non-negative"), "negative carry stderr must name the cause: {stderr:?}"); +``` + +Rename the function (`cli_run.rs:1826`) and adjust the doc line that says +"usage on stderr": + +```rust +/// Property (cycle 0085 + #153, cost-flag validation symmetry): a NEGATIVE +/// `--carry-per-cycle` is refused at the binary boundary — exit 2, a named +/// non-negativity diagnostic on stderr, nothing on stdout, no `runs/` write — the +/// same shape the sibling `--cost-per-trade`/`--slip-vol-mult` flags enforce. The +/// `v < 0.0` guard is load-bearing: without it a negative carry reaches +/// `CarryCost::new` and PANICs, and a negative price-unit carry would CREDIT R — +/// inverting the accrual sign. Deterministic; the refusal precedes any data access. +#[test] +fn stage1_r_negative_carry_per_cycle_refused_non_negative_exit_2() { +``` + +- [ ] **Step 7: Add a sibling negative-cost integration test** + +Add beside it in `crates/aura-cli/tests/cli_run.rs` (after the carry test, ~:1842): + +```rust +/// Property (#153): a negative `--cost-per-trade` is refused at the binary +/// boundary — exit 2, a named non-negativity diagnostic on stderr, no `runs/` +/// write — the sibling of the carry guard, exercising the per-trade arm. +#[test] +fn run_negative_cost_per_trade_refused_non_negative_exit_2() { + let dir = temp_cwd("cost-per-trade-neg"); + let out = Command::new(BIN) + .current_dir(&dir) + .args(["run", "--harness", "stage1-r", "--cost-per-trade", "-0.5"]) + .output() + .expect("spawn aura run --cost-per-trade -0.5"); + assert_eq!(out.status.code(), Some(2), "negative cost must exit 2; stderr: {}", + String::from_utf8_lossy(&out.stderr)); + assert!(out.stdout.is_empty(), "a refused run must not emit a report: {:?}", out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains("must be non-negative"), "stderr must name the cause: {stderr:?}"); + assert!(!dir.join("runs").exists(), "a refused run must not write the registry"); + let _ = std::fs::remove_dir_all(&dir); +} +``` + +- [ ] **Step 8: Run the integration tests to verify they pass** + +Run: `cargo test -p aura-cli --test cli_run stage1_r_negative_carry_per_cycle_refused_non_negative_exit_2` +Expected: PASS. +Run: `cargo test -p aura-cli --test cli_run run_negative_cost_per_trade_refused_non_negative_exit_2` +Expected: PASS. + +--- + +### Task 2: Non-R-harness guard (spec item 3 — the spec_gap) + +**Files:** +- Modify: `crates/aura-cli/src/main.rs` (insert guard after :3215) +- Test: `crates/aura-cli/src/main.rs` `mod tests` (:3338) +- Test: `crates/aura-cli/tests/cli_run.rs` (new integration test beside :1574-1589) + +- [ ] **Step 1: Write the failing unit tests** + +Add to `mod tests` in `crates/aura-cli/src/main.rs`, beside the Task-1 units: + +```rust +#[test] +fn parse_run_args_cost_flags_require_r_harness() { + let cases: [(&[&str], &str); 3] = [ + (&["--harness", "sma"], "--cost-per-trade"), + (&["--harness", "macd"], "--slip-vol-mult"), + (&["--harness", "sma"], "--carry-per-cycle"), + ]; + for (harness, flag) in cases { + let mut argv: Vec<&str> = harness.to_vec(); + argv.extend_from_slice(&[flag, "0.001"]); + let err = parse_run_args(&argv).expect_err("cost flag on a non-R harness must be refused"); + assert!(err.contains("R-evaluator harness"), "{harness:?}+{flag}: {err}"); + } +} + +#[test] +fn parse_run_args_cost_flag_without_harness_defaults_sma_and_is_refused() { + let err = parse_run_args(&["--carry-per-cycle", "0.5"]) + .expect_err("the default harness is sma → cost flag refused"); + assert!(err.contains("R-evaluator harness"), "{err}"); +} + +#[test] +fn parse_run_args_cost_flags_accepted_on_stage1_r() { + let a = parse_run_args(&["--harness", "stage1-r", "--cost-per-trade", "0.001"]) + .expect("cost flags are accepted on the R harness"); + assert!(matches!(a.harness, HarnessKind::Stage1R)); + assert_eq!(a.cost, Some(0.001)); +} +``` + +- [ ] **Step 2: Run the unit tests to verify they fail** + +Run: `cargo test -p aura-cli parse_run_args_cost_flags_require_r_harness` +Expected: FAIL — today a cost flag on `--harness sma` is accepted (silently +ignored downstream), so `parse_run_args` returns `Ok` and `expect_err` panics. + +- [ ] **Step 3: Insert the non-R-harness guard** + +In `parse_run_args`, immediately after `let harness = harness.unwrap_or(HarnessKind::Sma);` +(`main.rs:3215`) and before the `// --from/--to require --real.` comment: + +```rust + let harness = harness.unwrap_or(HarnessKind::Sma); + // Cost flags are defined only against the R chain (gross-R -> net-R); a non-R + // harness produces no R to charge against, so a cost flag there is a usage error, + // not a silent no-op (refuse-don't-guess, C10). + if !matches!(harness, HarnessKind::Stage1R) + && (cost.is_some() || slip_vol_mult.is_some() || carry_per_cycle.is_some()) + { + return Err( + "cost flags require an R-evaluator harness (stage1-r); \ + --harness sma/macd produces no R to charge against" + .to_string(), + ); + } +``` + +- [ ] **Step 4: Run the unit tests to verify they pass** + +Run: `cargo test -p aura-cli parse_run_args_cost_flags_require_r_harness` +Expected: PASS. +Run: `cargo test -p aura-cli parse_run_args_cost_flags_accepted_on_stage1_r` +Expected: PASS (the accepted path is unchanged). + +- [ ] **Step 5: Write the failing integration test** + +Add to `crates/aura-cli/tests/cli_run.rs`, beside `run_harness_sma_is_pip_only_no_r_block` +(~:1589): + +```rust +/// Property (#153): a cost flag on a non-R harness (sma/macd, which produce no R) +/// is a usage error — exit 2 with a named diagnostic, no silent no-op, no `runs/` +/// write. The pre-#153 behaviour silently ignored the flag and exited 0. +#[test] +fn run_cost_flag_on_non_r_harness_is_refused_exit_2() { + let dir = temp_cwd("sma-cost-refused"); + let out = Command::new(BIN) + .current_dir(&dir) + .args(["run", "--harness", "sma", "--cost-per-trade", "0.001"]) + .output() + .expect("spawn aura run --harness sma --cost-per-trade"); + assert_eq!(out.status.code(), Some(2), "cost on a non-R harness must exit 2; stderr: {}", + String::from_utf8_lossy(&out.stderr)); + assert!(out.stdout.is_empty(), "a refused run must not emit a report: {:?}", out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains("R-evaluator harness"), "stderr must name the cause: {stderr:?}"); + assert!(!dir.join("runs").exists(), "a refused run must not write the registry"); + let _ = std::fs::remove_dir_all(&dir); +} +``` + +- [ ] **Step 6: Run the integration test to verify it passes** + +Run: `cargo test -p aura-cli --test cli_run run_cost_flag_on_non_r_harness_is_refused_exit_2` +Expected: PASS. + +--- + +### Task 3: Cost-flag units note (spec item 1) + +**Files:** +- Modify: `crates/aura-cli/src/main.rs` (new `const COST_FLAGS_NOTE`; usage closure :3137; `--help` print :3265) +- Test: `crates/aura-cli/tests/cli_run.rs` (new `--help` note test beside :595-663) + +- [ ] **Step 1: Write the failing integration test** + +Add to `crates/aura-cli/tests/cli_run.rs`, beside the help tests (~:663): + +```rust +/// Property (#153): the cost-flag units/constraints are discoverable from +/// `aura run --help` — the appended note states the in-R charge, the per-ENGINE- +/// cycle carry semantics, the `>= 0` constraint, and that cost flags are stage1-r +/// only. Pre-#153 the usage listed the flags with no unit or scale guidance. +#[test] +fn run_help_surfaces_cost_flag_units_note() { + let out = Command::new(BIN).args(["run", "--help"]).output().expect("spawn aura run --help"); + assert_eq!(out.status.code(), Some(0), "run --help exits 0: {:?}", out.status); + let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); + assert!(stdout.contains("per ENGINE cycle"), "help must explain carry units: {stdout}"); + assert!(stdout.contains("stage1-r only"), "help must state the R-harness requirement: {stdout}"); +} +``` + +- [ ] **Step 2: Run the integration test to verify it fails** + +Run: `cargo test -p aura-cli --test cli_run run_help_surfaces_cost_flag_units_note` +Expected: FAIL — the current `--help` output (`USAGE`) carries no cost-flag note, +so `stdout.contains("per ENGINE cycle")` is false. + +- [ ] **Step 3: Add the `COST_FLAGS_NOTE` const** + +Insert immediately above `const USAGE: &str` (`main.rs:3244`): + +```rust +/// One-line note appended to the `aura run` usage surfaces (the grammar-error +/// usage closure and `--help`) so the cost-flag units, scale, per-engine-cycle +/// semantics, non-negativity, and R-harness requirement are discoverable at the +/// point of use (#153). A leading newline puts it on its own line under the usage. +const COST_FLAGS_NOTE: &str = "\ncost flags (stage1-r only; all >= 0, charged in R as cost/|entry−stop|): \ +--cost-per-trade = per-trade cost in price units; --slip-vol-mult = slippage multiplier on realized vol; \ +--carry-per-cycle = carry in price units per ENGINE cycle (not per day, not an overnight swap)"; +``` + +- [ ] **Step 4: Append the note to the `parse_run_args` usage closure** + +Replace the `usage` closure (`main.rs:3137-3140`): + +```rust + let usage = || { + format!( + "usage: aura run [--harness ] [--real [--from ] [--to ]] [--trace ] [--cost-per-trade ] [--slip-vol-mult ] [--carry-per-cycle ]{COST_FLAGS_NOTE}" + ) + }; +``` + +- [ ] **Step 5: Append the note to the `--help` print** + +Replace the `--help` short-circuit print (`main.rs:3265`): + +```rust + if args.iter().any(|a| a == "--help" || a == "-h") { + println!("{USAGE}{COST_FLAGS_NOTE}"); + return; + } +``` + +- [ ] **Step 6: Run the integration test to verify it passes** + +Run: `cargo test -p aura-cli --test cli_run run_help_surfaces_cost_flag_units_note` +Expected: PASS. + +- [ ] **Step 7: Regression — the uniform help/usage pins still hold** + +Run: `cargo test -p aura-cli --test cli_run per_subcommand_help_is_uniform_stdout_exit_zero` +Expected: PASS (the appended note keeps `contains("usage")` true and stderr empty). + +--- + +### Task 4: C10 ledger note + full-suite gate + +**Files:** +- Modify: `docs/design/INDEX.md` (C10 cost-section tail, after :831) + +- [ ] **Step 1: Add the C10 realization note** + +In `docs/design/INDEX.md`, replace the boundary between the cycle-0085 cost note +and the #117 HISTORY reframe (`:831-833`): + +```markdown +sweep-path cost. Decision log: #148. + +**Realization (cost-flag harness scoping, #153).** Cost flags are defined only +against an R-evaluator harness (the gross-R → net-R chain). On a non-R harness +(`--harness sma`/`macd`, which produce no R) a cost flag is a **usage error +(exit 2)**, not a silent no-op — refuse-don't-guess. Negative cost rates are +likewise rejected (exit 2) with a named diagnostic that identifies the offending +flag. CLI ergonomics only; the cost-model graph and the R math are untouched. + +**Reframe (2026-06-23, #117 — exposure → bias, R as the signal-quality unit).** +``` + +- [ ] **Step 2: Full aura-cli suite gate — new tests pass, goldens byte-green** + +Run: `cargo test -p aura-cli` +Expected: PASS — the whole `aura-cli` suite is green, including the eight accepted-path +cost goldens (`stage1_r_flat_cost_net_expectancy_r_golden` et al., cli_run.rs +:1686/1714/1736/1757/...) byte-unchanged, since only the grammar/usage strings and the +previously-unguarded sma-cost path changed. + +- [ ] **Step 3: Lint gate** + +Run: `cargo clippy -p aura-cli --all-targets -- -D warnings` +Expected: PASS — no warnings (the new helper, const, and guard are idiomatic).