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:
2026-06-26 21:35:10 +02:00
parent 1088320571
commit 3e8fd73f68
4 changed files with 74 additions and 238 deletions
+38 -22
View File
@@ -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]