use std::collections::{BTreeMap, BTreeSet};
use rustev_contract::decimal::Decimal;
use rustev_contract::ids::{ArtifactId, PlanId};
use serde::{Deserialize, Serialize};
use crate::config::{AdapterRules, ConfigError};
use crate::gold::MAX_NAME;
use crate::measures::declared_name;
pub const MAX_SCORERS: usize = 16;
pub const MAX_LEVELS: usize = 16;
pub const MODEL_PREFIX: &str = "model/";
pub const HUMAN_SIDE: &str = "human:";
pub mod code {
pub const SCALE_MISMATCH: &str = "scale-mismatch";
pub const JUDGE_OUTPUT_KIND: &str = "judge-output-kind";
pub const MODEL_PREFIX: &str = "model-prefix";
pub const METRIC_UNAVAILABLE: &str = "metric-unavailable";
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScorerDescriptor {
pub id: String,
pub version: String,
pub kind: ScorerKind,
pub output: ScorerOutput,
pub rules: AdapterRules,
pub reads_gold: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub judge: Option<JudgeRef>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ScorerKind {
Deterministic,
Model,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum ScorerOutput {
Ordinal { levels: Vec<String> },
Bounded { min: Decimal, max: Decimal },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct JudgeRef {
pub plan: PlanId,
pub artifacts: Vec<ArtifactId>,
pub level_param: String,
pub subject_field: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RubricMetric {
pub name: String,
pub scorer: String,
pub aggregate: RubricAggregate,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RubricAggregate {
MeanLevelIndex,
ShareAtLeast(String),
Mean,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HumanScale {
pub name: String,
pub levels: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AgreementDecl {
pub name: String,
pub a: String,
pub b: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OriginFamily {
TextBackstop,
Extraction,
Screening,
Corroboration,
AcquisitionTier,
}
impl OriginFamily {
pub fn name(self) -> &'static str {
match self {
OriginFamily::TextBackstop => "text_backstop",
OriginFamily::Extraction => "extraction",
OriginFamily::Screening => "screening",
OriginFamily::Corroboration => "corroboration",
OriginFamily::AcquisitionTier => "acquisition_tier",
}
}
pub fn available(self) -> bool {
true
}
}
pub struct Declarations<'a> {
pub scorers: &'a [ScorerDescriptor],
pub rubric_metrics: &'a [RubricMetric],
pub human_scales: &'a [HumanScale],
pub agreements: &'a [AgreementDecl],
pub origin: &'a [OriginFamily],
}
fn refuse<T>(code: &str, detail: impl std::fmt::Display) -> Result<T, ConfigError> {
Err(ConfigError(format!("{code}: {detail}")))
}
fn bad<T>(detail: &str) -> Result<T, ConfigError> {
Err(ConfigError(detail.into()))
}
fn label_ok(s: &str) -> bool {
(1..=MAX_NAME).contains(&s.len())
}
fn levels_ok(levels: &[String]) -> bool {
let set: BTreeSet<&str> = levels.iter().map(String::as_str).collect();
(2..=MAX_LEVELS).contains(&levels.len())
&& set.len() == levels.len()
&& levels.iter().all(|l| label_ok(l))
}
struct Side<'a> {
levels: &'a [String],
model: bool,
opaque: bool,
}
impl Declarations<'_> {
fn scorer(&self, id: &str) -> Option<&ScorerDescriptor> {
self.scorers.iter().find(|s| s.id == id)
}
fn side(&self, side: &str) -> Result<Side<'_>, ConfigError> {
if let Some(scale) = side.strip_prefix(HUMAN_SIDE) {
let Some(h) = self.human_scales.iter().find(|h| h.name == scale) else {
return bad("an agreement names a human scale the configuration does not declare");
};
return Ok(Side {
levels: &h.levels,
model: false,
opaque: false,
});
}
let Some(s) = self.scorer(side) else {
return bad("an agreement names a scorer the configuration does not declare");
};
match &s.output {
ScorerOutput::Ordinal { levels } => Ok(Side {
levels,
model: s.kind == ScorerKind::Model,
opaque: s.rules == AdapterRules::Opaque,
}),
ScorerOutput::Bounded { .. } => refuse(
code::SCALE_MISMATCH,
format!("agreement side {side} is not ordinal"),
),
}
}
pub fn metric_names(&self) -> BTreeMap<String, (bool, bool)> {
let mut out = BTreeMap::new();
for r in self.rubric_metrics {
if let Some(s) = self.scorer(&r.scorer) {
let model = s.kind == ScorerKind::Model;
let prefix = if model { MODEL_PREFIX } else { "" };
out.insert(
format!("{prefix}rubric/{}", r.name),
(model, s.rules == AdapterRules::Opaque),
);
}
}
for a in self.agreements {
if let (Ok(x), Ok(y)) = (self.side(&a.a), self.side(&a.b)) {
let model = x.model || y.model;
let prefix = if model { MODEL_PREFIX } else { "" };
for stat in ["exact", "adjacent", "mean_abs_diff", "qwk"] {
out.insert(
format!("{prefix}agreement/{}/{stat}", a.name),
(model, x.opaque || y.opaque),
);
}
}
}
out
}
pub fn check(&self) -> Result<(), ConfigError> {
if self.scorers.len() > MAX_SCORERS {
return bad("at most 16 scorers");
}
let mut ids = BTreeSet::new();
for s in self.scorers {
if !declared_name(&s.id) || !ids.insert(s.id.as_str()) {
return bad("scorer ids are unique, 1 to 64 bytes of [A-Za-z0-9._-]");
}
if !(1..=64).contains(&s.version.len()) {
return bad("a scorer version is 1 to 64 bytes");
}
match &s.output {
ScorerOutput::Ordinal { levels } if !levels_ok(levels) => {
return bad("an ordinal scorer declares 2 to 16 unique level names");
}
ScorerOutput::Bounded { min, max } if min >= max => {
return bad("a bounded scorer's min is below its max");
}
_ => {}
}
match (s.kind, &s.judge, &s.output) {
(ScorerKind::Model, Some(_), ScorerOutput::Bounded { .. }) => {
return refuse(code::JUDGE_OUTPUT_KIND, &s.id);
}
(ScorerKind::Model, None, _) => {
return bad("a model scorer names its judge");
}
(ScorerKind::Deterministic, Some(_), _) => {
return bad("only a model scorer names a judge");
}
_ => {}
}
if let Some(j) = &s.judge
&& (!(1..=256).contains(&j.level_param.len())
|| !(1..=256).contains(&j.subject_field.len()))
{
return bad("a judge's level_param and subject_field are 1 to 256 bytes");
}
}
let mut names = BTreeSet::new();
for r in self.rubric_metrics {
if !declared_name(&r.name) || !names.insert(r.name.as_str()) {
return bad("rubric metric names are unique, 1 to 64 bytes of [A-Za-z0-9._-]");
}
let Some(s) = self.scorer(&r.scorer) else {
return bad("a rubric metric names a scorer the configuration does not declare");
};
match (&r.aggregate, &s.output) {
(RubricAggregate::MeanLevelIndex, ScorerOutput::Ordinal { .. })
| (RubricAggregate::Mean, ScorerOutput::Bounded { .. }) => {}
(RubricAggregate::ShareAtLeast(l), ScorerOutput::Ordinal { levels })
if levels.contains(l) => {}
_ => return refuse(code::SCALE_MISMATCH, &r.name),
}
}
let mut scales = BTreeSet::new();
for h in self.human_scales {
if !label_ok(&h.name) || !scales.insert(h.name.as_str()) {
return bad("human scale names are unique, 1 to 256 bytes");
}
if !levels_ok(&h.levels) {
return bad("a human scale declares 2 to 16 unique level names");
}
}
let mut agreements = BTreeSet::new();
for a in self.agreements {
if !declared_name(&a.name) || !agreements.insert(a.name.as_str()) {
return bad("agreement names are unique, 1 to 64 bytes of [A-Za-z0-9._-]");
}
let (x, y) = (self.side(&a.a)?, self.side(&a.b)?);
if x.levels != y.levels {
return refuse(code::SCALE_MISMATCH, &a.name);
}
}
let mut families = BTreeSet::new();
for f in self.origin {
if !families.insert(f) {
return bad("origin families are unique");
}
if !f.available() {
return refuse(code::METRIC_UNAVAILABLE, f.name());
}
}
Ok(())
}
}
pub fn check_prefix(
metric: &str,
names: &BTreeMap<String, (bool, bool)>,
) -> Result<(), ConfigError> {
let bare = metric.strip_prefix(MODEL_PREFIX).unwrap_or(metric);
let base = bare.split('/').next().unwrap_or_default();
if base != "rubric" && base != "agreement" {
return Ok(());
}
let prefixed = format!("{MODEL_PREFIX}{bare}");
let model = names.get(bare).is_none() && names.get(&prefixed).is_some_and(|(m, _)| *m);
let named_model = metric.starts_with(MODEL_PREFIX);
let known = names.contains_key(bare) || names.contains_key(&prefixed);
if known && model != named_model {
return refuse(code::MODEL_PREFIX, metric);
}
Ok(())
}