From a5ca0e74816d0ab6ad6ccb23b6da00dcde5d5ab1 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 21 Jul 2026 14:59:35 +0200 Subject: [PATCH] feat: the select-rule refusal enumerates the accepted roster (closes #301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/aura-research/src/lib.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/aura-research/src/lib.rs b/crates/aura-research/src/lib.rs index f754de4..cea174e 100644 --- a/crates/aura-research/src/lib.rs +++ b/crates/aura-research/src/lib.rs @@ -295,10 +295,17 @@ fn select_from( ) -> Result { 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",