feat(cli): generalize applicability tag + honest R-expectancy refusal prose (fieldtest 0108 F8)

--metrics now tags the four generalizable names (expectancy_r,
net_expectancy_r, sqn, sqn_normalized) via check_r_metric as the
predicate — the registry's own R-expectancy rule, no fourth roster
site (#190). The GeneralizeNonRMetric refusal names the real rule
with the roster pointer instead of mislabeling R-denominated-but-
unranked metrics (max_r_drawdown is a drawdown in R, not a 'pip
metric').

RED-first (tdd-author handoff): the extended --metrics pins and the
flipped prose pins (unit + seam) observed failing on the old output.

Gates: workspace 1026/0, clippy -D warnings clean.

closes #207
This commit is contained in:
2026-07-04 02:14:58 +02:00
parent 38ffe50b57
commit 891c40d64b
3 changed files with 51 additions and 12 deletions
+12 -6
View File
@@ -68,8 +68,9 @@ pub(crate) fn exec_fault_prose(f: &ExecFault) -> String {
"campaign has {available} instrument(s); std::generalize needs at least 2"
),
ExecFault::GeneralizeNonRMetric { metric } => format!(
"std::generalize metric \"{metric}\" is not an R metric (gross/net R \
families generalize; pip metrics do not)"
"std::generalize metric \"{metric}\" is not in the rankable R-expectancy \
family (expectancy_r, net_expectancy_r, sqn, sqn_normalized generalize; \
see: aura process introspect --metrics)"
),
ExecFault::ZeroBootstrapParam { stage, field } => format!(
"process stage {stage}: monte_carlo {field} must be > 0"
@@ -500,16 +501,21 @@ mod tests {
}
#[test]
/// generalize's R-metric refusal names the metric and the R-only
/// rationale (pip metrics are not comparable across instruments).
/// #207 (fieldtest 0108 F8): generalize's refusal names the REAL rule — the
/// rankable R-expectancy family that generalizes — and points at the roster
/// verb, instead of the old "pip metrics do not" mislabel (which was wrong
/// for R-denominated-but-unranked names like max_r_drawdown). The four
/// accepted names are enumerated in the prose as a fail-safe hint; the
/// durable source is the `(see: …)` roster pointer.
fn generalize_non_r_metric_prose_names_the_metric() {
let prose = exec_fault_prose(&ExecFault::GeneralizeNonRMetric {
metric: "total_pips".into(),
});
assert_eq!(
prose,
"std::generalize metric \"total_pips\" is not an R metric (gross/net R \
families generalize; pip metrics do not)"
"std::generalize metric \"total_pips\" is not in the rankable R-expectancy \
family (expectancy_r, net_expectancy_r, sqn, sqn_normalized generalize; \
see: aura process introspect --metrics)"
);
assert!(!prose.contains("GeneralizeNonRMetric"), "Debug leak: {prose}");
}
+9 -2
View File
@@ -82,7 +82,10 @@ fn print_metric_roster() {
for name in aura_research::metric_vocabulary() {
let rankable = aura_campaign::RANKABLE_METRICS.contains(name);
let gate = aura_campaign::PER_MEMBER_METRICS.contains(name);
let tag = match (rankable, gate) {
// The generalize applicability is the registry's own R-expectancy
// predicate — no fourth roster site (#190/#207).
let generalize = aura_registry::check_r_metric(name).is_ok();
let base = match (rankable, gate) {
(true, true) => "rankable | gate",
(false, true) => "gate",
// Unreachable today (the rankable roster is a subset of the
@@ -90,7 +93,11 @@ fn print_metric_roster() {
(true, false) => "rankable",
(false, false) => "annotation",
};
println!("{name:<24} {tag}");
if generalize {
println!("{name:<24} {base} | generalize");
} else {
println!("{name:<24} {base}");
}
}
}