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:
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -193,8 +193,15 @@ fn process_introspect_no_flag_is_usage_exit_2() {
|
||||
/// scalar, from `aura_campaign::PER_MEMBER_METRICS`), or `annotation` (in
|
||||
/// neither roster — a selection annotation that can neither rank nor gate). The
|
||||
/// split the executor enforces at preflight (a metric may exist yet not be
|
||||
/// rankable, F9) is thereby readable before any trial run. Substrings are
|
||||
/// pinned, not exact column widths.
|
||||
/// rankable, F9) is thereby readable before any trial run. #207 (fieldtest
|
||||
/// 0108 F8) adds a fourth tag `generalize` on the rankable R-expectancy family
|
||||
/// (`expectancy_r`, `net_expectancy_r`, `sqn`, `sqn_normalized`) — exactly the
|
||||
/// names `aura_registry::check_r_metric` accepts, i.e. std::generalize's
|
||||
/// cross-instrument accept-set — so an author can read WHICH metrics generalize
|
||||
/// before authoring the stage. A rankable non-R metric (`total_pips`) and an
|
||||
/// R-denominated-but-unranked metric (`max_r_drawdown`) both stay OUT of it:
|
||||
/// the tag tracks `check_r_metric`, not the presence of `_r` in the name.
|
||||
/// Substrings are pinned, not exact column widths.
|
||||
#[test]
|
||||
fn process_introspect_metrics_lists_vocabulary_with_applicability() {
|
||||
let (out, code) = run_code(&["process", "introspect", "--metrics"]);
|
||||
@@ -228,6 +235,22 @@ fn process_introspect_metrics_lists_vocabulary_with_applicability() {
|
||||
// deflated_score is a selection annotation — in neither roster.
|
||||
let ds = line_for("deflated_score");
|
||||
assert!(ds.contains("annotation"), "deflated_score tag: {ds}");
|
||||
|
||||
// #207: the four rankable R-expectancy names — std::generalize's
|
||||
// cross-instrument accept-set (aura_registry::check_r_metric) — carry the
|
||||
// `generalize` tag, so an author reads WHICH metrics generalize from the
|
||||
// roster instead of guessing at the stage.
|
||||
for name in ["expectancy_r", "net_expectancy_r", "sqn", "sqn_normalized"] {
|
||||
let l = line_for(name);
|
||||
assert!(l.contains("generalize"), "{name} must carry the generalize tag: {l}");
|
||||
}
|
||||
|
||||
// A rankable-but-not-R metric (total_pips) and an R-DENOMINATED-but-unranked
|
||||
// metric (max_r_drawdown, a drawdown IN R) both stay out of the generalize
|
||||
// set: the tag tracks check_r_metric, never the `_r` in the name.
|
||||
assert!(!tp.contains("generalize"), "total_pips must not carry generalize: {tp}");
|
||||
let mrd = line_for("max_r_drawdown");
|
||||
assert!(!mrd.contains("generalize"), "max_r_drawdown must not carry generalize: {mrd}");
|
||||
}
|
||||
|
||||
/// `--metrics` joins the existing one-mode guard: exactly one introspect mode
|
||||
@@ -940,9 +963,12 @@ fn campaign_run_refuses_generalize_non_r_metric() {
|
||||
write_doc(dir, "genpip.campaign.json", &campaign_doc);
|
||||
let (out, code) = run_code_in(dir, &["campaign", "run", "genpip.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
// #207 (fieldtest 0108 F8): the refusal names the REAL rule (the rankable
|
||||
// R-expectancy family), never the mislabel "pip metric" — max_r_drawdown is
|
||||
// R-denominated yet unranked, so "pip metric" was always wrong.
|
||||
assert!(
|
||||
out.contains("std::generalize metric \"total_pips\" is not an R metric"),
|
||||
"the refusal names the offending metric: {out}"
|
||||
out.contains("std::generalize metric \"total_pips\" is not in the rankable R-expectancy family"),
|
||||
"the refusal names the offending metric and the real rule: {out}"
|
||||
);
|
||||
assert!(!out.contains("2 instrument"), "the arity guard must not fire when arity is fine: {out}");
|
||||
assert!(!out.contains("GeneralizeNonRMetric"), "Debug leak: {out}");
|
||||
|
||||
Reference in New Issue
Block a user