1use std::collections::{BTreeMap, BTreeSet};
12
13use rustev_contract::decimal::Decimal;
14use rustev_contract::ids::{ArtifactId, PlanId};
15use serde::{Deserialize, Serialize};
16
17use crate::config::{AdapterRules, ConfigError};
18use crate::gold::MAX_NAME;
19use crate::measures::declared_name;
20
21pub const MAX_SCORERS: usize = 16;
23pub const MAX_LEVELS: usize = 16;
25pub const MODEL_PREFIX: &str = "model/";
27pub const HUMAN_SIDE: &str = "human:";
29
30pub mod code {
32 pub const SCALE_MISMATCH: &str = "scale-mismatch";
33 pub const JUDGE_OUTPUT_KIND: &str = "judge-output-kind";
34 pub const MODEL_PREFIX: &str = "model-prefix";
35 pub const METRIC_UNAVAILABLE: &str = "metric-unavailable";
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct ScorerDescriptor {
42 pub id: String,
43 pub version: String,
44 pub kind: ScorerKind,
45 pub output: ScorerOutput,
46 pub rules: AdapterRules,
49 pub reads_gold: bool,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub judge: Option<JudgeRef>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum ScorerKind {
59 Deterministic,
60 Model,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case", deny_unknown_fields)]
66pub enum ScorerOutput {
67 Ordinal { levels: Vec<String> },
69 Bounded { min: Decimal, max: Decimal },
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct JudgeRef {
77 pub plan: PlanId,
78 pub artifacts: Vec<ArtifactId>,
79 pub level_param: String,
81 pub subject_field: String,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct RubricMetric {
90 pub name: String,
91 pub scorer: String,
92 pub aggregate: RubricAggregate,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "snake_case")]
97pub enum RubricAggregate {
98 MeanLevelIndex,
100 ShareAtLeast(String),
102 Mean,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(deny_unknown_fields)]
109pub struct HumanScale {
110 pub name: String,
111 pub levels: Vec<String>,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(deny_unknown_fields)]
118pub struct AgreementDecl {
119 pub name: String,
120 pub a: String,
121 pub b: String,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub enum OriginFamily {
128 TextBackstop,
129 Extraction,
130 Screening,
131 Corroboration,
132 AcquisitionTier,
133}
134
135impl OriginFamily {
136 pub fn name(self) -> &'static str {
137 match self {
138 OriginFamily::TextBackstop => "text_backstop",
139 OriginFamily::Extraction => "extraction",
140 OriginFamily::Screening => "screening",
141 OriginFamily::Corroboration => "corroboration",
142 OriginFamily::AcquisitionTier => "acquisition_tier",
143 }
144 }
145
146 pub fn available(self) -> bool {
149 true
150 }
151}
152
153pub struct Declarations<'a> {
155 pub scorers: &'a [ScorerDescriptor],
156 pub rubric_metrics: &'a [RubricMetric],
157 pub human_scales: &'a [HumanScale],
158 pub agreements: &'a [AgreementDecl],
159 pub origin: &'a [OriginFamily],
160}
161
162fn refuse<T>(code: &str, detail: impl std::fmt::Display) -> Result<T, ConfigError> {
163 Err(ConfigError(format!("{code}: {detail}")))
164}
165
166fn bad<T>(detail: &str) -> Result<T, ConfigError> {
167 Err(ConfigError(detail.into()))
168}
169
170fn label_ok(s: &str) -> bool {
173 (1..=MAX_NAME).contains(&s.len())
174}
175
176fn levels_ok(levels: &[String]) -> bool {
177 let set: BTreeSet<&str> = levels.iter().map(String::as_str).collect();
178 (2..=MAX_LEVELS).contains(&levels.len())
179 && set.len() == levels.len()
180 && levels.iter().all(|l| label_ok(l))
181}
182
183struct Side<'a> {
186 levels: &'a [String],
187 model: bool,
188 opaque: bool,
189}
190
191impl Declarations<'_> {
192 fn scorer(&self, id: &str) -> Option<&ScorerDescriptor> {
193 self.scorers.iter().find(|s| s.id == id)
194 }
195
196 fn side(&self, side: &str) -> Result<Side<'_>, ConfigError> {
197 if let Some(scale) = side.strip_prefix(HUMAN_SIDE) {
198 let Some(h) = self.human_scales.iter().find(|h| h.name == scale) else {
199 return bad("an agreement names a human scale the configuration does not declare");
200 };
201 return Ok(Side {
202 levels: &h.levels,
203 model: false,
204 opaque: false,
205 });
206 }
207 let Some(s) = self.scorer(side) else {
208 return bad("an agreement names a scorer the configuration does not declare");
209 };
210 match &s.output {
211 ScorerOutput::Ordinal { levels } => Ok(Side {
212 levels,
213 model: s.kind == ScorerKind::Model,
214 opaque: s.rules == AdapterRules::Opaque,
215 }),
216 ScorerOutput::Bounded { .. } => refuse(
217 code::SCALE_MISMATCH,
218 format!("agreement side {side} is not ordinal"),
219 ),
220 }
221 }
222
223 pub fn metric_names(&self) -> BTreeMap<String, (bool, bool)> {
227 let mut out = BTreeMap::new();
228 for r in self.rubric_metrics {
229 if let Some(s) = self.scorer(&r.scorer) {
230 let model = s.kind == ScorerKind::Model;
231 let prefix = if model { MODEL_PREFIX } else { "" };
232 out.insert(
233 format!("{prefix}rubric/{}", r.name),
234 (model, s.rules == AdapterRules::Opaque),
235 );
236 }
237 }
238 for a in self.agreements {
239 if let (Ok(x), Ok(y)) = (self.side(&a.a), self.side(&a.b)) {
240 let model = x.model || y.model;
241 let prefix = if model { MODEL_PREFIX } else { "" };
242 for stat in ["exact", "adjacent", "mean_abs_diff", "qwk"] {
243 out.insert(
244 format!("{prefix}agreement/{}/{stat}", a.name),
245 (model, x.opaque || y.opaque),
246 );
247 }
248 }
249 }
250 out
251 }
252
253 pub fn check(&self) -> Result<(), ConfigError> {
255 if self.scorers.len() > MAX_SCORERS {
256 return bad("at most 16 scorers");
257 }
258 let mut ids = BTreeSet::new();
259 for s in self.scorers {
260 if !declared_name(&s.id) || !ids.insert(s.id.as_str()) {
261 return bad("scorer ids are unique, 1 to 64 bytes of [A-Za-z0-9._-]");
262 }
263 if !(1..=64).contains(&s.version.len()) {
264 return bad("a scorer version is 1 to 64 bytes");
265 }
266 match &s.output {
267 ScorerOutput::Ordinal { levels } if !levels_ok(levels) => {
268 return bad("an ordinal scorer declares 2 to 16 unique level names");
269 }
270 ScorerOutput::Bounded { min, max } if min >= max => {
271 return bad("a bounded scorer's min is below its max");
272 }
273 _ => {}
274 }
275 match (s.kind, &s.judge, &s.output) {
276 (ScorerKind::Model, Some(_), ScorerOutput::Bounded { .. }) => {
277 return refuse(code::JUDGE_OUTPUT_KIND, &s.id);
278 }
279 (ScorerKind::Model, None, _) => {
280 return bad("a model scorer names its judge");
281 }
282 (ScorerKind::Deterministic, Some(_), _) => {
283 return bad("only a model scorer names a judge");
284 }
285 _ => {}
286 }
287 if let Some(j) = &s.judge
288 && (!(1..=256).contains(&j.level_param.len())
289 || !(1..=256).contains(&j.subject_field.len()))
290 {
291 return bad("a judge's level_param and subject_field are 1 to 256 bytes");
292 }
293 }
294 let mut names = BTreeSet::new();
295 for r in self.rubric_metrics {
296 if !declared_name(&r.name) || !names.insert(r.name.as_str()) {
297 return bad("rubric metric names are unique, 1 to 64 bytes of [A-Za-z0-9._-]");
298 }
299 let Some(s) = self.scorer(&r.scorer) else {
300 return bad("a rubric metric names a scorer the configuration does not declare");
301 };
302 match (&r.aggregate, &s.output) {
303 (RubricAggregate::MeanLevelIndex, ScorerOutput::Ordinal { .. })
304 | (RubricAggregate::Mean, ScorerOutput::Bounded { .. }) => {}
305 (RubricAggregate::ShareAtLeast(l), ScorerOutput::Ordinal { levels })
306 if levels.contains(l) => {}
307 _ => return refuse(code::SCALE_MISMATCH, &r.name),
308 }
309 }
310 let mut scales = BTreeSet::new();
311 for h in self.human_scales {
312 if !label_ok(&h.name) || !scales.insert(h.name.as_str()) {
313 return bad("human scale names are unique, 1 to 256 bytes");
314 }
315 if !levels_ok(&h.levels) {
316 return bad("a human scale declares 2 to 16 unique level names");
317 }
318 }
319 let mut agreements = BTreeSet::new();
320 for a in self.agreements {
321 if !declared_name(&a.name) || !agreements.insert(a.name.as_str()) {
322 return bad("agreement names are unique, 1 to 64 bytes of [A-Za-z0-9._-]");
323 }
324 let (x, y) = (self.side(&a.a)?, self.side(&a.b)?);
325 if x.levels != y.levels {
326 return refuse(code::SCALE_MISMATCH, &a.name);
327 }
328 }
329 let mut families = BTreeSet::new();
330 for f in self.origin {
331 if !families.insert(f) {
332 return bad("origin families are unique");
333 }
334 if !f.available() {
335 return refuse(code::METRIC_UNAVAILABLE, f.name());
336 }
337 }
338 Ok(())
339 }
340}
341
342pub fn check_prefix(
346 metric: &str,
347 names: &BTreeMap<String, (bool, bool)>,
348) -> Result<(), ConfigError> {
349 let bare = metric.strip_prefix(MODEL_PREFIX).unwrap_or(metric);
350 let base = bare.split('/').next().unwrap_or_default();
351 if base != "rubric" && base != "agreement" {
352 return Ok(());
353 }
354 let prefixed = format!("{MODEL_PREFIX}{bare}");
355 let model = names.get(bare).is_none() && names.get(&prefixed).is_some_and(|(m, _)| *m);
356 let named_model = metric.starts_with(MODEL_PREFIX);
357 let known = names.contains_key(bare) || names.contains_key(&prefixed);
358 if known && model != named_model {
359 return refuse(code::MODEL_PREFIX, metric);
360 }
361 Ok(())
362}