diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index c9b3698..e589e77 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -1935,7 +1935,25 @@ fn render_bind_error(e: &BindError) -> String { see `aura sweep --list-axes`" ), BindError::UnknownKnob(msg) => msg.clone(), - other => format!("{other:?}"), + BindError::DuplicateBinding(name) => format!( + "axis {name}: bound twice — each param takes exactly one axis — \ + see `aura sweep --list-axes`" + ), + BindError::EmptyAxis(name) => format!( + "axis {name}: supplies no values — give at least one, e.g. `--axis {name}=2,4`" + ), + BindError::EmptyRange(name) => format!( + "axis {name}: the named range is empty — give it at least one value" + ), + // A blueprint defect, not an axis usage error: the point passed name + // resolution but its bootstrap failed. Prose frame with the compile + // detail explicitly labelled as internal — per-variant prose for + // CompileError belongs to the graph-build surface, not this boundary. + BindError::Compile(e) => format!( + "the resolved axis point failed to bootstrap — the blueprint is \ + defective at this point, re-validate it with `aura graph build` \ + (internal detail: {e:?})" + ), } } @@ -4250,6 +4268,35 @@ mod tests { assert_eq!(intersect_shared_window(&symbols, &windows), Ok((100, 200))); } + /// Audit follow-up to #247/#269: `render_bind_error` is exhaustive — the + /// axis-usage variants the old catch-all Debug-framed render as prose + /// naming the axis, with no Rust identifier on the user's stderr. + #[test] + fn render_bind_error_prose_covers_the_axis_usage_variants() { + let dup = render_bind_error(&aura_engine::BindError::DuplicateBinding("a.b".into())); + assert!(dup.contains("a.b") && dup.contains("bound twice"), "{dup}"); + assert!(!dup.contains("DuplicateBinding"), "Debug leak: {dup}"); + let empty = render_bind_error(&aura_engine::BindError::EmptyAxis("a.b".into())); + assert!(empty.contains("a.b") && empty.contains("no values"), "{empty}"); + assert!(!empty.contains("EmptyAxis"), "Debug leak: {empty}"); + let range = render_bind_error(&aura_engine::BindError::EmptyRange("a.b".into())); + assert!(range.contains("a.b") && !range.contains("EmptyRange"), "Debug leak: {range}"); + } + + /// Audit follow-up to #247/#269: a `Compile` fault at the sweep boundary — + /// a blueprint defect, not an axis usage error — renders as a prose frame + /// that names the situation and labels the embedded compile detail as + /// internal, instead of leaking the bare Debug struct as the whole message. + #[test] + fn render_bind_error_frames_a_compile_fault_as_prose_with_labeled_detail() { + let msg = render_bind_error(&aura_engine::BindError::Compile( + aura_engine::CompileError::BadInteriorIndex, + )); + assert!(msg.contains("failed to bootstrap"), "{msg}"); + assert!(msg.contains("internal detail"), "{msg}"); + assert!(!msg.starts_with("Compile"), "the frame must lead with prose: {msg}"); + } + /// An unknown symbol (no archive files at all — an empty month list) /// refuses rather than reporting a bogus empty-span coverage (#264): the /// caller eprintln's the message and exits 1. diff --git a/crates/aura-cli/tests/research_docs.rs b/crates/aura-cli/tests/research_docs.rs index ad5b1e7..e75cbdd 100644 --- a/crates/aura-cli/tests/research_docs.rs +++ b/crates/aura-cli/tests/research_docs.rs @@ -717,6 +717,15 @@ fn campaign_introspect_unwired_annotates_cost_and_risk_knob_units() { low.contains("scales the stop distance"), "vol.k must be annotated as SCALING the stop distance (the risk-unit lever): {out}" ); + + // carry.carry_per_cycle — the identical price-unit-vs-R trap, accruing per + // held cycle (audit follow-up to #265: the note set must cover ALL three + // cost components, not two of three). + assert!(low.contains("carry_per_cycle"), "the carry knob must still be named: {out}"); + assert!( + low.contains("price units per held cycle"), + "carry_per_cycle must be annotated as a PRICE-unit per-held-cycle accrual: {out}" + ); } #[test] diff --git a/crates/aura-research/src/lib.rs b/crates/aura-research/src/lib.rs index 9de5278..8987d46 100644 --- a/crates/aura-research/src/lib.rs +++ b/crates/aura-research/src/lib.rs @@ -1095,6 +1095,7 @@ pub fn open_slots_campaign(text: &str) -> Result, DocError> { vec![ "constant.cost_per_trade is in price units, not R (charged per close as cost/|entry-stop| in R).".to_string(), "vol_slippage.slip_vol_mult is a multiplier on the per-cycle vol estimate (charged in R likewise).".to_string(), + "carry.carry_per_cycle is in price units per held cycle, not R (accrues over the hold, emitted in R likewise).".to_string(), ], )); } @@ -1966,6 +1967,7 @@ mod tests { vec![ "constant.cost_per_trade is in price units, not R (charged per close as cost/|entry-stop| in R).".to_string(), "vol_slippage.slip_vol_mult is a multiplier on the per-cycle vol estimate (charged in R likewise).".to_string(), + "carry.carry_per_cycle is in price units per held cycle, not R (accrues over the hold, emitted in R likewise).".to_string(), ], ), ] @@ -2046,6 +2048,7 @@ mod tests { vec![ "constant.cost_per_trade is in price units, not R (charged per close as cost/|entry-stop| in R).".to_string(), "vol_slippage.slip_vol_mult is a multiplier on the per-cycle vol estimate (charged in R likewise).".to_string(), + "carry.carry_per_cycle is in price units per held cycle, not R (accrues over the hold, emitted in R likewise).".to_string(), ], )] );