1use core::num::NonZeroU64;
38use std::collections::{BTreeMap, BTreeSet};
39
40use sha2::{Digest, Sha256};
41
42use crate::buckets::ClassBuckets;
43use crate::error::ContrastiveDataError;
44use crate::hash::{hex, CONTENT_NORMALIZATION_VERSION};
45use crate::ledger::AccessLedger;
46use crate::manifest::{
47 SelectedExampleRecord, SelectionManifest, SelectionPayload, SELECTION_SCHEMA_VERSION,
48 SUPPORTED_SELECTION_SCHEMA_VERSIONS,
49};
50use crate::prepared::{Canonical, DatasetProfile, PreparedDataset};
51use crate::rng::{bounded, derive_key, domains};
52use crate::split::{SplitRole, Train};
53
54pub const SELECTION_ALGORITHM_VERSION: u32 = 1;
57
58const ALLOWED_SHOTS: [u32; 4] = [8, 16, 32, 64];
60
61const ALLOWED_SHOTS_TEXT: &str = "{8, 16, 32, 64}";
63
64const SELECT_PURPOSE: &str = "select";
66
67const REPLAY_PURPOSE: &str = "select-replay";
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct SelectionConfig {
73 pub root_seed: u64,
75 pub shots_per_class: u32,
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
81pub struct SelectedExample {
82 pub id: String,
84 pub label: usize,
86 pub exact_hash: [u8; 32],
88 pub normalized_hash: [u8; 32],
90}
91
92#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
103pub struct SelectedId(u32);
104
105impl SelectedId {
106 pub fn ordinal(self) -> u32 {
116 self.0
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct Selection {
123 ordered: Vec<SelectedExample>,
124 by_id: BTreeMap<String, SelectedId>,
125 by_class: BTreeMap<usize, Vec<SelectedId>>,
126 class_sizes: Vec<(usize, u64)>,
127 payload: SelectionPayload,
128 semantic_hash: [u8; 32],
129 ledger_hash: [u8; 32],
130}
131
132#[derive(Debug, Clone, Copy)]
134pub struct FewShotSelector;
135
136impl FewShotSelector {
137 #[provable_contracts_macros::contract(
160 "contrastive-pair-protocol-v1",
161 equation = "few_shot_selection"
162 )]
163 pub fn select(
164 dataset: &PreparedDataset<Canonical>,
165 cfg: &SelectionConfig,
166 ledger: &mut AccessLedger,
167 ) -> Result<Selection, ContrastiveDataError> {
168 let ordered = compute_ordered(dataset, cfg.root_seed, cfg.shots_per_class)?;
169
170 let dataset_fingerprint = dataset.fingerprint().hex();
171 ledger.record(
172 Train::ROLE,
173 Canonical::PROFILE,
174 SELECT_PURPOSE,
175 &dataset_fingerprint,
176 );
177 let ledger_hash = ledger.ledger_hash();
178
179 let payload = SelectionPayload {
180 schema_version: SELECTION_SCHEMA_VERSION,
181 algorithm_version: SELECTION_ALGORITHM_VERSION,
182 profile: Canonical::PROFILE.to_string(),
183 dataset_fingerprint,
184 validation_fingerprint: dataset.validation_witness().fingerprint_hex(),
185 label_names: dataset.label_names().to_vec(),
186 normalization_version: CONTENT_NORMALIZATION_VERSION.to_string(),
187 root_seed: cfg.root_seed,
188 shots_per_class: cfg.shots_per_class,
189 ordered_examples: ordered
190 .iter()
191 .map(SelectedExampleRecord::from_example)
192 .collect(),
193 exclusions: dataset.exclusions().clone(),
194 access_ledger: ledger.records().to_vec(),
195 ledger_hash: hex(&ledger_hash),
196 };
197
198 Selection::assemble(ordered, payload, ledger_hash)
199 }
200}
201
202pub(crate) fn compute_ordered(
209 dataset: &PreparedDataset<Canonical>,
210 root_seed: u64,
211 shots_per_class: u32,
212) -> Result<Vec<SelectedExample>, ContrastiveDataError> {
213 if !ALLOWED_SHOTS.contains(&shots_per_class) {
214 return Err(ContrastiveDataError::InvalidShots {
215 got: shots_per_class as usize,
216 allowed: ALLOWED_SHOTS_TEXT,
217 });
218 }
219 let shots = shots_per_class as usize;
220
221 let buckets = ClassBuckets::from_prepared(dataset);
222 for (label, pool) in buckets.class_sizes() {
223 if (pool as usize) < shots {
224 return Err(ContrastiveDataError::CrossSplitDuplicateUnderflow {
225 class_label: label,
226 pool: pool as usize,
227 shots,
228 });
229 }
230 }
231
232 let train = dataset.train();
233 let labels = buckets.labels();
234 let mut ordered = Vec::with_capacity(shots.saturating_mul(labels.len()));
235
236 for label in labels {
237 let key = derive_key(root_seed, &domains::select(label));
238 let mut work: Vec<&str> = buckets.ids(label).iter().map(String::as_str).collect();
239 let pool_len = work.len();
240
241 for i in 0..shots {
242 let remaining = NonZeroU64::new((pool_len - i) as u64).ok_or_else(|| {
246 ContrastiveDataError::ArithmeticOverflow {
247 operation: format!("selection_remaining_pool/class-{label}"),
248 }
249 })?;
250 let j = i + bounded(&key, 0, i as u64, remaining) as usize;
251 work.swap(i, j);
252 }
253
254 for id in work.into_iter().take(shots) {
255 let missing = || ContrastiveDataError::SelectionReplayMismatch {
257 field: format!("row_hashes/{id}"),
258 };
259 let exact_hash = *train.exact_hash_of(id).ok_or_else(missing)?;
260 let normalized_hash = *train.normalized_hash_of(id).ok_or_else(missing)?;
261 ordered.push(SelectedExample {
262 id: id.to_string(),
263 label,
264 exact_hash,
265 normalized_hash,
266 });
267 }
268 }
269
270 Ok(ordered)
271}
272
273impl Selection {
274 pub(crate) fn assemble(
276 ordered: Vec<SelectedExample>,
277 payload: SelectionPayload,
278 ledger_hash: [u8; 32],
279 ) -> Result<Self, ContrastiveDataError> {
280 let semantic_hash: [u8; 32] = Sha256::digest(payload.to_canonical_bytes()?).into();
281
282 let mut by_id = BTreeMap::new();
283 let mut by_class: BTreeMap<usize, Vec<SelectedId>> = BTreeMap::new();
284 for (ordinal, example) in ordered.iter().enumerate() {
285 let selected = SelectedId(ordinal as u32);
286 by_id.insert(example.id.clone(), selected);
287 by_class.entry(example.label).or_default().push(selected);
288 }
289 let class_sizes = by_class
290 .iter()
291 .map(|(label, ids)| (*label, ids.len() as u64))
292 .collect();
293
294 Ok(Self {
295 ordered,
296 by_id,
297 by_class,
298 class_sizes,
299 payload,
300 semantic_hash,
301 ledger_hash,
302 })
303 }
304
305 pub fn examples(&self) -> &[SelectedExample] {
307 &self.ordered
308 }
309
310 pub fn ordered_ids(&self) -> Vec<&str> {
312 self.ordered.iter().map(|row| row.id.as_str()).collect()
313 }
314
315 pub fn len(&self) -> usize {
317 self.ordered.len()
318 }
319
320 pub fn is_empty(&self) -> bool {
323 self.ordered.is_empty()
324 }
325
326 pub fn semantic_hash(&self) -> [u8; 32] {
328 self.semantic_hash
329 }
330
331 pub fn ledger_hash(&self) -> [u8; 32] {
334 self.ledger_hash
335 }
336
337 pub fn payload(&self) -> &SelectionPayload {
339 &self.payload
340 }
341
342 pub fn dataset_fingerprint_hex(&self) -> &str {
344 &self.payload.dataset_fingerprint
345 }
346
347 pub fn validation_fingerprint_hex(&self) -> &str {
349 &self.payload.validation_fingerprint
350 }
351
352 pub fn root_seed(&self) -> u64 {
354 self.payload.root_seed
355 }
356
357 pub fn shots_per_class(&self) -> u32 {
359 self.payload.shots_per_class
360 }
361
362 pub fn class_sizes(&self) -> &[(usize, u64)] {
366 &self.class_sizes
367 }
368
369 pub fn selected_id(&self, id: &str) -> Option<SelectedId> {
374 self.by_id.get(id).copied()
375 }
376
377 pub fn id_of(&self, selected: SelectedId) -> &str {
383 self.example_of(selected).id.as_str()
384 }
385
386 pub fn label_of(&self, selected: SelectedId) -> usize {
392 self.example_of(selected).label
393 }
394
395 pub fn example_of(&self, selected: SelectedId) -> &SelectedExample {
404 self.ordered
405 .get(selected.0 as usize)
406 .expect("SelectedId ordinals are produced only by the Selection they index")
407 }
408
409 pub fn ids_in_class(&self, label: usize) -> &[SelectedId] {
412 self.by_class.get(&label).map_or(&[], Vec::as_slice)
413 }
414
415 #[provable_contracts_macros::contract(
452 "contrastive-pair-protocol-v1",
453 equation = "selection_replay"
454 )]
455 pub fn replay(
456 manifest: &SelectionManifest,
457 dataset: &PreparedDataset<Canonical>,
458 ledger: &mut AccessLedger,
459 ) -> Result<Self, ContrastiveDataError> {
460 let payload = &manifest.payload;
461 check_versions(payload)?;
462 check_provenance(payload, dataset)?;
463
464 let buckets = ClassBuckets::from_prepared(dataset);
465 check_membership(payload, dataset, &buckets)?;
466 check_uniqueness(payload)?;
467 check_class_balance(payload)?;
468 check_class_ordering(payload)?;
469 let recorded = rebuild_examples(payload, dataset)?;
470
471 manifest.verify_digest()?;
472
473 let recomputed = compute_ordered(dataset, payload.root_seed, payload.shots_per_class)?;
474 if recomputed != recorded {
475 return Err(ContrastiveDataError::SelectionReplayMismatch {
476 field: "ordered_examples".to_string(),
477 });
478 }
479
480 let ledger_hash = digest_from_hex(&payload.ledger_hash).ok_or_else(|| {
481 ContrastiveDataError::SelectionReplayMismatch {
482 field: "ledger_hash".to_string(),
483 }
484 })?;
485 let mut persisted = AccessLedger::new();
491 for record in &payload.access_ledger {
492 persisted.record(
493 &record.role,
494 &record.profile,
495 &record.purpose,
496 &record.fingerprint_hex,
497 );
498 }
499 if persisted.ledger_hash() != ledger_hash {
500 return Err(ContrastiveDataError::SelectionReplayMismatch {
501 field: "access_ledger".to_string(),
502 });
503 }
504
505 ledger.record(
506 Train::ROLE,
507 Canonical::PROFILE,
508 REPLAY_PURPOSE,
509 &payload.dataset_fingerprint,
510 );
511 Self::assemble(recorded, payload.clone(), ledger_hash)
512 }
513}
514
515fn digest_from_hex(text: &str) -> Option<[u8; 32]> {
522 let bytes = text.as_bytes();
523 if bytes.len() != 64 {
524 return None;
525 }
526 let nibble = |byte: u8| -> Option<u32> {
527 if byte.is_ascii_uppercase() {
528 return None;
529 }
530 char::from(byte).to_digit(16)
531 };
532 let mut digest = [0_u8; 32];
533 for (index, slot) in digest.iter_mut().enumerate() {
534 let high = nibble(bytes[index * 2])?;
535 let low = nibble(bytes[index * 2 + 1])?;
536 *slot = (high * 16 + low) as u8;
537 }
538 Some(digest)
539}
540
541fn check_versions(payload: &SelectionPayload) -> Result<(), ContrastiveDataError> {
543 if !SUPPORTED_SELECTION_SCHEMA_VERSIONS.contains(&payload.schema_version) {
544 return Err(ContrastiveDataError::UnsupportedSchemaVersion {
545 field: "selection".to_string(),
546 got: payload.schema_version,
547 supported: SELECTION_SCHEMA_VERSION,
548 });
549 }
550 if payload.algorithm_version != SELECTION_ALGORITHM_VERSION {
551 return Err(ContrastiveDataError::UnsupportedAlgorithmVersion {
552 got: payload.algorithm_version,
553 supported: SELECTION_ALGORITHM_VERSION,
554 });
555 }
556 Ok(())
557}
558
559fn check_provenance(
561 payload: &SelectionPayload,
562 dataset: &PreparedDataset<Canonical>,
563) -> Result<(), ContrastiveDataError> {
564 if payload.profile != Canonical::PROFILE {
565 return Err(ContrastiveDataError::ProfileMismatch {
566 expected: Canonical::PROFILE.to_string(),
567 got: payload.profile.clone(),
568 });
569 }
570 let dataset_fingerprint = dataset.fingerprint().hex();
571 if payload.dataset_fingerprint != dataset_fingerprint {
572 return Err(ContrastiveDataError::FingerprintMismatch {
573 expected: payload.dataset_fingerprint.clone(),
574 got: dataset_fingerprint,
575 });
576 }
577 let validation_fingerprint = dataset.validation_witness().fingerprint_hex();
578 if payload.validation_fingerprint != validation_fingerprint {
579 return Err(ContrastiveDataError::FingerprintMismatch {
580 expected: payload.validation_fingerprint.clone(),
581 got: validation_fingerprint,
582 });
583 }
584 if payload.label_names.as_slice() != dataset.label_names() {
592 return Err(ContrastiveDataError::SelectionReplayMismatch {
593 field: "label_names".to_string(),
594 });
595 }
596 if payload.normalization_version != CONTENT_NORMALIZATION_VERSION {
597 return Err(ContrastiveDataError::UnsupportedNormalizationVersion {
598 got: payload.normalization_version.clone(),
599 supported: CONTENT_NORMALIZATION_VERSION,
600 });
601 }
602 let recorded = hex(&payload.exclusions.hash());
603 let actual = hex(&dataset.exclusions().hash());
604 if recorded != actual {
605 return Err(ContrastiveDataError::ExclusionRecordMismatch {
606 expected: recorded,
607 got: actual,
608 });
609 }
610 Ok(())
611}
612
613fn locate(dataset: &PreparedDataset<Canonical>, id: &str) -> &'static str {
615 if dataset.train().exact_hash_of(id).is_some() {
616 return "train, but excluded from the selection pool";
617 }
618 if dataset.validation().exact_hash_of(id).is_some() {
619 return "validation";
620 }
621 if dataset.test().exact_hash_of(id).is_some() {
622 return "test";
623 }
624 "nowhere"
625}
626
627fn check_membership(
629 payload: &SelectionPayload,
630 dataset: &PreparedDataset<Canonical>,
631 buckets: &ClassBuckets,
632) -> Result<(), ContrastiveDataError> {
633 let pool: BTreeSet<&str> = buckets
634 .labels()
635 .into_iter()
636 .flat_map(|label| buckets.ids(label).iter().map(String::as_str))
637 .collect();
638 for record in &payload.ordered_examples {
639 if !pool.contains(record.id.as_str()) {
640 return Err(ContrastiveDataError::EndpointNotInSelection {
641 id: record.id.clone(),
642 found_in: locate(dataset, &record.id).to_string(),
643 });
644 }
645 }
646 Ok(())
647}
648
649fn check_uniqueness(payload: &SelectionPayload) -> Result<(), ContrastiveDataError> {
651 let mut seen: BTreeSet<&str> = BTreeSet::new();
652 for record in &payload.ordered_examples {
653 if !seen.insert(record.id.as_str()) {
654 return Err(ContrastiveDataError::DuplicateId {
655 split: "selection".to_string(),
656 id: record.id.clone(),
657 });
658 }
659 }
660 Ok(())
661}
662
663fn check_class_balance(payload: &SelectionPayload) -> Result<(), ContrastiveDataError> {
665 let mut counts: BTreeMap<usize, usize> = BTreeMap::new();
666 for record in &payload.ordered_examples {
667 *counts.entry(record.label).or_default() += 1;
668 }
669 let classes = payload.label_names.len();
670 let got: Vec<usize> = (0..classes)
671 .map(|label| counts.get(&label).copied().unwrap_or(0))
672 .collect();
673 let expected = vec![payload.shots_per_class as usize; classes];
674 if got != expected || counts.len() != classes {
675 return Err(ContrastiveDataError::InvalidClassCounts {
676 split: "selection".to_string(),
677 expected,
678 got,
679 });
680 }
681 Ok(())
682}
683
684fn check_class_ordering(payload: &SelectionPayload) -> Result<(), ContrastiveDataError> {
686 let out_of_order = payload
687 .ordered_examples
688 .windows(2)
689 .any(|pair| pair[1].label < pair[0].label);
690 if out_of_order {
691 return Err(ContrastiveDataError::SelectionReplayMismatch {
692 field: "class_order".to_string(),
693 });
694 }
695 Ok(())
696}
697
698fn rebuild_examples(
700 payload: &SelectionPayload,
701 dataset: &PreparedDataset<Canonical>,
702) -> Result<Vec<SelectedExample>, ContrastiveDataError> {
703 let train = dataset.train();
704 let mut rebuilt = Vec::with_capacity(payload.ordered_examples.len());
705 for record in &payload.ordered_examples {
706 let exact_hash = compare_row_hash(
707 &record.id,
708 &record.exact_hash,
709 train.exact_hash_of(&record.id),
710 )?;
711 let normalized_hash = compare_row_hash(
712 &record.id,
713 &record.normalized_hash,
714 train.normalized_hash_of(&record.id),
715 )?;
716 rebuilt.push(SelectedExample {
717 id: record.id.clone(),
718 label: record.label,
719 exact_hash,
720 normalized_hash,
721 });
722 }
723 Ok(rebuilt)
724}
725
726fn compare_row_hash(
728 id: &str,
729 recorded_hex: &str,
730 actual: Option<&[u8; 32]>,
731) -> Result<[u8; 32], ContrastiveDataError> {
732 let actual = actual
734 .copied()
735 .ok_or_else(|| ContrastiveDataError::RowHashMismatch {
736 id: id.to_string(),
737 expected: recorded_hex.to_string(),
738 got: "row absent from the training split".to_string(),
739 })?;
740 if hex(&actual) == recorded_hex {
741 return Ok(actual);
742 }
743 Err(ContrastiveDataError::RowHashMismatch {
744 id: id.to_string(),
745 expected: recorded_hex.to_string(),
746 got: hex(&actual),
747 })
748}
749
750#[cfg(test)]
751pub(crate) mod test_corpus {
752 use crate::ledger::AccessLedger;
761 use crate::prepared::{Canonical, CanonicalDeclarations, PreparedDataset};
762 use crate::schema::LabeledExample;
763 use crate::select::{FewShotSelector, Selection, SelectionConfig};
764 use crate::split::SplitDeclaration;
765
766 pub(crate) const LABEL_TEXTS: [&str; 3] = ["none", "against", "favor"];
767
768 pub(crate) const CONTRACTED_SEEDS: [u64; 10] = [13, 17, 23, 29, 31, 37, 41, 43, 47, 53];
777
778 pub(crate) fn label_names() -> Vec<String> {
779 LABEL_TEXTS.iter().map(|name| (*name).to_string()).collect()
780 }
781
782 fn row(id: String, input: String, label: usize, split: &str) -> LabeledExample {
783 LabeledExample {
784 id,
785 input,
786 label,
787 label_text: LABEL_TEXTS[label].to_string(),
788 source_split: split.to_string(),
789 }
790 }
791
792 fn train_row(label: usize, index: usize) -> LabeledExample {
793 row(
794 format!("train:{label}-{index:03}"),
795 format!("training post for class {label} item {index}"),
796 label,
797 "train",
798 )
799 }
800
801 pub(crate) fn rows(
804 per_class: usize,
805 ) -> (
806 Vec<LabeledExample>,
807 Vec<LabeledExample>,
808 Vec<LabeledExample>,
809 ) {
810 let mut train = Vec::with_capacity(per_class * 3);
811 for step in 0..per_class {
812 let index = per_class - 1 - step;
813 for label in 0..3 {
814 train.push(train_row(label, index));
815 }
816 }
817 let validation = (0..3)
818 .map(|label| {
819 row(
820 format!("validation:{label}"),
821 format!("validation post for class {label}"),
822 label,
823 "validation",
824 )
825 })
826 .collect();
827 let test = (0..3)
828 .map(|label| {
829 row(
830 format!("test:{label}"),
831 format!("held-out post for class {label}"),
832 label,
833 "test",
834 )
835 })
836 .collect();
837 (train, validation, test)
838 }
839
840 pub(crate) fn declarations(train_counts: Vec<usize>) -> CanonicalDeclarations {
841 let decl = |counts: Vec<usize>| SplitDeclaration {
842 expected_class_counts: counts,
843 label_names: label_names(),
844 };
845 CanonicalDeclarations {
846 train: decl(train_counts),
847 validation: decl(vec![1, 1, 1]),
848 test: decl(vec![1, 1, 1]),
849 label_names: label_names(),
850 }
851 }
852
853 pub(crate) fn build(
854 train: Vec<LabeledExample>,
855 validation: Vec<LabeledExample>,
856 test: Vec<LabeledExample>,
857 decls: &CanonicalDeclarations,
858 ledger: &mut AccessLedger,
859 ) -> PreparedDataset<Canonical> {
860 PreparedDataset::<Canonical>::from_labeled_rows(train, validation, test, decls, ledger)
861 .expect("the synthetic corpus must be valid")
862 }
863
864 pub(crate) fn dataset(
866 per_class: usize,
867 ledger: &mut AccessLedger,
868 ) -> PreparedDataset<Canonical> {
869 let (train, validation, test) = rows(per_class);
870 build(
871 train,
872 validation,
873 test,
874 &declarations(vec![per_class; 3]),
875 ledger,
876 )
877 }
878
879 pub(crate) fn dataset_with_cross_split_duplicate(
882 per_class: usize,
883 ledger: &mut AccessLedger,
884 ) -> PreparedDataset<Canonical> {
885 let (train, mut validation, test) = rows(per_class);
886 validation[0].input = train_row(0, 0).input;
887 build(
888 train,
889 validation,
890 test,
891 &declarations(vec![per_class; 3]),
892 ledger,
893 )
894 }
895
896 pub(crate) fn dataset_with_empty_class(
898 per_class: usize,
899 ledger: &mut AccessLedger,
900 ) -> PreparedDataset<Canonical> {
901 let (train, validation, test) = rows(per_class);
902 let train = train.into_iter().filter(|row| row.label != 2).collect();
903 build(
904 train,
905 validation,
906 test,
907 &declarations(vec![per_class, per_class, 0]),
908 ledger,
909 )
910 }
911
912 pub(crate) fn select(
913 dataset: &PreparedDataset<Canonical>,
914 root_seed: u64,
915 shots_per_class: u32,
916 ledger: &mut AccessLedger,
917 ) -> Selection {
918 FewShotSelector::select(
919 dataset,
920 &SelectionConfig {
921 root_seed,
922 shots_per_class,
923 },
924 ledger,
925 )
926 .expect("the synthetic corpus must support this selection")
927 }
928
929 pub(crate) fn fresh_selection(
932 per_class: usize,
933 root_seed: u64,
934 shots_per_class: u32,
935 ) -> (Selection, AccessLedger) {
936 let mut ledger = AccessLedger::new();
937 let prepared = dataset(per_class, &mut ledger);
938 let selection = select(&prepared, root_seed, shots_per_class, &mut ledger);
939 (selection, ledger)
940 }
941}
942
943#[cfg(test)]
944mod select_tests {
945 use super::test_corpus::{self, CONTRACTED_SEEDS};
946 use super::{FewShotSelector, SelectionConfig};
947 use crate::error::ContrastiveDataError;
948 use crate::ledger::AccessLedger;
949 use crate::prepared::PreparedDataset;
950 use proptest::prelude::{prop_assert_eq, proptest};
951
952 fn ordered(selection: &super::Selection) -> Vec<(String, usize)> {
953 selection
954 .examples()
955 .iter()
956 .map(|row| (row.id.clone(), row.label))
957 .collect()
958 }
959
960 #[test]
961 fn select_returns_exactly_shots_per_class_with_labels_and_hashes() {
962 let mut ledger = AccessLedger::new();
963 let dataset = test_corpus::dataset(12, &mut ledger);
964 let selection = test_corpus::select(&dataset, 13, 8, &mut ledger);
965
966 assert_eq!(selection.len(), 24);
967 assert!(!selection.is_empty());
968 assert_eq!(selection.class_sizes(), [(0, 8), (1, 8), (2, 8)]);
969
970 let ids = selection.ordered_ids();
971 let mut unique = ids.clone();
972 unique.sort_unstable();
973 unique.dedup();
974 assert_eq!(unique.len(), 24, "every selected id must be distinct");
975
976 for row in selection.examples() {
977 assert!(
978 row.id.starts_with(&format!("train:{}-", row.label)),
979 "row {row:?} must come from its own class's training pool"
980 );
981 assert_eq!(
982 Some(&row.exact_hash),
983 dataset.train().exact_hash_of(&row.id),
984 "hashes are COPIED from the split, never re-derived"
985 );
986 assert_eq!(
987 Some(&row.normalized_hash),
988 dataset.train().normalized_hash_of(&row.id)
989 );
990 }
991 }
992
993 #[test]
994 fn select_replays_identically_across_two_calls() {
995 let (first, _) = test_corpus::fresh_selection(12, 13, 8);
996 let (second, _) = test_corpus::fresh_selection(12, 13, 8);
997 assert_eq!(ordered(&first), ordered(&second));
998 assert_eq!(first.semantic_hash(), second.semantic_hash());
999 }
1000
1001 #[test]
1004 fn select_replays_identically_for_every_contracted_seed() {
1005 assert_eq!(CONTRACTED_SEEDS.len(), 10);
1006 assert!(
1007 !CONTRACTED_SEEDS.contains(&42),
1008 "42 is not a contracted seed — see data_tweeteval.rs:45"
1009 );
1010 let mut seen = Vec::new();
1011 for seed in CONTRACTED_SEEDS {
1012 let (first, _) = test_corpus::fresh_selection(12, seed, 8);
1013 let (second, _) = test_corpus::fresh_selection(12, seed, 8);
1014 assert_eq!(ordered(&first), ordered(&second), "seed {seed}");
1015 assert_eq!(first.semantic_hash(), second.semantic_hash(), "seed {seed}");
1016 seen.push(first.ordered_ids().join(","));
1017 }
1018 let mut distinct = seen.clone();
1019 distinct.sort_unstable();
1020 distinct.dedup();
1021 assert_eq!(
1022 distinct.len(),
1023 seen.len(),
1024 "different seeds must produce different orderings"
1025 );
1026 }
1027
1028 #[test]
1029 fn select_supports_all_four_contracted_shot_counts() {
1030 for shots in [8_u32, 16, 32, 64] {
1031 let (selection, _) = test_corpus::fresh_selection(64, 13, shots);
1032 assert_eq!(selection.len() as u32, shots * 3, "shots {shots}");
1033 assert!(selection
1034 .class_sizes()
1035 .iter()
1036 .all(|(_, size)| *size == u64::from(shots)));
1037 }
1038 }
1039
1040 #[test]
1044 fn select_is_invariant_under_permuted_ingest_order() {
1045 let (train, validation, test) = test_corpus::rows(12);
1046 let decls = test_corpus::declarations(vec![12; 3]);
1047
1048 let mut ledger_a = AccessLedger::new();
1049 let straight = test_corpus::build(
1050 train.clone(),
1051 validation.clone(),
1052 test.clone(),
1053 &decls,
1054 &mut ledger_a,
1055 );
1056 let selection_a = test_corpus::select(&straight, 29, 8, &mut ledger_a);
1057
1058 let mut reversed = train;
1059 reversed.reverse();
1060 let mut ledger_b = AccessLedger::new();
1061 let permuted = test_corpus::build(reversed, validation, test, &decls, &mut ledger_b);
1062 let selection_b = test_corpus::select(&permuted, 29, 8, &mut ledger_b);
1063
1064 assert_eq!(
1065 ordered(&selection_a),
1066 ordered(&selection_b),
1067 "buckets sort before any draw, so ingest order cannot reach the selection"
1068 );
1069 assert_ne!(
1070 straight.fingerprint().hex(),
1071 permuted.fingerprint().hex(),
1072 "a permuted file IS different bytes; provenance must say so"
1073 );
1074 assert_ne!(selection_a.semantic_hash(), selection_b.semantic_hash());
1075 }
1076
1077 #[test]
1078 fn select_rejects_an_invalid_shot_count_before_any_draw() {
1079 let mut ledger = AccessLedger::new();
1080 let dataset = test_corpus::dataset(12, &mut ledger);
1081 let before = ledger.records().len();
1082
1083 for shots in [0_u32, 1, 7, 9, 63, 65, 128] {
1084 let err = FewShotSelector::select(
1085 &dataset,
1086 &SelectionConfig {
1087 root_seed: 13,
1088 shots_per_class: shots,
1089 },
1090 &mut ledger,
1091 )
1092 .expect_err("an uncontracted shot count must be refused");
1093 match err {
1094 ContrastiveDataError::InvalidShots { got, allowed } => {
1095 assert_eq!(got, shots as usize);
1096 assert_eq!(allowed, "{8, 16, 32, 64}");
1097 }
1098 other => panic!("expected InvalidShots, got {other:?}"),
1099 }
1100 }
1101 assert_eq!(
1102 ledger.records().len(),
1103 before,
1104 "a refused request must not touch the ledger"
1105 );
1106 }
1107
1108 #[test]
1110 fn select_rejects_a_class_pool_smaller_than_shots() {
1111 let mut ledger = AccessLedger::new();
1112 let dataset = test_corpus::dataset_with_empty_class(8, &mut ledger);
1113 let err = FewShotSelector::select(
1114 &dataset,
1115 &SelectionConfig {
1116 root_seed: 13,
1117 shots_per_class: 8,
1118 },
1119 &mut ledger,
1120 )
1121 .expect_err("an exhausted class pool must be refused");
1122 match err {
1123 ContrastiveDataError::CrossSplitDuplicateUnderflow {
1124 class_label,
1125 pool,
1126 shots,
1127 } => {
1128 assert_eq!((class_label, pool, shots), (2, 0, 8));
1129 let message = ContrastiveDataError::CrossSplitDuplicateUnderflow {
1130 class_label,
1131 pool,
1132 shots,
1133 }
1134 .to_string();
1135 assert!(message.contains("class 2"), "{message}");
1136 assert!(message.contains("0 rows remain"), "{message}");
1137 assert!(message.contains("8 shots"), "{message}");
1138 }
1139 other => panic!("expected CrossSplitDuplicateUnderflow, got {other:?}"),
1140 }
1141 }
1142
1143 #[test]
1144 fn select_excludes_cross_split_duplicates_from_the_pool() {
1145 let mut ledger = AccessLedger::new();
1146 let dataset = test_corpus::dataset_with_cross_split_duplicate(12, &mut ledger);
1147 let excluded = dataset.exclusions().excluded_train_ids().to_vec();
1148 assert_eq!(
1149 excluded.len(),
1150 1,
1151 "the fixture must exclude exactly one row"
1152 );
1153
1154 let selection = test_corpus::select(&dataset, 13, 8, &mut ledger);
1155 for id in &excluded {
1156 assert!(
1157 selection.selected_id(id).is_none(),
1158 "excluded id {id:?} must be unselectable"
1159 );
1160 }
1161 }
1162
1163 #[test]
1164 fn select_selected_ids_round_trip_and_labels_agree() {
1165 let mut ledger = AccessLedger::new();
1166 let dataset = test_corpus::dataset(12, &mut ledger);
1167 let selection = test_corpus::select(&dataset, 37, 8, &mut ledger);
1168
1169 for row in selection.examples() {
1170 let selected = selection
1171 .selected_id(&row.id)
1172 .expect("every selected row resolves to an ordinal");
1173 assert_eq!(selection.id_of(selected), row.id);
1174 assert_eq!(selection.label_of(selected), row.label);
1175 assert_eq!(selection.example_of(selected), row);
1176 }
1177 assert!(selection.selected_id("train:0-999").is_none());
1178 assert!(selection.selected_id("validation:0").is_none());
1179 }
1180
1181 #[test]
1182 fn select_ids_in_class_concatenate_to_the_full_ordered_list() {
1183 let mut ledger = AccessLedger::new();
1184 let dataset = test_corpus::dataset(12, &mut ledger);
1185 let selection = test_corpus::select(&dataset, 41, 8, &mut ledger);
1186
1187 let mut rebuilt = Vec::new();
1188 for (label, _) in selection.class_sizes() {
1189 let ordinals = selection.ids_in_class(*label);
1190 assert!(
1191 ordinals.windows(2).all(|pair| pair[0] < pair[1]),
1192 "class {label} ordinals must ascend"
1193 );
1194 for selected in ordinals {
1195 assert_eq!(selection.label_of(*selected), *label);
1196 rebuilt.push(selection.id_of(*selected).to_string());
1197 }
1198 }
1199 let expected: Vec<String> = selection
1200 .ordered_ids()
1201 .into_iter()
1202 .map(str::to_string)
1203 .collect();
1204 assert_eq!(rebuilt, expected);
1205 assert!(selection.ids_in_class(99).is_empty());
1206 }
1207
1208 #[test]
1211 fn select_appends_one_access_record_naming_the_selection() {
1212 let mut ledger = AccessLedger::new();
1213 let dataset = test_corpus::dataset(12, &mut ledger);
1214 let ingest_records = ledger.records().len();
1215 let selection = test_corpus::select(&dataset, 43, 8, &mut ledger);
1216
1217 assert_eq!(ledger.records().len(), ingest_records + 1);
1218 let record = ledger.records().last().expect("a record was just appended");
1219 assert!(record.purpose.contains("select"));
1220 assert_eq!(record.role, "train", "selection reads ONLY the train pool");
1221 assert_eq!(record.profile, "canonical");
1222 assert_eq!(record.fingerprint_hex, dataset.fingerprint().hex());
1223 assert_eq!(
1224 record.fingerprint_hex,
1225 dataset.validation_witness().dataset_fingerprint_hex(),
1226 "reachable only from a dataset that has a validation witness (D-19)"
1227 );
1228 assert_eq!(selection.dataset_fingerprint_hex(), record.fingerprint_hex);
1229 }
1230
1231 #[test]
1232 fn select_ledger_hash_matches_the_live_ledger_immediately_after() {
1233 let mut ledger = AccessLedger::new();
1234 let dataset = test_corpus::dataset(12, &mut ledger);
1235 let selection = test_corpus::select(&dataset, 47, 8, &mut ledger);
1236 assert_eq!(selection.ledger_hash(), ledger.ledger_hash());
1237
1238 ledger.record("train", "canonical", "unrelated", "aa");
1239 assert_ne!(
1240 selection.ledger_hash(),
1241 ledger.ledger_hash(),
1242 "the retained hash describes the ledger AS OF selection, not the live one"
1243 );
1244 }
1245
1246 #[test]
1250 fn select_takes_exactly_one_dataset_value() {
1251 fn signature_check(
1252 dataset: &PreparedDataset<crate::prepared::Canonical>,
1253 cfg: &SelectionConfig,
1254 ledger: &mut AccessLedger,
1255 ) -> Result<super::Selection, ContrastiveDataError> {
1256 FewShotSelector::select(dataset, cfg, ledger)
1257 }
1258 let mut ledger = AccessLedger::new();
1259 let dataset = test_corpus::dataset(12, &mut ledger);
1260 let selection = signature_check(
1261 &dataset,
1262 &SelectionConfig {
1263 root_seed: 53,
1264 shots_per_class: 8,
1265 },
1266 &mut ledger,
1267 )
1268 .expect("selection succeeds");
1269 assert_eq!(selection.len(), 24);
1270 }
1271
1272 #[test]
1279 fn selected_id_ordinals_are_the_positions_in_the_ordered_list() {
1280 let mut ledger = AccessLedger::new();
1281 let dataset = test_corpus::dataset(12, &mut ledger);
1282 let selection = test_corpus::select(&dataset, 13, 8, &mut ledger);
1283 assert_eq!(
1284 selection.len(),
1285 24,
1286 "pin the population before relating over it"
1287 );
1288
1289 let mut seen = std::collections::BTreeSet::new();
1290 for (index, row) in selection.examples().iter().enumerate() {
1291 let selected = selection
1292 .selected_id(&row.id)
1293 .expect("every selected example resolves to an ordinal");
1294 assert_eq!(
1295 selected.ordinal() as usize,
1296 index,
1297 "the ordinal of {} must be its position",
1298 row.id
1299 );
1300 assert_eq!(selection.id_of(selected), row.id, "and it must round-trip");
1301 assert!(seen.insert(selected.ordinal()), "ordinals must be distinct");
1302 }
1303 assert_eq!(seen.len(), 24);
1304 }
1305
1306 #[test]
1313 fn the_validation_fingerprint_is_the_witness_digest_and_differs_from_the_dataset_one() {
1314 let mut ledger = AccessLedger::new();
1315 let dataset = test_corpus::dataset(12, &mut ledger);
1316 let selection = test_corpus::select(&dataset, 13, 8, &mut ledger);
1317
1318 let expected = dataset.validation_witness().fingerprint_hex();
1319 assert_eq!(selection.validation_fingerprint_hex(), expected);
1320 assert_eq!(expected.len(), 64, "SHA-256 rendered as lowercase hex");
1321 assert!(
1322 expected
1323 .chars()
1324 .all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c)),
1325 "not lowercase hex: {expected}"
1326 );
1327 assert_ne!(
1330 selection.validation_fingerprint_hex(),
1331 selection.dataset_fingerprint_hex()
1332 );
1333 }
1334
1335 proptest! {
1336 #[test]
1338 fn select_is_deterministic_for_any_seed(seed in 0_u64..u64::MAX) {
1339 let (first, _) = test_corpus::fresh_selection(12, seed, 8);
1340 let (second, _) = test_corpus::fresh_selection(12, seed, 8);
1341 prop_assert_eq!(first.ordered_ids(), second.ordered_ids());
1342 prop_assert_eq!(first.semantic_hash(), second.semantic_hash());
1343 }
1344 }
1345}
1346
1347#[cfg(test)]
1348mod manifest_replay_tests {
1349 use super::test_corpus;
1362 use super::{Selection, SelectionConfig};
1363 use crate::error::ContrastiveDataError;
1364 use crate::ledger::AccessLedger;
1365 use crate::manifest::{SelectedExampleRecord, SelectionManifest};
1366 use crate::prepared::{Canonical, PreparedDataset};
1367
1368 struct Fixture {
1369 dataset: PreparedDataset<Canonical>,
1370 ledger: AccessLedger,
1371 manifest: SelectionManifest,
1372 selection: Selection,
1373 }
1374
1375 fn fixture() -> Fixture {
1376 let mut ledger = AccessLedger::new();
1377 let dataset = test_corpus::dataset(12, &mut ledger);
1378 let selection = test_corpus::select(&dataset, 31, 8, &mut ledger);
1379 let manifest =
1380 SelectionManifest::from_selection(&selection, &ledger).expect("the wrap succeeds");
1381 Fixture {
1382 dataset,
1383 ledger,
1384 manifest,
1385 selection,
1386 }
1387 }
1388
1389 fn reseal(manifest: &mut SelectionManifest) {
1391 use sha2::{Digest, Sha256};
1392 let digest: [u8; 32] = Sha256::digest(
1393 manifest
1394 .payload
1395 .to_canonical_bytes()
1396 .expect("payload serializes"),
1397 )
1398 .into();
1399 manifest.semantic_hash = crate::hash::hex(&digest);
1400 }
1401
1402 fn reject(
1403 mutate: impl FnOnce(&mut SelectionManifest, &PreparedDataset<Canonical>),
1404 ) -> ContrastiveDataError {
1405 let mut fixture = fixture();
1406 mutate(&mut fixture.manifest, &fixture.dataset);
1407 Selection::replay(&fixture.manifest, &fixture.dataset, &mut fixture.ledger)
1408 .expect_err("the tampered manifest must be refused")
1409 }
1410
1411 #[test]
1412 fn manifest_replay_round_trips_through_the_file_form() {
1413 let mut fixture = fixture();
1414 let bytes = fixture.manifest.to_file_bytes().expect("file bytes");
1415 let parsed = SelectionManifest::from_bytes(&bytes).expect("the digest verifies");
1416
1417 let replayed = Selection::replay(&parsed, &fixture.dataset, &mut fixture.ledger)
1418 .expect("an honest manifest replays");
1419
1420 assert_eq!(replayed.examples(), fixture.selection.examples());
1421 assert_eq!(replayed.semantic_hash(), fixture.selection.semantic_hash());
1422 assert_eq!(replayed.ledger_hash(), fixture.selection.ledger_hash());
1423 assert_eq!(replayed.ordered_ids(), fixture.selection.ordered_ids());
1424 }
1425
1426 #[test]
1430 fn manifest_replay_succeeds_against_a_ledger_that_has_already_moved_on() {
1431 let mut fixture = fixture();
1432 fixture
1433 .ledger
1434 .record("train", "canonical", "unrelated-later-work", "aa");
1435 assert_ne!(
1436 crate::hash::hex(&fixture.ledger.ledger_hash()),
1437 fixture.manifest.payload.ledger_hash,
1438 "the fixture must actually have diverged, or this test is vacuous"
1439 );
1440 let before = fixture.ledger.records().len();
1441
1442 let replayed = Selection::replay(&fixture.manifest, &fixture.dataset, &mut fixture.ledger)
1443 .expect("replay must not depend on the live ledger");
1444
1445 assert_eq!(replayed.ordered_ids(), fixture.selection.ordered_ids());
1446 assert_eq!(fixture.ledger.records().len(), before + 1);
1447 assert_eq!(
1448 fixture
1449 .ledger
1450 .records()
1451 .last()
1452 .expect("a record was appended")
1453 .purpose,
1454 "select-replay"
1455 );
1456 }
1457
1458 #[test]
1459 fn manifest_replay_rejects_a_compatibility_profile() {
1460 let err = reject(|manifest, _| manifest.payload.profile = "compatibility".to_string());
1461 match err {
1462 ContrastiveDataError::ProfileMismatch { expected, got } => {
1463 assert_eq!(expected, "canonical");
1464 assert_eq!(got, "compatibility");
1465 }
1466 other => panic!("expected ProfileMismatch, got {other:?}"),
1467 }
1468 }
1469
1470 #[test]
1471 fn manifest_replay_rejects_an_altered_dataset_fingerprint() {
1472 let err = reject(|manifest, _| manifest.payload.dataset_fingerprint = "ab".repeat(32));
1473 match err {
1474 ContrastiveDataError::FingerprintMismatch { expected, got } => {
1475 assert_eq!(expected, "ab".repeat(32));
1476 assert_ne!(got, expected);
1477 }
1478 other => panic!("expected FingerprintMismatch, got {other:?}"),
1479 }
1480 }
1481
1482 #[test]
1483 fn manifest_replay_rejects_an_altered_validation_fingerprint() {
1484 let err = reject(|manifest, dataset| {
1485 assert_ne!(
1488 manifest.payload.dataset_fingerprint,
1489 manifest.payload.validation_fingerprint
1490 );
1491 assert_eq!(
1492 manifest.payload.dataset_fingerprint,
1493 dataset.fingerprint().hex()
1494 );
1495 manifest.payload.validation_fingerprint = "cd".repeat(32);
1496 });
1497 match err {
1498 ContrastiveDataError::FingerprintMismatch { expected, got } => {
1499 assert_eq!(expected, "cd".repeat(32));
1500 assert_ne!(got, expected);
1501 }
1502 other => panic!("expected FingerprintMismatch, got {other:?}"),
1503 }
1504 }
1505
1506 #[test]
1513 fn manifest_replay_rejects_a_renamed_label() {
1514 let err = reject(|manifest, dataset| {
1515 assert_eq!(
1516 manifest.payload.label_names,
1517 dataset.label_names().to_vec(),
1518 "the fixture must start in agreement, or this test proves nothing"
1519 );
1520 manifest.payload.label_names[1] = "opposed".to_string();
1521 reseal(manifest);
1522 });
1523 match err {
1524 ContrastiveDataError::SelectionReplayMismatch { field } => {
1525 assert_eq!(field, "label_names");
1526 }
1527 other => panic!("expected SelectionReplayMismatch on label_names, got {other:?}"),
1528 }
1529 }
1530
1531 #[test]
1532 fn manifest_replay_rejects_an_unsupported_normalization_version() {
1533 let err = reject(|manifest, _| {
1534 manifest.payload.normalization_version = "nfc-trim-ws-v2".to_string();
1535 reseal(manifest);
1536 });
1537 match err {
1538 ContrastiveDataError::UnsupportedNormalizationVersion { got, supported } => {
1539 assert_eq!(got, "nfc-trim-ws-v2");
1540 assert_eq!(supported, crate::hash::CONTENT_NORMALIZATION_VERSION);
1541 }
1542 other => panic!("expected UnsupportedNormalizationVersion, got {other:?}"),
1543 }
1544 }
1545
1546 #[test]
1548 fn manifest_replay_rejects_a_ledger_hash_that_does_not_describe_its_own_records() {
1549 let err = reject(|manifest, _| {
1550 manifest
1551 .payload
1552 .access_ledger
1553 .push(crate::ledger::AccessRecord {
1554 role: "train".to_string(),
1555 profile: "canonical".to_string(),
1556 purpose: "fabricated".to_string(),
1557 fingerprint_hex: "aa".repeat(32),
1558 });
1559 reseal(manifest);
1562 });
1563 match err {
1564 ContrastiveDataError::SelectionReplayMismatch { field } => {
1565 assert_eq!(field, "access_ledger");
1566 }
1567 other => panic!("expected SelectionReplayMismatch on access_ledger, got {other:?}"),
1568 }
1569 }
1570
1571 #[test]
1574 fn manifest_replay_rejects_an_uppercase_ledger_hash() {
1575 let err = reject(|manifest, _| {
1576 manifest.payload.ledger_hash = manifest.payload.ledger_hash.to_uppercase();
1577 reseal(manifest);
1578 });
1579 match err {
1580 ContrastiveDataError::SelectionReplayMismatch { field } => {
1581 assert_eq!(field, "ledger_hash");
1582 }
1583 other => panic!("expected SelectionReplayMismatch on ledger_hash, got {other:?}"),
1584 }
1585 }
1586
1587 #[test]
1588 fn manifest_replay_rejects_an_altered_exclusion_record() {
1589 let err = reject(|manifest, _| {
1590 let mut other_ledger = AccessLedger::new();
1591 let other = test_corpus::dataset_with_cross_split_duplicate(12, &mut other_ledger);
1592 assert_ne!(
1593 other.exclusions(),
1594 &manifest.payload.exclusions,
1595 "the substituted record must actually differ"
1596 );
1597 manifest.payload.exclusions = other.exclusions().clone();
1598 });
1599 match err {
1600 ContrastiveDataError::ExclusionRecordMismatch { expected, got } => {
1601 assert_ne!(expected, got);
1602 }
1603 other => panic!("expected ExclusionRecordMismatch, got {other:?}"),
1604 }
1605 }
1606
1607 #[test]
1608 fn manifest_replay_rejects_an_id_outside_the_selection_pool() {
1609 let err = reject(|manifest, dataset| {
1610 let row = dataset.validation().rows()[0].clone();
1611 manifest.payload.ordered_examples[0] = SelectedExampleRecord {
1612 id: row.id,
1613 label: 0,
1614 exact_hash: manifest.payload.ordered_examples[0].exact_hash.clone(),
1615 normalized_hash: manifest.payload.ordered_examples[0].normalized_hash.clone(),
1616 };
1617 });
1618 match err {
1619 ContrastiveDataError::EndpointNotInSelection { id, found_in } => {
1620 assert_eq!(id, "validation:0");
1621 assert_eq!(found_in, "validation");
1622 }
1623 other => panic!("expected EndpointNotInSelection, got {other:?}"),
1624 }
1625 }
1626
1627 #[test]
1628 fn manifest_replay_rejects_a_duplicated_id() {
1629 let err = reject(|manifest, _| {
1630 manifest.payload.ordered_examples[1] = manifest.payload.ordered_examples[0].clone();
1631 });
1632 match err {
1633 ContrastiveDataError::DuplicateId { split, id } => {
1634 assert_eq!(split, "selection");
1635 assert!(id.starts_with("train:0-"), "{id}");
1636 }
1637 other => panic!("expected DuplicateId, got {other:?}"),
1638 }
1639 }
1640
1641 #[test]
1642 fn manifest_replay_rejects_an_unbalanced_class() {
1643 let err = reject(|manifest, _| {
1644 manifest.payload.ordered_examples.remove(0);
1645 });
1646 match err {
1647 ContrastiveDataError::InvalidClassCounts {
1648 split,
1649 expected,
1650 got,
1651 } => {
1652 assert_eq!(split, "selection");
1653 assert_eq!(expected, vec![8, 8, 8]);
1654 assert_eq!(got, vec![7, 8, 8]);
1655 }
1656 other => panic!("expected InvalidClassCounts, got {other:?}"),
1657 }
1658 }
1659
1660 #[test]
1661 fn manifest_replay_rejects_examples_swapped_across_classes() {
1662 let err = reject(|manifest, _| {
1663 let rows = &mut manifest.payload.ordered_examples;
1664 assert_eq!((rows[0].label, rows[8].label), (0, 1));
1665 rows.swap(0, 8);
1666 });
1667 match err {
1668 ContrastiveDataError::SelectionReplayMismatch { field } => {
1669 assert_eq!(field, "class_order");
1670 }
1671 other => panic!("expected SelectionReplayMismatch, got {other:?}"),
1672 }
1673 }
1674
1675 #[test]
1676 fn manifest_replay_rejects_a_tampered_row_hash() {
1677 let err = reject(|manifest, _| {
1678 manifest.payload.ordered_examples[3].exact_hash = "0".repeat(64);
1679 });
1680 match err {
1681 ContrastiveDataError::RowHashMismatch { id, expected, got } => {
1682 assert!(id.starts_with("train:0-"), "{id}");
1683 assert_eq!(expected, "0".repeat(64));
1684 assert_ne!(got, expected);
1685 }
1686 other => panic!("expected RowHashMismatch, got {other:?}"),
1687 }
1688 }
1689
1690 #[test]
1691 fn manifest_replay_rejects_an_unsupported_schema_version() {
1692 let err = reject(|manifest, _| manifest.payload.schema_version = 99);
1693 match err {
1694 ContrastiveDataError::UnsupportedSchemaVersion {
1695 field,
1696 got,
1697 supported,
1698 } => {
1699 assert_eq!(field, "selection");
1700 assert_eq!((got, supported), (99, 1));
1701 }
1702 other => panic!("expected UnsupportedSchemaVersion, got {other:?}"),
1703 }
1704 }
1705
1706 #[test]
1707 fn manifest_replay_rejects_an_unsupported_algorithm_version() {
1708 let err = reject(|manifest, _| manifest.payload.algorithm_version = 99);
1709 match err {
1710 ContrastiveDataError::UnsupportedAlgorithmVersion { got, supported } => {
1711 assert_eq!((got, supported), (99, 1));
1712 }
1713 other => panic!("expected UnsupportedAlgorithmVersion, got {other:?}"),
1714 }
1715 }
1716
1717 #[test]
1725 fn manifest_replay_rejects_a_consistent_but_unreachable_ordered_list() {
1726 let err = reject(|manifest, dataset| {
1727 let selected: Vec<&str> = manifest
1728 .payload
1729 .ordered_examples
1730 .iter()
1731 .map(|row| row.id.as_str())
1732 .collect();
1733 let substitute = dataset
1734 .train()
1735 .rows()
1736 .iter()
1737 .find(|row| row.label == 0 && !selected.contains(&row.id.as_str()))
1738 .expect("the pool is larger than the selection")
1739 .clone();
1740 manifest.payload.ordered_examples[0] = SelectedExampleRecord {
1741 exact_hash: crate::hash::hex(
1742 dataset
1743 .train()
1744 .exact_hash_of(&substitute.id)
1745 .expect("the row is in the split"),
1746 ),
1747 normalized_hash: crate::hash::hex(
1748 dataset
1749 .train()
1750 .normalized_hash_of(&substitute.id)
1751 .expect("the row is in the split"),
1752 ),
1753 id: substitute.id,
1754 label: 0,
1755 };
1756 reseal(manifest);
1757 manifest
1758 .verify_digest()
1759 .expect("the forgery is internally consistent — that is the point");
1760 });
1761 match err {
1762 ContrastiveDataError::SelectionReplayMismatch { field } => {
1763 assert_eq!(field, "ordered_examples");
1764 }
1765 other => panic!("expected SelectionReplayMismatch, got {other:?}"),
1766 }
1767 }
1768
1769 #[test]
1771 fn manifest_replay_rejects_a_digest_that_disagrees_with_its_payload() {
1772 let err = reject(|manifest, _| manifest.semantic_hash = "f".repeat(64));
1773 match err {
1774 ContrastiveDataError::SemanticHashMismatch { expected, got } => {
1775 assert_eq!(expected, "f".repeat(64));
1776 assert_ne!(got, expected);
1777 }
1778 other => panic!("expected SemanticHashMismatch, got {other:?}"),
1779 }
1780 }
1781
1782 #[test]
1784 fn manifest_replay_returns_a_usable_selection() {
1785 let mut fixture = fixture();
1786 let replayed = Selection::replay(&fixture.manifest, &fixture.dataset, &mut fixture.ledger)
1787 .expect("an honest manifest replays");
1788
1789 assert_eq!(replayed.class_sizes(), fixture.selection.class_sizes());
1790 assert_eq!(replayed.root_seed(), 31);
1791 assert_eq!(replayed.shots_per_class(), 8);
1792 let first = replayed.ordered_ids()[0].to_string();
1793 let selected = replayed
1794 .selected_id(&first)
1795 .expect("the replayed selection indexes its own rows");
1796 assert_eq!(replayed.id_of(selected), first);
1797 assert_eq!(replayed.label_of(selected), 0);
1798
1799 let mut fresh_ledger = AccessLedger::new();
1801 let fresh_dataset = test_corpus::dataset(12, &mut fresh_ledger);
1802 let fresh = super::FewShotSelector::select(
1803 &fresh_dataset,
1804 &SelectionConfig {
1805 root_seed: 31,
1806 shots_per_class: 8,
1807 },
1808 &mut fresh_ledger,
1809 )
1810 .expect("a fresh selection succeeds");
1811 assert_eq!(fresh.examples(), replayed.examples());
1812 assert_eq!(fresh.semantic_hash(), replayed.semantic_hash());
1813 }
1814}