1use std::collections::{BTreeMap, BTreeSet};
17use std::fmt;
18
19use rustev_contract::bounded::ParseLimits;
20use rustev_contract::canonical::{CanonicalError, tagged_digest};
21use rustev_contract::decimal::Decimal;
22use rustev_contract::eval_report::{DatasetProvenance, Split};
23use rustev_contract::ids::{ContentDigest, DatasetId, SnapshotId};
24use rustev_contract::limits::REPLAY_V1;
25use rustev_contract::{Document, DocumentError, Identified};
26use serde::{Deserialize, Serialize};
27
28use crate::doc::eval_document;
29use crate::gold::Gold;
30
31pub const DATASET: &str = "rustev.dataset/1";
32pub const DATASET_V2: &str = "rustev.dataset/2";
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct DatasetManifest {
38 pub schema: String,
39 pub name: String,
40 pub provenance: DatasetProvenance,
42 pub adapter: AdapterRef,
44 pub cases: Vec<DatasetCase>,
45}
46
47impl Document for DatasetManifest {
51 const SCHEMA: &'static str = DATASET;
52 const LIMITS: ParseLimits = REPLAY_V1;
53 fn schema(&self) -> &str {
54 &self.schema
55 }
56
57 fn parse(bytes: &[u8]) -> Result<Self, DocumentError> {
58 let doc: Self = rustev_contract::bounded::parse_bounded(bytes, &Self::LIMITS)
59 .map_err(DocumentError::Parse)?;
60 if doc.schema != DATASET && doc.schema != DATASET_V2 {
61 return Err(DocumentError::Schema {
62 expected: DATASET_V2,
63 found: doc.schema.chars().take(80).collect(),
64 });
65 }
66 Ok(doc)
67 }
68
69 fn record_digest(&self) -> Result<ContentDigest, CanonicalError> {
70 let raw = self.record_canonical()?;
71 Ok(ContentDigest::parse(&tagged_digest(&self.schema, &raw))
72 .expect("a tagged digest is a content digest"))
73 }
74}
75
76impl Identified for DatasetManifest {
77 type Id = DatasetId;
78 fn wrap(digest: String) -> DatasetId {
79 DatasetId::parse(&digest).expect("a tagged digest is an identity")
80 }
81
82 fn id(&self) -> Result<DatasetId, CanonicalError> {
83 Ok(Self::wrap(tagged_digest(&self.schema, &self.canonical()?)))
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
89#[serde(deny_unknown_fields)]
90pub struct AdapterRef {
91 pub name: String,
92 pub version: String,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(deny_unknown_fields)]
97pub struct DatasetCase {
98 pub id: String,
99 pub source: String,
101 pub snapshot: SnapshotId,
102 pub split: Split,
103 pub label: Label,
104 pub subgroups: Vec<String>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub gold: Option<Gold>,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case", deny_unknown_fields)]
113pub enum Label {
114 Missing,
115 Value(String),
116}
117
118impl Label {
119 pub fn value(&self) -> Option<&str> {
120 match self {
121 Label::Value(v) => Some(v),
122 Label::Missing => None,
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum DatasetError {
130 Schema,
131 Empty,
132 Provenance,
134 DuplicateCase {
135 case: String,
136 },
137 Leakage {
139 case: String,
140 identity: String,
141 },
142 Label {
144 case: String,
145 detail: String,
146 },
147 Adapter,
149 Gold {
152 case: String,
153 detail: String,
154 },
155}
156
157impl fmt::Display for DatasetError {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 write!(f, "{self:?}")
160 }
161}
162
163impl std::error::Error for DatasetError {}
164
165impl DatasetManifest {
166 pub fn check_structure(&self) -> Result<(), DatasetError> {
169 let adapter = self.adapter.clone();
170 self.check(&adapter, |_| Ok(()))
171 }
172
173 pub fn check(
176 &self,
177 adapter: &AdapterRef,
178 check_label: impl Fn(&str) -> Result<(), String>,
179 ) -> Result<(), DatasetError> {
180 if self.schema != DATASET && self.schema != DATASET_V2 {
181 return Err(DatasetError::Schema);
182 }
183 if &self.adapter != adapter {
184 return Err(DatasetError::Adapter);
185 }
186 if self.cases.is_empty() {
187 return Err(DatasetError::Empty);
188 }
189 if let DatasetProvenance::Real {
190 source,
191 license,
192 labeling,
193 limitations,
194 } = &self.provenance
195 && [source, license, labeling, limitations]
196 .iter()
197 .any(|s| s.trim().is_empty())
198 {
199 return Err(DatasetError::Provenance);
200 }
201 let mut ids = BTreeSet::new();
202 let mut split_of: BTreeMap<String, Split> = BTreeMap::new();
203 for c in &self.cases {
204 if !ids.insert(&c.id) {
205 return Err(DatasetError::DuplicateCase { case: c.id.clone() });
206 }
207 for identity in [
208 format!("source:{}", c.source),
209 format!("snapshot:{}", c.snapshot),
210 ] {
211 match split_of.get(&identity) {
212 Some(s) if *s != c.split => {
213 return Err(DatasetError::Leakage {
214 case: c.id.clone(),
215 identity,
216 });
217 }
218 _ => {
219 split_of.insert(identity, c.split);
220 }
221 }
222 }
223 if let Some(gold) = &c.gold {
224 let detail = if self.schema == DATASET {
225 Err("a version 1 manifest holds no gold".to_owned())
226 } else {
227 gold.check()
228 };
229 detail.map_err(|detail| DatasetError::Gold {
230 case: c.id.clone(),
231 detail,
232 })?;
233 }
234 if let Label::Value(v) = &c.label {
235 check_label(v).map_err(|detail| DatasetError::Label {
236 case: c.id.clone(),
237 detail,
238 })?;
239 }
240 }
241 Ok(())
242 }
243
244 pub fn split(&self, split: Split) -> impl Iterator<Item = &DatasetCase> {
246 self.cases.iter().filter(move |c| c.split == split)
247 }
248}
249
250pub const DATASET_ADMISSION: &str = "rustev.dataset-admission/1";
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(deny_unknown_fields)]
266pub struct AdmissionRecord {
267 pub schema: String,
268 pub dataset: DatasetId,
270 pub source: AdmissionSource,
271 pub license: LicenseTerms,
272 pub privacy: PrivacyBasis,
273 pub labeling: LabelingMethod,
274 pub systems_under_test: Vec<ModelRef>,
277 pub task_mapping: TaskMapping,
278 pub splits: SplitAssignment,
279 pub population: Population,
280 pub exposure: Exposure,
281 pub review: OwnerReview,
282}
283
284eval_document!(AdmissionRecord, DATASET_ADMISSION, REPLAY_V1, ContentDigest);
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "snake_case", deny_unknown_fields)]
289pub enum AdmissionSource {
290 Public {
293 publisher: String,
294 version: String,
295 content_digest: ContentDigest,
296 },
297 Commissioned {
299 labeled_by: String,
300 instruction: String,
301 content_digest: ContentDigest,
302 },
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
307#[serde(deny_unknown_fields)]
308pub struct LicenseTerms {
309 pub license: String,
311 pub may_commit: bool,
312 pub may_retain_outside_git: bool,
313 pub may_send_to_provider: bool,
314 pub may_redistribute_in_report: bool,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
319#[serde(deny_unknown_fields)]
320pub struct PrivacyBasis {
321 pub personal_data: bool,
322 pub derived_from_real_user_data: bool,
325 pub privacy_decision: Option<String>,
327}
328
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
331#[serde(deny_unknown_fields)]
332pub struct LabelingMethod {
333 pub guidelines: String,
334 pub annotators_per_case: u64,
335 pub agreement: AgreementStatistic,
336 pub adjudication: String,
337 pub producers: Vec<LabelProducer>,
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
343#[serde(deny_unknown_fields)]
344pub struct AgreementStatistic {
345 pub statistic: String,
346 pub value: Decimal,
347 pub denominator: u64,
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353#[serde(rename_all = "snake_case", deny_unknown_fields)]
354pub enum LabelProducer {
355 Human { description: String },
356 Model(ModelRef),
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
361#[serde(deny_unknown_fields)]
362pub struct ModelRef {
363 pub family: String,
364 pub identity: String,
365}
366
367impl ModelRef {
368 pub fn related(&self, other: &ModelRef) -> bool {
370 self.family == other.family || self.identity == other.identity
371 }
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
376#[serde(deny_unknown_fields)]
377pub struct TaskMapping {
378 pub adapter: AdapterRef,
379 pub rules: ContentDigest,
382 pub labels: Vec<LabelMapping>,
384 pub unmapped: u64,
387 pub unlabeled: u64,
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
392#[serde(deny_unknown_fields)]
393pub struct LabelMapping {
394 pub from: String,
395 pub to: String,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
402#[serde(deny_unknown_fields)]
403pub struct SplitAssignment {
404 pub method: String,
405 pub seed: String,
406 pub training: u64,
407 pub model_selection: u64,
408 pub calibration: u64,
409 pub final_test: u64,
410 pub near_duplicates: String,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(deny_unknown_fields)]
418pub struct Population {
419 pub id: String,
421 pub description: String,
422 pub limitations: String,
423 pub slices: Vec<SliceSize>,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(deny_unknown_fields)]
429pub struct SliceSize {
430 pub name: String,
431 pub cases: u64,
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436#[serde(deny_unknown_fields)]
437pub struct Exposure {
438 pub training_data: String,
441 pub exposed_final_test: Vec<String>,
444 pub ranking_caveat: Option<String>,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
450#[serde(deny_unknown_fields)]
451pub struct OwnerReview {
452 pub reviewer: String,
453 pub date: String,
454}
455
456#[derive(Debug, Clone, PartialEq, Eq)]
458pub enum AdmissionError {
459 Schema,
460 Dataset(DatasetError),
462 Synthetic,
465 WrongDataset,
467 Incomplete(&'static str),
469 PrivacyDecisionMissing,
471 LabelsNotIndependent {
473 producer: ModelRef,
474 },
475 Adapter,
477 DuplicateMapping {
479 from: String,
480 },
481 MissingLabels {
484 declared: u64,
485 manifest: u64,
486 },
487 SplitCount {
489 split: &'static str,
490 },
491 Slice {
494 name: String,
495 },
496 Exposure {
499 case: String,
500 },
501}
502
503impl fmt::Display for AdmissionError {
504 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505 write!(f, "{self:?}")
506 }
507}
508
509impl std::error::Error for AdmissionError {}
510
511#[derive(Debug, Clone)]
514pub struct Admitted {
515 record: AdmissionRecord,
516 id: ContentDigest,
517}
518
519impl Admitted {
520 pub fn record(&self) -> &AdmissionRecord {
521 &self.record
522 }
523 pub fn id(&self) -> &ContentDigest {
525 &self.id
526 }
527 pub fn dataset(&self) -> &DatasetId {
528 &self.record.dataset
529 }
530 pub fn related_labeler(&self, model: &ModelRef) -> Option<&ModelRef> {
533 self.record.labeling.producers.iter().find_map(|p| match p {
534 LabelProducer::Model(m) if m.related(model) => Some(m),
535 _ => None,
536 })
537 }
538 pub fn declares_system(&self, model: &ModelRef) -> bool {
540 self.record.systems_under_test.contains(model)
541 }
542}
543
544pub fn admit(
550 manifest: &DatasetManifest,
551 record: &AdmissionRecord,
552) -> Result<Admitted, AdmissionError> {
553 use AdmissionError as E;
554 if record.schema != DATASET_ADMISSION {
555 return Err(E::Schema);
556 }
557 manifest.check_structure().map_err(E::Dataset)?;
558 if matches!(manifest.provenance, DatasetProvenance::Synthetic { .. }) {
559 return Err(E::Synthetic);
560 }
561 if manifest.id().ok().as_ref() != Some(&record.dataset) {
562 return Err(E::WrongDataset);
563 }
564 let blank = |s: &str| s.trim().is_empty();
565 let need = |ok: bool, what: &'static str| {
566 if ok { Ok(()) } else { Err(E::Incomplete(what)) }
567 };
568 match &record.source {
569 AdmissionSource::Public {
570 publisher, version, ..
571 } => {
572 need(!blank(publisher) && !blank(version), "source")?;
573 need(
574 !blank(&record.exposure.training_data),
575 "training-data exposure",
576 )?;
577 }
578 AdmissionSource::Commissioned {
579 labeled_by,
580 instruction,
581 ..
582 } => need(!blank(labeled_by) && !blank(instruction), "source")?,
583 }
584 need(!blank(&record.license.license), "license")?;
585 if record.privacy.derived_from_real_user_data
586 && record.privacy.privacy_decision.as_deref().is_none_or(blank)
587 {
588 return Err(E::PrivacyDecisionMissing);
589 }
590 let l = &record.labeling;
591 need(
592 !blank(&l.guidelines)
593 && !blank(&l.adjudication)
594 && l.annotators_per_case > 0
595 && !l.producers.is_empty(),
596 "labeling method",
597 )?;
598 need(
599 !blank(&l.agreement.statistic)
600 && l.agreement.denominator > 0
601 && !l.agreement.value.is_negative(),
602 "agreement statistic",
603 )?;
604 for p in &l.producers {
605 let ok = match p {
606 LabelProducer::Human { description } => !blank(description),
607 LabelProducer::Model(m) => !blank(&m.family) && !blank(&m.identity),
608 };
609 need(ok, "label producer")?;
610 }
611 need(!record.systems_under_test.is_empty(), "systems under test")?;
612 for s in &record.systems_under_test {
613 need(
614 !blank(&s.family) && !blank(&s.identity),
615 "systems under test",
616 )?;
617 for p in &l.producers {
618 if let LabelProducer::Model(m) = p
619 && m.related(s)
620 {
621 return Err(E::LabelsNotIndependent {
622 producer: m.clone(),
623 });
624 }
625 }
626 }
627 let m = &record.task_mapping;
628 if m.adapter != manifest.adapter {
629 return Err(E::Adapter);
630 }
631 let mut froms = BTreeSet::new();
632 for x in &m.labels {
633 need(!blank(&x.from) && !blank(&x.to), "label mapping")?;
634 if !froms.insert(&x.from) {
635 return Err(E::DuplicateMapping {
636 from: x.from.clone(),
637 });
638 }
639 }
640 let missing = manifest
641 .cases
642 .iter()
643 .filter(|c| c.label == Label::Missing)
644 .count() as u64;
645 let declared = m.unmapped.saturating_add(m.unlabeled);
646 if declared != missing {
647 return Err(E::MissingLabels {
648 declared,
649 manifest: missing,
650 });
651 }
652 let s = &record.splits;
653 need(
654 !blank(&s.method) && !blank(&s.seed) && !blank(&s.near_duplicates),
655 "split assignment",
656 )?;
657 for (split, n, name) in [
658 (Split::Training, s.training, "training"),
659 (Split::ModelSelection, s.model_selection, "model_selection"),
660 (Split::Calibration, s.calibration, "calibration"),
661 (Split::FinalTest, s.final_test, "final_test"),
662 ] {
663 if manifest.split(split).count() as u64 != n {
664 return Err(E::SplitCount { split: name });
665 }
666 }
667 let p = &record.population;
668 need(
669 !blank(&p.id) && !blank(&p.description) && !blank(&p.limitations),
670 "population",
671 )?;
672 let mut names = BTreeSet::new();
673 for slice in &p.slices {
674 let size = manifest
675 .cases
676 .iter()
677 .filter(|c| c.subgroups.contains(&slice.name))
678 .count() as u64;
679 if !names.insert(&slice.name) || size == 0 || size != slice.cases {
680 return Err(E::Slice {
681 name: slice.name.clone(),
682 });
683 }
684 }
685 let mut exposed = BTreeSet::new();
686 for case in &record.exposure.exposed_final_test {
687 let ok = manifest.split(Split::FinalTest).any(|c| &c.id == case);
688 if !ok || !exposed.insert(case) {
689 return Err(E::Exposure { case: case.clone() });
690 }
691 }
692 if record.exposure.ranking_caveat.as_deref().is_some_and(blank) {
693 return Err(E::Incomplete("ranking caveat"));
694 }
695 need(
696 !blank(&record.review.reviewer) && !blank(&record.review.date),
697 "owner review",
698 )?;
699 let id = record.id().map_err(|_| E::Incomplete("canonical form"))?;
700 Ok(Admitted {
701 record: record.clone(),
702 id,
703 })
704}