1use core::marker::PhantomData;
33use std::collections::BTreeMap;
34
35use crate::dedup::{coalesced_exclusions, ExclusionRecord};
36use crate::error::ContrastiveDataError;
37use crate::hash::{
38 DatasetFingerprint, DatasetFingerprintInput, SplitFingerprint, SplitFingerprintInput,
39 CONTENT_NORMALIZATION_VERSION,
40};
41use crate::ledger::AccessLedger;
42use crate::schema::LabeledExample;
43use crate::split::{
44 CompatibilityTest, Split, SplitDeclaration, SplitRole, Test, Train, Validation,
45};
46
47pub trait DatasetProfile {
49 const PROFILE: &'static str;
51 type Splits: core::fmt::Debug + Clone + PartialEq + Eq;
58}
59
60#[derive(Debug, Clone, Copy)]
62pub struct Canonical;
63
64#[derive(Debug, Clone, Copy)]
67pub struct Compatibility;
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct CanonicalSplits {
72 train: Split<Train>,
73 validation: Split<Validation>,
74 test: Split<Test>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct CompatibilitySplits {
80 train: Split<Train>,
81 compatibility_test: Split<CompatibilityTest>,
82}
83
84impl DatasetProfile for Canonical {
85 const PROFILE: &'static str = "canonical";
86 type Splits = CanonicalSplits;
87}
88
89impl DatasetProfile for Compatibility {
90 const PROFILE: &'static str = "compatibility";
91 type Splits = CompatibilitySplits;
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct CanonicalDeclarations {
97 pub train: SplitDeclaration,
99 pub validation: SplitDeclaration,
101 pub test: SplitDeclaration,
103 pub label_names: Vec<String>,
105}
106
107impl CanonicalDeclarations {
108 pub fn check_label_maps_agree(&self) -> Result<(), ContrastiveDataError> {
122 for (split, decl) in [
123 ("train", &self.train),
124 ("validation", &self.validation),
125 ("test", &self.test),
126 ] {
127 if decl.label_names != self.label_names {
128 return Err(ContrastiveDataError::DeclaredLabelMapMismatch {
129 split: split.to_string(),
130 shared: self.label_names.clone(),
131 got: decl.label_names.clone(),
132 });
133 }
134 }
135 Ok(())
136 }
137}
138
139impl CompatibilityDeclarations {
140 pub fn check_label_maps_agree(&self) -> Result<(), ContrastiveDataError> {
150 for (split, decl) in [
151 ("train", &self.train),
152 ("compatibility_test", &self.compatibility_test),
153 ] {
154 if decl.label_names != self.label_names {
155 return Err(ContrastiveDataError::DeclaredLabelMapMismatch {
156 split: split.to_string(),
157 shared: self.label_names.clone(),
158 got: decl.label_names.clone(),
159 });
160 }
161 }
162 Ok(())
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct CompatibilityDeclarations {
169 pub train: SplitDeclaration,
171 pub compatibility_test: SplitDeclaration,
173 pub label_names: Vec<String>,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct PreparedJsonl {
180 splits: BTreeMap<String, Vec<u8>>,
181}
182
183impl PreparedJsonl {
184 pub fn get(&self, role: &str) -> Option<&[u8]> {
186 self.splits.get(role).map(Vec::as_slice)
187 }
188
189 pub fn as_map(&self) -> &BTreeMap<String, Vec<u8>> {
191 &self.splits
192 }
193}
194
195#[derive(Debug)]
201pub struct ValidationWitness<'a> {
202 validation: &'a Split<Validation>,
203 split_fingerprint: SplitFingerprint,
204 dataset_fingerprint: DatasetFingerprint,
205}
206
207impl ValidationWitness<'_> {
208 pub fn fingerprint_hex(&self) -> String {
215 self.split_fingerprint.hex()
216 }
217
218 pub fn dataset_fingerprint_hex(&self) -> String {
220 self.dataset_fingerprint.hex()
221 }
222
223 pub fn validation(&self) -> &Split<Validation> {
225 self.validation
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct PreparedDataset<P: DatasetProfile> {
232 splits: P::Splits,
233 exclusions: ExclusionRecord,
234 fingerprint: DatasetFingerprint,
235 label_names: Vec<String>,
243 profile: PhantomData<P>,
244}
245
246impl PreparedDataset<Canonical> {
247 #[provable_contracts_macros::contract(
264 "contrastive-pair-protocol-v1",
265 equation = "prepared_dataset_typestate"
266 )]
267 pub fn from_labeled_rows(
268 train: Vec<LabeledExample>,
269 validation: Vec<LabeledExample>,
270 test: Vec<LabeledExample>,
271 decls: &CanonicalDeclarations,
272 ledger: &mut AccessLedger,
273 ) -> Result<Self, ContrastiveDataError> {
274 decls.check_label_maps_agree()?;
275 let train = Split::<Train>::from_rows(train, &decls.train)?;
276 let validation = Split::<Validation>::from_rows(validation, &decls.validation)?;
277 let test = Split::<Test>::from_rows(test, &decls.test)?;
278 Ok(Self::from_validated_splits(
279 train,
280 validation,
281 test,
282 &decls.label_names,
283 ledger,
284 ))
285 }
286
287 pub(crate) fn from_validated_splits(
297 train: Split<Train>,
298 validation: Split<Validation>,
299 test: Split<Test>,
300 label_names: &[String],
301 ledger: &mut AccessLedger,
302 ) -> Self {
303 let fingerprint = {
304 let train_pairs = train.exact_hash_pairs();
305 let validation_pairs = validation.exact_hash_pairs();
306 let test_pairs = test.exact_hash_pairs();
307 let splits = [
308 fingerprint_input::<Test>(&test, &test_pairs),
309 fingerprint_input::<Train>(&train, &train_pairs),
310 fingerprint_input::<Validation>(&validation, &validation_pairs),
311 ];
312 DatasetFingerprint::compute(&DatasetFingerprintInput {
313 profile: Canonical::PROFILE,
314 label_names,
315 normalization_version: CONTENT_NORMALIZATION_VERSION,
316 splits: &splits,
317 })
318 };
319
320 let exclusions = coalesced_exclusions(&[
321 (Test::ROLE, test.rows()),
322 (Train::ROLE, train.rows()),
323 (Validation::ROLE, validation.rows()),
324 ]);
325
326 let fingerprint_hex = fingerprint.hex();
327 for role in [Train::ROLE, Validation::ROLE, Test::ROLE] {
328 ledger.record(role, Canonical::PROFILE, "ingest", &fingerprint_hex);
329 }
330
331 Self {
332 splits: CanonicalSplits {
333 train,
334 validation,
335 test,
336 },
337 exclusions,
338 fingerprint,
339 label_names: label_names.to_vec(),
340 profile: PhantomData,
341 }
342 }
343
344 pub fn label_names(&self) -> &[String] {
346 &self.label_names
347 }
348
349 pub fn train(&self) -> &Split<Train> {
351 &self.splits.train
352 }
353
354 pub fn validation(&self) -> &Split<Validation> {
356 &self.splits.validation
357 }
358
359 pub fn test(&self) -> &Split<Test> {
361 &self.splits.test
362 }
363
364 pub fn validation_witness(&self) -> ValidationWitness<'_> {
388 ValidationWitness {
389 validation: &self.splits.validation,
390 split_fingerprint: split_fingerprint_of::<Validation>(&self.splits.validation),
391 dataset_fingerprint: self.fingerprint.clone(),
392 }
393 }
394
395 pub fn exclusions(&self) -> &ExclusionRecord {
397 &self.exclusions
398 }
399
400 pub fn fingerprint(&self) -> &DatasetFingerprint {
402 &self.fingerprint
403 }
404
405 pub fn encode_jsonl(&self) -> Result<PreparedJsonl, ContrastiveDataError> {
411 let mut splits = BTreeMap::new();
412 splits.insert(
413 Train::ROLE.to_string(),
414 crate::schema::encode_jsonl(self.splits.train.rows())?,
415 );
416 splits.insert(
417 Validation::ROLE.to_string(),
418 crate::schema::encode_jsonl(self.splits.validation.rows())?,
419 );
420 splits.insert(
421 Test::ROLE.to_string(),
422 crate::schema::encode_jsonl(self.splits.test.rows())?,
423 );
424 Ok(PreparedJsonl { splits })
425 }
426}
427
428impl PreparedDataset<Compatibility> {
429 pub fn from_labeled_rows(
459 train: Vec<LabeledExample>,
460 compatibility_test: Vec<LabeledExample>,
461 decls: &CompatibilityDeclarations,
462 ledger: &mut AccessLedger,
463 ) -> Result<Self, ContrastiveDataError> {
464 decls.check_label_maps_agree()?;
465 let train = Split::<Train>::from_rows(train, &decls.train)?;
466 let compatibility_test =
467 Split::<CompatibilityTest>::from_rows(compatibility_test, &decls.compatibility_test)?;
468 Ok(Self::from_validated_splits(
469 train,
470 compatibility_test,
471 &decls.label_names,
472 ledger,
473 ))
474 }
475
476 pub(crate) fn from_validated_splits(
481 train: Split<Train>,
482 compatibility_test: Split<CompatibilityTest>,
483 label_names: &[String],
484 ledger: &mut AccessLedger,
485 ) -> Self {
486 let fingerprint = {
487 let train_pairs = train.exact_hash_pairs();
488 let compatibility_pairs = compatibility_test.exact_hash_pairs();
489 let splits = [
490 fingerprint_input::<CompatibilityTest>(&compatibility_test, &compatibility_pairs),
491 fingerprint_input::<Train>(&train, &train_pairs),
492 ];
493 DatasetFingerprint::compute(&DatasetFingerprintInput {
494 profile: Compatibility::PROFILE,
495 label_names,
496 normalization_version: CONTENT_NORMALIZATION_VERSION,
497 splits: &splits,
498 })
499 };
500
501 let exclusions = coalesced_exclusions(&[
502 (CompatibilityTest::ROLE, compatibility_test.rows()),
503 (Train::ROLE, train.rows()),
504 ]);
505
506 let fingerprint_hex = fingerprint.hex();
507 for role in [Train::ROLE, CompatibilityTest::ROLE] {
508 ledger.record(role, Compatibility::PROFILE, "ingest", &fingerprint_hex);
509 }
510
511 Self {
512 splits: CompatibilitySplits {
513 train,
514 compatibility_test,
515 },
516 exclusions,
517 fingerprint,
518 label_names: label_names.to_vec(),
519 profile: PhantomData,
520 }
521 }
522
523 pub fn label_names(&self) -> &[String] {
525 &self.label_names
526 }
527
528 pub fn train(&self) -> &Split<Train> {
530 &self.splits.train
531 }
532
533 pub fn compatibility_test(&self) -> &Split<CompatibilityTest> {
535 &self.splits.compatibility_test
536 }
537
538 pub fn exclusions(&self) -> &ExclusionRecord {
540 &self.exclusions
541 }
542
543 pub fn fingerprint(&self) -> &DatasetFingerprint {
545 &self.fingerprint
546 }
547
548 pub fn encode_jsonl(&self) -> Result<PreparedJsonl, ContrastiveDataError> {
554 let mut splits = BTreeMap::new();
555 splits.insert(
556 Train::ROLE.to_string(),
557 crate::schema::encode_jsonl(self.splits.train.rows())?,
558 );
559 splits.insert(
560 CompatibilityTest::ROLE.to_string(),
561 crate::schema::encode_jsonl(self.splits.compatibility_test.rows())?,
562 );
563 Ok(PreparedJsonl { splits })
564 }
565}
566
567fn fingerprint_input<'a, R: SplitRole>(
572 split: &'a Split<R>,
573 pairs: &'a [(&'a str, [u8; 32])],
574) -> SplitFingerprintInput<'a> {
575 SplitFingerprintInput {
576 role: R::ROLE,
577 source_hash: split.source_hash(),
578 class_counts: split.class_counts(),
579 rows: pairs,
580 }
581}
582
583fn split_fingerprint_of<R: SplitRole>(split: &Split<R>) -> SplitFingerprint {
587 let pairs = split.exact_hash_pairs();
588 SplitFingerprint::compute(&fingerprint_input::<R>(split, &pairs))
589}
590
591#[cfg(test)]
592mod prepared_tests {
593 use super::{
594 Canonical, CanonicalDeclarations, Compatibility, CompatibilityDeclarations, PreparedDataset,
595 };
596 use crate::dedup::coalesced_exclusions;
597 use crate::error::ContrastiveDataError;
598 use crate::ledger::AccessLedger;
599 use crate::schema::{encode_jsonl, parse_jsonl_bytes, LabeledExample};
600 use crate::split::SplitDeclaration;
601
602 fn label_names() -> Vec<String> {
603 vec![
604 "none".to_string(),
605 "against".to_string(),
606 "favor".to_string(),
607 ]
608 }
609
610 fn row(id: &str, input: &str, label: usize, split: &str) -> LabeledExample {
611 LabeledExample {
612 id: id.to_string(),
613 input: input.to_string(),
614 label,
615 label_text: label_names()[label].clone(),
616 source_split: split.to_string(),
617 }
618 }
619
620 fn decl(counts: Vec<usize>) -> SplitDeclaration {
621 SplitDeclaration {
622 expected_class_counts: counts,
623 label_names: label_names(),
624 }
625 }
626
627 fn train_rows() -> Vec<LabeledExample> {
628 vec![
629 row("train:0", "alpha post", 0, "train"),
630 row("train:1", "beta post", 1, "train"),
631 row("train:2", "gamma post", 2, "train"),
632 row("train:3", "delta post", 0, "train"),
633 ]
634 }
635
636 fn validation_rows() -> Vec<LabeledExample> {
637 vec![
638 row("validation:0", "epsilon post", 0, "validation"),
639 row("validation:1", "zeta post", 1, "validation"),
640 ]
641 }
642
643 fn test_rows() -> Vec<LabeledExample> {
644 vec![
645 row("test:0", "eta post", 2, "test"),
646 row("test:1", "theta post", 1, "test"),
647 ]
648 }
649
650 fn canonical_decls() -> CanonicalDeclarations {
651 CanonicalDeclarations {
652 train: decl(vec![2, 1, 1]),
653 validation: decl(vec![1, 1, 0]),
654 test: decl(vec![0, 1, 1]),
655 label_names: label_names(),
656 }
657 }
658
659 fn build_canonical(
660 ledger: &mut AccessLedger,
661 ) -> Result<PreparedDataset<Canonical>, ContrastiveDataError> {
662 PreparedDataset::<Canonical>::from_labeled_rows(
663 train_rows(),
664 validation_rows(),
665 test_rows(),
666 &canonical_decls(),
667 ledger,
668 )
669 }
670
671 fn compatibility_rows() -> Vec<LabeledExample> {
672 vec![
673 row("compatibility_test:0", "eta post", 2, "compatibility_test"),
674 row(
675 "compatibility_test:1",
676 "theta post",
677 1,
678 "compatibility_test",
679 ),
680 ]
681 }
682
683 fn compatibility_decls() -> CompatibilityDeclarations {
684 CompatibilityDeclarations {
685 train: decl(vec![2, 1, 1]),
686 compatibility_test: decl(vec![0, 1, 1]),
687 label_names: label_names(),
688 }
689 }
690
691 #[test]
692 fn prepared_canonical_binds_three_splits_and_records_three_accesses() {
693 let mut ledger = AccessLedger::new();
694 let dataset = build_canonical(&mut ledger).expect("valid canonical corpus");
695
696 assert_eq!(dataset.train().rows().len(), 4);
697 assert_eq!(dataset.validation().rows().len(), 2);
698 assert_eq!(dataset.test().rows().len(), 2);
699 assert_eq!(ledger.records().len(), 3);
700 assert!(ledger
701 .records()
702 .iter()
703 .all(|record| record.profile == "canonical"));
704 let roles: Vec<&str> = ledger
705 .records()
706 .iter()
707 .map(|record| record.role.as_str())
708 .collect();
709 assert_eq!(roles, vec!["train", "validation", "test"]);
710 assert!(ledger
711 .records()
712 .iter()
713 .all(|record| record.fingerprint_hex == dataset.fingerprint().hex()));
714 }
715
716 #[test]
717 fn prepared_compatibility_binds_two_splits_and_records_two_accesses() {
718 let mut ledger = AccessLedger::new();
719 let dataset = PreparedDataset::<Compatibility>::from_labeled_rows(
720 train_rows(),
721 compatibility_rows(),
722 &compatibility_decls(),
723 &mut ledger,
724 )
725 .expect("valid compatibility corpus");
726
727 assert_eq!(dataset.train().rows().len(), 4);
728 assert_eq!(dataset.compatibility_test().rows().len(), 2);
729 assert_eq!(ledger.records().len(), 2);
730 assert!(ledger
731 .records()
732 .iter()
733 .all(|record| record.profile == "compatibility"));
734 let roles: Vec<&str> = ledger
735 .records()
736 .iter()
737 .map(|record| record.role.as_str())
738 .collect();
739 assert_eq!(roles, vec!["train", "compatibility_test"]);
740 }
741
742 #[test]
743 fn prepared_witness_describes_this_dataset_and_the_validation_split_separately() {
744 let mut ledger = AccessLedger::new();
745 let dataset = build_canonical(&mut ledger).expect("valid canonical corpus");
746 let witness = dataset.validation_witness();
747
748 assert_eq!(
749 witness.dataset_fingerprint_hex(),
750 dataset.fingerprint().hex()
751 );
752 assert_ne!(
753 witness.fingerprint_hex(),
754 witness.dataset_fingerprint_hex(),
755 "the validation fingerprint must not be a second copy of the dataset fingerprint"
756 );
757 assert_eq!(witness.validation().rows().len(), 2);
758 }
759
760 #[test]
761 fn prepared_the_two_construction_paths_fingerprint_identically() {
762 let mut direct_ledger = AccessLedger::new();
763 let direct = build_canonical(&mut direct_ledger).expect("direct path");
764
765 let reparse = |rows: Vec<LabeledExample>, role: &str| {
766 parse_jsonl_bytes(&encode_jsonl(&rows).expect("encode"), role).expect("parse")
767 };
768 let mut round_ledger = AccessLedger::new();
769 let round_trip = PreparedDataset::<Canonical>::from_labeled_rows(
770 reparse(train_rows(), "train"),
771 reparse(validation_rows(), "validation"),
772 reparse(test_rows(), "test"),
773 &canonical_decls(),
774 &mut round_ledger,
775 )
776 .expect("round-trip path");
777
778 assert_eq!(direct.fingerprint().hex(), round_trip.fingerprint().hex());
779 assert_eq!(direct.exclusions(), round_trip.exclusions());
780 }
781
782 #[test]
783 fn prepared_encode_jsonl_round_trips_into_an_equal_dataset() {
784 let mut ledger = AccessLedger::new();
785 let dataset = build_canonical(&mut ledger).expect("valid canonical corpus");
786 let encoded = dataset.encode_jsonl().expect("encode must succeed");
787
788 assert_eq!(encoded.as_map().len(), 3);
789 let take = |role: &str| {
790 parse_jsonl_bytes(encoded.get(role).expect("role is present"), role).expect("parse")
791 };
792 let mut replay_ledger = AccessLedger::new();
793 let replayed = PreparedDataset::<Canonical>::from_labeled_rows(
794 take("train"),
795 take("validation"),
796 take("test"),
797 &canonical_decls(),
798 &mut replay_ledger,
799 )
800 .expect("replay must succeed");
801
802 assert_eq!(replayed.fingerprint().hex(), dataset.fingerprint().hex());
803 assert_eq!(replayed.exclusions(), dataset.exclusions());
804 }
805
806 #[test]
809 fn prepare_time_duplicate_content_is_excluded_not_fatal() {
810 let mut validation = validation_rows();
811 validation[0].input = "alpha post".to_string();
812
813 let mut ledger = AccessLedger::new();
814 let dataset = PreparedDataset::<Canonical>::from_labeled_rows(
815 train_rows(),
816 validation,
817 test_rows(),
818 &canonical_decls(),
819 &mut ledger,
820 )
821 .expect("a cross-split duplicate must NOT be fatal at prepare time");
822
823 assert_eq!(
824 dataset.exclusions().excluded_train_ids(),
825 ["train:0".to_string()]
826 );
827 assert_eq!(dataset.exclusions().groups().len(), 1);
828 assert_eq!(dataset.exclusions().reduced_pools().get(&0), Some(&1));
829 }
830
831 #[test]
833 fn split_role_span_is_fail_closed() {
834 let mut ledger = AccessLedger::new();
835 let err = PreparedDataset::<Canonical>::from_labeled_rows(
836 train_rows(),
837 validation_rows(),
838 compatibility_rows(),
839 &canonical_decls(),
840 &mut ledger,
841 )
842 .expect_err("compatibility rows must not become a canonical test split");
843
844 match err {
845 ContrastiveDataError::SplitRoleMismatch {
846 expected_role,
847 embedded_role,
848 } => {
849 assert_eq!(expected_role, "test");
850 assert_eq!(embedded_role, "compatibility_test");
851 }
852 other => panic!("expected SplitRoleMismatch, got {other:?}"),
853 }
854 assert!(
855 ledger.records().is_empty(),
856 "a rejected dataset must leave no access record"
857 );
858 }
859
860 #[test]
861 fn prepared_runs_dedup_over_its_own_typed_splits() {
862 let mut validation = validation_rows();
863 validation[0].input = "alpha post".to_string();
864 let mut ledger = AccessLedger::new();
865 let dataset = PreparedDataset::<Canonical>::from_labeled_rows(
866 train_rows(),
867 validation.clone(),
868 test_rows(),
869 &canonical_decls(),
870 &mut ledger,
871 )
872 .expect("valid canonical corpus");
873
874 let expected = coalesced_exclusions(&[
875 ("test", &test_rows()),
876 ("train", &train_rows()),
877 ("validation", &validation),
878 ]);
879 assert_eq!(dataset.exclusions(), &expected);
880 }
881
882 #[test]
883 fn prepared_fingerprint_separates_the_two_profiles() {
884 let mut canonical_ledger = AccessLedger::new();
885 let canonical = build_canonical(&mut canonical_ledger).expect("canonical");
886 let mut compatibility_ledger = AccessLedger::new();
887 let compatibility = PreparedDataset::<Compatibility>::from_labeled_rows(
888 train_rows(),
889 compatibility_rows(),
890 &compatibility_decls(),
891 &mut compatibility_ledger,
892 )
893 .expect("compatibility");
894
895 assert_ne!(
896 canonical.fingerprint().hex(),
897 compatibility.fingerprint().hex()
898 );
899 }
900
901 #[test]
902 fn prepared_rejects_a_declaration_whose_counts_disagree() {
903 let mut decls = canonical_decls();
904 decls.validation = decl(vec![2, 0, 0]);
905 let mut ledger = AccessLedger::new();
906 let err = PreparedDataset::<Canonical>::from_labeled_rows(
907 train_rows(),
908 validation_rows(),
909 test_rows(),
910 &decls,
911 &mut ledger,
912 )
913 .expect_err("class-count contract must fail");
914 assert!(matches!(
915 err,
916 ContrastiveDataError::InvalidClassCounts { .. }
917 ));
918 }
919
920 #[test]
926 fn prepared_refuses_a_shared_label_map_that_contradicts_the_split_maps() {
927 let mut decls = canonical_decls();
928 decls.label_names = vec![
929 "favor".to_string(),
930 "against".to_string(),
931 "none".to_string(),
932 ];
933 let mut ledger = AccessLedger::new();
934 let err = PreparedDataset::<Canonical>::from_labeled_rows(
935 train_rows(),
936 validation_rows(),
937 test_rows(),
938 &decls,
939 &mut ledger,
940 )
941 .expect_err("a shared map contradicting the split maps must be refused");
942 match err {
945 ContrastiveDataError::DeclaredLabelMapMismatch { split, shared, got } => {
946 assert_eq!(split, "train");
947 assert_eq!(shared[0], "favor");
948 assert_eq!(got[0], "none");
949 }
950 other => panic!("expected DeclaredLabelMapMismatch, got {other:?}"),
951 }
952 }
953
954 #[test]
957 fn prepared_names_the_split_whose_label_map_diverges() {
958 let mut decls = canonical_decls();
959 decls.test = SplitDeclaration {
960 expected_class_counts: vec![1, 1, 1],
961 label_names: vec![
962 "none".to_string(),
963 "against".to_string(),
964 "FAVOUR".to_string(),
965 ],
966 };
967 let mut ledger = AccessLedger::new();
968 let err = PreparedDataset::<Canonical>::from_labeled_rows(
969 train_rows(),
970 validation_rows(),
971 test_rows(),
972 &decls,
973 &mut ledger,
974 )
975 .expect_err("a divergent test map must be refused");
976 match err {
977 ContrastiveDataError::DeclaredLabelMapMismatch { split, .. } => {
978 assert_eq!(split, "test");
979 }
980 other => panic!("expected DeclaredLabelMapMismatch, got {other:?}"),
981 }
982 }
983
984 #[test]
988 fn prepared_refuses_a_shared_label_map_of_a_different_length() {
989 let mut decls = canonical_decls();
990 decls.label_names = vec!["none".to_string(), "against".to_string()];
991 let mut ledger = AccessLedger::new();
992 let err = PreparedDataset::<Canonical>::from_labeled_rows(
993 train_rows(),
994 validation_rows(),
995 test_rows(),
996 &decls,
997 &mut ledger,
998 )
999 .expect_err("a shared map of the wrong arity must be refused");
1000 assert!(matches!(
1001 err,
1002 ContrastiveDataError::DeclaredLabelMapMismatch { .. }
1003 ));
1004 }
1005
1006 #[test]
1008 fn compatibility_refuses_a_shared_label_map_that_contradicts_the_split_maps() {
1009 let mut decls = compatibility_decls();
1010 decls.compatibility_test = SplitDeclaration {
1011 expected_class_counts: decls.compatibility_test.expected_class_counts.clone(),
1012 label_names: vec![
1013 "favor".to_string(),
1014 "against".to_string(),
1015 "none".to_string(),
1016 ],
1017 };
1018 let mut ledger = AccessLedger::new();
1019 let err = PreparedDataset::<Compatibility>::from_labeled_rows(
1020 train_rows(),
1021 compatibility_rows(),
1022 &decls,
1023 &mut ledger,
1024 )
1025 .expect_err("a divergent compatibility map must be refused");
1026 match err {
1027 ContrastiveDataError::DeclaredLabelMapMismatch { split, .. } => {
1028 assert_eq!(split, "compatibility_test");
1029 }
1030 other => panic!("expected DeclaredLabelMapMismatch, got {other:?}"),
1031 }
1032 }
1033
1034 #[test]
1037 fn prepared_accepts_declarations_whose_label_maps_agree() {
1038 canonical_decls()
1039 .check_label_maps_agree()
1040 .expect("the honest corpus must pass");
1041 compatibility_decls()
1042 .check_label_maps_agree()
1043 .expect("the honest compatibility corpus must pass");
1044 let mut ledger = AccessLedger::new();
1045 build_canonical(&mut ledger).expect("the honest corpus must still build");
1046 }
1047}