use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use rustev_contract::bounded::ParseLimits;
use rustev_contract::canonical::{CanonicalError, tagged_digest};
use rustev_contract::decimal::Decimal;
use rustev_contract::eval_report::{DatasetProvenance, Split};
use rustev_contract::ids::{ContentDigest, DatasetId, SnapshotId};
use rustev_contract::limits::REPLAY_V1;
use rustev_contract::{Document, DocumentError, Identified};
use serde::{Deserialize, Serialize};
use crate::doc::eval_document;
use crate::gold::Gold;
pub const DATASET: &str = "rustev.dataset/1";
pub const DATASET_V2: &str = "rustev.dataset/2";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DatasetManifest {
pub schema: String,
pub name: String,
pub provenance: DatasetProvenance,
pub adapter: AdapterRef,
pub cases: Vec<DatasetCase>,
}
impl Document for DatasetManifest {
const SCHEMA: &'static str = DATASET;
const LIMITS: ParseLimits = REPLAY_V1;
fn schema(&self) -> &str {
&self.schema
}
fn parse(bytes: &[u8]) -> Result<Self, DocumentError> {
let doc: Self = rustev_contract::bounded::parse_bounded(bytes, &Self::LIMITS)
.map_err(DocumentError::Parse)?;
if doc.schema != DATASET && doc.schema != DATASET_V2 {
return Err(DocumentError::Schema {
expected: DATASET_V2,
found: doc.schema.chars().take(80).collect(),
});
}
Ok(doc)
}
fn record_digest(&self) -> Result<ContentDigest, CanonicalError> {
let raw = self.record_canonical()?;
Ok(ContentDigest::parse(&tagged_digest(&self.schema, &raw))
.expect("a tagged digest is a content digest"))
}
}
impl Identified for DatasetManifest {
type Id = DatasetId;
fn wrap(digest: String) -> DatasetId {
DatasetId::parse(&digest).expect("a tagged digest is an identity")
}
fn id(&self) -> Result<DatasetId, CanonicalError> {
Ok(Self::wrap(tagged_digest(&self.schema, &self.canonical()?)))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AdapterRef {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DatasetCase {
pub id: String,
pub source: String,
pub snapshot: SnapshotId,
pub split: Split,
pub label: Label,
pub subgroups: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gold: Option<Gold>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum Label {
Missing,
Value(String),
}
impl Label {
pub fn value(&self) -> Option<&str> {
match self {
Label::Value(v) => Some(v),
Label::Missing => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DatasetError {
Schema,
Empty,
Provenance,
DuplicateCase {
case: String,
},
Leakage {
case: String,
identity: String,
},
Label {
case: String,
detail: String,
},
Adapter,
Gold {
case: String,
detail: String,
},
}
impl fmt::Display for DatasetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for DatasetError {}
impl DatasetManifest {
pub fn check_structure(&self) -> Result<(), DatasetError> {
let adapter = self.adapter.clone();
self.check(&adapter, |_| Ok(()))
}
pub fn check(
&self,
adapter: &AdapterRef,
check_label: impl Fn(&str) -> Result<(), String>,
) -> Result<(), DatasetError> {
if self.schema != DATASET && self.schema != DATASET_V2 {
return Err(DatasetError::Schema);
}
if &self.adapter != adapter {
return Err(DatasetError::Adapter);
}
if self.cases.is_empty() {
return Err(DatasetError::Empty);
}
if let DatasetProvenance::Real {
source,
license,
labeling,
limitations,
} = &self.provenance
&& [source, license, labeling, limitations]
.iter()
.any(|s| s.trim().is_empty())
{
return Err(DatasetError::Provenance);
}
let mut ids = BTreeSet::new();
let mut split_of: BTreeMap<String, Split> = BTreeMap::new();
for c in &self.cases {
if !ids.insert(&c.id) {
return Err(DatasetError::DuplicateCase { case: c.id.clone() });
}
for identity in [
format!("source:{}", c.source),
format!("snapshot:{}", c.snapshot),
] {
match split_of.get(&identity) {
Some(s) if *s != c.split => {
return Err(DatasetError::Leakage {
case: c.id.clone(),
identity,
});
}
_ => {
split_of.insert(identity, c.split);
}
}
}
if let Some(gold) = &c.gold {
let detail = if self.schema == DATASET {
Err("a version 1 manifest holds no gold".to_owned())
} else {
gold.check()
};
detail.map_err(|detail| DatasetError::Gold {
case: c.id.clone(),
detail,
})?;
}
if let Label::Value(v) = &c.label {
check_label(v).map_err(|detail| DatasetError::Label {
case: c.id.clone(),
detail,
})?;
}
}
Ok(())
}
pub fn split(&self, split: Split) -> impl Iterator<Item = &DatasetCase> {
self.cases.iter().filter(move |c| c.split == split)
}
}
pub const DATASET_ADMISSION: &str = "rustev.dataset-admission/1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AdmissionRecord {
pub schema: String,
pub dataset: DatasetId,
pub source: AdmissionSource,
pub license: LicenseTerms,
pub privacy: PrivacyBasis,
pub labeling: LabelingMethod,
pub systems_under_test: Vec<ModelRef>,
pub task_mapping: TaskMapping,
pub splits: SplitAssignment,
pub population: Population,
pub exposure: Exposure,
pub review: OwnerReview,
}
eval_document!(AdmissionRecord, DATASET_ADMISSION, REPLAY_V1, ContentDigest);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum AdmissionSource {
Public {
publisher: String,
version: String,
content_digest: ContentDigest,
},
Commissioned {
labeled_by: String,
instruction: String,
content_digest: ContentDigest,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LicenseTerms {
pub license: String,
pub may_commit: bool,
pub may_retain_outside_git: bool,
pub may_send_to_provider: bool,
pub may_redistribute_in_report: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PrivacyBasis {
pub personal_data: bool,
pub derived_from_real_user_data: bool,
pub privacy_decision: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LabelingMethod {
pub guidelines: String,
pub annotators_per_case: u64,
pub agreement: AgreementStatistic,
pub adjudication: String,
pub producers: Vec<LabelProducer>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AgreementStatistic {
pub statistic: String,
pub value: Decimal,
pub denominator: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum LabelProducer {
Human { description: String },
Model(ModelRef),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ModelRef {
pub family: String,
pub identity: String,
}
impl ModelRef {
pub fn related(&self, other: &ModelRef) -> bool {
self.family == other.family || self.identity == other.identity
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TaskMapping {
pub adapter: AdapterRef,
pub rules: ContentDigest,
pub labels: Vec<LabelMapping>,
pub unmapped: u64,
pub unlabeled: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LabelMapping {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SplitAssignment {
pub method: String,
pub seed: String,
pub training: u64,
pub model_selection: u64,
pub calibration: u64,
pub final_test: u64,
pub near_duplicates: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Population {
pub id: String,
pub description: String,
pub limitations: String,
pub slices: Vec<SliceSize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SliceSize {
pub name: String,
pub cases: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Exposure {
pub training_data: String,
pub exposed_final_test: Vec<String>,
pub ranking_caveat: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OwnerReview {
pub reviewer: String,
pub date: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AdmissionError {
Schema,
Dataset(DatasetError),
Synthetic,
WrongDataset,
Incomplete(&'static str),
PrivacyDecisionMissing,
LabelsNotIndependent {
producer: ModelRef,
},
Adapter,
DuplicateMapping {
from: String,
},
MissingLabels {
declared: u64,
manifest: u64,
},
SplitCount {
split: &'static str,
},
Slice {
name: String,
},
Exposure {
case: String,
},
}
impl fmt::Display for AdmissionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for AdmissionError {}
#[derive(Debug, Clone)]
pub struct Admitted {
record: AdmissionRecord,
id: ContentDigest,
}
impl Admitted {
pub fn record(&self) -> &AdmissionRecord {
&self.record
}
pub fn id(&self) -> &ContentDigest {
&self.id
}
pub fn dataset(&self) -> &DatasetId {
&self.record.dataset
}
pub fn related_labeler(&self, model: &ModelRef) -> Option<&ModelRef> {
self.record.labeling.producers.iter().find_map(|p| match p {
LabelProducer::Model(m) if m.related(model) => Some(m),
_ => None,
})
}
pub fn declares_system(&self, model: &ModelRef) -> bool {
self.record.systems_under_test.contains(model)
}
}
pub fn admit(
manifest: &DatasetManifest,
record: &AdmissionRecord,
) -> Result<Admitted, AdmissionError> {
use AdmissionError as E;
if record.schema != DATASET_ADMISSION {
return Err(E::Schema);
}
manifest.check_structure().map_err(E::Dataset)?;
if matches!(manifest.provenance, DatasetProvenance::Synthetic { .. }) {
return Err(E::Synthetic);
}
if manifest.id().ok().as_ref() != Some(&record.dataset) {
return Err(E::WrongDataset);
}
let blank = |s: &str| s.trim().is_empty();
let need = |ok: bool, what: &'static str| {
if ok { Ok(()) } else { Err(E::Incomplete(what)) }
};
match &record.source {
AdmissionSource::Public {
publisher, version, ..
} => {
need(!blank(publisher) && !blank(version), "source")?;
need(
!blank(&record.exposure.training_data),
"training-data exposure",
)?;
}
AdmissionSource::Commissioned {
labeled_by,
instruction,
..
} => need(!blank(labeled_by) && !blank(instruction), "source")?,
}
need(!blank(&record.license.license), "license")?;
if record.privacy.derived_from_real_user_data
&& record.privacy.privacy_decision.as_deref().is_none_or(blank)
{
return Err(E::PrivacyDecisionMissing);
}
let l = &record.labeling;
need(
!blank(&l.guidelines)
&& !blank(&l.adjudication)
&& l.annotators_per_case > 0
&& !l.producers.is_empty(),
"labeling method",
)?;
need(
!blank(&l.agreement.statistic)
&& l.agreement.denominator > 0
&& !l.agreement.value.is_negative(),
"agreement statistic",
)?;
for p in &l.producers {
let ok = match p {
LabelProducer::Human { description } => !blank(description),
LabelProducer::Model(m) => !blank(&m.family) && !blank(&m.identity),
};
need(ok, "label producer")?;
}
need(!record.systems_under_test.is_empty(), "systems under test")?;
for s in &record.systems_under_test {
need(
!blank(&s.family) && !blank(&s.identity),
"systems under test",
)?;
for p in &l.producers {
if let LabelProducer::Model(m) = p
&& m.related(s)
{
return Err(E::LabelsNotIndependent {
producer: m.clone(),
});
}
}
}
let m = &record.task_mapping;
if m.adapter != manifest.adapter {
return Err(E::Adapter);
}
let mut froms = BTreeSet::new();
for x in &m.labels {
need(!blank(&x.from) && !blank(&x.to), "label mapping")?;
if !froms.insert(&x.from) {
return Err(E::DuplicateMapping {
from: x.from.clone(),
});
}
}
let missing = manifest
.cases
.iter()
.filter(|c| c.label == Label::Missing)
.count() as u64;
let declared = m.unmapped.saturating_add(m.unlabeled);
if declared != missing {
return Err(E::MissingLabels {
declared,
manifest: missing,
});
}
let s = &record.splits;
need(
!blank(&s.method) && !blank(&s.seed) && !blank(&s.near_duplicates),
"split assignment",
)?;
for (split, n, name) in [
(Split::Training, s.training, "training"),
(Split::ModelSelection, s.model_selection, "model_selection"),
(Split::Calibration, s.calibration, "calibration"),
(Split::FinalTest, s.final_test, "final_test"),
] {
if manifest.split(split).count() as u64 != n {
return Err(E::SplitCount { split: name });
}
}
let p = &record.population;
need(
!blank(&p.id) && !blank(&p.description) && !blank(&p.limitations),
"population",
)?;
let mut names = BTreeSet::new();
for slice in &p.slices {
let size = manifest
.cases
.iter()
.filter(|c| c.subgroups.contains(&slice.name))
.count() as u64;
if !names.insert(&slice.name) || size == 0 || size != slice.cases {
return Err(E::Slice {
name: slice.name.clone(),
});
}
}
let mut exposed = BTreeSet::new();
for case in &record.exposure.exposed_final_test {
let ok = manifest.split(Split::FinalTest).any(|c| &c.id == case);
if !ok || !exposed.insert(case) {
return Err(E::Exposure { case: case.clone() });
}
}
if record.exposure.ranking_caveat.as_deref().is_some_and(blank) {
return Err(E::Incomplete("ranking caveat"));
}
need(
!blank(&record.review.reviewer) && !blank(&record.review.date),
"owner review",
)?;
let id = record.id().map_err(|_| E::Incomplete("canonical form"))?;
Ok(Admitted {
record: record.clone(),
id,
})
}