diff --git a/docs/plans/0086-cost-flag-cli-ergonomics.md b/docs/plans/0086-cost-flag-cli-ergonomics.md deleted file mode 100644 index d953f69..0000000 --- a/docs/plans/0086-cost-flag-cli-ergonomics.md +++ /dev/null @@ -1,422 +0,0 @@ -# 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). diff --git a/docs/specs/0086-cost-flag-cli-ergonomics.md b/docs/specs/0086-cost-flag-cli-ergonomics.md deleted file mode 100644 index f11b3c9..0000000 --- a/docs/specs/0086-cost-flag-cli-ergonomics.md +++ /dev/null @@ -1,210 +0,0 @@ -# Cost-flag CLI ergonomics — Design Spec - -**Date:** 2026-06-29 -**Status:** Draft — awaiting user spec review -**Authors:** orchestrator + Claude - -## Goal - -Close the three non-blocking ergonomics/diagnostic gaps the milestone-close -fieldtest of #148 surfaced on the `aura run` cost-flag surface -(`--cost-per-trade`, `--slip-vol-mult`, `--carry-per-cycle`), plus the one -named spec_gap. The cost-model-graph capability already ships end-to-end; this -cycle only makes its CLI surface *honest and self-describing*, changing no -cost-model node, no R chain, and no `net_r_equity` / `summarize_r` math. - -Three observable defects, all instances of one anti-pattern (a misuse that -produces no actionable feedback): - -1. **Units undiscoverable.** The usage string lists the three cost flags with no - unit, scale, or per-close-vs-per-held-cycle note. A user must trial magnitudes - to find one that moves net-R. -2. **Negative-rate diagnostic is mute.** A negative rate already exits 2, but the - message is the bare 1-line usage dump — it never names the cause, and the - usage never states the non-negativity constraint, so a sign typo is - indistinguishable from a mistyped flag. -3. **Silent no-op on a non-R harness (the spec_gap).** `aura run --harness sma - --cost-per-trade 0.001` exits 0 with output byte-identical to the no-cost run: - the flag is silently ignored. C10 defines cost only against the R chain and - never constrained what a cost flag does on a harness that produces no R. - -## Architecture - -All three changes live in `crates/aura-cli/src/main.rs`, in and around the -**pure** `parse_run_args` grammar (line 3136) — the function the codebase -deliberately keeps side-effect-free so the grammar is unit-testable; `main` -already maps its `Err(String)` to `eprintln!("aura: {msg}")` + `exit(2)` (line -3277). Keeping every new rejection inside `parse_run_args` means each is a pure, -unit-testable `Result::Err`, and the fail-loud exit path is the existing one — -no new exit site, no new I/O. One ledger note pins the decision in C10. - -**Decision recorded (reference issue #153, 2026-06-29 — derived, not preference):** -item 3 resolves to **reject, exit 2** (not warn-and-continue). Basis: aura's -settled *refuse-don't-guess / fail-loud* discipline — C10 refuse-don't-guess -(exit 2 on absent geometry), the "every error path exits 2, never panics" rule -(#107), and the negative-rate guard in this very surface already exits 2. A -silent no-op is the lone exception in an otherwise fail-loud surface; rejecting -is the only behaviour consistent with every sibling guard. - -## Concrete code shapes - -### Worked user-facing surface (the acceptance evidence) - -```text -# Item 1 — units/constraints discoverable at the point of use (--help, exit 0) -$ aura run --help -usage: aura run [--harness ] [--real [--from ] [--to ]] [--trace ] [--cost-per-trade ] [--slip-vol-mult ] [--carry-per-cycle ] | aura chart ... -cost 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) - -# Item 2 — a negative rate names the cause + flag (exit 2, stderr), no runs/ write -$ aura run --harness stage1-r --cost-per-trade -0.5 -aura: --cost-per-trade must be non-negative, got -0.5 -$ echo $? -2 - -# Item 3 — a cost flag on a non-R harness is a usage error (exit 2, stderr), no runs/ write -$ aura run --harness sma --cost-per-trade 0.001 -aura: cost flags require an R-evaluator harness (stage1-r); --harness sma/macd produces no R to charge against -$ echo $? -2 - -# Unchanged: the accepted path is byte-identical to today -$ aura run --harness stage1-r --cost-per-trade 2 # → net_expectancy_r as before -$ aura run --harness stage1-r --carry-per-cycle 0 # → exactly free (zero-rate boundary) -``` - -### Implementation shape (before → after, secondary) - -**(a) Non-negativity guard — named cause, factored (items 2).** The three -flags currently restate an identical guard that returns the bare usage: - -```rust -// BEFORE (×3, lines 3188 / 3197 / 3206): -"--cost-per-trade" if cost.is_none() => { - let (value, t) = t.split_first().ok_or_else(usage)?; - let v: f64 = value.parse().map_err(|_| usage())?; - if v < 0.0 { return Err(usage()); } // mute: bare usage dump - cost = Some(v); - tail = t; -} - -// AFTER: one helper, named diagnostic; the three arms call it. -fn parse_nonneg_rate(flag: &str, value: &str, usage: &impl Fn() -> String) -> Result { - let v: f64 = value.parse().map_err(|_| usage())?; // malformed → grammar usage error (unchanged) - if v < 0.0 { - return Err(format!("{flag} must be non-negative, got {v}")); // named cause + offending flag - } - Ok(v) -} -// arm: -"--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; -} -``` - -A *malformed* value (`--cost-per-trade x`) stays a grammar usage error -(unchanged); only the *parsed-but-negative* case gets the named message. - -**(b) Non-R-harness guard — post-parse, pure (item 3).** After the parse loop, -once `harness` is resolved (line 3215), before constructing `RunArgs`: - -```rust -let harness = harness.unwrap_or(HarnessKind::Sma); -// Cost flags are defined only against the R chain; a non-R harness produces no R -// to charge against, so a cost flag there is a usage error (refuse-don't-guess). -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(), - ); -} -``` - -**(c) Usage note — units + constraints, one appended line (item 1).** A -`const COST_FLAGS_NOTE: &str` appended to both the `parse_run_args` `usage` -closure (line 3137) and the top-level `USAGE` (line 3244), so the same note -shows on a grammar error and on `--help`: - -```rust -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)"; -``` - -### Ledger note (C10) — pin the spec_gap decision - -Add one note to contract C10 in `docs/design/INDEX.md` (cost-model area), e.g.: - -> **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. - -## Components - -- `parse_nonneg_rate` (new, pure, private) — parse + non-negativity, named error. -- `parse_run_args` (edited) — three arms call the helper; one post-parse R-harness - guard; the `usage` closure appends `COST_FLAGS_NOTE`. -- `COST_FLAGS_NOTE` (new const) + `USAGE` (edited to append it). -- C10 ledger note (one paragraph). - -## Data flow - -Unchanged at runtime. `parse_run_args` either returns `Ok(RunArgs)` (the -accepted path → `run_dispatch` → identical to today) or `Err(String)` (the new -named messages → `main`'s existing `eprintln!` + `exit(2)`). No cost-model node, -harness, tap, or metric is touched. - -## Error handling - -Every misuse is a single, named, fail-loud stderr line + `exit(2)`, no `runs/` -write, no panic — the established CLI convention. Three messages: -malformed value → existing grammar usage; negative rate → ` must be -non-negative, got `; cost flag on non-R harness → `cost flags require an -R-evaluator harness (stage1-r); …`. - -## Testing strategy - -**Unit (pure `parse_run_args`, in `main.rs` tests):** -- `parse_run_args(["--cost-per-trade","-0.5"])` → `Err` containing `non-negative` - and `-0.5`. -- `parse_run_args(["--carry-per-cycle","-0.001"])` and `["--slip-vol-mult","-1"]` - → `Err` containing `non-negative` (guard symmetry across all three). -- `parse_run_args(["--harness","sma","--cost-per-trade","0.001"])` → `Err` - containing `R-evaluator harness`. -- `parse_run_args(["--harness","macd","--slip-vol-mult","0.5"])` → `Err`. -- `parse_run_args(["--carry-per-cycle","0.5"])` (no `--harness`, defaults to sma) - → `Err` (R-evaluator requirement). -- `parse_run_args(["--harness","stage1-r","--cost-per-trade","0.001"])` → `Ok` - (the accepted path is unchanged). -- Malformed value still maps to the grammar usage error (regression). - -**CLI integration (`crates/aura-cli/tests/cli_run.rs`):** -- `aura run --harness sma --cost-per-trade 0.001` → exit 2, stderr contains - `R-evaluator harness`, no `runs/` write. -- `aura run --cost-per-trade -0.5` → exit 2, stderr contains `non-negative`. -- `aura run --help` → exit 0, stdout contains the cost-flags note (units + - per-engine-cycle + `>= 0` + stage1-r-only). -- **Regression:** the existing stage1-r cost tests (cli_run.rs:1635/1688/1716/ - 1738/1759/1830/1879) stay green byte-for-byte — the accepted and negative-rate - exit-code paths are unchanged in code-behaviour; only message text and the - previously-unguarded sma path change. - -## Acceptance criteria - -Judged against aura's feature-acceptance criterion (improves correctness / -removes a foot-gun; reintroduces no failure class the core constraints forbid): - -- The cost-flag units, scale, per-engine-cycle semantics, non-negativity, and - R-harness requirement are all discoverable from `aura run --help`. -- No cost-flag misuse is silent: a negative rate and a cost flag on a non-R - harness each produce a named stderr diagnostic + exit 2, never a silent no-op - and never a bare usage dump for these two named causes. -- The accepted stage1-r cost path and all its goldens are byte-unchanged. -- C10 carries a one-line note pinning "cost flags require an R-evaluator harness; - non-R harness ⇒ usage error (exit 2)", closing the spec_gap.