tidy: clear the milestone fieldtest findings (refusal reasons, --help, ledger + glossary)
Resolves the four non-bug findings from the inferential-validation milestone
fieldtest (the bug — the broken-pipe panic — is fixed in 1088320):
- [friction] generalize refusals now carry a reason instead of a bare usage dump:
<2 instruments, a multi-value candidate flag, a missing knob, a duplicate
symbol, and a non-stage1-r strategy each get a one-line why (mirroring the
R-only metric refusal). Two parser tests strengthened to pin the message.
- [friction] --select <argmax|plateau:mean|plateau:worst> added to the
walkforward branch of the global USAGE (aura --help) — the plateau objective
was undiscoverable from the only help the CLI prints (the per-subcommand usage
already carried it).
- [spec_gap] C1 gains a scope note: bit-identity is per-run; a derived metric
recomputed by two different command paths may differ by floating-point
reassociation (<=1 ULP), which is not a C1 violation — ratifies the
fieldtester's reading of the sweep-vs-generalize sqn difference.
- [spec_gap] the milestone vocabulary is added to docs/glossary.md:
cross-instrument generalization, deflated score, overfit probability,
neighbourhood score, plateau selection, sign-agreement, worst-case floor.
The fieldtest triage spec (docs/specs/fieldtest-*) is removed now its findings
are resolved; the tracked fieldtests/ scenario corpus stays.
Verified: cargo test --workspace green (0 failed), clippy --workspace
--all-targets -D warnings clean; `aura --help` shows --select; the refusals
print their reason.
refs #146
This commit is contained in:
+38
-22
@@ -1683,44 +1683,56 @@ fn parse_generalize_args(
|
||||
let mut stop_k: Option<f64> = None;
|
||||
let mut metric = "expectancy_r".to_string();
|
||||
let mut name = "generalize".to_string();
|
||||
// a single grid knob, required and single-valued (csv with exactly one item)
|
||||
let one = |value: &str, usage: &dyn Fn() -> String| -> Result<(), String> {
|
||||
// generalize validates ONE candidate cell, so each grid knob takes a single value;
|
||||
// refuse a multi-value flag with a reason, not a bare usage dump (fieldtest friction).
|
||||
let one = |value: &str| -> Result<(), String> {
|
||||
let items: Vec<&str> = value.split(',').collect();
|
||||
if items.len() != 1 || items[0].is_empty() { return Err(usage()); }
|
||||
if items.len() != 1 || items[0].is_empty() {
|
||||
return Err("generalize validates a single candidate: --fast / --slow / --stop-length / --stop-k each take exactly one value".to_string());
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
let knobs = || "generalize requires all four candidate knobs: --fast --slow --stop-length --stop-k, each a single value".to_string();
|
||||
let mut tail = rest;
|
||||
while let Some((flag, t)) = tail.split_first() {
|
||||
let (value, t) = t.split_first().ok_or_else(usage)?;
|
||||
match *flag {
|
||||
"--strategy" => { if *value != "stage1-r" { return Err(usage()); } }
|
||||
"--strategy" => { if *value != "stage1-r" { return Err("generalize requires --strategy stage1-r (the candidate must produce R)".to_string()); } }
|
||||
"--real" => {
|
||||
let parts: Vec<String> = value.split(',').map(|s| s.to_string()).collect();
|
||||
if parts.iter().any(|s| s.is_empty()) { return Err(usage()); }
|
||||
if parts.iter().any(|s| s.is_empty()) { return Err("generalize: --real takes a comma list of non-empty symbols (e.g. GER40,USDJPY)".to_string()); }
|
||||
symbols = Some(parts);
|
||||
}
|
||||
"--from" => from_ms = Some(value.parse().map_err(|_| usage())?),
|
||||
"--to" => to_ms = Some(value.parse().map_err(|_| usage())?),
|
||||
"--fast" => { one(value, &usage)?; fast = Some(value.parse().map_err(|_| usage())?); }
|
||||
"--slow" => { one(value, &usage)?; slow = Some(value.parse().map_err(|_| usage())?); }
|
||||
"--stop-length" => { one(value, &usage)?; stop_length = Some(value.parse().map_err(|_| usage())?); }
|
||||
"--stop-k" => { one(value, &usage)?; stop_k = Some(value.parse().map_err(|_| usage())?); }
|
||||
"--fast" => { one(value)?; fast = Some(value.parse().map_err(|_| usage())?); }
|
||||
"--slow" => { one(value)?; slow = Some(value.parse().map_err(|_| usage())?); }
|
||||
"--stop-length" => { one(value)?; stop_length = Some(value.parse().map_err(|_| usage())?); }
|
||||
"--stop-k" => { one(value)?; stop_k = Some(value.parse().map_err(|_| usage())?); }
|
||||
"--metric" => metric = (*value).to_string(),
|
||||
"--name" => name = (*value).to_string(),
|
||||
_ => return Err(usage()),
|
||||
}
|
||||
tail = t;
|
||||
}
|
||||
let symbols = symbols.ok_or_else(usage)?;
|
||||
if symbols.len() < 2 { return Err(usage()); }
|
||||
let symbols = symbols
|
||||
.ok_or_else(|| "generalize requires --real <SYM1,SYM2,...> — a comma list of two or more instruments".to_string())?;
|
||||
if symbols.len() < 2 {
|
||||
return Err(format!(
|
||||
"generalize needs at least two instruments to compare across; got {} (--real takes a comma list of >=2 symbols)",
|
||||
symbols.len()
|
||||
));
|
||||
}
|
||||
// distinct: a duplicate would double-count one instrument in the floor / sign count
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
if !symbols.iter().all(|s| seen.insert(s.clone())) { return Err(usage()); }
|
||||
if !symbols.iter().all(|s| seen.insert(s.clone())) {
|
||||
return Err("generalize: each instrument may appear once; --real has a duplicate symbol".to_string());
|
||||
}
|
||||
let grid = Stage1RGrid {
|
||||
fast: vec![fast.ok_or_else(usage)?],
|
||||
slow: vec![slow.ok_or_else(usage)?],
|
||||
stop_length: vec![stop_length.ok_or_else(usage)?],
|
||||
stop_k: vec![stop_k.ok_or_else(usage)?],
|
||||
fast: vec![fast.ok_or_else(knobs)?],
|
||||
slow: vec![slow.ok_or_else(knobs)?],
|
||||
stop_length: vec![stop_length.ok_or_else(knobs)?],
|
||||
stop_k: vec![stop_k.ok_or_else(knobs)?],
|
||||
..Stage1RGrid::default()
|
||||
};
|
||||
Ok((name, symbols, grid, metric, from_ms, to_ms))
|
||||
@@ -3079,7 +3091,7 @@ fn run_dispatch(args: RunArgs) -> Result<RunReport, String> {
|
||||
}
|
||||
|
||||
const USAGE: &str =
|
||||
"usage: aura run [--harness <sma|macd|stage1-r>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--trace <name>] | aura chart <name> [--tap <t>] [--panels] | aura graph | aura sweep [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] | aura mc [--name <n>|--trace <n>] | aura mc --strategy stage1-r [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--block-len <n>] [--resamples <n>] [--seed <n>] | aura walkforward [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] | aura generalize [--strategy stage1-r] --real <SYM1,SYM2,...> --fast <n> --slow <n> --stop-length <n> --stop-k <f> [--from <ms>] [--to <ms>] [--metric <r-metric>] [--name <n>] | aura runs families | aura runs family <id> [rank <metric>]";
|
||||
"usage: aura run [--harness <sma|macd|stage1-r>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--trace <name>] | aura chart <name> [--tap <t>] [--panels] | aura graph | aura sweep [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] | aura mc [--name <n>|--trace <n>] | aura mc --strategy stage1-r [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--block-len <n>] [--resamples <n>] [--seed <n>] | aura walkforward [--strategy <sma|momentum|stage1-r|stage1-breakout|stage1-meanrev>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--name <n>|--trace <n>] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--select <argmax|plateau:mean|plateau:worst>] | aura generalize [--strategy stage1-r] --real <SYM1,SYM2,...> --fast <n> --slow <n> --stop-length <n> --stop-k <f> [--from <ms>] [--to <ms>] [--metric <r-metric>] [--name <n>] | aura runs families | aura runs family <id> [rank <metric>]";
|
||||
|
||||
fn main() {
|
||||
// Restore the default SIGPIPE disposition. Rust's runtime sets SIGPIPE to SIG_IGN
|
||||
@@ -4097,11 +4109,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_generalize_requires_a_single_value_candidate() {
|
||||
// a candidate is one cell: a multi-value grid flag is refused
|
||||
assert!(parse_generalize_args(&[
|
||||
// a candidate is one cell: a multi-value grid flag is refused WITH A REASON,
|
||||
// not a bare usage dump (fieldtest friction).
|
||||
let err = parse_generalize_args(&[
|
||||
"--real", "GER40,USDJPY", "--fast", "2,3", "--slow", "12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
]).is_err());
|
||||
]).unwrap_err();
|
||||
assert!(err.contains("single candidate"), "refusal must say why: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4115,10 +4129,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_generalize_refuses_fewer_than_two_instruments() {
|
||||
assert!(parse_generalize_args(&[
|
||||
// refused WITH A REASON naming the >=2 requirement (fieldtest friction).
|
||||
let err = parse_generalize_args(&[
|
||||
"--real", "GER40", "--fast", "3", "--slow", "12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
]).is_err());
|
||||
]).unwrap_err();
|
||||
assert!(err.contains("at least two instruments"), "refusal must say why: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user