1use std::{
4 collections::{BTreeMap, BTreeSet},
5 num::NonZeroUsize,
6};
7
8use eredu_core::{RealtimeFrameSlot, RealtimeSlotCoordinate, RealtimeSpeechConfig};
9
10use crate::generation::TokenDomain;
11
12#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct RealtimePayloadOwnerIdentity(u64);
15
16impl RealtimePayloadOwnerIdentity {
17 pub const fn new(value: u64) -> Option<Self> {
19 if value == 0 {
20 None
21 } else {
22 Some(Self(value))
23 }
24 }
25
26 pub const fn value(self) -> u64 {
28 self.0
29 }
30}
31
32#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub struct RealtimePayloadGeneration(u64);
35
36impl RealtimePayloadGeneration {
37 pub const fn new(value: u64) -> Option<Self> {
39 if value == 0 {
40 None
41 } else {
42 Some(Self(value))
43 }
44 }
45
46 pub const fn value(self) -> u64 {
48 self.0
49 }
50}
51
52#[derive(Debug, Clone, Eq, PartialEq)]
54pub struct RealtimePayloadContract {
55 schedule: RealtimeSpeechConfig,
56 batch: NonZeroUsize,
57 text_domain: TokenDomain,
58 audio_domain: TokenDomain,
59 generation: RealtimePayloadGeneration,
60 owner: RealtimePayloadOwnerIdentity,
61}
62
63impl RealtimePayloadContract {
64 pub fn new(
66 schedule: RealtimeSpeechConfig,
67 batch: usize,
68 text_domain: TokenDomain,
69 audio_domain: TokenDomain,
70 generation: RealtimePayloadGeneration,
71 owner: RealtimePayloadOwnerIdentity,
72 ) -> Result<Self, RealtimePayloadContractError> {
73 let batch = NonZeroUsize::new(batch).ok_or(RealtimePayloadContractError::EmptyBatch)?;
74 Ok(Self {
75 schedule,
76 batch,
77 text_domain,
78 audio_domain,
79 generation,
80 owner,
81 })
82 }
83
84 pub const fn schedule(&self) -> &RealtimeSpeechConfig {
86 &self.schedule
87 }
88
89 pub const fn batch(&self) -> NonZeroUsize {
91 self.batch
92 }
93
94 pub const fn text_domain(&self) -> TokenDomain {
96 self.text_domain
97 }
98
99 pub const fn audio_domain(&self) -> TokenDomain {
101 self.audio_domain
102 }
103
104 pub const fn generation(&self) -> RealtimePayloadGeneration {
106 self.generation
107 }
108
109 pub const fn owner(&self) -> RealtimePayloadOwnerIdentity {
111 self.owner
112 }
113
114 pub fn slot_domain(
116 &self,
117 slot: RealtimeFrameSlot,
118 ) -> Result<TokenDomain, RealtimePayloadContractError> {
119 match slot {
120 RealtimeFrameSlot::Text => Ok(self.text_domain),
121 RealtimeFrameSlot::Audio(codebook)
122 if codebook < self.schedule.total_audio_codebooks() =>
123 {
124 Ok(self.audio_domain)
125 }
126 _ => Err(RealtimePayloadContractError::InvalidSlot {
127 slot,
128 total_audio_codebooks: self.schedule.total_audio_codebooks(),
129 }),
130 }
131 }
132
133 pub fn validate(&self, contract: &Self) -> Result<(), RealtimePayloadContractError> {
135 if self.schedule != contract.schedule {
136 return Err(RealtimePayloadContractError::ScheduleMismatch);
137 }
138 if self.batch != contract.batch {
139 return Err(RealtimePayloadContractError::BatchMismatch);
140 }
141 if self.text_domain != contract.text_domain {
142 return Err(RealtimePayloadContractError::TextDomainMismatch);
143 }
144 if self.audio_domain != contract.audio_domain {
145 return Err(RealtimePayloadContractError::AudioDomainMismatch);
146 }
147 if self.generation != contract.generation {
148 return Err(RealtimePayloadContractError::GenerationMismatch);
149 }
150 if self.owner != contract.owner {
151 return Err(RealtimePayloadContractError::OwnerMismatch);
152 }
153 Ok(())
154 }
155}
156
157#[derive(Debug, Clone, Eq, PartialEq)]
159pub struct RealtimePayloadEnvelope<P> {
160 contract: RealtimePayloadContract,
161 coordinate: RealtimeSlotCoordinate,
162 domain: TokenDomain,
163 payload: P,
164}
165
166impl<P> RealtimePayloadEnvelope<P> {
167 pub fn new(
169 contract: RealtimePayloadContract,
170 coordinate: RealtimeSlotCoordinate,
171 payload: P,
172 ) -> Result<Self, RealtimePayloadContractError> {
173 let domain = contract.slot_domain(coordinate.slot())?;
174 Ok(Self {
175 contract,
176 coordinate,
177 domain,
178 payload,
179 })
180 }
181
182 pub const fn contract(&self) -> &RealtimePayloadContract {
184 &self.contract
185 }
186
187 pub const fn coordinate(&self) -> RealtimeSlotCoordinate {
189 self.coordinate
190 }
191
192 pub const fn domain(&self) -> TokenDomain {
194 self.domain
195 }
196
197 pub const fn payload(&self) -> &P {
199 &self.payload
200 }
201
202 pub fn validate(
204 &self,
205 contract: &RealtimePayloadContract,
206 coordinate: RealtimeSlotCoordinate,
207 ) -> Result<(), RealtimePayloadContractError> {
208 self.contract.validate(contract)?;
209 if self.coordinate != coordinate {
210 return Err(RealtimePayloadContractError::CoordinateMismatch);
211 }
212 let domain = contract.slot_domain(coordinate.slot())?;
213 if self.domain != domain {
214 return Err(RealtimePayloadContractError::SlotDomainMismatch);
215 }
216 Ok(())
217 }
218
219 pub fn into_parts(
221 self,
222 ) -> (
223 RealtimePayloadContract,
224 RealtimeSlotCoordinate,
225 TokenDomain,
226 P,
227 ) {
228 (self.contract, self.coordinate, self.domain, self.payload)
229 }
230}
231
232#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
234#[non_exhaustive]
235pub enum RealtimePayloadContractError {
236 #[error("realtime payload contract batch is empty")]
238 EmptyBatch,
239 #[error("realtime payload contract schedule does not match")]
241 ScheduleMismatch,
242 #[error("realtime payload contract batch does not match")]
244 BatchMismatch,
245 #[error("realtime payload contract text-token domain does not match")]
247 TextDomainMismatch,
248 #[error("realtime payload contract audio-token domain does not match")]
250 AudioDomainMismatch,
251 #[error("realtime payload contract generation does not match")]
253 GenerationMismatch,
254 #[error("realtime payload contract owner does not match")]
256 OwnerMismatch,
257 #[error("realtime payload coordinate does not match")]
259 CoordinateMismatch,
260 #[error("realtime payload slot token domain does not match")]
262 SlotDomainMismatch,
263 #[error(
265 "realtime payload slot {slot:?} is outside text plus {total_audio_codebooks} audio codebooks"
266 )]
267 InvalidSlot {
268 slot: RealtimeFrameSlot,
270 total_audio_codebooks: usize,
272 },
273}
274
275#[derive(Debug, Clone, Eq, PartialEq)]
277pub struct RealtimePayloadHistory<P> {
278 schedule: RealtimeSpeechConfig,
279 contract: Option<RealtimePayloadContract>,
280 payloads: BTreeMap<RealtimeSlotCoordinate, RealtimePayloadEnvelope<P>>,
281}
282
283impl<P> RealtimePayloadHistory<P> {
284 pub fn new(schedule: RealtimeSpeechConfig) -> Self {
290 Self {
291 schedule,
292 contract: None,
293 payloads: BTreeMap::new(),
294 }
295 }
296
297 pub fn with_contract(contract: RealtimePayloadContract) -> Self {
299 Self {
300 schedule: contract.schedule().clone(),
301 contract: Some(contract),
302 payloads: BTreeMap::new(),
303 }
304 }
305
306 pub const fn schedule(&self) -> &RealtimeSpeechConfig {
308 &self.schedule
309 }
310
311 pub const fn contract(&self) -> Option<&RealtimePayloadContract> {
313 self.contract.as_ref()
314 }
315
316 pub fn bind_or_validate_contract(
320 &mut self,
321 contract: &RealtimePayloadContract,
322 ) -> Result<(), RealtimePayloadHistoryError> {
323 if &self.schedule != contract.schedule() {
324 return Err(RealtimePayloadHistoryError::PayloadContract(
325 RealtimePayloadContractError::ScheduleMismatch,
326 ));
327 }
328 if let Some(current) = &self.contract {
329 current
330 .validate(contract)
331 .map_err(RealtimePayloadHistoryError::PayloadContract)
332 } else {
333 debug_assert!(self.payloads.is_empty());
334 self.contract = Some(contract.clone());
335 Ok(())
336 }
337 }
338
339 pub fn validate_successor(&self, successor: &Self) -> Result<(), RealtimePayloadHistoryError> {
341 self.validate_schedule(&successor.schedule)?;
342 match (&self.contract, &successor.contract) {
343 (Some(current), Some(candidate)) => current
344 .validate(candidate)
345 .map_err(RealtimePayloadHistoryError::PayloadContract),
346 (None, Some(_)) if self.payloads.is_empty() => Ok(()),
347 (None, None) => Ok(()),
348 (Some(_), None) | (None, Some(_)) => Err(RealtimePayloadHistoryError::UnboundContract),
349 }
350 }
351
352 pub fn len(&self) -> usize {
354 self.payloads.len()
355 }
356
357 pub fn is_empty(&self) -> bool {
359 self.payloads.is_empty()
360 }
361
362 pub fn validate_schedule(
364 &self,
365 schedule: &RealtimeSpeechConfig,
366 ) -> Result<(), RealtimePayloadHistoryError> {
367 if &self.schedule == schedule {
368 Ok(())
369 } else {
370 Err(RealtimePayloadHistoryError::ScheduleMismatch)
371 }
372 }
373
374 pub fn delayed_coordinate(
379 &self,
380 schedule: &RealtimeSpeechConfig,
381 base_position: usize,
382 slot: RealtimeFrameSlot,
383 ) -> Result<RealtimeSlotCoordinate, RealtimePayloadHistoryError> {
384 self.validate_schedule(schedule)?;
385 let delay = self.slot_delay(slot)?;
386 let position = base_position.checked_add(delay).ok_or(
387 RealtimePayloadHistoryError::CoordinateOverflow {
388 base_position,
389 delay,
390 },
391 )?;
392 Ok(RealtimeSlotCoordinate::new(position, slot))
393 }
394
395 pub fn insert(
397 &mut self,
398 schedule: &RealtimeSpeechConfig,
399 coordinate: RealtimeSlotCoordinate,
400 payload: P,
401 ) -> Result<(), RealtimePayloadHistoryError> {
402 self.insert_many(schedule, [(coordinate, payload)])
403 }
404
405 pub fn insert_delayed(
407 &mut self,
408 schedule: &RealtimeSpeechConfig,
409 base_position: usize,
410 slot: RealtimeFrameSlot,
411 payload: P,
412 ) -> Result<RealtimeSlotCoordinate, RealtimePayloadHistoryError> {
413 let coordinate = self.delayed_coordinate(schedule, base_position, slot)?;
414 self.insert(schedule, coordinate, payload)?;
415 Ok(coordinate)
416 }
417
418 pub fn insert_many(
422 &mut self,
423 schedule: &RealtimeSpeechConfig,
424 payloads: impl IntoIterator<Item = (RealtimeSlotCoordinate, P)>,
425 ) -> Result<(), RealtimePayloadHistoryError> {
426 self.validate_schedule(schedule)?;
427 let contract = self
428 .contract
429 .as_ref()
430 .ok_or(RealtimePayloadHistoryError::UnboundContract)?
431 .clone();
432 let payloads = payloads.into_iter().collect::<Vec<_>>();
433 let mut pending = BTreeSet::new();
434 for (coordinate, _) in &payloads {
435 self.validate_coordinate(*coordinate)?;
436 if self.payloads.contains_key(coordinate) || !pending.insert(*coordinate) {
437 return Err(RealtimePayloadHistoryError::DuplicatePayload {
438 coordinate: *coordinate,
439 });
440 }
441 }
442 let envelopes = payloads
443 .into_iter()
444 .map(|(coordinate, payload)| {
445 RealtimePayloadEnvelope::new(contract.clone(), coordinate, payload)
446 .map(|envelope| (coordinate, envelope))
447 .map_err(RealtimePayloadHistoryError::PayloadContract)
448 })
449 .collect::<Result<Vec<_>, _>>()?;
450 self.payloads.extend(envelopes);
451 Ok(())
452 }
453
454 pub fn overwrite_many(
460 &mut self,
461 schedule: &RealtimeSpeechConfig,
462 payloads: impl IntoIterator<Item = (RealtimeSlotCoordinate, P)>,
463 ) -> Result<(), RealtimePayloadHistoryError> {
464 self.validate_schedule(schedule)?;
465 let contract = self
466 .contract
467 .as_ref()
468 .ok_or(RealtimePayloadHistoryError::UnboundContract)?
469 .clone();
470 let payloads = payloads.into_iter().collect::<Vec<_>>();
471 for (coordinate, _) in &payloads {
472 self.validate_coordinate(*coordinate)?;
473 }
474 let envelopes = payloads
475 .into_iter()
476 .map(|(coordinate, payload)| {
477 RealtimePayloadEnvelope::new(contract.clone(), coordinate, payload)
478 .map(|envelope| (coordinate, envelope))
479 .map_err(RealtimePayloadHistoryError::PayloadContract)
480 })
481 .collect::<Result<Vec<_>, _>>()?;
482 self.payloads.extend(envelopes);
483 Ok(())
484 }
485
486 pub fn get(
488 &self,
489 schedule: &RealtimeSpeechConfig,
490 coordinate: RealtimeSlotCoordinate,
491 ) -> Result<Option<&P>, RealtimePayloadHistoryError> {
492 self.validate_schedule(schedule)?;
493 self.validate_coordinate(coordinate)?;
494 Ok(self
495 .payloads
496 .get(&coordinate)
497 .map(RealtimePayloadEnvelope::payload))
498 }
499
500 pub fn envelope(
502 &self,
503 schedule: &RealtimeSpeechConfig,
504 coordinate: RealtimeSlotCoordinate,
505 ) -> Result<Option<&RealtimePayloadEnvelope<P>>, RealtimePayloadHistoryError> {
506 self.validate_schedule(schedule)?;
507 self.validate_coordinate(coordinate)?;
508 Ok(self.payloads.get(&coordinate))
509 }
510
511 pub fn required(
513 &self,
514 schedule: &RealtimeSpeechConfig,
515 coordinate: RealtimeSlotCoordinate,
516 ) -> Result<&P, RealtimePayloadHistoryError> {
517 self.get(schedule, coordinate)?
518 .ok_or(RealtimePayloadHistoryError::MissingPayload { coordinate })
519 }
520
521 pub fn resolve_required(
523 &self,
524 schedule: &RealtimeSpeechConfig,
525 coordinates: impl IntoIterator<Item = RealtimeSlotCoordinate>,
526 ) -> Result<Vec<&P>, RealtimePayloadHistoryError> {
527 self.validate_schedule(schedule)?;
528 coordinates
529 .into_iter()
530 .map(|coordinate| self.required(schedule, coordinate))
531 .collect()
532 }
533
534 pub fn prune_for_next_frontier(
542 &mut self,
543 schedule: &RealtimeSpeechConfig,
544 next_frontier: usize,
545 ) -> Result<usize, RealtimePayloadHistoryError> {
546 self.validate_schedule(schedule)?;
547 let retained_positions = schedule.max_delay().checked_add(2).ok_or(
548 RealtimePayloadHistoryError::RetentionWindowOverflow {
549 max_delay: schedule.max_delay(),
550 },
551 )?;
552 let minimum = next_frontier.saturating_sub(retained_positions);
553 let previous = self.payloads.len();
554 self.payloads
555 .retain(|coordinate, _| coordinate.position() >= minimum);
556 Ok(previous - self.payloads.len())
557 }
558
559 pub fn retained_values(&self) -> impl Iterator<Item = &P> {
561 self.payloads.values().map(RealtimePayloadEnvelope::payload)
562 }
563
564 pub fn envelopes(&self) -> impl Iterator<Item = &RealtimePayloadEnvelope<P>> {
566 self.payloads.values()
567 }
568
569 pub fn entries(&self) -> impl Iterator<Item = (RealtimeSlotCoordinate, &P)> {
571 self.payloads
572 .iter()
573 .map(|(coordinate, envelope)| (*coordinate, envelope.payload()))
574 }
575
576 fn validate_coordinate(
577 &self,
578 coordinate: RealtimeSlotCoordinate,
579 ) -> Result<(), RealtimePayloadHistoryError> {
580 self.slot_delay(coordinate.slot()).map(|_| ())
581 }
582
583 fn slot_delay(&self, slot: RealtimeFrameSlot) -> Result<usize, RealtimePayloadHistoryError> {
584 match slot {
585 RealtimeFrameSlot::Text => Ok(self.schedule.text_delay()),
586 RealtimeFrameSlot::Audio(codebook) => {
587 self.schedule.audio_delays().get(codebook).copied().ok_or(
588 RealtimePayloadHistoryError::InvalidSlot {
589 slot,
590 total_audio_codebooks: self.schedule.total_audio_codebooks(),
591 },
592 )
593 }
594 _ => Err(RealtimePayloadHistoryError::InvalidSlot {
595 slot,
596 total_audio_codebooks: self.schedule.total_audio_codebooks(),
597 }),
598 }
599 }
600}
601
602#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
604#[non_exhaustive]
605pub enum RealtimePayloadHistoryError {
606 #[error("realtime payload history has no bound payload contract")]
608 UnboundContract,
609 #[error(transparent)]
611 PayloadContract(RealtimePayloadContractError),
612 #[error("realtime payload history does not match the normalized schedule")]
614 ScheduleMismatch,
615 #[error(
617 "realtime payload slot {slot:?} is outside text plus {total_audio_codebooks} audio codebooks"
618 )]
619 InvalidSlot {
620 slot: RealtimeFrameSlot,
622 total_audio_codebooks: usize,
624 },
625 #[error("realtime coordinate {coordinate:?} already contains a payload")]
627 DuplicatePayload {
628 coordinate: RealtimeSlotCoordinate,
630 },
631 #[error("realtime coordinate {coordinate:?} has no retained payload")]
633 MissingPayload {
634 coordinate: RealtimeSlotCoordinate,
636 },
637 #[error("realtime payload coordinate overflowed from base {base_position} plus delay {delay}")]
639 CoordinateOverflow {
640 base_position: usize,
642 delay: usize,
644 },
645 #[error("realtime payload retention window overflowed for maximum delay {max_delay}")]
647 RetentionWindowOverflow {
648 max_delay: usize,
650 },
651}
652
653#[cfg(test)]
654mod tests {
655 use eredu_core::RealtimeFrameConvention;
656
657 use super::*;
658
659 fn schedule() -> RealtimeSpeechConfig {
660 RealtimeSpeechConfig::new(
661 2,
662 1,
663 1,
664 1,
665 0,
666 1,
667 RealtimeFrameConvention::FeedbackAlignedHistory,
668 vec![2, 0, 3],
669 )
670 .unwrap()
671 }
672
673 fn coordinate(position: usize, slot: RealtimeFrameSlot) -> RealtimeSlotCoordinate {
674 RealtimeSlotCoordinate::new(position, slot)
675 }
676
677 fn payload_contract(
678 schedule: RealtimeSpeechConfig,
679 batch: usize,
680 text_domain: TokenDomain,
681 audio_domain: TokenDomain,
682 generation: u64,
683 owner: u64,
684 ) -> RealtimePayloadContract {
685 RealtimePayloadContract::new(
686 schedule,
687 batch,
688 text_domain,
689 audio_domain,
690 RealtimePayloadGeneration::new(generation).unwrap(),
691 RealtimePayloadOwnerIdentity::new(owner).unwrap(),
692 )
693 .unwrap()
694 }
695
696 fn exact_payload_contract() -> RealtimePayloadContract {
697 payload_contract(
698 schedule(),
699 2,
700 TokenDomain::new(32),
701 TokenDomain::new(16),
702 7,
703 3,
704 )
705 }
706
707 fn payload_history<P>() -> RealtimePayloadHistory<P> {
708 RealtimePayloadHistory::with_contract(exact_payload_contract())
709 }
710
711 #[test]
712 fn payload_contract_requires_nonempty_batch_and_typed_nonempty_identities() {
713 assert_eq!(
714 RealtimePayloadContract::new(
715 schedule(),
716 0,
717 TokenDomain::new(32),
718 TokenDomain::new(16),
719 RealtimePayloadGeneration::new(7).unwrap(),
720 RealtimePayloadOwnerIdentity::new(3).unwrap(),
721 ),
722 Err(RealtimePayloadContractError::EmptyBatch)
723 );
724 assert_eq!(RealtimePayloadGeneration::new(0), None);
725 assert_eq!(RealtimePayloadOwnerIdentity::new(0), None);
726
727 let contract = exact_payload_contract();
728 assert_eq!(contract.schedule(), &schedule());
729 assert_eq!(contract.batch().get(), 2);
730 assert_eq!(contract.text_domain(), TokenDomain::new(32));
731 assert_eq!(contract.audio_domain(), TokenDomain::new(16));
732 assert_eq!(contract.generation().value(), 7);
733 assert_eq!(contract.owner().value(), 3);
734 }
735
736 #[test]
737 fn payload_contract_rejects_each_exact_identity_perturbation() {
738 let exact = exact_payload_contract();
739 let other_schedule = RealtimeSpeechConfig::new(
740 2,
741 1,
742 1,
743 1,
744 0,
745 1,
746 RealtimeFrameConvention::AbsoluteDelayedSlots,
747 vec![2, 0, 3],
748 )
749 .unwrap();
750
751 assert_eq!(
752 exact.validate(&payload_contract(
753 other_schedule,
754 2,
755 TokenDomain::new(32),
756 TokenDomain::new(16),
757 7,
758 3,
759 )),
760 Err(RealtimePayloadContractError::ScheduleMismatch)
761 );
762 assert_eq!(
763 exact.validate(&payload_contract(
764 schedule(),
765 3,
766 TokenDomain::new(32),
767 TokenDomain::new(16),
768 7,
769 3,
770 )),
771 Err(RealtimePayloadContractError::BatchMismatch)
772 );
773 assert_eq!(
774 exact.validate(&payload_contract(
775 schedule(),
776 2,
777 TokenDomain::new(33),
778 TokenDomain::new(16),
779 7,
780 3,
781 )),
782 Err(RealtimePayloadContractError::TextDomainMismatch)
783 );
784 assert_eq!(
785 exact.validate(&payload_contract(
786 schedule(),
787 2,
788 TokenDomain::new(32),
789 TokenDomain::new(17),
790 7,
791 3,
792 )),
793 Err(RealtimePayloadContractError::AudioDomainMismatch)
794 );
795 assert_eq!(
796 exact.validate(&payload_contract(
797 schedule(),
798 2,
799 TokenDomain::new(32),
800 TokenDomain::new(16),
801 8,
802 3,
803 )),
804 Err(RealtimePayloadContractError::GenerationMismatch)
805 );
806 assert_eq!(
807 exact.validate(&payload_contract(
808 schedule(),
809 2,
810 TokenDomain::new(32),
811 TokenDomain::new(16),
812 7,
813 4,
814 )),
815 Err(RealtimePayloadContractError::OwnerMismatch)
816 );
817 }
818
819 #[test]
820 fn payload_envelope_derives_slot_domain_and_validates_exact_coordinate() {
821 let contract = exact_payload_contract();
822 let text_coordinate = coordinate(5, RealtimeFrameSlot::Text);
823 let text = RealtimePayloadEnvelope::new(contract.clone(), text_coordinate, "text").unwrap();
824 assert_eq!(text.contract(), &contract);
825 assert_eq!(text.coordinate(), text_coordinate);
826 assert_eq!(text.domain(), TokenDomain::new(32));
827 assert_eq!(text.payload(), &"text");
828 assert_eq!(text.validate(&contract, text_coordinate), Ok(()));
829
830 let audio_coordinate = coordinate(5, RealtimeFrameSlot::Audio(1));
831 let audio =
832 RealtimePayloadEnvelope::new(contract.clone(), audio_coordinate, "audio").unwrap();
833 assert_eq!(audio.domain(), TokenDomain::new(16));
834 assert_eq!(audio.validate(&contract, audio_coordinate), Ok(()));
835 assert_eq!(
836 audio.validate(&contract, coordinate(6, RealtimeFrameSlot::Audio(1))),
837 Err(RealtimePayloadContractError::CoordinateMismatch)
838 );
839
840 assert_eq!(
841 RealtimePayloadEnvelope::new(
842 contract,
843 coordinate(5, RealtimeFrameSlot::Audio(2)),
844 "invalid",
845 ),
846 Err(RealtimePayloadContractError::InvalidSlot {
847 slot: RealtimeFrameSlot::Audio(2),
848 total_audio_codebooks: 2,
849 })
850 );
851 }
852
853 #[test]
854 fn history_requires_first_contract_binding_and_stores_only_typed_envelopes() {
855 let schedule = schedule();
856 let text = coordinate(0, RealtimeFrameSlot::Text);
857 let audio = coordinate(0, RealtimeFrameSlot::Audio(0));
858 let mut history = RealtimePayloadHistory::new(schedule.clone());
859 assert!(history.contract().is_none());
860 assert_eq!(history.get(&schedule, text), Ok(None));
861 assert_eq!(
862 history.insert(&schedule, text, 3),
863 Err(RealtimePayloadHistoryError::UnboundContract)
864 );
865 assert!(history.is_empty());
866
867 let contract = exact_payload_contract();
868 history.bind_or_validate_contract(&contract).unwrap();
869 history
870 .insert_many(&schedule, [(text, 3), (audio, 5)])
871 .unwrap();
872 assert_eq!(history.contract(), Some(&contract));
873 let envelopes = history.envelopes().collect::<Vec<_>>();
874 assert_eq!(envelopes.len(), 2);
875 assert_eq!(envelopes[0].contract(), &contract);
876 assert_eq!(envelopes[0].coordinate(), text);
877 assert_eq!(envelopes[0].domain(), TokenDomain::new(32));
878 assert_eq!(envelopes[0].payload(), &3);
879 assert_eq!(envelopes[1].contract(), &contract);
880 assert_eq!(envelopes[1].coordinate(), audio);
881 assert_eq!(envelopes[1].domain(), TokenDomain::new(16));
882 assert_eq!(
883 history.envelope(&schedule, audio).unwrap(),
884 Some(envelopes[1])
885 );
886 }
887
888 #[test]
889 fn bound_history_rejects_every_contract_perturbation_without_mutation() {
890 let exact = exact_payload_contract();
891 let schedule = schedule();
892 let text = coordinate(0, RealtimeFrameSlot::Text);
893 let mut history = RealtimePayloadHistory::with_contract(exact.clone());
894 history.insert(&schedule, text, 3).unwrap();
895 let other_schedule = RealtimeSpeechConfig::new(
896 2,
897 1,
898 1,
899 1,
900 0,
901 1,
902 RealtimeFrameConvention::AbsoluteDelayedSlots,
903 vec![2, 0, 3],
904 )
905 .unwrap();
906 let perturbations = [
907 (
908 payload_contract(
909 other_schedule,
910 2,
911 TokenDomain::new(32),
912 TokenDomain::new(16),
913 7,
914 3,
915 ),
916 RealtimePayloadContractError::ScheduleMismatch,
917 ),
918 (
919 payload_contract(
920 schedule.clone(),
921 3,
922 TokenDomain::new(32),
923 TokenDomain::new(16),
924 7,
925 3,
926 ),
927 RealtimePayloadContractError::BatchMismatch,
928 ),
929 (
930 payload_contract(
931 schedule.clone(),
932 2,
933 TokenDomain::new(33),
934 TokenDomain::new(16),
935 7,
936 3,
937 ),
938 RealtimePayloadContractError::TextDomainMismatch,
939 ),
940 (
941 payload_contract(
942 schedule.clone(),
943 2,
944 TokenDomain::new(32),
945 TokenDomain::new(17),
946 7,
947 3,
948 ),
949 RealtimePayloadContractError::AudioDomainMismatch,
950 ),
951 (
952 payload_contract(
953 schedule.clone(),
954 2,
955 TokenDomain::new(32),
956 TokenDomain::new(16),
957 8,
958 3,
959 ),
960 RealtimePayloadContractError::GenerationMismatch,
961 ),
962 (
963 payload_contract(
964 schedule.clone(),
965 2,
966 TokenDomain::new(32),
967 TokenDomain::new(16),
968 7,
969 4,
970 ),
971 RealtimePayloadContractError::OwnerMismatch,
972 ),
973 ];
974
975 for (candidate, expected) in perturbations {
976 assert_eq!(
977 history.bind_or_validate_contract(&candidate),
978 Err(RealtimePayloadHistoryError::PayloadContract(expected))
979 );
980 assert_eq!(history.contract(), Some(&exact));
981 assert_eq!(history.required(&schedule, text), Ok(&3));
982 assert_eq!(history.len(), 1);
983 }
984 }
985
986 #[test]
987 fn history_clone_and_prune_preserve_envelope_identity() {
988 let schedule = schedule();
989 let contract = exact_payload_contract();
990 let mut history = RealtimePayloadHistory::with_contract(contract.clone());
991 for position in 0..=6 {
992 history
993 .insert(
994 &schedule,
995 coordinate(position, RealtimeFrameSlot::Text),
996 position,
997 )
998 .unwrap();
999 }
1000 let mut branch = history.clone();
1001 assert_eq!(branch.contract(), Some(&contract));
1002 assert!(branch
1003 .envelopes()
1004 .all(|envelope| envelope.contract() == &contract));
1005
1006 assert_eq!(branch.prune_for_next_frontier(&schedule, 7), Ok(2));
1007 assert_eq!(branch.len(), 5);
1008 assert_eq!(history.len(), 7);
1009 assert!(branch.envelopes().all(|envelope| {
1010 envelope.coordinate().position() >= 2 && envelope.contract() == &contract
1011 }));
1012 }
1013
1014 #[test]
1015 fn exact_insert_get_and_required_resolution_preserve_coordinate_order() {
1016 let schedule = schedule();
1017 let mut history = payload_history();
1018 let text = coordinate(2, RealtimeFrameSlot::Text);
1019 let audio_zero = coordinate(2, RealtimeFrameSlot::Audio(0));
1020 let audio_one = coordinate(2, RealtimeFrameSlot::Audio(1));
1021 history
1022 .insert_many(
1023 &schedule,
1024 [(audio_one, "a1"), (text, "text"), (audio_zero, "a0")],
1025 )
1026 .unwrap();
1027
1028 assert_eq!(history.get(&schedule, text).unwrap(), Some(&"text"));
1029 assert_eq!(history.required(&schedule, audio_one).unwrap(), &"a1");
1030 assert_eq!(
1031 history
1032 .resolve_required(&schedule, [audio_one, text, audio_one])
1033 .unwrap(),
1034 vec![&"a1", &"text", &"a1"]
1035 );
1036 assert_eq!(
1037 history.retained_values().copied().collect::<Vec<_>>(),
1038 vec!["text", "a0", "a1"]
1039 );
1040 assert_eq!(history.schedule(), &schedule);
1041 }
1042
1043 #[test]
1044 fn invalid_or_duplicate_publications_are_atomic() {
1045 let schedule = schedule();
1046 let mut history = payload_history();
1047 let retained = coordinate(0, RealtimeFrameSlot::Text);
1048 history.insert(&schedule, retained, 7).unwrap();
1049 let invalid = coordinate(1, RealtimeFrameSlot::Audio(2));
1050
1051 assert_eq!(
1052 history.insert_many(
1053 &schedule,
1054 [
1055 (coordinate(1, RealtimeFrameSlot::Audio(0)), 8),
1056 (invalid, 9)
1057 ]
1058 ),
1059 Err(RealtimePayloadHistoryError::InvalidSlot {
1060 slot: RealtimeFrameSlot::Audio(2),
1061 total_audio_codebooks: 2,
1062 })
1063 );
1064 assert_eq!(history.len(), 1);
1065 assert_eq!(
1066 history.insert_many(&schedule, [(invalid, 8), (invalid, 9)]),
1067 Err(RealtimePayloadHistoryError::InvalidSlot {
1068 slot: RealtimeFrameSlot::Audio(2),
1069 total_audio_codebooks: 2,
1070 })
1071 );
1072 let pending = coordinate(1, RealtimeFrameSlot::Audio(0));
1073 assert_eq!(
1074 history.insert_many(&schedule, [(pending, 8), (pending, 9)]),
1075 Err(RealtimePayloadHistoryError::DuplicatePayload {
1076 coordinate: pending,
1077 })
1078 );
1079 assert_eq!(history.len(), 1);
1080 assert_eq!(
1081 history.insert(&schedule, retained, 10),
1082 Err(RealtimePayloadHistoryError::DuplicatePayload {
1083 coordinate: retained,
1084 })
1085 );
1086 assert_eq!(history.required(&schedule, retained), Ok(&7));
1087 }
1088
1089 #[test]
1090 fn mismatch_missing_and_arithmetic_fail_without_mutation() {
1091 let schedule = schedule();
1092 let other = RealtimeSpeechConfig::new(
1093 2,
1094 1,
1095 1,
1096 1,
1097 0,
1098 1,
1099 RealtimeFrameConvention::AbsoluteDelayedSlots,
1100 vec![2, 0, 3],
1101 )
1102 .unwrap();
1103 let mut history = payload_history();
1104 let retained = coordinate(0, RealtimeFrameSlot::Text);
1105 history.insert(&schedule, retained, 7).unwrap();
1106
1107 assert_eq!(
1108 history.insert(&other, coordinate(1, RealtimeFrameSlot::Text), 8),
1109 Err(RealtimePayloadHistoryError::ScheduleMismatch)
1110 );
1111 let missing = coordinate(1, RealtimeFrameSlot::Audio(0));
1112 assert_eq!(
1113 history.required(&schedule, missing),
1114 Err(RealtimePayloadHistoryError::MissingPayload {
1115 coordinate: missing,
1116 })
1117 );
1118 assert_eq!(
1119 history.insert_delayed(&schedule, usize::MAX, RealtimeFrameSlot::Text, 9,),
1120 Err(RealtimePayloadHistoryError::CoordinateOverflow {
1121 base_position: usize::MAX,
1122 delay: 2,
1123 })
1124 );
1125 assert_eq!(history.len(), 1);
1126 assert_eq!(history.required(&schedule, retained), Ok(&7));
1127 }
1128
1129 #[test]
1130 fn pruning_uses_next_frontier_and_maximum_delay_deterministically() {
1131 let schedule = schedule();
1132 let mut history = payload_history();
1133 for position in 0..=5 {
1134 history
1135 .insert(
1136 &schedule,
1137 coordinate(position, RealtimeFrameSlot::Text),
1138 position,
1139 )
1140 .unwrap();
1141 }
1142
1143 assert_eq!(history.prune_for_next_frontier(&schedule, 3), Ok(0));
1144 assert_eq!(history.prune_for_next_frontier(&schedule, 6), Ok(1));
1145 assert_eq!(
1146 history
1147 .entries()
1148 .map(|(coordinate, payload)| (coordinate.position(), *payload))
1149 .collect::<Vec<_>>(),
1150 vec![(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
1151 );
1152 }
1153}