Skip to main content

animsmith_core/
contact_transform.rs

1//! Strict contact-fragment transform result V1.
2//!
3//! This module implements the format-neutral Appendix F.10 mapping and result
4//! contract. It transforms only contact facts; asset mutation, dependency
5//! capture, filesystem publication, and engine policy remain frontend work.
6
7use 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
19/// Immutable contact-transform-result V1 schema identity.
20pub const CONTACT_TRANSFORM_RESULT_V1_ID: &str = "urn:animsmith:schema:contact-transform-result:1";
21/// Immutable contact-transform-result V1 schema version.
22pub const CONTACT_TRANSFORM_RESULT_V1_SCHEMA_VERSION: u32 = 1;
23/// Maximum accepted UTF-8 result bytes.
24pub const CONTACT_TRANSFORM_RESULT_V1_MAX_SOURCE_BYTES: usize = 16 * 1024 * 1024;
25/// Maximum canonical RFC 8785 result bytes.
26pub const CONTACT_TRANSFORM_RESULT_V1_MAX_CANONICAL_BYTES: usize = 16 * 1024 * 1024;
27/// Maximum time-warp control points.
28pub const CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS: usize = 4_096;
29/// Maximum event outcomes.
30pub const CONTACT_TRANSFORM_RESULT_V1_MAX_EVENT_OUTCOMES: usize = 4_096;
31/// Maximum UTF-8 bytes in a display refusal message.
32pub const CONTACT_TRANSFORM_RESULT_V1_MAX_MESSAGE_BYTES: usize = 4_096;
33
34/// A strict result could not be decoded, represented, or verified.
35#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36#[non_exhaustive]
37pub enum ContactTransformError {
38    /// Input exceeded the immutable source-byte cap.
39    #[error("contact transform result source has {bytes} bytes, exceeding V1 limit {limit}")]
40    SourceTooLarge {
41        /// Observed bytes.
42        bytes: usize,
43        /// Frozen limit.
44        limit: usize,
45    },
46    /// JSON or a strict wire invariant was invalid.
47    #[error("invalid contact transform result: {message}")]
48    Invalid {
49        /// Decoder or verification detail.
50        message: String,
51    },
52    /// A row cap was exceeded.
53    #[error("contact transform result {field} has {found} rows, exceeding V1 limit {limit}")]
54    LimitExceeded {
55        /// Stable field.
56        field: &'static str,
57        /// Observed rows, including the N+1 witness.
58        found: usize,
59        /// Frozen limit.
60        limit: usize,
61    },
62    /// Canonical output exceeded its immutable cap.
63    #[error("contact transform result canonical JSON exceeds V1 limit {limit}")]
64    CanonicalTooLarge {
65        /// Frozen limit.
66        limit: usize,
67    },
68    /// The embedded contact fragment was invalid.
69    #[error("invalid transformed contact fragment: {0}")]
70    Fragment(#[from] ContactFragmentError),
71}
72
73/// One normalized retained interval for trim or slice.
74#[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    /// Construct a structurally representable interval.
83    ///
84    /// Domain and ordering are deliberately checked by operation validation so
85    /// a known operation can produce a typed `invalid_mapping` refusal.
86    pub const fn new(start: f64, end: f64) -> Self {
87        Self { start, end }
88    }
89    /// Declared normalized start.
90    pub const fn start(self) -> f64 {
91        self.start
92    }
93    /// Declared normalized end.
94    pub const fn end(self) -> f64 {
95        self.end
96    }
97}
98
99/// One source-to-output normalized time-warp knot.
100#[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    /// Construct one structurally representable control point.
109    pub const fn new(input_time: f64, output_time: f64) -> Self {
110        Self {
111            input_time,
112            output_time,
113        }
114    }
115    /// Source normalized time.
116    pub const fn input_time(self) -> f64 {
117        self.input_time
118    }
119    /// Output normalized time.
120    pub const fn output_time(self) -> f64 {
121        self.output_time
122    }
123}
124
125/// The closed V1 contact operation vocabulary.
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
128pub enum ContactTransformOperationV1 {
129    /// Retain and normalize one interval.
130    Trim {
131        /// Operation version.
132        version: u32,
133        /// Retained normalized interval.
134        interval: ContactTransformIntervalV1,
135    },
136    /// Retain and normalize one interval with slice semantics.
137    Slice {
138        /// Operation version.
139        version: u32,
140        /// Retained normalized interval.
141        interval: ContactTransformIntervalV1,
142    },
143    /// Identity normalized-time mapping.
144    Resample {
145        /// Operation version.
146        version: u32,
147        /// Must be `identity`.
148        mapping: String,
149    },
150    /// Strictly monotone piecewise-linear source-to-output mapping.
151    TimeWarp {
152        /// Operation version.
153        version: u32,
154        /// Exact output clip duration.
155        output_duration_s: f64,
156        /// Ordered normalized control points.
157        #[serde(deserialize_with = "deserialize_control_points")]
158        control_points: Vec<ContactTimeWarpControlPointV1>,
159    },
160}
161
162impl ContactTransformOperationV1 {
163    /// Construct a V1 trim request.
164    pub const fn trim(interval: ContactTransformIntervalV1) -> Self {
165        Self::Trim {
166            version: 1,
167            interval,
168        }
169    }
170    /// Construct a V1 slice request.
171    pub const fn slice(interval: ContactTransformIntervalV1) -> Self {
172        Self::Slice {
173            version: 1,
174            interval,
175        }
176    }
177    /// Construct a V1 identity resample request.
178    pub fn resample() -> Self {
179        Self::Resample {
180            version: 1,
181            mapping: "identity".into(),
182        }
183    }
184    /// Construct a V1 time-warp request.
185    ///
186    /// Mapping validity is evaluated into a typed result rather than rejected
187    /// by this structural constructor.
188    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    /// Ordered time-warp knots, or `None` for another operation.
199    pub fn control_points(&self) -> Option<&[ContactTimeWarpControlPointV1]> {
200        match self {
201            Self::TimeWarp { control_points, .. } => Some(control_points),
202            _ => None,
203        }
204    }
205    /// Exact output duration declared by time-warp.
206    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/// Exact three-way input binding.
217#[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    /// Construct one exact input binding.
227    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    /// Source artifact identity.
239    pub const fn artifact(&self) -> &InputIdentity {
240        &self.artifact
241    }
242    /// Source dependency-closure identity.
243    pub const fn dependency_closure_identity(&self) -> &DependencyClosureIdentityV1 {
244        &self.dependency_closure_identity
245    }
246    /// Canonical source fragment identity.
247    pub const fn fragment(&self) -> &InputIdentity {
248        &self.fragment
249    }
250}
251
252/// Independently captured same-operation artifact, closure, producer, and
253/// extension-output context.
254#[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    /// Construct the exact context captured by one artifact operation.
266    ///
267    /// `transformed_extensions` is `None` when no operation-specific extension
268    /// handler ran. When present, it contains handler-produced outputs in exact
269    /// input extension order, not an opaque copy performed by core.
270    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/// Exact transformed point or window value.
290#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
291#[serde(untagged, deny_unknown_fields)]
292pub enum ContactTransformedValueV1 {
293    /// Mapped point.
294    Point {
295        /// Normalized output time.
296        time: f64,
297    },
298    /// Mapped inclusive window.
299    Window {
300        /// Inclusive normalized output window.
301        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/// One input event's operation outcome.
332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
333#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
334pub enum ContactEventOutcomeV1 {
335    /// Event was mapped and retained.
336    Transformed {
337        /// Stable input event id.
338        event_id: String,
339        /// Exact mapped value.
340        value: ContactTransformedValueV1,
341    },
342    /// Event lay wholly outside a trim/slice interval.
343    Outside {
344        /// Stable input event id.
345        event_id: String,
346    },
347    /// Event made the whole operation refuse.
348    Refused {
349        /// Stable input event id.
350        event_id: String,
351        /// Stable refusal code.
352        code: ContactTransformRefusalCodeV1,
353    },
354}
355
356impl ContactEventOutcomeV1 {
357    /// Stable input event id.
358    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/// Closed V1 refusal vocabulary.
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
369#[serde(rename_all = "snake_case")]
370pub enum ContactTransformRefusalCodeV1 {
371    /// A window crossed a retained interval boundary.
372    PartialWindow,
373    /// A known operation's numeric domain or ordering was invalid.
374    InvalidMapping,
375    /// Input identities did not match the supplied fragment.
376    InvalidBinding,
377    /// Finite inputs produced an invalid derived value.
378    InvalidValue,
379    /// An extension has no explicit operation-specific support.
380    UnsupportedExtension,
381}
382
383/// Stable refusal record.
384#[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    /// Stable refusal code.
393    pub const fn code(&self) -> ContactTransformRefusalCodeV1 {
394        self.code
395    }
396    /// Display-only detail.
397    pub fn message(&self) -> &str {
398        &self.message
399    }
400}
401
402/// Successful output binding plus the complete transformed fragment.
403#[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    /// Output artifact identity.
413    pub const fn artifact(&self) -> &InputIdentity {
414        &self.artifact
415    }
416    /// Fresh output dependency-closure identity.
417    pub const fn dependency_closure_identity(&self) -> &DependencyClosureIdentityV1 {
418        &self.dependency_closure_identity
419    }
420    /// Canonical output-fragment identity.
421    pub const fn fragment(&self) -> &InputIdentity {
422        &self.fragment
423    }
424    /// Complete transformed fragment.
425    pub const fn contact_fragment(&self) -> &ContactFragmentV1 {
426        &self.contact_fragment
427    }
428}
429
430/// Strict V1 transform result.
431#[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/// Global transform outcome.
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
447#[serde(rename_all = "snake_case")]
448pub enum ContactTransformOutcomeV1 {
449    /// Complete transformed output exists.
450    Transformed,
451    /// No output exists and a refusal is present.
452    Refused,
453}
454
455impl ContactTransformResultV1 {
456    /// Strictly decode and verify one bounded result against its separately
457    /// supplied input fragment and independently captured transform context.
458    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    /// Canonical RFC 8785 result bytes.
479    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    /// Operation echoed exactly in the result.
499    pub const fn operation(&self) -> &ContactTransformOperationV1 {
500        &self.operation
501    }
502    /// Exact source bindings.
503    pub const fn input(&self) -> &ContactTransformBindingV1 {
504        &self.input
505    }
506    /// Global outcome.
507    pub const fn outcome(&self) -> ContactTransformOutcomeV1 {
508        self.outcome
509    }
510    /// Outcomes in canonical input event order.
511    pub fn event_outcomes(&self) -> &[ContactEventOutcomeV1] {
512        &self.event_outcomes
513    }
514    /// Successful output, when transformed.
515    pub const fn output(&self) -> Option<&ContactTransformOutputV1> {
516        self.output.as_ref()
517    }
518    /// Refusal, when refused.
519    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
625/// Transform a contact fragment with explicit output identities and explicit
626/// operation-specific extension support.
627///
628/// The context's optional transformed extension list is supplied by
629/// operation-specific handlers. It must retain input order and the exact
630/// `(schema, version)` at every position; absence refuses a fragment that has
631/// extensions. Core never treats opaque payload copying as transformation.
632pub 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        &current_input_dependency_closure,
648        &current_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}