Files
Aura/docs/plans/0086-cost-flag-cli-ergonomics.md
T
Brummel dfc01bf837 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
2026-06-29 11:35:03 +02:00

423 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<f64, String> {
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/|entrystop|): \
--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 <sma|macd|stage1-r>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--trace <name>] [--cost-per-trade <f64>] [--slip-vol-mult <f64>] [--carry-per-cycle <f64>]{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).