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

17 KiB
Raw Blame History

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):

#[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):

/// 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):

            "--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):

            "--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):

            "--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):

    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":

/// 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):

/// 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:

#[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:

    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):

/// 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):

/// 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):

/// 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):

    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):

    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):

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).