1use std::io::{self, Write};
8
9use serde::de::{Error as _, IgnoredAny, SeqAccess, Visitor};
10use serde::{Deserialize, Deserializer, Serialize};
11
12use crate::contact_fragment::StrictJsonValue;
13use crate::{
14 CONTACT_FRAGMENT_V1_MAX_SAFE_INTEGER, ContactEventKindV1, ContactEventV1, ContactEventWindowV1,
15 ContactExtensionV1, ContactFragmentError, ContactFragmentV1, ContactProducerV1,
16 DependencyClosureIdentityV1, DependencyClosureV1, InputIdentity,
17};
18
19pub const CONTACT_TRANSFORM_RESULT_V1_ID: &str = "urn:animsmith:schema:contact-transform-result:1";
21pub const CONTACT_TRANSFORM_RESULT_V1_SCHEMA_VERSION: u32 = 1;
23pub const CONTACT_TRANSFORM_RESULT_V1_MAX_SOURCE_BYTES: usize = 16 * 1024 * 1024;
25pub const CONTACT_TRANSFORM_RESULT_V1_MAX_CANONICAL_BYTES: usize = 16 * 1024 * 1024;
27pub const CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS: usize = 4_096;
29pub const CONTACT_TRANSFORM_RESULT_V1_MAX_EVENT_OUTCOMES: usize = 4_096;
31pub const CONTACT_TRANSFORM_RESULT_V1_MAX_MESSAGE_BYTES: usize = 4_096;
33
34#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36#[non_exhaustive]
37pub enum ContactTransformError {
38 #[error("contact transform result source has {bytes} bytes, exceeding V1 limit {limit}")]
40 SourceTooLarge {
41 bytes: usize,
43 limit: usize,
45 },
46 #[error("invalid contact transform result: {message}")]
48 Invalid {
49 message: String,
51 },
52 #[error("contact transform result {field} has {found} rows, exceeding V1 limit {limit}")]
54 LimitExceeded {
55 field: &'static str,
57 found: usize,
59 limit: usize,
61 },
62 #[error("contact transform result canonical JSON exceeds V1 limit {limit}")]
64 CanonicalTooLarge {
65 limit: usize,
67 },
68 #[error("invalid transformed contact fragment: {0}")]
70 Fragment(#[from] ContactFragmentError),
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct ContactTransformIntervalV1 {
77 start: f64,
78 end: f64,
79}
80
81impl ContactTransformIntervalV1 {
82 pub const fn new(start: f64, end: f64) -> Self {
87 Self { start, end }
88 }
89 pub const fn start(self) -> f64 {
91 self.start
92 }
93 pub const fn end(self) -> f64 {
95 self.end
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
101#[serde(deny_unknown_fields)]
102pub struct ContactTimeWarpControlPointV1 {
103 input_time: f64,
104 output_time: f64,
105}
106
107impl ContactTimeWarpControlPointV1 {
108 pub const fn new(input_time: f64, output_time: f64) -> Self {
110 Self {
111 input_time,
112 output_time,
113 }
114 }
115 pub const fn input_time(self) -> f64 {
117 self.input_time
118 }
119 pub const fn output_time(self) -> f64 {
121 self.output_time
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
128pub enum ContactTransformOperationV1 {
129 Trim {
131 version: u32,
133 interval: ContactTransformIntervalV1,
135 },
136 Slice {
138 version: u32,
140 interval: ContactTransformIntervalV1,
142 },
143 Resample {
145 version: u32,
147 mapping: String,
149 },
150 TimeWarp {
152 version: u32,
154 output_duration_s: f64,
156 #[serde(deserialize_with = "deserialize_control_points")]
158 control_points: Vec<ContactTimeWarpControlPointV1>,
159 },
160}
161
162impl ContactTransformOperationV1 {
163 pub const fn trim(interval: ContactTransformIntervalV1) -> Self {
165 Self::Trim {
166 version: 1,
167 interval,
168 }
169 }
170 pub const fn slice(interval: ContactTransformIntervalV1) -> Self {
172 Self::Slice {
173 version: 1,
174 interval,
175 }
176 }
177 pub fn resample() -> Self {
179 Self::Resample {
180 version: 1,
181 mapping: "identity".into(),
182 }
183 }
184 pub fn time_warp(
189 output_duration_s: f64,
190 control_points: Vec<ContactTimeWarpControlPointV1>,
191 ) -> Self {
192 Self::TimeWarp {
193 version: 1,
194 output_duration_s,
195 control_points,
196 }
197 }
198 pub fn control_points(&self) -> Option<&[ContactTimeWarpControlPointV1]> {
200 match self {
201 Self::TimeWarp { control_points, .. } => Some(control_points),
202 _ => None,
203 }
204 }
205 pub const fn output_duration_s(&self) -> Option<f64> {
207 match self {
208 Self::TimeWarp {
209 output_duration_s, ..
210 } => Some(*output_duration_s),
211 _ => None,
212 }
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(deny_unknown_fields)]
219pub struct ContactTransformBindingV1 {
220 artifact: InputIdentity,
221 dependency_closure_identity: DependencyClosureIdentityV1,
222 fragment: InputIdentity,
223}
224
225impl ContactTransformBindingV1 {
226 pub fn new(
228 artifact: InputIdentity,
229 dependency_closure_identity: DependencyClosureIdentityV1,
230 fragment: InputIdentity,
231 ) -> Self {
232 Self {
233 artifact,
234 dependency_closure_identity,
235 fragment,
236 }
237 }
238 pub const fn artifact(&self) -> &InputIdentity {
240 &self.artifact
241 }
242 pub const fn dependency_closure_identity(&self) -> &DependencyClosureIdentityV1 {
244 &self.dependency_closure_identity
245 }
246 pub const fn fragment(&self) -> &InputIdentity {
248 &self.fragment
249 }
250}
251
252#[derive(Debug, Clone)]
255pub struct ContactTransformContextV1 {
256 current_input_artifact: InputIdentity,
257 current_input_dependency_closure: DependencyClosureV1,
258 output_artifact: InputIdentity,
259 output_dependency_closure: DependencyClosureV1,
260 output_producer: ContactProducerV1,
261 transformed_extensions: Option<Vec<ContactExtensionV1>>,
262}
263
264impl ContactTransformContextV1 {
265 pub fn new(
271 current_input_artifact: InputIdentity,
272 current_input_dependency_closure: DependencyClosureV1,
273 output_artifact: InputIdentity,
274 output_dependency_closure: DependencyClosureV1,
275 output_producer: ContactProducerV1,
276 transformed_extensions: Option<Vec<ContactExtensionV1>>,
277 ) -> Self {
278 Self {
279 current_input_artifact,
280 current_input_dependency_closure,
281 output_artifact,
282 output_dependency_closure,
283 output_producer,
284 transformed_extensions,
285 }
286 }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
291#[serde(untagged, deny_unknown_fields)]
292pub enum ContactTransformedValueV1 {
293 Point {
295 time: f64,
297 },
298 Window {
300 window: ContactEventWindowV1,
302 },
303}
304
305impl<'de> Deserialize<'de> for ContactTransformedValueV1 {
306 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
307 where
308 D: Deserializer<'de>,
309 {
310 #[derive(Deserialize)]
311 #[serde(untagged, deny_unknown_fields)]
312 enum Wire {
313 Point { time: f64 },
314 Window { window: ContactEventWindowV1 },
315 }
316
317 match Wire::deserialize(deserializer)? {
318 Wire::Point { time } if time.is_finite() && (0.0..=1.0).contains(&time) => {
319 Ok(Self::Point {
320 time: if time == 0.0 { 0.0 } else { time },
321 })
322 }
323 Wire::Point { .. } => Err(D::Error::custom(
324 "transformed point time must be finite and normalized",
325 )),
326 Wire::Window { window } => Ok(Self::Window { window }),
327 }
328 }
329}
330
331#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
333#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
334pub enum ContactEventOutcomeV1 {
335 Transformed {
337 event_id: String,
339 value: ContactTransformedValueV1,
341 },
342 Outside {
344 event_id: String,
346 },
347 Refused {
349 event_id: String,
351 code: ContactTransformRefusalCodeV1,
353 },
354}
355
356impl ContactEventOutcomeV1 {
357 pub fn event_id(&self) -> &str {
359 match self {
360 Self::Transformed { event_id, .. }
361 | Self::Outside { event_id }
362 | Self::Refused { event_id, .. } => event_id,
363 }
364 }
365}
366
367#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
369#[serde(rename_all = "snake_case")]
370pub enum ContactTransformRefusalCodeV1 {
371 PartialWindow,
373 InvalidMapping,
375 InvalidBinding,
377 InvalidValue,
379 UnsupportedExtension,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
385#[serde(deny_unknown_fields)]
386pub struct ContactTransformRefusalV1 {
387 code: ContactTransformRefusalCodeV1,
388 message: String,
389}
390
391impl ContactTransformRefusalV1 {
392 pub const fn code(&self) -> ContactTransformRefusalCodeV1 {
394 self.code
395 }
396 pub fn message(&self) -> &str {
398 &self.message
399 }
400}
401
402#[derive(Debug, Clone, PartialEq, Serialize)]
404pub struct ContactTransformOutputV1 {
405 artifact: InputIdentity,
406 dependency_closure_identity: DependencyClosureIdentityV1,
407 fragment: InputIdentity,
408 contact_fragment: ContactFragmentV1,
409}
410
411impl ContactTransformOutputV1 {
412 pub const fn artifact(&self) -> &InputIdentity {
414 &self.artifact
415 }
416 pub const fn dependency_closure_identity(&self) -> &DependencyClosureIdentityV1 {
418 &self.dependency_closure_identity
419 }
420 pub const fn fragment(&self) -> &InputIdentity {
422 &self.fragment
423 }
424 pub const fn contact_fragment(&self) -> &ContactFragmentV1 {
426 &self.contact_fragment
427 }
428}
429
430#[derive(Debug, Clone, PartialEq, Serialize)]
432pub struct ContactTransformResultV1 {
433 schema: &'static str,
434 schema_version: u32,
435 operation: ContactTransformOperationV1,
436 input: ContactTransformBindingV1,
437 outcome: ContactTransformOutcomeV1,
438 event_outcomes: Vec<ContactEventOutcomeV1>,
439 #[serde(skip_serializing_if = "Option::is_none")]
440 output: Option<ContactTransformOutputV1>,
441 #[serde(skip_serializing_if = "Option::is_none")]
442 refusal: Option<ContactTransformRefusalV1>,
443}
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
447#[serde(rename_all = "snake_case")]
448pub enum ContactTransformOutcomeV1 {
449 Transformed,
451 Refused,
453}
454
455impl ContactTransformResultV1 {
456 pub fn read_json(
459 bytes: &[u8],
460 input_fragment: &ContactFragmentV1,
461 context: &ContactTransformContextV1,
462 ) -> Result<Self, ContactTransformError> {
463 if bytes.len() > CONTACT_TRANSFORM_RESULT_V1_MAX_SOURCE_BYTES {
464 return Err(ContactTransformError::SourceTooLarge {
465 bytes: bytes.len(),
466 limit: CONTACT_TRANSFORM_RESULT_V1_MAX_SOURCE_BYTES,
467 });
468 }
469 let mut deserializer = serde_json::Deserializer::from_slice(bytes);
470 let wire = ResultWire::deserialize(&mut deserializer).map_err(invalid_decode)?;
471 deserializer.end().map_err(invalid_decode)?;
472 let result = wire.into_result()?;
473 result.verify_structure(input_fragment)?;
474 result.verify_semantics(input_fragment, context)?;
475 Ok(result)
476 }
477
478 pub fn canonical_json(&self) -> Result<Vec<u8>, ContactTransformError> {
480 let mut writer = CappedWriter {
481 bytes: Vec::new(),
482 overflowed: false,
483 };
484 serde_jcs::to_writer(&mut writer, self).map_err(|error| {
485 if writer.overflowed {
486 ContactTransformError::CanonicalTooLarge {
487 limit: CONTACT_TRANSFORM_RESULT_V1_MAX_CANONICAL_BYTES,
488 }
489 } else {
490 ContactTransformError::Invalid {
491 message: error.to_string(),
492 }
493 }
494 })?;
495 Ok(writer.bytes)
496 }
497
498 pub const fn operation(&self) -> &ContactTransformOperationV1 {
500 &self.operation
501 }
502 pub const fn input(&self) -> &ContactTransformBindingV1 {
504 &self.input
505 }
506 pub const fn outcome(&self) -> ContactTransformOutcomeV1 {
508 self.outcome
509 }
510 pub fn event_outcomes(&self) -> &[ContactEventOutcomeV1] {
512 &self.event_outcomes
513 }
514 pub const fn output(&self) -> Option<&ContactTransformOutputV1> {
516 self.output.as_ref()
517 }
518 pub const fn refusal(&self) -> Option<&ContactTransformRefusalV1> {
520 self.refusal.as_ref()
521 }
522
523 fn verify_structure(
524 &self,
525 input_fragment: &ContactFragmentV1,
526 ) -> Result<(), ContactTransformError> {
527 if self.schema != CONTACT_TRANSFORM_RESULT_V1_ID
528 || self.schema_version != CONTACT_TRANSFORM_RESULT_V1_SCHEMA_VERSION
529 {
530 return invalid("schema identity/version mismatch");
531 }
532 validate_operation_structure(&self.operation)?;
533 let input_fragment_identity = input_fragment.canonical_identity()?;
534 validate_identity(&self.input.artifact)?;
535 validate_identity(self.input.dependency_closure_identity.input_identity())?;
536 validate_identity(&self.input.fragment)?;
537 let binding_matches = self.input.artifact == *input_fragment.artifact()
538 && self.input.dependency_closure_identity
539 == *input_fragment.dependency_closure_identity()
540 && self.input.fragment == input_fragment_identity;
541 if self.event_outcomes.len() > CONTACT_TRANSFORM_RESULT_V1_MAX_EVENT_OUTCOMES {
542 return limit(
543 "event_outcomes",
544 self.event_outcomes.len(),
545 CONTACT_TRANSFORM_RESULT_V1_MAX_EVENT_OUTCOMES,
546 );
547 }
548 match self.outcome {
549 ContactTransformOutcomeV1::Transformed => {
550 if !binding_matches {
551 return invalid("transformed input binding does not match supplied fragment");
552 }
553 if self.refusal.is_some() || self.output.is_none() {
554 return invalid("transformed outcome requires output and forbids refusal");
555 }
556 if self
557 .event_outcomes
558 .iter()
559 .any(|row| matches!(row, ContactEventOutcomeV1::Refused { .. }))
560 {
561 return invalid("transformed outcome contains a refused event");
562 }
563 let output = self.output.as_ref().expect("checked");
564 validate_identity(&output.artifact)?;
565 validate_identity(output.dependency_closure_identity.input_identity())?;
566 validate_identity(&output.fragment)?;
567 if output.artifact != *output.contact_fragment.artifact()
568 || output.dependency_closure_identity
569 != *output.contact_fragment.dependency_closure_identity()
570 || output.fragment != output.contact_fragment.canonical_identity()?
571 {
572 return invalid("output identities do not match inline contact fragment");
573 }
574 }
575 ContactTransformOutcomeV1::Refused => {
576 if self.output.is_some() || self.refusal.is_none() {
577 return invalid("refused outcome requires refusal and forbids output");
578 }
579 let refusal = self.refusal.as_ref().expect("checked");
580 if refusal.code == ContactTransformRefusalCodeV1::InvalidBinding {
581 if binding_matches || !self.event_outcomes.is_empty() {
582 return invalid(
583 "invalid_binding requires a mismatched input and empty event outcomes",
584 );
585 }
586 return Ok(());
587 }
588 if !binding_matches {
589 return invalid("non-binding refusal carries a mismatched input binding");
590 }
591 }
592 }
593 if self.event_outcomes.is_empty() {
594 return Ok(());
595 }
596 if self.event_outcomes.len() != input_fragment.events().len()
597 || self
598 .event_outcomes
599 .iter()
600 .zip(input_fragment.events())
601 .any(|(outcome, event)| outcome.event_id() != event.event_id())
602 {
603 return invalid("event outcomes do not exactly cover canonical input event order");
604 }
605 Ok(())
606 }
607
608 fn verify_semantics(
609 &self,
610 input_fragment: &ContactFragmentV1,
611 context: &ContactTransformContextV1,
612 ) -> Result<(), ContactTransformError> {
613 let mut expected =
614 transform_contact_fragment_v1(self.operation.clone(), input_fragment, context.clone())?;
615 if let (Some(expected), Some(actual)) = (&mut expected.refusal, &self.refusal) {
616 expected.message.clone_from(&actual.message);
617 }
618 if *self != expected {
619 return invalid("result does not equal independent V1 operation rederivation");
620 }
621 Ok(())
622 }
623}
624
625pub fn transform_contact_fragment_v1(
633 operation: ContactTransformOperationV1,
634 input_fragment: &ContactFragmentV1,
635 context: ContactTransformContextV1,
636) -> Result<ContactTransformResultV1, ContactTransformError> {
637 validate_operation_structure(&operation)?;
638 let ContactTransformContextV1 {
639 current_input_artifact,
640 current_input_dependency_closure,
641 output_artifact,
642 output_dependency_closure,
643 output_producer,
644 transformed_extensions,
645 } = context;
646 let current_input_dependency_closure_identity = complete_closure_identity(
647 ¤t_input_dependency_closure,
648 ¤t_input_artifact,
649 "current input",
650 )?;
651 let input = ContactTransformBindingV1::new(
652 current_input_artifact,
653 current_input_dependency_closure_identity,
654 input_fragment.canonical_identity()?,
655 );
656 if input.artifact != *input_fragment.artifact()
657 || input.dependency_closure_identity != *input_fragment.dependency_closure_identity()
658 {
659 return refusal(
660 operation,
661 input,
662 ContactTransformRefusalCodeV1::InvalidBinding,
663 "contact fragment does not bind the current source artifact and dependency closure",
664 Vec::new(),
665 );
666 }
667 if operation_invalid(&operation) {
668 return refusal(
669 operation,
670 input,
671 ContactTransformRefusalCodeV1::InvalidMapping,
672 "operation mapping is outside the V1 domain",
673 Vec::new(),
674 );
675 }
676 let transformed_extensions = match transformed_extensions {
677 None if input_fragment.extensions().is_empty() => Vec::new(),
678 None => {
679 return refusal(
680 operation,
681 input,
682 ContactTransformRefusalCodeV1::UnsupportedExtension,
683 "contact extension has no operation-specific transform support",
684 Vec::new(),
685 );
686 }
687 Some(outputs)
688 if outputs.len() == input_fragment.extensions().len()
689 && outputs
690 .iter()
691 .zip(input_fragment.extensions())
692 .all(|(output, input)| {
693 output.schema() == input.schema()
694 && output.schema_version() == input.schema_version()
695 }) =>
696 {
697 outputs
698 }
699 Some(_) if input_fragment.extensions().is_empty() => {
700 return invalid("extension outputs exist for an extension-free fragment");
701 }
702 Some(_) => {
703 return refusal(
704 operation,
705 input,
706 ContactTransformRefusalCodeV1::UnsupportedExtension,
707 "contact extension transform output does not cover the exact input inventory",
708 Vec::new(),
709 );
710 }
711 };
712 let output_duration_s =
713 mapped_duration(&operation, input_fragment.duration_s()).ok_or_else(|| {
714 ContactTransformError::Invalid {
715 message: "valid mapping produced an invalid duration".into(),
716 }
717 })?;
718 if !output_duration_s.is_finite() || output_duration_s <= 0.0 {
719 return refusal(
720 operation,
721 input,
722 ContactTransformRefusalCodeV1::InvalidValue,
723 "mapped duration is not finite and positive",
724 Vec::new(),
725 );
726 }
727 let mut outcomes = Vec::with_capacity(input_fragment.events().len());
728 let mut transformed_events = Vec::with_capacity(input_fragment.events().len());
729 let mut partial = false;
730 for event in input_fragment.events() {
731 match map_event(&operation, event)? {
732 MappedEvent::Transformed { outcome, event } => {
733 outcomes.push(ContactEventOutcomeV1::Transformed {
734 event_id: event.event_id().to_owned(),
735 value: outcome,
736 });
737 transformed_events.push(event);
738 }
739 MappedEvent::Outside => outcomes.push(ContactEventOutcomeV1::Outside {
740 event_id: event.event_id().to_owned(),
741 }),
742 MappedEvent::PartialWindow => {
743 partial = true;
744 outcomes.push(ContactEventOutcomeV1::Refused {
745 event_id: event.event_id().to_owned(),
746 code: ContactTransformRefusalCodeV1::PartialWindow,
747 });
748 }
749 }
750 }
751 if partial {
752 return refusal(
753 operation,
754 input,
755 ContactTransformRefusalCodeV1::PartialWindow,
756 "one or more contact windows cross a retained interval boundary",
757 outcomes,
758 );
759 }
760 let output_dependency_closure_identity =
761 complete_closure_identity(&output_dependency_closure, &output_artifact, "output")?;
762 let output_fragment = ContactFragmentV1::new(
763 output_producer,
764 output_artifact.clone(),
765 output_dependency_closure_identity.clone(),
766 input_fragment.clip().clone(),
767 output_duration_s,
768 transformed_events,
769 transformed_extensions,
770 )?;
771 let output = ContactTransformOutputV1 {
772 artifact: output_artifact,
773 dependency_closure_identity: output_dependency_closure_identity,
774 fragment: output_fragment.canonical_identity()?,
775 contact_fragment: output_fragment,
776 };
777 let result = ContactTransformResultV1 {
778 schema: CONTACT_TRANSFORM_RESULT_V1_ID,
779 schema_version: CONTACT_TRANSFORM_RESULT_V1_SCHEMA_VERSION,
780 operation,
781 input,
782 outcome: ContactTransformOutcomeV1::Transformed,
783 event_outcomes: outcomes,
784 output: Some(output),
785 refusal: None,
786 };
787 result.verify_structure(input_fragment)?;
788 let _ = result.canonical_json()?;
789 Ok(result)
790}
791
792enum MappedEvent {
793 Transformed {
794 outcome: ContactTransformedValueV1,
795 event: ContactEventV1,
796 },
797 Outside,
798 PartialWindow,
799}
800
801fn map_event(
802 operation: &ContactTransformOperationV1,
803 event: &ContactEventV1,
804) -> Result<MappedEvent, ContactTransformError> {
805 match event.kind() {
806 ContactEventKindV1::Point(time) => match map_time(operation, time)? {
807 Some(time) => Ok(MappedEvent::Transformed {
808 outcome: ContactTransformedValueV1::Point { time },
809 event: ContactEventV1::point(
810 event.event_id(),
811 event.role(),
812 event.phase(),
813 time,
814 event.confidence(),
815 )?,
816 }),
817 None => Ok(MappedEvent::Outside),
818 },
819 ContactEventKindV1::Window(window) => {
820 if let Some(interval) = retained_interval(operation) {
821 if window.end() < interval.start || window.start() > interval.end {
822 return Ok(MappedEvent::Outside);
823 }
824 if !(interval.start <= window.start() && window.end() <= interval.end) {
825 return Ok(MappedEvent::PartialWindow);
826 }
827 }
828 let start = map_time(operation, window.start())?.ok_or_else(|| {
829 ContactTransformError::Invalid {
830 message: "contained window start mapped outside".into(),
831 }
832 })?;
833 let end = map_time(operation, window.end())?.ok_or_else(|| {
834 ContactTransformError::Invalid {
835 message: "contained window end mapped outside".into(),
836 }
837 })?;
838 let mapped = ContactEventWindowV1::new(start, end)?;
839 Ok(MappedEvent::Transformed {
840 outcome: ContactTransformedValueV1::Window { window: mapped },
841 event: ContactEventV1::window(
842 event.event_id(),
843 event.role(),
844 event.phase(),
845 mapped,
846 event.confidence(),
847 )?,
848 })
849 }
850 }
851}
852
853fn map_time(
854 operation: &ContactTransformOperationV1,
855 time: f64,
856) -> Result<Option<f64>, ContactTransformError> {
857 let value = match operation {
858 ContactTransformOperationV1::Trim { interval, .. }
859 | ContactTransformOperationV1::Slice { interval, .. } => {
860 if time < interval.start || time > interval.end {
861 return Ok(None);
862 }
863 let span = interval.end - interval.start;
864 (time - interval.start) / span
865 }
866 ContactTransformOperationV1::Resample { .. } => time,
867 ContactTransformOperationV1::TimeWarp { control_points, .. } => {
868 if let Some(point) = control_points.iter().find(|point| point.input_time == time) {
869 point.output_time
870 } else {
871 let pair = control_points
872 .windows(2)
873 .find(|pair| pair[0].input_time < time && time < pair[1].input_time)
874 .ok_or_else(|| ContactTransformError::Invalid {
875 message: "valid mapping does not cover normalized input time".into(),
876 })?;
877 let dx = pair[1].input_time - pair[0].input_time;
878 let alpha = (time - pair[0].input_time) / dx;
879 let dy = pair[1].output_time - pair[0].output_time;
880 let product = alpha * dy;
881 pair[0].output_time + product
882 }
883 }
884 };
885 if value.is_finite() {
886 Ok(Some(if value == 0.0 { 0.0 } else { value }))
887 } else {
888 Err(ContactTransformError::Invalid {
889 message: "mapping produced a non-finite value".into(),
890 })
891 }
892}
893
894fn retained_interval(
895 operation: &ContactTransformOperationV1,
896) -> Option<ContactTransformIntervalV1> {
897 match operation {
898 ContactTransformOperationV1::Trim { interval, .. }
899 | ContactTransformOperationV1::Slice { interval, .. } => Some(*interval),
900 _ => None,
901 }
902}
903
904fn mapped_duration(operation: &ContactTransformOperationV1, input: f64) -> Option<f64> {
905 let value = match operation {
906 ContactTransformOperationV1::Trim { interval, .. }
907 | ContactTransformOperationV1::Slice { interval, .. } => {
908 let span = interval.end - interval.start;
909 input * span
910 }
911 ContactTransformOperationV1::Resample { .. } => input,
912 ContactTransformOperationV1::TimeWarp {
913 output_duration_s, ..
914 } => *output_duration_s,
915 };
916 value.is_finite().then_some(value)
917}
918
919fn operation_invalid(operation: &ContactTransformOperationV1) -> bool {
920 match operation {
921 ContactTransformOperationV1::Trim { version, interval }
922 | ContactTransformOperationV1::Slice { version, interval } => {
923 debug_assert_eq!(*version, 1);
924 !(0.0 <= interval.start && interval.start < interval.end && interval.end <= 1.0)
925 }
926 ContactTransformOperationV1::Resample { version, mapping } => {
927 debug_assert_eq!(*version, 1);
928 debug_assert_eq!(mapping, "identity");
929 false
930 }
931 ContactTransformOperationV1::TimeWarp {
932 version,
933 output_duration_s,
934 control_points,
935 } => {
936 debug_assert_eq!(*version, 1);
937 *output_duration_s <= 0.0
938 || control_points.len() < 2
939 || control_points
940 .first()
941 .is_none_or(|point| point.input_time != 0.0 || point.output_time != 0.0)
942 || control_points
943 .last()
944 .is_none_or(|point| point.input_time != 1.0 || point.output_time != 1.0)
945 || control_points.iter().any(|point| {
946 !(0.0..=1.0).contains(&point.input_time)
947 || !(0.0..=1.0).contains(&point.output_time)
948 })
949 || control_points.windows(2).any(|pair| {
950 pair[0].input_time >= pair[1].input_time
951 || pair[0].output_time >= pair[1].output_time
952 })
953 }
954 }
955}
956
957fn validate_operation_structure(
958 operation: &ContactTransformOperationV1,
959) -> Result<(), ContactTransformError> {
960 match operation {
961 ContactTransformOperationV1::Trim { version, interval }
962 | ContactTransformOperationV1::Slice { version, interval } => {
963 if *version != 1 {
964 return invalid("unknown contact operation version");
965 }
966 if !safe_number(interval.start) || !safe_number(interval.end) {
967 return invalid("contact interval contains an RFC 8785 unsafe number");
968 }
969 }
970 ContactTransformOperationV1::Resample { version, mapping } => {
971 if *version != 1 || mapping != "identity" {
972 return invalid("unknown contact resample version or mapping token");
973 }
974 }
975 ContactTransformOperationV1::TimeWarp {
976 version,
977 output_duration_s,
978 control_points,
979 } => {
980 if *version != 1 {
981 return invalid("unknown contact operation version");
982 }
983 if control_points.len() > CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS {
984 return limit(
985 "control_points",
986 control_points.len(),
987 CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS,
988 );
989 }
990 if !safe_number(*output_duration_s)
991 || control_points
992 .iter()
993 .any(|point| !safe_number(point.input_time) || !safe_number(point.output_time))
994 {
995 return invalid("contact time-warp contains an RFC 8785 unsafe number");
996 }
997 }
998 }
999 Ok(())
1000}
1001
1002fn validate_identity(identity: &InputIdentity) -> Result<(), ContactTransformError> {
1003 if identity.bytes() > CONTACT_FRAGMENT_V1_MAX_SAFE_INTEGER {
1004 return invalid("contact transform identity bytes exceed the RFC 8785 safe integer");
1005 }
1006 Ok(())
1007}
1008
1009fn complete_closure_identity(
1010 closure: &DependencyClosureV1,
1011 artifact: &InputIdentity,
1012 field: &'static str,
1013) -> Result<DependencyClosureIdentityV1, ContactTransformError> {
1014 if closure.primary_input() != artifact {
1015 return invalid(format!(
1016 "{field} dependency closure primary input does not match its artifact"
1017 ));
1018 }
1019 closure
1020 .identity()
1021 .cloned()
1022 .ok_or_else(|| ContactTransformError::Invalid {
1023 message: format!("{field} dependency closure is not complete"),
1024 })
1025}
1026
1027fn safe_number(value: f64) -> bool {
1028 value.is_finite() && value.abs() <= CONTACT_FRAGMENT_V1_MAX_SAFE_INTEGER as f64
1029}
1030
1031fn refusal(
1032 operation: ContactTransformOperationV1,
1033 input: ContactTransformBindingV1,
1034 code: ContactTransformRefusalCodeV1,
1035 message: &str,
1036 event_outcomes: Vec<ContactEventOutcomeV1>,
1037) -> Result<ContactTransformResultV1, ContactTransformError> {
1038 if message.is_empty() || message.len() > CONTACT_TRANSFORM_RESULT_V1_MAX_MESSAGE_BYTES {
1039 return invalid("refusal message is outside the V1 byte bound");
1040 }
1041 let result = ContactTransformResultV1 {
1042 schema: CONTACT_TRANSFORM_RESULT_V1_ID,
1043 schema_version: CONTACT_TRANSFORM_RESULT_V1_SCHEMA_VERSION,
1044 operation,
1045 input,
1046 outcome: ContactTransformOutcomeV1::Refused,
1047 event_outcomes,
1048 output: None,
1049 refusal: Some(ContactTransformRefusalV1 {
1050 code,
1051 message: message.into(),
1052 }),
1053 };
1054 let _ = result.canonical_json()?;
1055 Ok(result)
1056}
1057
1058#[derive(Deserialize)]
1059#[serde(deny_unknown_fields)]
1060struct ResultWire {
1061 schema: String,
1062 schema_version: u32,
1063 operation: ContactTransformOperationV1,
1064 input: ContactTransformBindingV1,
1065 outcome: ContactTransformOutcomeV1,
1066 #[serde(deserialize_with = "deserialize_event_outcomes")]
1067 event_outcomes: Vec<ContactEventOutcomeV1>,
1068 #[serde(default)]
1069 output: Present<OutputWire>,
1070 #[serde(default)]
1071 refusal: Present<ContactTransformRefusalV1>,
1072}
1073
1074impl ResultWire {
1075 fn into_result(self) -> Result<ContactTransformResultV1, ContactTransformError> {
1076 if self.schema != CONTACT_TRANSFORM_RESULT_V1_ID || self.schema_version != 1 {
1077 return invalid("schema identity/version mismatch");
1078 }
1079 if let Some(refusal) = &self.refusal.0
1080 && (refusal.message.is_empty()
1081 || refusal.message.len() > CONTACT_TRANSFORM_RESULT_V1_MAX_MESSAGE_BYTES)
1082 {
1083 return invalid("refusal message is outside the V1 byte bound");
1084 }
1085 let output = self.output.0.map(OutputWire::into_output).transpose()?;
1086 Ok(ContactTransformResultV1 {
1087 schema: CONTACT_TRANSFORM_RESULT_V1_ID,
1088 schema_version: 1,
1089 operation: self.operation,
1090 input: self.input,
1091 outcome: self.outcome,
1092 event_outcomes: self.event_outcomes,
1093 output,
1094 refusal: self.refusal.0,
1095 })
1096 }
1097}
1098
1099struct Present<T>(Option<T>);
1100
1101impl<T> Default for Present<T> {
1102 fn default() -> Self {
1103 Self(None)
1104 }
1105}
1106
1107impl<'de, T: Deserialize<'de>> Deserialize<'de> for Present<T> {
1108 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1109 where
1110 D: Deserializer<'de>,
1111 {
1112 T::deserialize(deserializer).map(|value| Self(Some(value)))
1113 }
1114}
1115
1116#[derive(Deserialize)]
1117#[serde(deny_unknown_fields)]
1118struct OutputWire {
1119 artifact: InputIdentity,
1120 dependency_closure_identity: DependencyClosureIdentityV1,
1121 fragment: InputIdentity,
1122 contact_fragment: StrictJsonValue,
1123}
1124
1125impl OutputWire {
1126 fn into_output(self) -> Result<ContactTransformOutputV1, ContactTransformError> {
1127 let bytes = serde_json::to_vec(&self.contact_fragment.into_value()).map_err(|error| {
1128 ContactTransformError::Invalid {
1129 message: error.to_string(),
1130 }
1131 })?;
1132 Ok(ContactTransformOutputV1 {
1133 artifact: self.artifact,
1134 dependency_closure_identity: self.dependency_closure_identity,
1135 fragment: self.fragment,
1136 contact_fragment: ContactFragmentV1::read_json(&bytes)?,
1137 })
1138 }
1139}
1140
1141fn deserialize_control_points<'de, D>(
1142 deserializer: D,
1143) -> Result<Vec<ContactTimeWarpControlPointV1>, D::Error>
1144where
1145 D: Deserializer<'de>,
1146{
1147 deserialize_capped(
1148 deserializer,
1149 CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS,
1150 "control_points",
1151 )
1152}
1153
1154fn deserialize_event_outcomes<'de, D>(
1155 deserializer: D,
1156) -> Result<Vec<ContactEventOutcomeV1>, D::Error>
1157where
1158 D: Deserializer<'de>,
1159{
1160 deserialize_capped(
1161 deserializer,
1162 CONTACT_TRANSFORM_RESULT_V1_MAX_EVENT_OUTCOMES,
1163 "event_outcomes",
1164 )
1165}
1166
1167fn deserialize_capped<'de, D, T>(
1168 deserializer: D,
1169 maximum: usize,
1170 field: &'static str,
1171) -> Result<Vec<T>, D::Error>
1172where
1173 D: Deserializer<'de>,
1174 T: Deserialize<'de>,
1175{
1176 struct CappedVisitor<T> {
1177 maximum: usize,
1178 field: &'static str,
1179 marker: std::marker::PhantomData<T>,
1180 }
1181 impl<'de, T: Deserialize<'de>> Visitor<'de> for CappedVisitor<T> {
1182 type Value = Vec<T>;
1183 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1184 write!(formatter, "at most {} {} rows", self.maximum, self.field)
1185 }
1186 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1187 where
1188 A: SeqAccess<'de>,
1189 {
1190 let mut values =
1191 Vec::with_capacity(sequence.size_hint().unwrap_or(0).min(self.maximum));
1192 while values.len() < self.maximum {
1193 let Some(value) = sequence.next_element()? else {
1194 return Ok(values);
1195 };
1196 values.push(value);
1197 }
1198 if sequence.next_element::<IgnoredAny>()?.is_some() {
1199 while sequence.next_element::<IgnoredAny>()?.is_some() {}
1200 return Err(A::Error::custom(format!(
1201 "{} exceeds V1 row limit {}",
1202 self.field, self.maximum
1203 )));
1204 }
1205 Ok(values)
1206 }
1207 }
1208 deserializer.deserialize_seq(CappedVisitor {
1209 maximum,
1210 field,
1211 marker: std::marker::PhantomData,
1212 })
1213}
1214
1215struct CappedWriter {
1216 bytes: Vec<u8>,
1217 overflowed: bool,
1218}
1219
1220impl Write for CappedWriter {
1221 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
1222 if self.bytes.len().saturating_add(bytes.len())
1223 > CONTACT_TRANSFORM_RESULT_V1_MAX_CANONICAL_BYTES
1224 {
1225 self.overflowed = true;
1226 return Err(io::Error::other("contact-transform canonical limit"));
1227 }
1228 self.bytes.extend_from_slice(bytes);
1229 Ok(bytes.len())
1230 }
1231 fn flush(&mut self) -> io::Result<()> {
1232 Ok(())
1233 }
1234}
1235
1236fn invalid_decode(error: serde_json::Error) -> ContactTransformError {
1237 ContactTransformError::Invalid {
1238 message: error.to_string(),
1239 }
1240}
1241
1242fn invalid<T>(message: impl Into<String>) -> Result<T, ContactTransformError> {
1243 Err(ContactTransformError::Invalid {
1244 message: message.into(),
1245 })
1246}
1247
1248fn limit<T>(field: &'static str, found: usize, limit: usize) -> Result<T, ContactTransformError> {
1249 Err(ContactTransformError::LimitExceeded {
1250 field,
1251 found,
1252 limit,
1253 })
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258 use super::*;
1259 use crate::{
1260 ContactClipReferenceV1, ContactExtensionV1, ContactPhaseV1, ContactRoleV1,
1261 DependencyClosureBuilderV1, SourceResourceKindV1, SourceSetCoverageV1,
1262 };
1263
1264 fn closure(primary: &InputIdentity) -> DependencyClosureV1 {
1265 DependencyClosureBuilderV1::new(primary.clone(), SourceSetCoverageV1::complete(), 0)
1266 .finish()
1267 .unwrap()
1268 }
1269
1270 fn closure_with_primary_reference(primary: &InputIdentity) -> DependencyClosureV1 {
1271 let mut builder =
1272 DependencyClosureBuilderV1::new(primary.clone(), SourceSetCoverageV1::complete(), 1);
1273 assert!(builder.begin_reference(4, 1));
1274 builder
1275 .push_primary(0, SourceResourceKindV1::Buffer, 0)
1276 .unwrap();
1277 builder.finish().unwrap()
1278 }
1279
1280 fn fragment(with_extension: bool) -> ContactFragmentV1 {
1281 let artifact = InputIdentity::from_bytes(b"source artifact");
1282 let events = vec![
1283 ContactEventV1::point(
1284 "left/point",
1285 ContactRoleV1::LeftFoot,
1286 ContactPhaseV1::Marker,
1287 0.25,
1288 Some(0.8),
1289 )
1290 .unwrap(),
1291 ContactEventV1::window(
1292 "right/window",
1293 ContactRoleV1::RightFoot,
1294 ContactPhaseV1::Begin,
1295 ContactEventWindowV1::new(0.5, 0.75).unwrap(),
1296 None,
1297 )
1298 .unwrap(),
1299 ];
1300 let extensions = with_extension
1301 .then(|| {
1302 ContactExtensionV1::new(
1303 "urn:animsmith:contact-detector:stance-support:1",
1304 1,
1305 serde_json::json!({"algorithm": "fixture"}),
1306 )
1307 .unwrap()
1308 })
1309 .into_iter()
1310 .collect();
1311 ContactFragmentV1::new(
1312 ContactProducerV1::new("fixture", "1").unwrap(),
1313 artifact.clone(),
1314 closure(&artifact).identity().unwrap().clone(),
1315 ContactClipReferenceV1::document("walk").unwrap(),
1316 2.0,
1317 events,
1318 extensions,
1319 )
1320 .unwrap()
1321 }
1322
1323 fn warp() -> ContactTransformOperationV1 {
1324 ContactTransformOperationV1::time_warp(
1325 2.0,
1326 vec![
1327 ContactTimeWarpControlPointV1::new(0.0, 0.0),
1328 ContactTimeWarpControlPointV1::new(0.5, 0.25),
1329 ContactTimeWarpControlPointV1::new(1.0, 1.0),
1330 ],
1331 )
1332 }
1333
1334 fn context(
1335 source: &ContactFragmentV1,
1336 transformed_extensions: Option<Vec<ContactExtensionV1>>,
1337 ) -> ContactTransformContextV1 {
1338 let output = InputIdentity::from_bytes(b"output artifact");
1339 ContactTransformContextV1::new(
1340 source.artifact().clone(),
1341 closure(source.artifact()),
1342 output.clone(),
1343 closure(&output),
1344 ContactProducerV1::new("fixture", "2").unwrap(),
1345 transformed_extensions,
1346 )
1347 }
1348
1349 fn transform(
1350 source: &ContactFragmentV1,
1351 operation: ContactTransformOperationV1,
1352 transformed_extensions: Option<Vec<ContactExtensionV1>>,
1353 ) -> ContactTransformResultV1 {
1354 transform_contact_fragment_v1(operation, source, context(source, transformed_extensions))
1355 .unwrap()
1356 }
1357
1358 #[test]
1359 fn time_warp_maps_points_and_both_window_boundaries() {
1360 let source = fragment(false);
1361 let context = context(&source, None);
1362 let result = transform_contact_fragment_v1(warp(), &source, context.clone()).unwrap();
1363 assert_eq!(result.outcome(), ContactTransformOutcomeV1::Transformed);
1364 assert_eq!(
1365 result.event_outcomes(),
1366 &[
1367 ContactEventOutcomeV1::Transformed {
1368 event_id: "left/point".into(),
1369 value: ContactTransformedValueV1::Point { time: 0.125 },
1370 },
1371 ContactEventOutcomeV1::Transformed {
1372 event_id: "right/window".into(),
1373 value: ContactTransformedValueV1::Window {
1374 window: ContactEventWindowV1::new(0.25, 0.625).unwrap(),
1375 },
1376 },
1377 ]
1378 );
1379 let output = result.output().unwrap().contact_fragment();
1380 assert_eq!(output.duration_s(), 2.0);
1381 assert_eq!(output.events()[0].kind(), ContactEventKindV1::Point(0.125));
1382 assert_eq!(
1383 output.events()[1].kind(),
1384 ContactEventKindV1::Window(ContactEventWindowV1::new(0.25, 0.625).unwrap())
1385 );
1386
1387 let bytes = result.canonical_json().unwrap();
1388 assert_eq!(
1389 ContactTransformResultV1::read_json(&bytes, &source, &context).unwrap(),
1390 result
1391 );
1392 }
1393
1394 #[test]
1395 fn exact_knots_bypass_interpolation() {
1396 let source = fragment(false);
1397 let result = transform(&source, warp(), None);
1398 let ContactEventOutcomeV1::Transformed {
1399 value: ContactTransformedValueV1::Window { window },
1400 ..
1401 } = &result.event_outcomes()[1]
1402 else {
1403 panic!("window outcome");
1404 };
1405 assert_eq!(window.start().to_bits(), 0.25_f64.to_bits());
1406 }
1407
1408 #[test]
1409 fn trim_and_slice_map_successful_points_windows_and_duration() {
1410 let source = fragment(false);
1411 for operation in [
1412 ContactTransformOperationV1::trim(ContactTransformIntervalV1::new(0.25, 0.75)),
1413 ContactTransformOperationV1::slice(ContactTransformIntervalV1::new(0.25, 0.75)),
1414 ] {
1415 let result = transform(&source, operation, None);
1416 assert_eq!(result.outcome(), ContactTransformOutcomeV1::Transformed);
1417 assert_eq!(
1418 result.event_outcomes(),
1419 &[
1420 ContactEventOutcomeV1::Transformed {
1421 event_id: "left/point".into(),
1422 value: ContactTransformedValueV1::Point { time: 0.0 },
1423 },
1424 ContactEventOutcomeV1::Transformed {
1425 event_id: "right/window".into(),
1426 value: ContactTransformedValueV1::Window {
1427 window: ContactEventWindowV1::new(0.5, 1.0).unwrap(),
1428 },
1429 },
1430 ]
1431 );
1432 let output = result.output().unwrap().contact_fragment();
1433 assert_eq!(output.duration_s(), 1.0);
1434 assert_eq!(output.events()[0].kind(), ContactEventKindV1::Point(0.0));
1435 assert_eq!(
1436 output.events()[1].kind(),
1437 ContactEventKindV1::Window(ContactEventWindowV1::new(0.5, 1.0).unwrap())
1438 );
1439 }
1440 }
1441
1442 #[test]
1443 fn resample_preserves_times_windows_and_duration_through_readback() {
1444 let source = fragment(false);
1445 let output = InputIdentity::from_bytes(b"resampled output artifact");
1446 let context = ContactTransformContextV1::new(
1447 source.artifact().clone(),
1448 closure(source.artifact()),
1449 output.clone(),
1450 closure(&output),
1451 ContactProducerV1::new("fixture", "2").unwrap(),
1452 None,
1453 );
1454 let result = transform_contact_fragment_v1(
1455 ContactTransformOperationV1::resample(),
1456 &source,
1457 context.clone(),
1458 )
1459 .unwrap();
1460
1461 assert_eq!(result.outcome(), ContactTransformOutcomeV1::Transformed);
1462 assert_eq!(
1463 result.event_outcomes(),
1464 &[
1465 ContactEventOutcomeV1::Transformed {
1466 event_id: "left/point".into(),
1467 value: ContactTransformedValueV1::Point { time: 0.25 },
1468 },
1469 ContactEventOutcomeV1::Transformed {
1470 event_id: "right/window".into(),
1471 value: ContactTransformedValueV1::Window {
1472 window: ContactEventWindowV1::new(0.5, 0.75).unwrap(),
1473 },
1474 },
1475 ]
1476 );
1477 let output_fragment = result.output().unwrap().contact_fragment();
1478 assert_eq!(output_fragment.duration_s(), source.duration_s());
1479 assert_eq!(output_fragment.events(), source.events());
1480
1481 let bytes = result.canonical_json().unwrap();
1482 assert_eq!(
1483 ContactTransformResultV1::read_json(&bytes, &source, &context).unwrap(),
1484 result
1485 );
1486 }
1487
1488 #[test]
1489 fn time_warp_refuses_nonmonotone_interior_with_valid_endpoints() {
1490 let source = fragment(false);
1491 let invalid = transform(
1492 &source,
1493 ContactTransformOperationV1::time_warp(
1494 2.0,
1495 vec![
1496 ContactTimeWarpControlPointV1::new(0.0, 0.0),
1497 ContactTimeWarpControlPointV1::new(0.4, 0.8),
1498 ContactTimeWarpControlPointV1::new(0.6, 0.7),
1499 ContactTimeWarpControlPointV1::new(1.0, 1.0),
1500 ],
1501 ),
1502 None,
1503 );
1504 assert_eq!(
1505 invalid.refusal().unwrap().code(),
1506 ContactTransformRefusalCodeV1::InvalidMapping
1507 );
1508 assert!(invalid.event_outcomes().is_empty());
1509 }
1510
1511 #[test]
1512 fn invalid_mapping_and_stale_binding_refuse_before_event_inventory() {
1513 let source = fragment(false);
1514 let invalid = transform(
1515 &source,
1516 ContactTransformOperationV1::time_warp(
1517 2.0,
1518 vec![
1519 ContactTimeWarpControlPointV1::new(0.0, 0.0),
1520 ContactTimeWarpControlPointV1::new(1.0, 0.5),
1521 ],
1522 ),
1523 None,
1524 );
1525 assert_eq!(
1526 invalid.refusal().unwrap().code(),
1527 ContactTransformRefusalCodeV1::InvalidMapping
1528 );
1529 assert!(invalid.event_outcomes().is_empty());
1530
1531 let output = InputIdentity::from_bytes(b"output artifact");
1532 let changed_source = InputIdentity::from_bytes(b"changed source");
1533 let stale_context = ContactTransformContextV1::new(
1534 changed_source.clone(),
1535 closure(&changed_source),
1536 output.clone(),
1537 closure(&output),
1538 ContactProducerV1::new("fixture", "2").unwrap(),
1539 None,
1540 );
1541 let stale = transform_contact_fragment_v1(warp(), &source, stale_context).unwrap();
1542 assert_eq!(
1543 stale.refusal().unwrap().code(),
1544 ContactTransformRefusalCodeV1::InvalidBinding
1545 );
1546 assert!(stale.event_outcomes().is_empty());
1547
1548 let stale_closure_context = ContactTransformContextV1::new(
1549 source.artifact().clone(),
1550 closure_with_primary_reference(source.artifact()),
1551 output.clone(),
1552 closure(&output),
1553 ContactProducerV1::new("fixture", "2").unwrap(),
1554 None,
1555 );
1556 let stale_closure =
1557 transform_contact_fragment_v1(warp(), &source, stale_closure_context).unwrap();
1558 assert_eq!(
1559 stale_closure.refusal().unwrap().code(),
1560 ContactTransformRefusalCodeV1::InvalidBinding
1561 );
1562 assert!(stale_closure.event_outcomes().is_empty());
1563 }
1564
1565 #[test]
1566 fn output_closure_must_be_complete_and_bound_to_the_output_artifact() {
1567 let source = fragment(false);
1568 let output = InputIdentity::from_bytes(b"output artifact");
1569 let unrelated = InputIdentity::from_bytes(b"unrelated artifact");
1570 let context = ContactTransformContextV1::new(
1571 source.artifact().clone(),
1572 closure(source.artifact()),
1573 output,
1574 closure(&unrelated),
1575 ContactProducerV1::new("fixture", "2").unwrap(),
1576 None,
1577 );
1578 assert!(matches!(
1579 transform_contact_fragment_v1(warp(), &source, context),
1580 Err(ContactTransformError::Invalid { message })
1581 if message.contains("output dependency closure primary input")
1582 ));
1583 }
1584
1585 #[test]
1586 fn readback_checks_all_three_input_bindings_against_external_context() {
1587 let source = fragment(false);
1588 let context = context(&source, None);
1589 let result = transform_contact_fragment_v1(warp(), &source, context.clone()).unwrap();
1590
1591 for field in ["artifact", "dependency_closure_identity", "fragment"] {
1592 let mut value: serde_json::Value =
1593 serde_json::from_slice(&result.canonical_json().unwrap()).unwrap();
1594 value["input"][field]["sha256"] = "0".repeat(64).into();
1595 assert!(
1596 ContactTransformResultV1::read_json(
1597 &serde_json::to_vec(&value).unwrap(),
1598 &source,
1599 &context,
1600 )
1601 .is_err(),
1602 "mutated input binding {field} was accepted"
1603 );
1604 }
1605
1606 let changed_source = InputIdentity::from_bytes(b"changed source");
1607 let stale_context = ContactTransformContextV1::new(
1608 changed_source.clone(),
1609 closure(&changed_source),
1610 InputIdentity::from_bytes(b"unused output"),
1611 closure(&InputIdentity::from_bytes(b"unused output")),
1612 ContactProducerV1::new("fixture", "2").unwrap(),
1613 None,
1614 );
1615 let refused =
1616 transform_contact_fragment_v1(warp(), &source, stale_context.clone()).unwrap();
1617 assert_eq!(
1618 ContactTransformResultV1::read_json(
1619 &refused.canonical_json().unwrap(),
1620 &source,
1621 &stale_context,
1622 )
1623 .unwrap(),
1624 refused
1625 );
1626 }
1627
1628 #[test]
1629 fn public_window_deserialization_preserves_constructor_invariants() {
1630 assert!(
1631 serde_json::from_str::<ContactEventWindowV1>(r#"{"start":0.25,"end":0.75}"#).is_ok()
1632 );
1633 assert!(
1634 serde_json::from_str::<ContactEventWindowV1>(r#"{"start":0.9,"end":0.1}"#).is_err()
1635 );
1636 assert!(
1637 serde_json::from_str::<ContactEventWindowV1>(r#"{"start":-0.1,"end":0.1}"#).is_err()
1638 );
1639 assert!(serde_json::from_str::<ContactTransformedValueV1>(r#"{"time":-0.1}"#).is_err());
1640 }
1641
1642 #[test]
1643 fn partial_window_refuses_whole_operation_without_output() {
1644 let source = fragment(false);
1645 let result = transform(
1646 &source,
1647 ContactTransformOperationV1::trim(ContactTransformIntervalV1::new(0.6, 1.0)),
1648 None,
1649 );
1650 assert_eq!(result.outcome(), ContactTransformOutcomeV1::Refused);
1651 assert!(result.output().is_none());
1652 assert_eq!(
1653 result.refusal().unwrap().code(),
1654 ContactTransformRefusalCodeV1::PartialWindow
1655 );
1656 assert!(matches!(
1657 result.event_outcomes()[1],
1658 ContactEventOutcomeV1::Refused {
1659 code: ContactTransformRefusalCodeV1::PartialWindow,
1660 ..
1661 }
1662 ));
1663 }
1664
1665 #[test]
1666 fn extensions_require_exact_explicit_support() {
1667 let source = fragment(true);
1668 let refused = transform(&source, warp(), None);
1669 assert_eq!(
1670 refused.refusal().unwrap().code(),
1671 ContactTransformRefusalCodeV1::UnsupportedExtension
1672 );
1673
1674 let wrong_version = vec![
1675 ContactExtensionV1::new(
1676 "urn:animsmith:contact-detector:stance-support:1",
1677 2,
1678 serde_json::json!({"algorithm": "fixture", "mapped": true}),
1679 )
1680 .unwrap(),
1681 ];
1682 assert_eq!(
1683 transform(&source, warp(), Some(wrong_version))
1684 .refusal()
1685 .unwrap()
1686 .code(),
1687 ContactTransformRefusalCodeV1::UnsupportedExtension
1688 );
1689
1690 let transformed = vec![
1691 ContactExtensionV1::new(
1692 "urn:animsmith:contact-detector:stance-support:1",
1693 1,
1694 serde_json::json!({"algorithm": "fixture", "mapped": true}),
1695 )
1696 .unwrap(),
1697 ];
1698 let context = context(&source, Some(transformed.clone()));
1699 let result = transform_contact_fragment_v1(warp(), &source, context.clone()).unwrap();
1700 assert_eq!(result.outcome(), ContactTransformOutcomeV1::Transformed);
1701 assert_eq!(
1702 result.output().unwrap().contact_fragment().extensions(),
1703 transformed
1704 );
1705 assert_eq!(
1706 ContactTransformResultV1::read_json(
1707 &result.canonical_json().unwrap(),
1708 &source,
1709 &context,
1710 )
1711 .unwrap(),
1712 result
1713 );
1714 }
1715
1716 #[test]
1717 fn reader_rederives_event_values_instead_of_trusting_result_rows() {
1718 let source = fragment(false);
1719 let context = context(&source, None);
1720 let result = transform_contact_fragment_v1(warp(), &source, context.clone()).unwrap();
1721 let mut value: serde_json::Value =
1722 serde_json::from_slice(&result.canonical_json().unwrap()).unwrap();
1723 value["event_outcomes"][0]["value"]["time"] = serde_json::json!(0.2);
1724 let bytes = serde_json::to_vec(&value).unwrap();
1725 assert!(ContactTransformResultV1::read_json(&bytes, &source, &context).is_err());
1726 }
1727
1728 #[test]
1729 fn reader_rejects_unknown_duplicate_and_n_plus_one_control_points() {
1730 let source = fragment(false);
1731 let context = context(&source, None);
1732 let result = transform_contact_fragment_v1(warp(), &source, context.clone()).unwrap();
1733 let mut value: serde_json::Value =
1734 serde_json::from_slice(&result.canonical_json().unwrap()).unwrap();
1735 value["unknown"] = serde_json::json!(true);
1736 assert!(
1737 ContactTransformResultV1::read_json(
1738 &serde_json::to_vec(&value).unwrap(),
1739 &source,
1740 &context,
1741 )
1742 .is_err()
1743 );
1744
1745 let duplicate = result
1746 .canonical_json()
1747 .unwrap()
1748 .into_iter()
1749 .collect::<Vec<_>>();
1750 let text = String::from_utf8(duplicate).unwrap();
1751 let duplicate = text.replacen(
1752 "{\"event_outcomes\"",
1753 "{\"schema\":\"urn:animsmith:schema:contact-transform-result:1\",\"event_outcomes\"",
1754 1,
1755 );
1756 assert!(
1757 ContactTransformResultV1::read_json(duplicate.as_bytes(), &source, &context).is_err()
1758 );
1759
1760 let mut value: serde_json::Value =
1761 serde_json::from_slice(&result.canonical_json().unwrap()).unwrap();
1762 value["operation"]["control_points"] = serde_json::Value::Array(
1763 (0..=CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS)
1764 .map(|index| {
1765 let value =
1766 index as f64 / CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS as f64;
1767 serde_json::json!({"input_time": value, "output_time": value})
1768 })
1769 .collect(),
1770 );
1771 assert!(
1772 ContactTransformResultV1::read_json(
1773 &serde_json::to_vec(&value).unwrap(),
1774 &source,
1775 &context,
1776 )
1777 .is_err()
1778 );
1779
1780 let mut value: serde_json::Value =
1781 serde_json::from_slice(&result.canonical_json().unwrap()).unwrap();
1782 value["event_outcomes"] = serde_json::Value::Array(
1783 (0..=CONTACT_TRANSFORM_RESULT_V1_MAX_EVENT_OUTCOMES)
1784 .map(|_| serde_json::json!({"event_id": "left/point", "outcome": "outside"}))
1785 .collect(),
1786 );
1787 assert!(
1788 ContactTransformResultV1::read_json(
1789 &serde_json::to_vec(&value).unwrap(),
1790 &source,
1791 &context,
1792 )
1793 .is_err()
1794 );
1795 }
1796
1797 #[test]
1798 fn source_and_canonical_byte_caps_fail_closed() {
1799 let source = fragment(false);
1800 let context = context(&source, None);
1801 assert!(matches!(
1802 ContactTransformResultV1::read_json(
1803 &vec![b' '; CONTACT_TRANSFORM_RESULT_V1_MAX_SOURCE_BYTES + 1],
1804 &source,
1805 &context,
1806 ),
1807 Err(ContactTransformError::SourceTooLarge { .. })
1808 ));
1809
1810 let mut writer = CappedWriter {
1811 bytes: Vec::new(),
1812 overflowed: false,
1813 };
1814 writer
1815 .write_all(&vec![b'x'; CONTACT_TRANSFORM_RESULT_V1_MAX_CANONICAL_BYTES])
1816 .unwrap();
1817 assert_eq!(
1818 writer.bytes.len(),
1819 CONTACT_TRANSFORM_RESULT_V1_MAX_CANONICAL_BYTES
1820 );
1821 assert!(writer.write_all(b"x").is_err());
1822 assert!(writer.overflowed);
1823 }
1824}