feat: the select-rule refusal enumerates the accepted roster (closes #301)

An unknown select rule in a process document now refuses with the
accepted vocabulary beside the quoted offending value:
  block std::sweep: unknown select rule "plateau" (accepted: argmax |
  plateau:mean | plateau:worst)
mirroring the roster-naming metric-refusal shape. The roster is derived
from slot_kind_label(SlotKind::SelectRule) — CLI describe output and
the document refusal stay in sync by construction — and a companion
test pins the "select rule: " prefix that derivation trims, so a
label-wording drift breaks a test instead of silently degrading the
prose. No alias-hint sentence: the enumeration alone rescues the
bare-plateau paste case (fieldtest df_3), and the document schema stays
strict per #300 F7.
This commit is contained in:
2026-07-21 14:59:35 +02:00
parent edb8c37916
commit a5ca0e7481
+25 -4
View File
@@ -295,10 +295,17 @@ fn select_from(
) -> Result<SelectRule, String> {
let s = require_str(m, "select", block)?;
// SelectRule's own derived Deserialize (which carries the #[serde(rename)]
// wire strings) is the type oracle, mirroring kind_tag/scalar_from_bare:
// no second hardcoded copy of the three select-rule wire strings.
serde_json::from_value(serde_json::Value::String(s.clone()))
.map_err(|_| format!("block {block}: unknown select rule \"{s}\""))
// wire strings) is the type oracle for *parsing*, mirroring
// kind_tag/scalar_from_bare. The refusal's roster text below is sourced
// from slot_kind_label(SlotKind::SelectRule) rather than a second
// hardcoded copy of the three wire strings: CLI describe output and the
// document refusal stay in sync by construction (add a SelectRule
// variant, update one string).
serde_json::from_value(serde_json::Value::String(s.clone())).map_err(|_| {
let roster =
slot_kind_label(SlotKind::SelectRule).trim_start_matches("select rule: ");
format!("block {block}: unknown select rule \"{s}\" (accepted: {roster})")
})
}
fn mode_from(
@@ -1413,6 +1420,20 @@ mod tests {
assert!(!msg.contains("alias"), "refusal grew an alias hint: {msg}");
}
/// The refusal derives its roster from slot_kind_label by trimming the
/// "select rule: " prefix. That trim is a positional contract: if the
/// label's wording ever drifts, trim_start_matches degrades to a no-op
/// and the refusal grows a redundant prefix without any test noticing.
/// This pin makes that drift loud.
#[test]
fn select_rule_label_keeps_the_prefix_the_refusal_trims() {
assert!(
slot_kind_label(SlotKind::SelectRule).starts_with("select rule: "),
"slot_kind_label(SelectRule) no longer starts with \"select rule: \"\
update select_from's roster derivation alongside the label"
);
}
pub(crate) const CAMPAIGN_FIXTURE: &str = r#"{
"format_version": 1,
"kind": "campaign",