1use std::collections::{BTreeMap, BTreeSet};
12use std::fmt;
13
14use rustev_contract::Identified;
15use rustev_contract::decimal::Decimal;
16use rustev_contract::eval_report::{DatasetProvenance, Split};
17use rustev_contract::ids::{ContentDigest, DatasetId, SnapshotId};
18use rustev_contract::limits::REPLAY_V1;
19use serde::{Deserialize, Serialize};
20
21use crate::doc::eval_document;
22
23pub const DATASET: &str = "rustev.dataset/1";
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct DatasetManifest {
28 pub schema: String,
29 pub name: String,
30 pub provenance: DatasetProvenance,
32 pub adapter: AdapterRef,
34 pub cases: Vec<DatasetCase>,
35}
36
37eval_document!(DatasetManifest, DATASET, REPLAY_V1, DatasetId);
38
39#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct AdapterRef {
43 pub name: String,
44 pub version: String,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49pub struct DatasetCase {
50 pub id: String,
51 pub source: String,
53 pub snapshot: SnapshotId,
54 pub split: Split,
55 pub label: Label,
56 pub subgroups: Vec<String>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case", deny_unknown_fields)]
62pub enum Label {
63 Missing,
64 Value(String),
65}
66
67impl Label {
68 pub fn value(&self) -> Option<&str> {
69 match self {
70 Label::Value(v) => Some(v),
71 Label::Missing => None,
72 }
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum DatasetError {
79 Schema,
80 Empty,
81 Provenance,
83 DuplicateCase {
84 case: String,
85 },
86 Leakage {
88 case: String,
89 identity: String,
90 },
91 Label {
93 case: String,
94 detail: String,
95 },
96 Adapter,
98}
99
100impl fmt::Display for DatasetError {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "{self:?}")
103 }
104}
105
106impl std::error::Error for DatasetError {}
107
108impl DatasetManifest {
109 pub fn check_structure(&self) -> Result<(), DatasetError> {
112 let adapter = self.adapter.clone();
113 self.check(&adapter, |_| Ok(()))
114 }
115
116 pub fn check(
119 &self,
120 adapter: &AdapterRef,
121 check_label: impl Fn(&str) -> Result<(), String>,
122 ) -> Result<(), DatasetError> {
123 if self.schema != DATASET {
124 return Err(DatasetError::Schema);
125 }
126 if &self.adapter != adapter {
127 return Err(DatasetError::Adapter);
128 }
129 if self.cases.is_empty() {
130 return Err(DatasetError::Empty);
131 }
132 if let DatasetProvenance::Real {
133 source,
134 license,
135 labeling,
136 limitations,
137 } = &self.provenance
138 && [source, license, labeling, limitations]
139 .iter()
140 .any(|s| s.trim().is_empty())
141 {
142 return Err(DatasetError::Provenance);
143 }
144 let mut ids = BTreeSet::new();
145 let mut split_of: BTreeMap<String, Split> = BTreeMap::new();
146 for c in &self.cases {
147 if !ids.insert(&c.id) {
148 return Err(DatasetError::DuplicateCase { case: c.id.clone() });
149 }
150 for identity in [
151 format!("source:{}", c.source),
152 format!("snapshot:{}", c.snapshot),
153 ] {
154 match split_of.get(&identity) {
155 Some(s) if *s != c.split => {
156 return Err(DatasetError::Leakage {
157 case: c.id.clone(),
158 identity,
159 });
160 }
161 _ => {
162 split_of.insert(identity, c.split);
163 }
164 }
165 }
166 if let Label::Value(v) = &c.label {
167 check_label(v).map_err(|detail| DatasetError::Label {
168 case: c.id.clone(),
169 detail,
170 })?;
171 }
172 }
173 Ok(())
174 }
175
176 pub fn split(&self, split: Split) -> impl Iterator<Item = &DatasetCase> {
178 self.cases.iter().filter(move |c| c.split == split)
179 }
180}
181
182pub const DATASET_ADMISSION: &str = "rustev.dataset-admission/1";
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197#[serde(deny_unknown_fields)]
198pub struct AdmissionRecord {
199 pub schema: String,
200 pub dataset: DatasetId,
202 pub source: AdmissionSource,
203 pub license: LicenseTerms,
204 pub privacy: PrivacyBasis,
205 pub labeling: LabelingMethod,
206 pub systems_under_test: Vec<ModelRef>,
209 pub task_mapping: TaskMapping,
210 pub splits: SplitAssignment,
211 pub population: Population,
212 pub exposure: Exposure,
213 pub review: OwnerReview,
214}
215
216eval_document!(AdmissionRecord, DATASET_ADMISSION, REPLAY_V1, ContentDigest);
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(rename_all = "snake_case", deny_unknown_fields)]
221pub enum AdmissionSource {
222 Public {
225 publisher: String,
226 version: String,
227 content_digest: ContentDigest,
228 },
229 Commissioned {
231 labeled_by: String,
232 instruction: String,
233 content_digest: ContentDigest,
234 },
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239#[serde(deny_unknown_fields)]
240pub struct LicenseTerms {
241 pub license: String,
243 pub may_commit: bool,
244 pub may_retain_outside_git: bool,
245 pub may_send_to_provider: bool,
246 pub may_redistribute_in_report: bool,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(deny_unknown_fields)]
252pub struct PrivacyBasis {
253 pub personal_data: bool,
254 pub derived_from_real_user_data: bool,
257 pub privacy_decision: Option<String>,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263#[serde(deny_unknown_fields)]
264pub struct LabelingMethod {
265 pub guidelines: String,
266 pub annotators_per_case: u64,
267 pub agreement: AgreementStatistic,
268 pub adjudication: String,
269 pub producers: Vec<LabelProducer>,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
275#[serde(deny_unknown_fields)]
276pub struct AgreementStatistic {
277 pub statistic: String,
278 pub value: Decimal,
279 pub denominator: u64,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case", deny_unknown_fields)]
286pub enum LabelProducer {
287 Human { description: String },
288 Model(ModelRef),
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
293#[serde(deny_unknown_fields)]
294pub struct ModelRef {
295 pub family: String,
296 pub identity: String,
297}
298
299impl ModelRef {
300 pub fn related(&self, other: &ModelRef) -> bool {
302 self.family == other.family || self.identity == other.identity
303 }
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(deny_unknown_fields)]
309pub struct TaskMapping {
310 pub adapter: AdapterRef,
311 pub rules: ContentDigest,
314 pub labels: Vec<LabelMapping>,
316 pub unmapped: u64,
319 pub unlabeled: u64,
321}
322
323#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(deny_unknown_fields)]
325pub struct LabelMapping {
326 pub from: String,
327 pub to: String,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(deny_unknown_fields)]
335pub struct SplitAssignment {
336 pub method: String,
337 pub seed: String,
338 pub training: u64,
339 pub model_selection: u64,
340 pub calibration: u64,
341 pub final_test: u64,
342 pub near_duplicates: String,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
349#[serde(deny_unknown_fields)]
350pub struct Population {
351 pub id: String,
353 pub description: String,
354 pub limitations: String,
355 pub slices: Vec<SliceSize>,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
360#[serde(deny_unknown_fields)]
361pub struct SliceSize {
362 pub name: String,
363 pub cases: u64,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368#[serde(deny_unknown_fields)]
369pub struct Exposure {
370 pub training_data: String,
373 pub exposed_final_test: Vec<String>,
376 pub ranking_caveat: Option<String>,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
382#[serde(deny_unknown_fields)]
383pub struct OwnerReview {
384 pub reviewer: String,
385 pub date: String,
386}
387
388#[derive(Debug, Clone, PartialEq, Eq)]
390pub enum AdmissionError {
391 Schema,
392 Dataset(DatasetError),
394 Synthetic,
397 WrongDataset,
399 Incomplete(&'static str),
401 PrivacyDecisionMissing,
403 LabelsNotIndependent {
405 producer: ModelRef,
406 },
407 Adapter,
409 DuplicateMapping {
411 from: String,
412 },
413 MissingLabels {
416 declared: u64,
417 manifest: u64,
418 },
419 SplitCount {
421 split: &'static str,
422 },
423 Slice {
426 name: String,
427 },
428 Exposure {
431 case: String,
432 },
433}
434
435impl fmt::Display for AdmissionError {
436 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437 write!(f, "{self:?}")
438 }
439}
440
441impl std::error::Error for AdmissionError {}
442
443#[derive(Debug, Clone)]
446pub struct Admitted {
447 record: AdmissionRecord,
448 id: ContentDigest,
449}
450
451impl Admitted {
452 pub fn record(&self) -> &AdmissionRecord {
453 &self.record
454 }
455 pub fn id(&self) -> &ContentDigest {
457 &self.id
458 }
459 pub fn dataset(&self) -> &DatasetId {
460 &self.record.dataset
461 }
462 pub fn related_labeler(&self, model: &ModelRef) -> Option<&ModelRef> {
465 self.record.labeling.producers.iter().find_map(|p| match p {
466 LabelProducer::Model(m) if m.related(model) => Some(m),
467 _ => None,
468 })
469 }
470 pub fn declares_system(&self, model: &ModelRef) -> bool {
472 self.record.systems_under_test.contains(model)
473 }
474}
475
476pub fn admit(
482 manifest: &DatasetManifest,
483 record: &AdmissionRecord,
484) -> Result<Admitted, AdmissionError> {
485 use AdmissionError as E;
486 if record.schema != DATASET_ADMISSION {
487 return Err(E::Schema);
488 }
489 manifest.check_structure().map_err(E::Dataset)?;
490 if matches!(manifest.provenance, DatasetProvenance::Synthetic { .. }) {
491 return Err(E::Synthetic);
492 }
493 if manifest.id().ok().as_ref() != Some(&record.dataset) {
494 return Err(E::WrongDataset);
495 }
496 let blank = |s: &str| s.trim().is_empty();
497 let need = |ok: bool, what: &'static str| {
498 if ok { Ok(()) } else { Err(E::Incomplete(what)) }
499 };
500 match &record.source {
501 AdmissionSource::Public {
502 publisher, version, ..
503 } => {
504 need(!blank(publisher) && !blank(version), "source")?;
505 need(
506 !blank(&record.exposure.training_data),
507 "training-data exposure",
508 )?;
509 }
510 AdmissionSource::Commissioned {
511 labeled_by,
512 instruction,
513 ..
514 } => need(!blank(labeled_by) && !blank(instruction), "source")?,
515 }
516 need(!blank(&record.license.license), "license")?;
517 if record.privacy.derived_from_real_user_data
518 && record.privacy.privacy_decision.as_deref().is_none_or(blank)
519 {
520 return Err(E::PrivacyDecisionMissing);
521 }
522 let l = &record.labeling;
523 need(
524 !blank(&l.guidelines)
525 && !blank(&l.adjudication)
526 && l.annotators_per_case > 0
527 && !l.producers.is_empty(),
528 "labeling method",
529 )?;
530 need(
531 !blank(&l.agreement.statistic)
532 && l.agreement.denominator > 0
533 && !l.agreement.value.is_negative(),
534 "agreement statistic",
535 )?;
536 for p in &l.producers {
537 let ok = match p {
538 LabelProducer::Human { description } => !blank(description),
539 LabelProducer::Model(m) => !blank(&m.family) && !blank(&m.identity),
540 };
541 need(ok, "label producer")?;
542 }
543 need(!record.systems_under_test.is_empty(), "systems under test")?;
544 for s in &record.systems_under_test {
545 need(
546 !blank(&s.family) && !blank(&s.identity),
547 "systems under test",
548 )?;
549 for p in &l.producers {
550 if let LabelProducer::Model(m) = p
551 && m.related(s)
552 {
553 return Err(E::LabelsNotIndependent {
554 producer: m.clone(),
555 });
556 }
557 }
558 }
559 let m = &record.task_mapping;
560 if m.adapter != manifest.adapter {
561 return Err(E::Adapter);
562 }
563 let mut froms = BTreeSet::new();
564 for x in &m.labels {
565 need(!blank(&x.from) && !blank(&x.to), "label mapping")?;
566 if !froms.insert(&x.from) {
567 return Err(E::DuplicateMapping {
568 from: x.from.clone(),
569 });
570 }
571 }
572 let missing = manifest
573 .cases
574 .iter()
575 .filter(|c| c.label == Label::Missing)
576 .count() as u64;
577 let declared = m.unmapped.saturating_add(m.unlabeled);
578 if declared != missing {
579 return Err(E::MissingLabels {
580 declared,
581 manifest: missing,
582 });
583 }
584 let s = &record.splits;
585 need(
586 !blank(&s.method) && !blank(&s.seed) && !blank(&s.near_duplicates),
587 "split assignment",
588 )?;
589 for (split, n, name) in [
590 (Split::Training, s.training, "training"),
591 (Split::ModelSelection, s.model_selection, "model_selection"),
592 (Split::Calibration, s.calibration, "calibration"),
593 (Split::FinalTest, s.final_test, "final_test"),
594 ] {
595 if manifest.split(split).count() as u64 != n {
596 return Err(E::SplitCount { split: name });
597 }
598 }
599 let p = &record.population;
600 need(
601 !blank(&p.id) && !blank(&p.description) && !blank(&p.limitations),
602 "population",
603 )?;
604 let mut names = BTreeSet::new();
605 for slice in &p.slices {
606 let size = manifest
607 .cases
608 .iter()
609 .filter(|c| c.subgroups.contains(&slice.name))
610 .count() as u64;
611 if !names.insert(&slice.name) || size == 0 || size != slice.cases {
612 return Err(E::Slice {
613 name: slice.name.clone(),
614 });
615 }
616 }
617 let mut exposed = BTreeSet::new();
618 for case in &record.exposure.exposed_final_test {
619 let ok = manifest.split(Split::FinalTest).any(|c| &c.id == case);
620 if !ok || !exposed.insert(case) {
621 return Err(E::Exposure { case: case.clone() });
622 }
623 }
624 if record.exposure.ranking_caveat.as_deref().is_some_and(blank) {
625 return Err(E::Incomplete("ranking caveat"));
626 }
627 need(
628 !blank(&record.review.reviewer) && !blank(&record.review.date),
629 "owner review",
630 )?;
631 let id = record.id().map_err(|_| E::Incomplete("canonical form"))?;
632 Ok(Admitted {
633 record: record.clone(),
634 id,
635 })
636}