Skip to main content

animsmith_core/
transition_pose_evaluation.rs

1//! Strict, format-neutral transition-pose evaluation V1.
2//!
3//! This module consumes the validated transition-family declaration and a
4//! mutable loader-facing [`Document`] plus the same-load [`DependencyClosureV1`]
5//! that binds every byte on which that document depends. It deliberately owns
6//! strict endpoint sampling rather than changing the tolerant general-purpose
7//! sampler, and it deliberately has no filesystem, config, collection, or
8//! command authority.
9
10use serde::Serialize;
11use std::collections::BTreeMap;
12use std::io::{self, Write};
13
14use crate::model::validate_track_shape;
15use crate::{
16    Bone, Clip, DependencyClosureIdentityV1, DependencyClosureV1, Document, InputIdentity,
17    LoadedSource, Property, Skeleton, Track, TrackSample, TransitionFamilyBoundaryV1,
18    TransitionFamilyDeclarationInputV1, TransitionFamilyManifestIdentityV1,
19    TransitionFamilyTolerancesV1,
20};
21
22/// Schema identity for a transition-pose evaluation result.
23pub const TRANSITION_POSE_EVALUATION_V1_ID: &str =
24    "urn:animsmith:schema:transition-pose-evaluation:1";
25/// Schema version for a transition-pose evaluation result.
26pub const TRANSITION_POSE_EVALUATION_V1_SCHEMA_VERSION: u32 = 1;
27/// Maximum skeleton bones admitted by one V1 basis.
28pub const TRANSITION_POSE_EVALUATION_V1_MAX_BONES: usize = 4_096;
29/// Maximum total authored UTF-8 bone-name bytes admitted to one V1 basis.
30///
31/// This shares the declaration V1 normalized-byte budget so basis identity
32/// construction never clones unbounded skeleton text.
33pub const TRANSITION_POSE_EVALUATION_V1_MAX_BASIS_TEXT_BYTES: usize =
34    crate::TRANSITION_FAMILY_V1_MAX_NORMALIZED_BYTES as usize;
35/// Maximum clips admitted to document transition-pose witness resolution.
36///
37/// This aligns with the V1 collection/source clip domain. Above it, direct
38/// declaration index/name contradictions still fail structurally, while an
39/// otherwise valid declaration receives normal `input_limit` result rows
40/// without a global duplicate-name scan.
41pub const TRANSITION_POSE_EVALUATION_V1_MAX_DOCUMENT_CLIPS: usize = 4_096;
42/// Maximum raw flat track rows admitted for one selected clip.
43///
44/// The loader-facing document stores every property in one vector, so V1 must
45/// bound that vector before it can discover the T/R rows it consumes. Scale
46/// remains semantically ignored after this resource admission.
47pub const TRANSITION_POSE_EVALUATION_V1_MAX_RAW_TRACK_ROWS_PER_CLIP: usize =
48    TRANSITION_POSE_EVALUATION_V1_MAX_BONES * 3;
49/// Maximum selected tracks in one clip: translation and rotation per admitted
50/// V1 skeleton bone. Scale is outside the V1 endpoint domain and is ignored.
51pub const TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACKS_PER_CLIP: usize =
52    TRANSITION_POSE_EVALUATION_V1_MAX_BONES * 2;
53/// Maximum aggregate selected time/value elements inspected by one evaluator
54/// call. This reuses the immutable aggregate comparison-work bound.
55pub const TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACK_ELEMENTS: usize =
56    TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_COMPARISONS;
57/// Maximum pair/boundary comparisons in one family.
58pub const TRANSITION_POSE_EVALUATION_V1_MAX_FAMILY_PAIR_BOUNDARIES: usize = 4_096;
59/// Maximum pair/boundary comparisons across one result.
60pub const TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_PAIR_BOUNDARIES: usize = 65_536;
61/// Maximum pair/boundary/bone comparisons across one result.
62pub const TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_COMPARISONS: usize = 16_777_216;
63/// Maximum retained translation offenders in one pair/boundary row.
64pub const TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS: usize = 16;
65/// Maximum retained rotation offenders in one pair/boundary row.
66pub const TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS: usize = 16;
67/// Maximum retained offenders across one result.
68pub const TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_OFFENDERS: usize = 65_536;
69/// Maximum serialized V1 result bytes.
70pub const TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES: usize = 256 * 1024 * 1024;
71/// Conservative fixed JCS bytes reserved for every detailed pair/boundary
72/// before endpoint sampling. It covers a complete pair row and its two
73/// bounded offender arrays excluding escaped bone-name text.
74const MAX_DETAILED_PAIR_FIXED_BYTES: usize = 4_096;
75
76/// A normalized local-rest bone record contributing to [`SkeletonBasisV1`].
77#[derive(Debug, Clone, PartialEq, Serialize)]
78pub struct SkeletonBasisBoneV1 {
79    ordinal: usize,
80    name: String,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    parent_ordinal: Option<usize>,
83    rest_translation_m: [f64; 3],
84    rest_rotation: [f64; 4],
85}
86
87impl SkeletonBasisBoneV1 {
88    /// Parent-before-child ordinal.
89    pub const fn ordinal(&self) -> usize {
90        self.ordinal
91    }
92    /// Exact normalized bone name.
93    pub fn name(&self) -> &str {
94        &self.name
95    }
96    /// Parent ordinal, when this bone is not a root.
97    pub const fn parent_ordinal(&self) -> Option<usize> {
98        self.parent_ordinal
99    }
100    /// Finite local-rest translation in metres.
101    pub const fn rest_translation_m(&self) -> [f64; 3] {
102        self.rest_translation_m
103    }
104    /// Unit, hemisphere-canonical local-rest quaternion in `[x, y, z, w]` order.
105    pub const fn rest_rotation(&self) -> [f64; 4] {
106        self.rest_rotation
107    }
108}
109
110/// First-class normalized skeleton identity used by V1 comparisons.
111#[derive(Debug, Clone, PartialEq, Serialize)]
112pub struct SkeletonBasisV1 {
113    schema: &'static str,
114    schema_version: u32,
115    bones: Vec<SkeletonBasisBoneV1>,
116    #[serde(skip)]
117    identity: InputIdentity,
118}
119
120impl SkeletonBasisV1 {
121    /// Build a strict scale/bind/mesh-independent skeleton basis.
122    pub fn from_skeleton(skeleton: &Skeleton) -> Result<Self, SkeletonBasisError> {
123        if skeleton.bones.len() > TRANSITION_POSE_EVALUATION_V1_MAX_BONES {
124            return Err(SkeletonBasisError::TooManyBones);
125        }
126        if !skeleton_text_is_within_limit(
127            skeleton,
128            TRANSITION_POSE_EVALUATION_V1_MAX_BASIS_TEXT_BYTES,
129        ) {
130            return Err(SkeletonBasisError::TooMuchText);
131        }
132        let mut bones = Vec::with_capacity(skeleton.bones.len());
133        for (ordinal, bone) in skeleton.bones.iter().enumerate() {
134            let parent_ordinal = match bone.parent {
135                Some(parent) if parent < ordinal => Some(parent),
136                Some(_) => return Err(SkeletonBasisError::InvalidParent { ordinal }),
137                None => None,
138            };
139            bones.push(SkeletonBasisBoneV1 {
140                ordinal,
141                name: bone.name.clone(),
142                parent_ordinal,
143                rest_translation_m: finite_translation(bone, ordinal)?,
144                rest_rotation: canonical_quaternion(bone.rest.rotation, ordinal)?,
145            });
146        }
147        let wire = SkeletonBasisWire {
148            schema: "urn:animsmith:schema:skeleton-basis:1",
149            schema_version: 1,
150            bones: &bones,
151        };
152        let bytes = canonical_bytes(&wire, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)
153            .map_err(|_| SkeletonBasisError::IdentityTooLarge)?;
154        Ok(Self {
155            schema: "urn:animsmith:schema:skeleton-basis:1",
156            schema_version: 1,
157            bones,
158            identity: InputIdentity::from_bytes(&bytes),
159        })
160    }
161    /// Skeleton-basis schema identity.
162    pub const fn schema(&self) -> &'static str {
163        self.schema
164    }
165    /// Skeleton-basis schema version.
166    pub const fn schema_version(&self) -> u32 {
167        self.schema_version
168    }
169    /// Parent-before-child normalized bones.
170    pub fn bones(&self) -> &[SkeletonBasisBoneV1] {
171        &self.bones
172    }
173    /// Exact JCS identity of this basis.
174    pub const fn identity(&self) -> &InputIdentity {
175        &self.identity
176    }
177}
178
179#[derive(Serialize)]
180struct SkeletonBasisWire<'a> {
181    schema: &'static str,
182    schema_version: u32,
183    bones: &'a [SkeletonBasisBoneV1],
184}
185
186/// Strict skeleton-basis construction failure.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
188#[non_exhaustive]
189pub enum SkeletonBasisError {
190    /// A basis exceeded its fixed bone limit.
191    #[error("transition-pose skeleton basis exceeds the bone cap")]
192    TooManyBones,
193    /// Authored bone names exceeded the basis text cap before normalization.
194    #[error("transition-pose skeleton basis exceeds the bone-name text cap")]
195    TooMuchText,
196    /// A parent was absent from the required parent-before-child prefix.
197    #[error("transition-pose skeleton basis has an invalid parent at bone {ordinal}")]
198    InvalidParent {
199        /// Child ordinal whose parent was invalid.
200        ordinal: usize,
201    },
202    /// One local-rest translation or quaternion was non-finite or degenerate.
203    #[error("transition-pose skeleton basis has an invalid rest transform at bone {ordinal}")]
204    InvalidRest {
205        /// Bone ordinal whose rest transform was invalid.
206        ordinal: usize,
207    },
208    /// The normalized identity could not fit the bounded canonical writer.
209    #[error("transition-pose skeleton basis identity exceeds the result cap")]
210    IdentityTooLarge,
211}
212
213/// Closed result lifecycle.
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
215#[serde(rename_all = "snake_case")]
216#[allow(missing_docs)]
217pub enum TransitionPoseStatusV1 {
218    Complete,
219    Incomplete,
220}
221/// Closed V1 decision vocabulary.
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
223#[serde(rename_all = "snake_case")]
224#[allow(missing_docs)]
225pub enum TransitionPoseDecisionV1 {
226    Pass,
227    Finding,
228    NotEvaluated,
229}
230/// Closed typed reason vocabulary for no-family and incomplete outcomes.
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
232#[serde(rename_all = "snake_case")]
233#[allow(missing_docs)]
234pub enum TransitionPoseReasonV1 {
235    NoConfiguredFamilies,
236    DependencyClosureIncomplete,
237    MemberUnavailable,
238    ZeroDuration,
239    SkeletonBasisMismatch,
240    TimeToleranceUnsupported,
241    UnsupportedSampling,
242    InputLimit,
243    FamilyWorkLimit,
244    AggregateWorkLimit,
245    RetentionLimit,
246    ResultLimit,
247}
248
249/// Exact resolved member authority retained in a family result.
250#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
251pub struct TransitionPoseMemberV1 {
252    take_index: u64,
253    take_name: String,
254    source_input: Option<InputIdentity>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    source_dependency_closure_identity: Option<DependencyClosureIdentityV1>,
257}
258impl TransitionPoseMemberV1 {
259    /// Witnessed embedded take index.
260    pub const fn take_index(&self) -> u64 {
261        self.take_index
262    }
263    /// Witnessed embedded take name.
264    pub fn take_name(&self) -> &str {
265        &self.take_name
266    }
267    /// Exact raw source identity when the selected source was available.
268    pub const fn source_input(&self) -> Option<&InputIdentity> {
269        self.source_input.as_ref()
270    }
271    /// Exact complete dependency-closure identity for this selected source.
272    pub const fn source_dependency_closure_identity(&self) -> Option<&DependencyClosureIdentityV1> {
273        self.source_dependency_closure_identity.as_ref()
274    }
275}
276
277/// One translation offender, sorted independently from rotation offenders.
278#[derive(Debug, Clone, PartialEq, Serialize)]
279pub struct TransitionPoseTranslationOffenderV1 {
280    bone_ordinal: usize,
281    bone_name: String,
282    delta_m: f64,
283}
284impl TransitionPoseTranslationOffenderV1 {
285    /// Stable skeleton-basis bone ordinal.
286    pub const fn bone_ordinal(&self) -> usize {
287        self.bone_ordinal
288    }
289    /// Exact skeleton-basis bone name.
290    pub fn bone_name(&self) -> &str {
291        &self.bone_name
292    }
293    /// Measured Euclidean translation delta in metres.
294    pub const fn delta_m(&self) -> f64 {
295        self.delta_m
296    }
297}
298/// One rotation offender, sorted independently from translation offenders.
299#[derive(Debug, Clone, PartialEq, Serialize)]
300pub struct TransitionPoseRotationOffenderV1 {
301    bone_ordinal: usize,
302    bone_name: String,
303    delta_deg: f64,
304}
305impl TransitionPoseRotationOffenderV1 {
306    /// Stable skeleton-basis bone ordinal.
307    pub const fn bone_ordinal(&self) -> usize {
308        self.bone_ordinal
309    }
310    /// Exact skeleton-basis bone name.
311    pub fn bone_name(&self) -> &str {
312        &self.bone_name
313    }
314    /// Measured shortest-path rotation delta in degrees.
315    pub const fn delta_deg(&self) -> f64 {
316        self.delta_deg
317    }
318}
319
320/// Canonically ordered member-pair endpoint comparison.
321#[derive(Debug, Clone, PartialEq, Serialize)]
322pub struct TransitionPosePairEvaluationV1 {
323    member_indices: [usize; 2],
324    boundary: TransitionFamilyBoundaryV1,
325    max_translation_delta_m: f64,
326    max_rotation_delta_deg: f64,
327    translation_tolerance_m: f64,
328    rotation_tolerance_deg: f64,
329    translation_offenders: Vec<TransitionPoseTranslationOffenderV1>,
330    rotation_offenders: Vec<TransitionPoseRotationOffenderV1>,
331}
332impl TransitionPosePairEvaluationV1 {
333    /// Canonically ordered declaration-member indices.
334    pub const fn member_indices(&self) -> [usize; 2] {
335        self.member_indices
336    }
337    /// Compared endpoint boundary.
338    pub const fn boundary(&self) -> TransitionFamilyBoundaryV1 {
339        self.boundary
340    }
341    /// Maximum measured translation delta in metres.
342    pub const fn max_translation_delta_m(&self) -> f64 {
343        self.max_translation_delta_m
344    }
345    /// Maximum measured rotation delta in degrees.
346    pub const fn max_rotation_delta_deg(&self) -> f64 {
347        self.max_rotation_delta_deg
348    }
349    /// Applied inclusive translation tolerance in metres.
350    pub const fn translation_tolerance_m(&self) -> f64 {
351        self.translation_tolerance_m
352    }
353    /// Applied inclusive rotation tolerance in degrees.
354    pub const fn rotation_tolerance_deg(&self) -> f64 {
355        self.rotation_tolerance_deg
356    }
357    /// Translation offenders in V1 order.
358    pub fn translation_offenders(&self) -> &[TransitionPoseTranslationOffenderV1] {
359        &self.translation_offenders
360    }
361    /// Rotation offenders in V1 order.
362    pub fn rotation_offenders(&self) -> &[TransitionPoseRotationOffenderV1] {
363        &self.rotation_offenders
364    }
365}
366
367/// Immutable per-family result.
368#[derive(Debug, Clone, PartialEq, Serialize)]
369pub struct TransitionPoseFamilyEvaluationV1 {
370    family_id: String,
371    status: TransitionPoseStatusV1,
372    decision: TransitionPoseDecisionV1,
373    #[serde(skip_serializing_if = "Option::is_none")]
374    reason: Option<TransitionPoseReasonV1>,
375    members: Vec<TransitionPoseMemberV1>,
376    #[serde(skip_serializing_if = "Option::is_none")]
377    skeleton_basis_input: Option<InputIdentity>,
378    pairs: Vec<TransitionPosePairEvaluationV1>,
379}
380impl TransitionPoseFamilyEvaluationV1 {
381    /// Stable transition-family identifier.
382    pub fn family_id(&self) -> &str {
383        &self.family_id
384    }
385    /// Family lifecycle.
386    pub const fn status(&self) -> TransitionPoseStatusV1 {
387        self.status
388    }
389    /// Family decision.
390    pub const fn decision(&self) -> TransitionPoseDecisionV1 {
391        self.decision
392    }
393    /// Typed non-evaluation reason, when present.
394    pub const fn reason(&self) -> Option<TransitionPoseReasonV1> {
395        self.reason
396    }
397    /// Exact selected member/source authorities in declared member order.
398    pub fn members(&self) -> &[TransitionPoseMemberV1] {
399        &self.members
400    }
401    /// Exact matching skeleton-basis identity, when one was established.
402    pub const fn skeleton_basis_input(&self) -> Option<&InputIdentity> {
403        self.skeleton_basis_input.as_ref()
404    }
405    /// Canonical pair/boundary comparison rows.
406    pub fn pairs(&self) -> &[TransitionPosePairEvaluationV1] {
407        &self.pairs
408    }
409}
410
411/// Immutable scope-neutral V1 result contract.
412#[derive(Debug, Clone, PartialEq, Serialize)]
413pub struct TransitionPoseEvaluationV1 {
414    schema: &'static str,
415    schema_version: u32,
416    status: TransitionPoseStatusV1,
417    decision: TransitionPoseDecisionV1,
418    #[serde(skip_serializing_if = "Option::is_none")]
419    reason: Option<TransitionPoseReasonV1>,
420    declaration_input: InputIdentity,
421    declaration_normalized: InputIdentity,
422    subject_input: InputIdentity,
423    #[serde(skip_serializing_if = "Option::is_none")]
424    subject_dependency_closure_identity: Option<DependencyClosureIdentityV1>,
425    families: Vec<TransitionPoseFamilyEvaluationV1>,
426}
427
428impl TransitionPoseEvaluationV1 {
429    /// Result schema identity.
430    pub const fn schema(&self) -> &'static str {
431        self.schema
432    }
433    /// Result schema version.
434    pub const fn schema_version(&self) -> u32 {
435        self.schema_version
436    }
437    /// Aggregate lifecycle.
438    pub const fn status(&self) -> TransitionPoseStatusV1 {
439        self.status
440    }
441    /// Aggregate decision.
442    pub const fn decision(&self) -> TransitionPoseDecisionV1 {
443        self.decision
444    }
445    /// Aggregate typed reason, when there are no configured families.
446    pub const fn reason(&self) -> Option<TransitionPoseReasonV1> {
447        self.reason
448    }
449    /// Exact declaration source identity.
450    pub const fn declaration_input(&self) -> &InputIdentity {
451        &self.declaration_input
452    }
453    /// Independently normalized declaration identity.
454    pub const fn declaration_normalized(&self) -> &InputIdentity {
455        &self.declaration_normalized
456    }
457    /// Exact raw identity of the declaration scope subject.
458    ///
459    /// The document evaluator binds the loaded document bytes here; the
460    /// collection adapter binds the manifest bytes under the same V1 schema.
461    pub const fn subject_input(&self) -> &InputIdentity {
462        &self.subject_input
463    }
464    /// Exact complete dependency-closure identity for the declaration subject.
465    ///
466    /// A document result omits this only for `no_configured_families` (which
467    /// evaluates no source data) or `incomplete/not_evaluated` with
468    /// `dependency_closure_incomplete`. A collection result omits it because
469    /// its manifest subject has no asset dependency closure; member closure
470    /// identities are the collection evaluation authority.
471    pub const fn subject_dependency_closure_identity(
472        &self,
473    ) -> Option<&DependencyClosureIdentityV1> {
474        self.subject_dependency_closure_identity.as_ref()
475    }
476    /// Family results in canonical declaration-family order.
477    pub fn families(&self) -> &[TransitionPoseFamilyEvaluationV1] {
478        &self.families
479    }
480    /// Serialize this result through the V1 bounded canonical writer.
481    pub fn normalized_jcs(&self) -> Result<Vec<u8>, TransitionPoseEvaluationControlError> {
482        canonical_bytes(self, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)
483            .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)
484    }
485}
486
487/// Invalid evaluator input is a control error. Incomplete families are normal
488/// successful results so callers can emit their immutable contract.
489#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
490#[non_exhaustive]
491pub enum TransitionPoseEvaluationControlError {
492    /// The declaration is collection-owned and needs the deferred collection adapter.
493    #[error("document transition-pose evaluation requires a document declaration")]
494    WrongDeclarationScope,
495    /// The strict normalized skeleton identity could not be constructed.
496    #[error("transition-pose skeleton basis is invalid: {0}")]
497    InvalidSkeletonBasis(SkeletonBasisError),
498    /// A declaration member witness did not resolve exactly once in the document.
499    #[error("transition-pose declaration member witness is structurally contradictory")]
500    InvalidMemberWitness,
501    /// Even the deterministic bounded result summary could not be serialized.
502    #[error("transition-pose result exceeds the V1 result cap")]
503    ResultTooLarge,
504    /// A prepared collection member no longer agrees with its immutable
505    /// declaration witness.
506    #[error("collection transition-pose declaration member witness is structurally contradictory")]
507    InvalidCollectionMemberWitness,
508    /// The supplied collection authority does not exactly match the declaration.
509    #[error("collection transition-pose manifest authority is structurally contradictory")]
510    InvalidCollectionManifestBinding,
511}
512
513/// One already resolved collection member supplied to the format-neutral
514/// collection evaluator.
515///
516/// The collection CLI owns filesystem access and constructs this only after
517/// it has checked the manifest's logical/source/take binding. The core repeats
518/// that witness comparison before sampling so a stale or reordered adapter
519/// input cannot become an apparently valid result.
520#[derive(Clone, Copy)]
521pub struct CollectionTransitionPoseMemberInputV1<'a> {
522    logical_id: &'a crate::CollectionLogicalIdV1,
523    source: &'a crate::CollectionSourceKeyV1,
524    take_index: u64,
525    take_name: &'a str,
526    source_input: Option<&'a InputIdentity>,
527    loaded_source: Option<&'a LoadedSource>,
528    unavailable_reason: Option<TransitionPoseReasonV1>,
529}
530
531impl<'a> CollectionTransitionPoseMemberInputV1<'a> {
532    /// Construct one available member from its exact raw source and decoded
533    /// document.
534    #[must_use]
535    pub const fn available(
536        logical_id: &'a crate::CollectionLogicalIdV1,
537        source: &'a crate::CollectionSourceKeyV1,
538        take_index: u64,
539        take_name: &'a str,
540        loaded_source: &'a LoadedSource,
541    ) -> Self {
542        Self {
543            logical_id,
544            source,
545            take_index,
546            take_name,
547            source_input: None,
548            loaded_source: Some(loaded_source),
549            unavailable_reason: None,
550        }
551    }
552
553    /// Construct one unavailable member. It deliberately retains no invented
554    /// source identity or substitute document.
555    #[must_use]
556    pub const fn unavailable(
557        logical_id: &'a crate::CollectionLogicalIdV1,
558        source: &'a crate::CollectionSourceKeyV1,
559        take_index: u64,
560        take_name: &'a str,
561    ) -> Self {
562        Self {
563            logical_id,
564            source,
565            take_index,
566            take_name,
567            source_input: None,
568            loaded_source: None,
569            unavailable_reason: Some(TransitionPoseReasonV1::MemberUnavailable),
570        }
571    }
572
573    /// Construct an unavailable member whose raw source bytes were read and
574    /// identified but could not provide a usable document/take.
575    #[must_use]
576    pub const fn unavailable_with_source_input(
577        logical_id: &'a crate::CollectionLogicalIdV1,
578        source: &'a crate::CollectionSourceKeyV1,
579        take_index: u64,
580        take_name: &'a str,
581        source_input: &'a InputIdentity,
582    ) -> Self {
583        Self {
584            logical_id,
585            source,
586            take_index,
587            take_name,
588            source_input: Some(source_input),
589            loaded_source: None,
590            unavailable_reason: Some(TransitionPoseReasonV1::MemberUnavailable),
591        }
592    }
593
594    /// Construct a member with readable primary bytes but an incomplete
595    /// dependency closure. The primary identity is retained while the family
596    /// receives the distinct closure-incomplete outcome.
597    #[must_use]
598    pub const fn dependency_closure_incomplete(
599        logical_id: &'a crate::CollectionLogicalIdV1,
600        source: &'a crate::CollectionSourceKeyV1,
601        take_index: u64,
602        take_name: &'a str,
603        loaded_source: &'a LoadedSource,
604    ) -> Self {
605        Self {
606            logical_id,
607            source,
608            take_index,
609            take_name,
610            source_input: None,
611            loaded_source: Some(loaded_source),
612            unavailable_reason: Some(TransitionPoseReasonV1::DependencyClosureIncomplete),
613        }
614    }
615
616    fn primary_input(self) -> Option<&'a InputIdentity> {
617        self.loaded_source
618            .map(|source| source.source_facts().primary_identity())
619            .or(self.source_input)
620    }
621
622    fn dependency_closure(self) -> Option<&'a DependencyClosureV1> {
623        self.loaded_source.map(LoadedSource::dependency_closure)
624    }
625}
626
627/// Evaluate document-local transition families without I/O.
628///
629/// `dependency_closure.primary_input()` is the sole primary-byte authority.
630/// Configured-family evaluation requires both complete closure coverage and
631/// its exact [`DependencyClosureIdentityV1`]; otherwise every family is
632/// retained as `dependency_closure_incomplete`. An empty declaration evaluates
633/// no source data and preserves `no_configured_families` without requiring a
634/// closure identity.
635///
636/// The mutable document's admitted skeleton and selected T/R tracks are
637/// revalidated at this public boundary. All work planning happens before
638/// endpoint sampling; an unavailable family is retained as
639/// `incomplete/not_evaluated`, never evaluated as a survivor subset.
640pub fn evaluate_document_transition_poses_v1(
641    declaration: &TransitionFamilyDeclarationInputV1,
642    dependency_closure: &DependencyClosureV1,
643    document: &Document,
644) -> Result<TransitionPoseEvaluationV1, TransitionPoseEvaluationControlError> {
645    evaluate_document_transition_poses_v1_with_result_limit(
646        declaration,
647        dependency_closure,
648        document,
649        TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES,
650    )
651}
652
653/// Evaluate collection-owned transition families over real resolved members.
654///
655/// The caller must preserve declaration family/member order in `members` and
656/// provide the exact manifest input identity. Filesystem and loader failures
657/// are represented with [`CollectionTransitionPoseMemberInputV1::unavailable`]
658/// and become a whole-family `member_unavailable` result; stale declaration
659/// witnesses remain control errors.
660///
661/// # Errors
662///
663/// Returns a control error for the wrong declaration scope, manifest mismatch,
664/// a stale or contradictory member/source-key witness, or an unrepresentable
665/// bounded result. Invalid collection skeleton authority is instead retained
666/// as a whole-family `member_unavailable` result.
667pub fn evaluate_collection_transition_poses_v1(
668    declaration: &TransitionFamilyDeclarationInputV1,
669    manifest: &TransitionFamilyManifestIdentityV1,
670    members: &[CollectionTransitionPoseMemberInputV1<'_>],
671) -> Result<TransitionPoseEvaluationV1, TransitionPoseEvaluationControlError> {
672    evaluate_collection_transition_poses_v1_with_probes(
673        declaration,
674        manifest,
675        members,
676        |_| {},
677        |_| {},
678    )
679}
680
681fn evaluate_collection_transition_poses_v1_with_probes(
682    declaration: &TransitionFamilyDeclarationInputV1,
683    manifest: &TransitionFamilyManifestIdentityV1,
684    members: &[CollectionTransitionPoseMemberInputV1<'_>],
685    mut observe_basis_build: impl FnMut(&LoadedSource),
686    mut observe_track_visit: impl FnMut(&Track),
687) -> Result<TransitionPoseEvaluationV1, TransitionPoseEvaluationControlError> {
688    let declared_manifest = match declaration.declaration() {
689        crate::TransitionFamilyDeclarationV1::Collection { manifest, .. } => manifest,
690        _ => return Err(TransitionPoseEvaluationControlError::WrongDeclarationScope),
691    };
692    if declared_manifest != manifest {
693        return Err(TransitionPoseEvaluationControlError::InvalidCollectionManifestBinding);
694    }
695    let families = declaration
696        .declaration()
697        .collection_families()
698        .ok_or(TransitionPoseEvaluationControlError::WrongDeclarationScope)?;
699    let expected_members = families.iter().try_fold(0usize, |total, family| {
700        total.checked_add(family.members().len())
701    });
702    let Some(expected_members) = expected_members else {
703        return Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness);
704    };
705    if expected_members != members.len() {
706        return Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness);
707    }
708    let mut result = TransitionPoseEvaluationV1 {
709        schema: TRANSITION_POSE_EVALUATION_V1_ID,
710        schema_version: TRANSITION_POSE_EVALUATION_V1_SCHEMA_VERSION,
711        status: TransitionPoseStatusV1::Complete,
712        decision: TransitionPoseDecisionV1::Pass,
713        reason: None,
714        declaration_input: declaration.source_identity().clone(),
715        declaration_normalized: declaration.normalized_identity().clone(),
716        subject_input: manifest.input().clone(),
717        subject_dependency_closure_identity: None,
718        families: Vec::with_capacity(families.len()),
719    };
720    // Phase one is deliberately only O(total declared members): bind every
721    // witness, establish one coherent authority per source key, resolve the
722    // selected take by index/name, and classify closure/runtime availability.
723    // No skeleton bone or track is visited until every structural witness has
724    // survived this pass.
725    let mut source_authorities = BTreeMap::<&str, CollectionSourceAuthority<'_>>::new();
726    let mut cursor = 0usize;
727    let mut structurally_ready = Vec::<Option<CollectionFamilyReady<'_>>>::new();
728    for (family_index, family) in families.iter().enumerate() {
729        let family_members = &members[cursor..cursor + family.members().len()];
730        cursor += family_members.len();
731        let mut row = collection_family_result_row(family, family_members)?;
732        let mut selected = Vec::with_capacity(family_members.len());
733        for (declared, prepared) in family.members().iter().zip(family_members) {
734            if declared.logical_id() != prepared.logical_id
735                || declared.source() != prepared.source
736                || declared.take_index() != prepared.take_index
737                || declared.take_name() != prepared.take_name
738            {
739                return Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness);
740            }
741            let authority = CollectionSourceAuthority::from_member(*prepared);
742            match source_authorities.entry(declared.source().as_str()) {
743                std::collections::btree_map::Entry::Vacant(entry) => {
744                    entry.insert(authority);
745                }
746                std::collections::btree_map::Entry::Occupied(entry)
747                    if !entry.get().same_authority(authority) =>
748                {
749                    return Err(
750                        TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness,
751                    );
752                }
753                std::collections::btree_map::Entry::Occupied(_) => {}
754            }
755            if let Some(reason) = prepared.unavailable_reason {
756                retain_collection_runtime_reason(&mut row.reason, reason);
757            }
758            let Some(loaded_source) = prepared.loaded_source else {
759                if prepared.unavailable_reason.is_none() {
760                    return Err(
761                        TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness,
762                    );
763                }
764                continue;
765            };
766            let source_input = loaded_source.source_facts().primary_identity();
767            let dependency_closure = loaded_source.dependency_closure();
768            let document = loaded_source.document();
769            let closure_is_complete = dependency_closure.primary_input() == source_input
770                && dependency_closure.coverage().is_complete()
771                && dependency_closure.identity().is_some();
772            if prepared.unavailable_reason
773                == Some(TransitionPoseReasonV1::DependencyClosureIncomplete)
774                && closure_is_complete
775            {
776                return Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness);
777            }
778            if !closure_is_complete {
779                retain_collection_runtime_reason(
780                    &mut row.reason,
781                    TransitionPoseReasonV1::DependencyClosureIncomplete,
782                );
783            }
784            let Some(clip) = usize::try_from(declared.take_index())
785                .ok()
786                .and_then(|index| document.clips.get(index))
787                .filter(|clip| clip.name == declared.take_name())
788            else {
789                retain_collection_runtime_reason(
790                    &mut row.reason,
791                    TransitionPoseReasonV1::MemberUnavailable,
792                );
793                continue;
794            };
795            if prepared.unavailable_reason.is_none() && closure_is_complete {
796                selected.push(CollectionResolvedMember {
797                    source: declared.source(),
798                    take_index: declared.take_index(),
799                    loaded_source,
800                    document,
801                    clip,
802                });
803            }
804        }
805        let row_index = result.families.len();
806        result.families.push(row);
807        if selected.len() == family_members.len() {
808            structurally_ready.push(Some(CollectionFamilyReady {
809                family_index,
810                row_index,
811                family,
812                members: selected,
813                basis_cache_index: None,
814            }));
815        } else {
816            structurally_ready.push(None);
817        }
818    }
819
820    // Phase two uses only O(1) family/member counts and skeleton lengths.
821    // Policy and comparison-work refusals therefore cannot trigger an O(S)
822    // basis build or an O(T) selected-track scan.
823    let policy_rejected = families
824        .iter()
825        .map(|family| family.tolerances().time_normalized() != 0.0)
826        .collect::<Vec<_>>();
827    let plans = plan_collection_families(families, &structurally_ready, &policy_rejected);
828    for ready in structurally_ready.iter().flatten() {
829        let row = &mut result.families[ready.row_index];
830        if policy_rejected[ready.family_index] {
831            row.reason = Some(TransitionPoseReasonV1::TimeToleranceUnsupported);
832        } else if let Some(reason) = plans[ready.family_index] {
833            row.reason = Some(reason);
834        }
835    }
836
837    // Phase three builds each admitted source-key basis at most once. The
838    // authority pass above has already proved that a key cannot alias distinct
839    // LoadedSource owners or disagree about its runtime state.
840    let mut basis_by_source = BTreeMap::<&str, usize>::new();
841    let mut basis_cache = Vec::<CollectionCachedBasis>::new();
842    for ready in structurally_ready.iter_mut().flatten() {
843        if result.families[ready.row_index].reason.is_some() {
844            continue;
845        }
846        let mut family_basis_index: Option<usize> = None;
847        for member in &ready.members {
848            let source_key = member.source.as_str();
849            let cache_index = if let Some(&index) = basis_by_source.get(&source_key) {
850                index
851            } else {
852                observe_basis_build(member.loaded_source);
853                let cached = CollectionCachedBasis::build(&member.document.skeleton);
854                let index = basis_cache.len();
855                basis_cache.push(cached);
856                basis_by_source.insert(source_key, index);
857                index
858            };
859            let Some(current) = basis_cache[cache_index].basis.as_ref() else {
860                retain_collection_runtime_reason(
861                    &mut result.families[ready.row_index].reason,
862                    TransitionPoseReasonV1::MemberUnavailable,
863                );
864                continue;
865            };
866            if let Some(existing_index) = family_basis_index {
867                let existing = basis_cache[existing_index]
868                    .basis
869                    .as_ref()
870                    .expect("validated cached collection basis");
871                if existing.identity() != current.identity() {
872                    retain_collection_runtime_reason(
873                        &mut result.families[ready.row_index].reason,
874                        TransitionPoseReasonV1::SkeletonBasisMismatch,
875                    );
876                }
877            } else {
878                family_basis_index = Some(cache_index);
879            }
880        }
881        if result.families[ready.row_index].reason.is_none() {
882            let basis_index = family_basis_index.expect("collection families have members");
883            ready.basis_cache_index = Some(basis_index);
884            result.families[ready.row_index].skeleton_basis_input = Some(
885                basis_cache[basis_index]
886                    .basis
887                    .as_ref()
888                    .expect("validated cached collection basis")
889                    .identity()
890                    .clone(),
891            );
892        }
893    }
894
895    // Result-detail reservation still precedes all selected-track traversal.
896    // Its skeleton-name statistic is retained beside the cached basis, so a
897    // repeated family does not rescan the source skeleton merely to budget.
898    let mut detailed_name_budget = collection_detailed_budget_after_base(&result)?;
899    for ready_family in structurally_ready.iter().flatten() {
900        let row = &mut result.families[ready_family.row_index];
901        let Some(basis_index) = ready_family.basis_cache_index else {
902            continue;
903        };
904        if !reserve_collection_detailed_name_budget(
905            &mut detailed_name_budget,
906            ready_family.family,
907            &basis_cache[basis_index],
908        ) {
909            row.reason = Some(TransitionPoseReasonV1::ResultLimit);
910        }
911    }
912
913    // Phase four admits each unique selected source/take once. Families
914    // rejected by structure, policy, comparison work, basis, or result budget
915    // never visit a track. Cached admission is sound because a LoadedSource is
916    // immutable and the take index/name was already bound in phase one.
917    let mut track_cache = BTreeMap::<(&str, u64), CollectionTrackAdmission>::new();
918    let mut aggregate_track_elements = 0usize;
919    for ready_family in structurally_ready.iter().flatten() {
920        let row = &mut result.families[ready_family.row_index];
921        if row.reason.is_some() {
922            continue;
923        }
924        for member in &ready_family.members {
925            let key = (member.source.as_str(), member.take_index);
926            let admission = if let Some(cached) = track_cache.get(&key) {
927                *cached
928            } else {
929                let inspected = inspect_collection_track_admission(
930                    member.clip,
931                    member.document.skeleton.bones.len(),
932                    &mut observe_track_visit,
933                );
934                let admission = match inspected {
935                    Ok(elements) => {
936                        let total = aggregate_track_elements.checked_add(elements);
937                        if total.is_some_and(|total| {
938                            total <= TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACK_ELEMENTS
939                        }) {
940                            aggregate_track_elements = total.expect("checked aggregate");
941                            CollectionTrackAdmission::Accepted
942                        } else {
943                            CollectionTrackAdmission::Rejected(TransitionPoseReasonV1::InputLimit)
944                        }
945                    }
946                    Err(reason) => CollectionTrackAdmission::Rejected(reason),
947                };
948                track_cache.insert(key, admission);
949                admission
950            };
951            if let CollectionTrackAdmission::Rejected(reason) = admission {
952                row.reason = Some(reason);
953                break;
954            }
955        }
956        if row.reason.is_none() {
957            let endpoints = match strict_collection_endpoints(
958                &ready_family.members,
959                ready_family.family.boundary(),
960            ) {
961                Ok(value) => value,
962                Err(reason) => {
963                    row.reason = Some(reason);
964                    continue;
965                }
966            };
967            row.pairs = match compare_pairs(
968                &endpoints,
969                ready_family.family.boundary(),
970                ready_family.family.tolerances(),
971                &ready_family.members[0].document.skeleton,
972            ) {
973                Ok(pairs) => pairs,
974                Err(reason) => {
975                    row.reason = Some(reason);
976                    continue;
977                }
978            };
979            row.status = TransitionPoseStatusV1::Complete;
980            row.decision = if row.pairs.iter().any(pair_has_finding) {
981                TransitionPoseDecisionV1::Finding
982            } else {
983                TransitionPoseDecisionV1::Pass
984            };
985        }
986    }
987    derive_result_state(&mut result);
988    enforce_result_limit(&mut result, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)?;
989    Ok(result)
990}
991
992/// Collection source gaps are scanned in declared member order but their
993/// family outcome is deliberately not order-dependent. Closure incompleteness
994/// has the highest runtime-authority priority because it means sampled bytes
995/// may differ despite an unchanged primary; a missing/unusable source follows,
996/// then a cross-member skeleton disagreement.
997fn retain_collection_runtime_reason(
998    current: &mut Option<TransitionPoseReasonV1>,
999    candidate: TransitionPoseReasonV1,
1000) {
1001    fn priority(reason: TransitionPoseReasonV1) -> u8 {
1002        match reason {
1003            TransitionPoseReasonV1::DependencyClosureIncomplete => 3,
1004            TransitionPoseReasonV1::MemberUnavailable => 2,
1005            TransitionPoseReasonV1::SkeletonBasisMismatch => 1,
1006            _ => 0,
1007        }
1008    }
1009    if current.is_none_or(|previous| priority(candidate) > priority(previous)) {
1010        *current = Some(candidate);
1011    }
1012}
1013
1014struct CollectionFamilyReady<'a> {
1015    family_index: usize,
1016    row_index: usize,
1017    family: &'a crate::CollectionTransitionFamilyV1,
1018    members: Vec<CollectionResolvedMember<'a>>,
1019    basis_cache_index: Option<usize>,
1020}
1021
1022#[derive(Clone, Copy)]
1023struct CollectionResolvedMember<'a> {
1024    source: &'a crate::CollectionSourceKeyV1,
1025    take_index: u64,
1026    loaded_source: &'a LoadedSource,
1027    document: &'a Document,
1028    clip: &'a Clip,
1029}
1030
1031#[derive(Clone, Copy)]
1032enum CollectionSourceAuthority<'a> {
1033    Unavailable {
1034        primary: Option<&'a InputIdentity>,
1035        reason: TransitionPoseReasonV1,
1036    },
1037    Loaded {
1038        source: &'a LoadedSource,
1039        reason: Option<TransitionPoseReasonV1>,
1040    },
1041}
1042
1043impl<'a> CollectionSourceAuthority<'a> {
1044    fn from_member(member: CollectionTransitionPoseMemberInputV1<'a>) -> Self {
1045        if let Some(loaded) = member.loaded_source {
1046            Self::Loaded {
1047                source: loaded,
1048                reason: member.unavailable_reason,
1049            }
1050        } else {
1051            Self::Unavailable {
1052                primary: member.source_input,
1053                reason: member
1054                    .unavailable_reason
1055                    .expect("collection input without a loaded source is unavailable"),
1056            }
1057        }
1058    }
1059
1060    fn same_authority(self, other: Self) -> bool {
1061        match (self, other) {
1062            (
1063                Self::Unavailable {
1064                    primary: left_primary,
1065                    reason: left_reason,
1066                },
1067                Self::Unavailable {
1068                    primary: right_primary,
1069                    reason: right_reason,
1070                },
1071            ) => left_primary == right_primary && left_reason == right_reason,
1072            (
1073                Self::Loaded {
1074                    source: left_source,
1075                    reason: left_reason,
1076                },
1077                Self::Loaded {
1078                    source: right_source,
1079                    reason: right_reason,
1080                },
1081            ) => std::ptr::eq(left_source, right_source) && left_reason == right_reason,
1082            _ => false,
1083        }
1084    }
1085}
1086
1087struct CollectionCachedBasis {
1088    basis: Option<SkeletonBasisV1>,
1089    max_escaped_name_bytes: usize,
1090}
1091
1092impl CollectionCachedBasis {
1093    fn build(skeleton: &Skeleton) -> Self {
1094        let Ok(basis) = SkeletonBasisV1::from_skeleton(skeleton) else {
1095            return Self {
1096                basis: None,
1097                max_escaped_name_bytes: 0,
1098            };
1099        };
1100        let max_escaped_name_bytes = basis
1101            .bones()
1102            .iter()
1103            .map(|bone| bone.name().len().saturating_mul(6))
1104            .max()
1105            .unwrap_or(0);
1106        Self {
1107            basis: Some(basis),
1108            max_escaped_name_bytes,
1109        }
1110    }
1111}
1112
1113#[derive(Clone, Copy)]
1114enum CollectionTrackAdmission {
1115    Accepted,
1116    Rejected(TransitionPoseReasonV1),
1117}
1118
1119fn collection_family_result_row(
1120    family: &crate::CollectionTransitionFamilyV1,
1121    members: &[CollectionTransitionPoseMemberInputV1<'_>],
1122) -> Result<TransitionPoseFamilyEvaluationV1, TransitionPoseEvaluationControlError> {
1123    if family.members().len() != members.len() {
1124        return Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness);
1125    }
1126    Ok(TransitionPoseFamilyEvaluationV1 {
1127        family_id: family.family_id().as_str().to_owned(),
1128        status: TransitionPoseStatusV1::Incomplete,
1129        decision: TransitionPoseDecisionV1::NotEvaluated,
1130        reason: None,
1131        members: family
1132            .members()
1133            .iter()
1134            .zip(members)
1135            .map(|(declared, prepared)| TransitionPoseMemberV1 {
1136                take_index: declared.take_index(),
1137                take_name: declared.take_name().to_owned(),
1138                source_input: prepared.primary_input().cloned(),
1139                source_dependency_closure_identity: prepared
1140                    .dependency_closure()
1141                    .and_then(complete_dependency_closure_identity),
1142            })
1143            .collect(),
1144        skeleton_basis_input: None,
1145        pairs: Vec::new(),
1146    })
1147}
1148
1149fn plan_collection_families(
1150    families: &[crate::CollectionTransitionFamilyV1],
1151    ready: &[Option<CollectionFamilyReady<'_>>],
1152    policy_rejected: &[bool],
1153) -> Vec<Option<TransitionPoseReasonV1>> {
1154    let mut aggregate_pairs = 0usize;
1155    let mut aggregate_comparisons = 0usize;
1156    let mut aggregate_retention = 0usize;
1157    families
1158        .iter()
1159        .enumerate()
1160        .map(|(index, family)| {
1161            let ready = ready[index].as_ref()?;
1162            if policy_rejected[index] {
1163                return None;
1164            }
1165            let boundaries = match family.boundary() {
1166                TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Exit => 1usize,
1167                TransitionFamilyBoundaryV1::Both => 2usize,
1168            };
1169            let pair_boundaries = checked_pair_count(family.members().len())
1170                .and_then(|pairs| pairs.checked_mul(boundaries));
1171            let bones = ready.members[0].document.skeleton.bones.len();
1172            let comparisons = pair_boundaries.and_then(|pairs| pairs.checked_mul(bones));
1173            let retention = pair_boundaries.and_then(|pairs| {
1174                bones
1175                    .min(TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS)
1176                    .checked_add(bones.min(TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS))
1177                    .and_then(|per_pair| pairs.checked_mul(per_pair))
1178            });
1179            match (pair_boundaries, comparisons, retention) {
1180                (Some(pairs), _, _)
1181                    if pairs > TRANSITION_POSE_EVALUATION_V1_MAX_FAMILY_PAIR_BOUNDARIES =>
1182                {
1183                    Some(TransitionPoseReasonV1::FamilyWorkLimit)
1184                }
1185                (Some(pairs), Some(comparisons), Some(_))
1186                    if aggregate_pairs.checked_add(pairs).is_none_or(|value| {
1187                        value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_PAIR_BOUNDARIES
1188                    }) || aggregate_comparisons.checked_add(comparisons).is_none_or(
1189                        |value| value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_COMPARISONS,
1190                    ) =>
1191                {
1192                    Some(TransitionPoseReasonV1::AggregateWorkLimit)
1193                }
1194                (Some(_), Some(_), Some(retention))
1195                    if aggregate_retention
1196                        .checked_add(retention)
1197                        .is_none_or(|value| {
1198                            value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_OFFENDERS
1199                        }) =>
1200                {
1201                    Some(TransitionPoseReasonV1::RetentionLimit)
1202                }
1203                (Some(pairs), Some(comparisons), Some(retention)) => {
1204                    aggregate_pairs += pairs;
1205                    aggregate_comparisons += comparisons;
1206                    aggregate_retention += retention;
1207                    None
1208                }
1209                _ => Some(TransitionPoseReasonV1::AggregateWorkLimit),
1210            }
1211        })
1212        .collect()
1213}
1214
1215fn collection_detailed_budget_after_base(
1216    result: &TransitionPoseEvaluationV1,
1217) -> Result<usize, TransitionPoseEvaluationControlError> {
1218    let mut base = result.clone();
1219    base.status = TransitionPoseStatusV1::Incomplete;
1220    base.decision = TransitionPoseDecisionV1::NotEvaluated;
1221    for family in &mut base.families {
1222        // Keep an already-real incomplete reason: replacing
1223        // dependency_closure_incomplete with a shorter spelling would make
1224        // detail admission optimistic. Ready rows have no reason yet, so use
1225        // the longest closed V1 incomplete spelling as their pair-free bound.
1226        if family.reason.is_none() {
1227            family.reason = Some(TransitionPoseReasonV1::DependencyClosureIncomplete);
1228        }
1229        family.pairs.clear();
1230    }
1231    let bytes = canonical_bytes(&base, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)
1232        .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)?
1233        .len();
1234    // A null member source is truthful only for an unavailable source. Reserve
1235    // the larger possible identity spelling nevertheless, so a future source
1236    // becoming available cannot make detail admission depend on that absence.
1237    let absent_source_reserve = base
1238        .families
1239        .iter()
1240        .flat_map(|family| family.members.iter())
1241        .filter(|member| member.source_input.is_none())
1242        .count()
1243        .checked_mul(max_member_input_identity_replacement_bytes()?)
1244        .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)?;
1245    let absent_closure_reserve = base
1246        .families
1247        .iter()
1248        .flat_map(|family| family.members.iter())
1249        .filter(|member| member.source_dependency_closure_identity.is_none())
1250        .count()
1251        .checked_mul(max_member_closure_identity_addition_bytes()?)
1252        .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)?;
1253    TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES
1254        .checked_sub(bytes)
1255        .and_then(|remaining| remaining.checked_sub(absent_source_reserve))
1256        .and_then(|remaining| remaining.checked_sub(absent_closure_reserve))
1257        .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)
1258}
1259
1260/// Measure, rather than hand-wave, the only possible expansion from the
1261/// truthful unavailable `null` spelling to an `InputIdentity`. The digest is
1262/// always 32 bytes and the byte count is a `u64`, so all real identities fit
1263/// this canonical worst-case record. Field punctuation is already present in
1264/// the pair-free base; only the value spelling can grow.
1265fn max_member_input_identity_replacement_bytes()
1266-> Result<usize, TransitionPoseEvaluationControlError> {
1267    let identity = InputIdentity::from_sha256_digest([0xff; 32], u64::MAX);
1268    let identity_bytes = canonical_bytes(&identity, 256)
1269        .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)?
1270        .len();
1271    identity_bytes
1272        .checked_sub(b"null".len())
1273        .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)
1274}
1275
1276/// Reserve the omitted member closure field itself plus the largest possible
1277/// transparent closure identity. `DependencyClosureIdentityV1` serializes as
1278/// its `InputIdentity`, so this is a measured upper bound rather than a
1279/// guessed record size.
1280fn max_member_closure_identity_addition_bytes()
1281-> Result<usize, TransitionPoseEvaluationControlError> {
1282    let identity = InputIdentity::from_sha256_digest([0xff; 32], u64::MAX);
1283    let value = canonical_bytes(&identity, 256)
1284        .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)?
1285        .len();
1286    b",\"source_dependency_closure_identity\":"
1287        .len()
1288        .checked_add(value)
1289        .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)
1290}
1291
1292fn reserve_collection_detailed_name_budget(
1293    remaining: &mut usize,
1294    family: &crate::CollectionTransitionFamilyV1,
1295    cached: &CollectionCachedBasis,
1296) -> bool {
1297    let Some(basis) = cached.basis.as_ref() else {
1298        return false;
1299    };
1300    let boundaries = match family.boundary() {
1301        TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Exit => 1usize,
1302        TransitionFamilyBoundaryV1::Both => 2usize,
1303    };
1304    let Some(pair_boundaries) =
1305        checked_pair_count(family.members().len()).and_then(|pairs| pairs.checked_mul(boundaries))
1306    else {
1307        return false;
1308    };
1309    let Some(rows) = basis
1310        .bones()
1311        .len()
1312        .min(TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS)
1313        .checked_add(
1314            basis
1315                .bones()
1316                .len()
1317                .min(TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS),
1318        )
1319    else {
1320        return false;
1321    };
1322    let Some(bytes) = pair_boundaries
1323        .checked_mul(MAX_DETAILED_PAIR_FIXED_BYTES)
1324        .and_then(|fixed| {
1325            pair_boundaries
1326                .checked_mul(rows)?
1327                .checked_mul(cached.max_escaped_name_bytes)
1328                .and_then(|names| fixed.checked_add(names))
1329        })
1330    else {
1331        return false;
1332    };
1333    if bytes > *remaining {
1334        return false;
1335    }
1336    *remaining -= bytes;
1337    true
1338}
1339
1340fn strict_collection_endpoints(
1341    members: &[CollectionResolvedMember<'_>],
1342    boundary: TransitionFamilyBoundaryV1,
1343) -> Result<Vec<Endpoints>, TransitionPoseReasonV1> {
1344    members
1345        .iter()
1346        .map(|member| {
1347            let document = member.document;
1348            let clip = member.clip;
1349            if clip.duration_s == 0.0 {
1350                return Err(TransitionPoseReasonV1::ZeroDuration);
1351            }
1352            if !clip.duration_s.is_finite() || clip.duration_s < 0.0 {
1353                return Err(TransitionPoseReasonV1::UnsupportedSampling);
1354            }
1355            let entry = matches!(
1356                boundary,
1357                TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Both
1358            )
1359            .then(|| strict_endpoint(&document.skeleton, clip, 0.0))
1360            .transpose()?;
1361            let exit = matches!(
1362                boundary,
1363                TransitionFamilyBoundaryV1::Exit | TransitionFamilyBoundaryV1::Both
1364            )
1365            .then(|| {
1366                let time = clip.duration_s as f32;
1367                if !time.is_finite() || f64::from(time) != clip.duration_s {
1368                    return Err(TransitionPoseReasonV1::UnsupportedSampling);
1369                }
1370                strict_endpoint(&document.skeleton, clip, time)
1371            })
1372            .transpose()?;
1373            Ok(Endpoints { entry, exit })
1374        })
1375        .collect()
1376}
1377
1378fn inspect_collection_track_admission(
1379    clip: &Clip,
1380    bone_count: usize,
1381    observe_track_visit: &mut impl FnMut(&Track),
1382) -> Result<usize, TransitionPoseReasonV1> {
1383    let (Some(raw_track_limit), Some(selected_track_limit)) =
1384        (bone_count.checked_mul(3), bone_count.checked_mul(2))
1385    else {
1386        return Err(TransitionPoseReasonV1::InputLimit);
1387    };
1388    if clip.tracks.len() > raw_track_limit {
1389        return Err(TransitionPoseReasonV1::InputLimit);
1390    }
1391    let mut selected_tracks = 0usize;
1392    let mut selected_elements = 0usize;
1393    let mut seen = vec![false; selected_track_limit];
1394    let mut unsupported = false;
1395    for track in &clip.tracks {
1396        observe_track_visit(track);
1397        let Some(channel) = transition_pose_channel(track.property) else {
1398            continue;
1399        };
1400        selected_tracks = selected_tracks
1401            .checked_add(1)
1402            .ok_or(TransitionPoseReasonV1::InputLimit)?;
1403        if selected_tracks > selected_track_limit {
1404            return Err(TransitionPoseReasonV1::InputLimit);
1405        }
1406        let elements = track
1407            .times
1408            .len()
1409            .checked_add(track.values.len())
1410            .ok_or(TransitionPoseReasonV1::InputLimit)?;
1411        selected_elements = selected_elements
1412            .checked_add(elements)
1413            .ok_or(TransitionPoseReasonV1::InputLimit)?;
1414        if selected_elements > TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACK_ELEMENTS {
1415            return Err(TransitionPoseReasonV1::InputLimit);
1416        }
1417        let target = track
1418            .bone
1419            .checked_mul(2)
1420            .and_then(|base| base.checked_add(channel));
1421        match target {
1422            Some(target)
1423                if target < seen.len()
1424                    && !seen[target]
1425                    && validate_track_shape(0, track).is_ok() =>
1426            {
1427                seen[target] = true;
1428            }
1429            _ => unsupported = true,
1430        }
1431    }
1432    if unsupported {
1433        Err(TransitionPoseReasonV1::UnsupportedSampling)
1434    } else {
1435        Ok(selected_elements)
1436    }
1437}
1438
1439fn evaluate_document_transition_poses_v1_with_result_limit(
1440    declaration: &TransitionFamilyDeclarationInputV1,
1441    dependency_closure: &DependencyClosureV1,
1442    document: &Document,
1443    result_limit: usize,
1444) -> Result<TransitionPoseEvaluationV1, TransitionPoseEvaluationControlError> {
1445    let families = declaration
1446        .declaration()
1447        .document_families()
1448        .ok_or(TransitionPoseEvaluationControlError::WrongDeclarationScope)?;
1449    let mut result = TransitionPoseEvaluationV1 {
1450        schema: TRANSITION_POSE_EVALUATION_V1_ID,
1451        schema_version: TRANSITION_POSE_EVALUATION_V1_SCHEMA_VERSION,
1452        status: TransitionPoseStatusV1::Complete,
1453        decision: TransitionPoseDecisionV1::Pass,
1454        reason: None,
1455        declaration_input: declaration.source_identity().clone(),
1456        declaration_normalized: declaration.normalized_identity().clone(),
1457        subject_input: dependency_closure.primary_input().clone(),
1458        subject_dependency_closure_identity: complete_dependency_closure_identity(
1459            dependency_closure,
1460        ),
1461        families: Vec::with_capacity(families.len()),
1462    };
1463    if families.is_empty() {
1464        result.reason = Some(TransitionPoseReasonV1::NoConfiguredFamilies);
1465        return Ok(result);
1466    }
1467    // A declaration witness is a structural authority, not an evaluation
1468    // policy. Resolve every family before any tolerance or work-cap outcome
1469    // can produce a retained incomplete row.
1470    let clip_admission = resolve_document_family_clips(document, families)?;
1471    if result.subject_dependency_closure_identity.is_none() {
1472        for family in families {
1473            let mut row = family_result_row(family, dependency_closure, None);
1474            row.reason = Some(TransitionPoseReasonV1::DependencyClosureIncomplete);
1475            result.families.push(row);
1476        }
1477        derive_result_state(&mut result);
1478        canonical_bytes(&result, result_limit)
1479            .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)?;
1480        return Ok(result);
1481    }
1482    let resolved_clips = match clip_admission {
1483        DocumentClipAdmission::InputLimit => {
1484            for family in families {
1485                let mut row = family_result_row(family, dependency_closure, None);
1486                row.reason = Some(TransitionPoseReasonV1::InputLimit);
1487                result.families.push(row);
1488            }
1489            derive_result_state(&mut result);
1490            return Ok(result);
1491        }
1492        DocumentClipAdmission::Resolved(clips) => clips,
1493    };
1494    if !skeleton_input_is_within_limits(&document.skeleton) {
1495        for family in families {
1496            let mut row = family_result_row(family, dependency_closure, None);
1497            row.reason = Some(TransitionPoseReasonV1::InputLimit);
1498            result.families.push(row);
1499        }
1500        derive_result_state(&mut result);
1501        return Ok(result);
1502    }
1503    validate_transition_pose_skeleton(&document.skeleton)
1504        .map_err(TransitionPoseEvaluationControlError::InvalidSkeletonBasis)?;
1505    let basis = SkeletonBasisV1::from_skeleton(&document.skeleton)
1506        .map_err(TransitionPoseEvaluationControlError::InvalidSkeletonBasis)?;
1507    let policy_rejected = families
1508        .iter()
1509        .map(|family| family.tolerances().time_normalized() != 0.0)
1510        .collect::<Vec<_>>();
1511    let track_limits =
1512        plan_selected_track_input_limits(&resolved_clips, basis.bones().len(), &policy_rejected);
1513    let plans = plan_families(families, basis.bones().len(), &track_limits);
1514    let mut detailed_name_budget = detailed_result_budget_after_base(
1515        &result,
1516        families,
1517        dependency_closure,
1518        basis.identity(),
1519        result_limit,
1520    )?;
1521    for (((family, plan), clips), track_limit) in families
1522        .iter()
1523        .zip(plans)
1524        .zip(resolved_clips)
1525        .zip(track_limits)
1526    {
1527        let mut row = family_result_row(family, dependency_closure, Some(basis.identity()));
1528        if family.tolerances().time_normalized() != 0.0 {
1529            row.reason = Some(TransitionPoseReasonV1::TimeToleranceUnsupported);
1530        } else if let Some(reason) = plan.reason {
1531            row.reason = Some(reason);
1532        } else if track_limit {
1533            row.reason = Some(TransitionPoseReasonV1::InputLimit);
1534        } else if !reserve_detailed_name_budget(
1535            &mut detailed_name_budget,
1536            family,
1537            &document.skeleton,
1538        ) {
1539            row.reason = Some(TransitionPoseReasonV1::ResultLimit);
1540        } else {
1541            let endpoints = match strict_endpoints(document, &clips, family.boundary()) {
1542                Ok(value) => value,
1543                Err(reason) => {
1544                    row.reason = Some(reason);
1545                    result.families.push(row);
1546                    continue;
1547                }
1548            };
1549            row.pairs = match compare_pairs(
1550                &endpoints,
1551                family.boundary(),
1552                family.tolerances(),
1553                &document.skeleton,
1554            ) {
1555                Ok(pairs) => pairs,
1556                Err(reason) => {
1557                    row.reason = Some(reason);
1558                    result.families.push(row);
1559                    continue;
1560                }
1561            };
1562            row.status = TransitionPoseStatusV1::Complete;
1563            row.decision = if row.pairs.iter().any(pair_has_finding) {
1564                TransitionPoseDecisionV1::Finding
1565            } else {
1566                TransitionPoseDecisionV1::Pass
1567            };
1568        }
1569        result.families.push(row);
1570    }
1571    derive_result_state(&mut result);
1572    enforce_result_limit(&mut result, result_limit)?;
1573    Ok(result)
1574}
1575
1576fn family_result_row(
1577    family: &crate::DocumentTransitionFamilyV1,
1578    dependency_closure: &DependencyClosureV1,
1579    skeleton_basis_input: Option<&InputIdentity>,
1580) -> TransitionPoseFamilyEvaluationV1 {
1581    TransitionPoseFamilyEvaluationV1 {
1582        family_id: family.family_id().to_owned(),
1583        status: TransitionPoseStatusV1::Incomplete,
1584        decision: TransitionPoseDecisionV1::NotEvaluated,
1585        reason: None,
1586        members: family
1587            .members()
1588            .iter()
1589            .map(|member| TransitionPoseMemberV1 {
1590                take_index: member.take_index(),
1591                take_name: member.take_name().to_owned(),
1592                source_input: Some(dependency_closure.primary_input().clone()),
1593                source_dependency_closure_identity: complete_dependency_closure_identity(
1594                    dependency_closure,
1595                ),
1596            })
1597            .collect(),
1598        skeleton_basis_input: skeleton_basis_input.cloned(),
1599        pairs: Vec::new(),
1600    }
1601}
1602
1603fn complete_dependency_closure_identity(
1604    dependency_closure: &DependencyClosureV1,
1605) -> Option<DependencyClosureIdentityV1> {
1606    if !dependency_closure.coverage().is_complete() {
1607        return None;
1608    }
1609    dependency_closure.identity().cloned()
1610}
1611
1612/// Keep the immutable binding/member rows when detailed comparisons exceed a
1613/// bounded result envelope. The retry has one strictly smaller representation,
1614/// so it cannot oscillate.
1615fn enforce_result_limit(
1616    result: &mut TransitionPoseEvaluationV1,
1617    limit: usize,
1618) -> Result<(), TransitionPoseEvaluationControlError> {
1619    if canonical_bytes(result, limit).is_ok() {
1620        return Ok(());
1621    }
1622    for family in &mut result.families {
1623        family.status = TransitionPoseStatusV1::Incomplete;
1624        family.decision = TransitionPoseDecisionV1::NotEvaluated;
1625        family.reason = Some(TransitionPoseReasonV1::ResultLimit);
1626        family.pairs.clear();
1627    }
1628    derive_result_state(result);
1629    canonical_bytes(result, limit)
1630        .map(|_| ())
1631        .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)
1632}
1633
1634#[derive(Clone, Copy)]
1635struct FamilyPlan {
1636    reason: Option<TransitionPoseReasonV1>,
1637}
1638
1639fn plan_families(
1640    families: &[crate::DocumentTransitionFamilyV1],
1641    bone_count: usize,
1642    input_limited: &[bool],
1643) -> Vec<FamilyPlan> {
1644    let mut aggregate_pairs = 0usize;
1645    let mut aggregate_comparisons = 0usize;
1646    let mut aggregate_retention = 0usize;
1647    families
1648        .iter()
1649        .zip(input_limited.iter().copied())
1650        .map(|(family, input_limited)| {
1651            if input_limited || family.tolerances().time_normalized() != 0.0 {
1652                return FamilyPlan { reason: None };
1653            }
1654            let boundaries = match family.boundary() {
1655                TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Exit => 1usize,
1656                TransitionFamilyBoundaryV1::Both => 2usize,
1657            };
1658            let pairs = checked_pair_count(family.members().len());
1659            let pair_boundaries = pairs.and_then(|value| value.checked_mul(boundaries));
1660            let comparisons = pair_boundaries.and_then(|value| value.checked_mul(bone_count));
1661            let retained_per_pair_boundary = bone_count
1662                .min(TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS)
1663                .checked_add(bone_count.min(TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS));
1664            let retention = pair_boundaries.and_then(|value| {
1665                retained_per_pair_boundary.and_then(|cap| value.checked_mul(cap))
1666            });
1667            let reason = match (pair_boundaries, comparisons, retention) {
1668                (Some(pair_boundaries), _, _)
1669                    if pair_boundaries
1670                        > TRANSITION_POSE_EVALUATION_V1_MAX_FAMILY_PAIR_BOUNDARIES =>
1671                {
1672                    Some(TransitionPoseReasonV1::FamilyWorkLimit)
1673                }
1674                (Some(pair_boundaries), Some(comparisons), Some(_retention))
1675                    if aggregate_pairs
1676                        .checked_add(pair_boundaries)
1677                        .is_none_or(|value| {
1678                            value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_PAIR_BOUNDARIES
1679                        })
1680                        || aggregate_comparisons
1681                            .checked_add(comparisons)
1682                            .is_none_or(|value| {
1683                                value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_COMPARISONS
1684                            }) =>
1685                {
1686                    Some(TransitionPoseReasonV1::AggregateWorkLimit)
1687                }
1688                (Some(_pair_boundaries), Some(_comparisons), Some(retention))
1689                    if aggregate_retention
1690                        .checked_add(retention)
1691                        .is_none_or(|value| {
1692                            value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_OFFENDERS
1693                        }) =>
1694                {
1695                    Some(TransitionPoseReasonV1::RetentionLimit)
1696                }
1697                (Some(pair_boundaries), Some(comparisons), Some(retention)) => {
1698                    aggregate_pairs += pair_boundaries;
1699                    aggregate_comparisons += comparisons;
1700                    aggregate_retention += retention;
1701                    None
1702                }
1703                _ => Some(TransitionPoseReasonV1::AggregateWorkLimit),
1704            };
1705            FamilyPlan { reason }
1706        })
1707        .collect()
1708}
1709
1710fn checked_pair_count(member_count: usize) -> Option<usize> {
1711    member_count
1712        .checked_mul(member_count.checked_sub(1)?)
1713        .and_then(|value| value.checked_div(2))
1714}
1715
1716enum DocumentClipAdmission<'a> {
1717    InputLimit,
1718    Resolved(Vec<Vec<&'a Clip>>),
1719}
1720
1721/// Resolve all declaration witnesses with one bounded document-name pass.
1722/// Direct index/name contradictions are checked before the document clip cap;
1723/// global duplicate-name proof is intentionally only attempted in the
1724/// admitted domain.
1725fn resolve_document_family_clips<'a>(
1726    document: &'a Document,
1727    families: &[crate::DocumentTransitionFamilyV1],
1728) -> Result<DocumentClipAdmission<'a>, TransitionPoseEvaluationControlError> {
1729    let mut names = BTreeMap::<&str, usize>::new();
1730    for family in families {
1731        for member in family.members() {
1732            let index = usize::try_from(member.take_index())
1733                .map_err(|_| TransitionPoseEvaluationControlError::InvalidMemberWitness)?;
1734            document
1735                .clips
1736                .get(index)
1737                .filter(|clip| clip.name == member.take_name())
1738                .ok_or(TransitionPoseEvaluationControlError::InvalidMemberWitness)?;
1739            names.entry(member.take_name()).or_insert(0);
1740        }
1741    }
1742    if document.clips.len() > TRANSITION_POSE_EVALUATION_V1_MAX_DOCUMENT_CLIPS {
1743        return Ok(DocumentClipAdmission::InputLimit);
1744    }
1745    for clip in &document.clips {
1746        if let Some(count) = names.get_mut(clip.name.as_str()) {
1747            *count = count.saturating_add(1);
1748        }
1749    }
1750    if names.values().any(|&count| count != 1) {
1751        return Err(TransitionPoseEvaluationControlError::InvalidMemberWitness);
1752    }
1753    families
1754        .iter()
1755        .map(|family| {
1756            family
1757                .members()
1758                .iter()
1759                .map(|member| {
1760                    let index = usize::try_from(member.take_index())
1761                        .map_err(|_| TransitionPoseEvaluationControlError::InvalidMemberWitness)?;
1762                    document
1763                        .clips
1764                        .get(index)
1765                        .ok_or(TransitionPoseEvaluationControlError::InvalidMemberWitness)
1766                })
1767                .collect()
1768        })
1769        .collect::<Result<Vec<Vec<_>>, _>>()
1770        .map(DocumentClipAdmission::Resolved)
1771}
1772
1773/// Bound skeleton text before `SkeletonBasisV1` clones a single bone name.
1774fn skeleton_input_is_within_limits(skeleton: &Skeleton) -> bool {
1775    skeleton_input_is_within_limits_with(
1776        skeleton,
1777        TRANSITION_POSE_EVALUATION_V1_MAX_BASIS_TEXT_BYTES,
1778    )
1779}
1780
1781fn skeleton_input_is_within_limits_with(skeleton: &Skeleton, text_limit: usize) -> bool {
1782    if skeleton.bones.len() > TRANSITION_POSE_EVALUATION_V1_MAX_BONES {
1783        return false;
1784    }
1785    skeleton_text_is_within_limit(skeleton, text_limit)
1786}
1787
1788fn skeleton_text_is_within_limit(skeleton: &Skeleton, text_limit: usize) -> bool {
1789    skeleton
1790        .bones
1791        .iter()
1792        .try_fold(0usize, |total, bone| total.checked_add(bone.name.len()))
1793        .is_some_and(|total| total <= text_limit)
1794}
1795
1796/// Validate only transition-pose's skeleton authority. Source projections,
1797/// mesh assets, inverse binds, and unselected clips are deliberately not an
1798/// evaluator input domain.
1799fn validate_transition_pose_skeleton(skeleton: &Skeleton) -> Result<(), SkeletonBasisError> {
1800    for (ordinal, bone) in skeleton.bones.iter().enumerate() {
1801        if matches!(bone.parent, Some(parent) if parent >= ordinal) {
1802            return Err(SkeletonBasisError::InvalidParent { ordinal });
1803        }
1804        if finite_translation(bone, ordinal).is_err()
1805            || canonical_quaternion(bone.rest.rotation, ordinal).is_err()
1806        {
1807            return Err(SkeletonBasisError::InvalidRest { ordinal });
1808        }
1809    }
1810    Ok(())
1811}
1812
1813/// Bound selected-track shape work before allocating duplicate-target state or
1814/// sampling. Only translation/rotation tracks are selected: scale is outside
1815/// V1 and is never counted, validated, or sampled. Each selected clip can
1816/// therefore target two channels per admitted bone.
1817fn plan_selected_track_input_limits(
1818    resolved_clips: &[Vec<&Clip>],
1819    bone_count: usize,
1820    policy_rejected: &[bool],
1821) -> Vec<bool> {
1822    let (Some(raw_track_limit), Some(track_limit)) =
1823        (bone_count.checked_mul(3), bone_count.checked_mul(2))
1824    else {
1825        return vec![true; resolved_clips.len()];
1826    };
1827    plan_selected_track_input_limits_with(
1828        resolved_clips,
1829        raw_track_limit,
1830        track_limit,
1831        TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACK_ELEMENTS,
1832        policy_rejected,
1833    )
1834}
1835
1836fn plan_selected_track_input_limits_with(
1837    resolved_clips: &[Vec<&Clip>],
1838    raw_track_limit: usize,
1839    track_limit: usize,
1840    element_limit: usize,
1841    policy_rejected: &[bool],
1842) -> Vec<bool> {
1843    let mut aggregate_elements = 0usize;
1844    resolved_clips
1845        .iter()
1846        .zip(policy_rejected.iter().copied())
1847        .map(|(clips, policy_rejected)| {
1848            if policy_rejected {
1849                return false;
1850            }
1851            let mut family_elements = 0usize;
1852            for clip in clips {
1853                if clip.tracks.len() > raw_track_limit {
1854                    return true;
1855                }
1856                let mut selected_tracks = 0usize;
1857                for track in clip
1858                    .tracks
1859                    .iter()
1860                    .filter(|track| is_transition_pose_property(track.property))
1861                {
1862                    let Some(count) = selected_tracks.checked_add(1) else {
1863                        return true;
1864                    };
1865                    if count > track_limit {
1866                        return true;
1867                    }
1868                    selected_tracks = count;
1869                    let elements = track.times.len().checked_add(track.values.len());
1870                    let Some(total) =
1871                        elements.and_then(|elements| family_elements.checked_add(elements))
1872                    else {
1873                        return true;
1874                    };
1875                    if total > element_limit {
1876                        return true;
1877                    }
1878                    family_elements = total;
1879                }
1880            }
1881            let Some(total) = aggregate_elements.checked_add(family_elements) else {
1882                return true;
1883            };
1884            if total > element_limit {
1885                return true;
1886            }
1887            aggregate_elements = total;
1888            false
1889        })
1890        .collect()
1891}
1892
1893/// Serialize a conservative pair-free authority before retaining any detailed
1894/// row. Every family is represented with the longest V1 incomplete reason,
1895/// so this is an upper bound for all final no-pair rows while preserving the
1896/// exact declaration, subject, member, and basis bindings.
1897fn detailed_result_budget_after_base(
1898    result: &TransitionPoseEvaluationV1,
1899    families: &[crate::DocumentTransitionFamilyV1],
1900    dependency_closure: &DependencyClosureV1,
1901    basis_input: &InputIdentity,
1902    limit: usize,
1903) -> Result<usize, TransitionPoseEvaluationControlError> {
1904    let mut base = result.clone();
1905    base.status = TransitionPoseStatusV1::Incomplete;
1906    base.decision = TransitionPoseDecisionV1::NotEvaluated;
1907    base.reason = None;
1908    base.families = families
1909        .iter()
1910        .map(|family| {
1911            let mut row = family_result_row(family, dependency_closure, Some(basis_input));
1912            row.reason = Some(TransitionPoseReasonV1::TimeToleranceUnsupported);
1913            row
1914        })
1915        .collect();
1916    let base_bytes = canonical_bytes(&base, limit)
1917        .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)?
1918        .len();
1919    limit
1920        .checked_sub(base_bytes)
1921        .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)
1922}
1923
1924/// Reserve a conservative complete detailed-pair envelope before sampling.
1925/// The fixed component covers every pair field and the two 16-row offender
1926/// arrays. Names are escaped by JSON/JCS, so six bytes per source byte is a
1927/// safe extra bound for every retained name. A row's ordinal is unique,
1928/// permitting this bound without cloning names just to decide whether they
1929/// would be retained.
1930fn reserve_detailed_name_budget(
1931    remaining: &mut usize,
1932    family: &crate::DocumentTransitionFamilyV1,
1933    skeleton: &Skeleton,
1934) -> bool {
1935    let boundaries = match family.boundary() {
1936        TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Exit => 1usize,
1937        TransitionFamilyBoundaryV1::Both => 2usize,
1938    };
1939    let Some(pair_boundaries) =
1940        checked_pair_count(family.members().len()).and_then(|pairs| pairs.checked_mul(boundaries))
1941    else {
1942        return false;
1943    };
1944    let Some(rows_per_pair_boundary) = skeleton
1945        .bones
1946        .len()
1947        .min(TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS)
1948        .checked_add(
1949            skeleton
1950                .bones
1951                .len()
1952                .min(TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS),
1953        )
1954    else {
1955        return false;
1956    };
1957    let fixed_bytes = pair_boundaries.checked_mul(MAX_DETAILED_PAIR_FIXED_BYTES);
1958    let max_escaped_name_bytes = skeleton
1959        .bones
1960        .iter()
1961        .map(|bone| bone.name.len().checked_mul(6))
1962        .max()
1963        .unwrap_or(Some(0));
1964    let Some(bytes) = fixed_bytes.and_then(|fixed| {
1965        max_escaped_name_bytes.and_then(|name| {
1966            pair_boundaries
1967                .checked_mul(rows_per_pair_boundary)?
1968                .checked_mul(name)
1969                .and_then(|names| fixed.checked_add(names))
1970        })
1971    }) else {
1972        return false;
1973    };
1974    if bytes > *remaining {
1975        return false;
1976    }
1977    *remaining -= bytes;
1978    true
1979}
1980
1981#[derive(Clone)]
1982struct Endpoints {
1983    entry: Option<Vec<EndpointPose>>,
1984    exit: Option<Vec<EndpointPose>>,
1985}
1986
1987#[derive(Clone, Copy)]
1988struct EndpointPose {
1989    translation: [f64; 3],
1990    rotation: [f64; 4],
1991}
1992
1993fn strict_endpoints(
1994    document: &Document,
1995    clips: &[&Clip],
1996    boundary: TransitionFamilyBoundaryV1,
1997) -> Result<Vec<Endpoints>, TransitionPoseReasonV1> {
1998    clips
1999        .iter()
2000        .map(|clip| {
2001            if !selected_tracks_are_strict(clip, document.skeleton.bones.len()) {
2002                return Err(TransitionPoseReasonV1::UnsupportedSampling);
2003            }
2004            if clip.duration_s == 0.0 {
2005                return Err(TransitionPoseReasonV1::ZeroDuration);
2006            }
2007            if !clip.duration_s.is_finite() || clip.duration_s < 0.0 {
2008                return Err(TransitionPoseReasonV1::UnsupportedSampling);
2009            }
2010            let needs_exit = matches!(
2011                boundary,
2012                TransitionFamilyBoundaryV1::Exit | TransitionFamilyBoundaryV1::Both
2013            );
2014            let exit_time = needs_exit.then(|| {
2015                let time = clip.duration_s as f32;
2016                if !time.is_finite() || f64::from(time) != clip.duration_s {
2017                    Err(TransitionPoseReasonV1::UnsupportedSampling)
2018                } else {
2019                    Ok(time)
2020                }
2021            });
2022            Ok(Endpoints {
2023                entry: matches!(
2024                    boundary,
2025                    TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Both
2026                )
2027                .then(|| strict_endpoint(&document.skeleton, clip, 0.0))
2028                .transpose()?,
2029                exit: exit_time
2030                    .transpose()?
2031                    .map(|time| strict_endpoint(&document.skeleton, clip, time))
2032                    .transpose()?,
2033            })
2034        })
2035        .collect()
2036}
2037
2038fn selected_tracks_are_strict(clip: &Clip, bone_count: usize) -> bool {
2039    selected_tracks_are_strict_with(clip, bone_count, |_| {})
2040}
2041
2042/// Validate each selected T/R track exactly once. The two-channel seen table
2043/// is bounded by the already-admitted skeleton and avoids quadratic duplicate
2044/// checks at the V1 track cap.
2045fn selected_tracks_are_strict_with(
2046    clip: &Clip,
2047    bone_count: usize,
2048    mut observe_selected: impl FnMut(&Track),
2049) -> bool {
2050    let (Some(raw_track_limit), Some(seen_len)) =
2051        (bone_count.checked_mul(3), bone_count.checked_mul(2))
2052    else {
2053        return false;
2054    };
2055    if clip.tracks.len() > raw_track_limit {
2056        return false;
2057    }
2058    let mut seen = vec![false; seen_len];
2059    for track in &clip.tracks {
2060        let Some(channel) = transition_pose_channel(track.property) else {
2061            continue;
2062        };
2063        observe_selected(track);
2064        if track.bone >= bone_count {
2065            return false;
2066        }
2067        let Some(index) = track
2068            .bone
2069            .checked_mul(2)
2070            .and_then(|base| base.checked_add(channel))
2071        else {
2072            return false;
2073        };
2074        if seen[index] || validate_track_shape(0, track).is_err() {
2075            return false;
2076        }
2077        seen[index] = true;
2078    }
2079    true
2080}
2081
2082fn is_transition_pose_property(property: Property) -> bool {
2083    transition_pose_channel(property).is_some()
2084}
2085
2086fn transition_pose_channel(property: Property) -> Option<usize> {
2087    match property {
2088        Property::Translation => Some(0),
2089        Property::Rotation => Some(1),
2090        Property::Scale => None,
2091    }
2092}
2093
2094fn strict_endpoint(
2095    skeleton: &Skeleton,
2096    clip: &Clip,
2097    time: f32,
2098) -> Result<Vec<EndpointPose>, TransitionPoseReasonV1> {
2099    if !time.is_finite() {
2100        return Err(TransitionPoseReasonV1::UnsupportedSampling);
2101    }
2102    let mut poses = skeleton
2103        .bones
2104        .iter()
2105        .enumerate()
2106        .map(|(ordinal, bone)| {
2107            Ok(EndpointPose {
2108                translation: finite_translation(bone, ordinal)
2109                    .map_err(|_| TransitionPoseReasonV1::UnsupportedSampling)?,
2110                rotation: canonical_quaternion(bone.rest.rotation, ordinal)
2111                    .map_err(|_| TransitionPoseReasonV1::UnsupportedSampling)?,
2112            })
2113        })
2114        .collect::<Result<Vec<_>, _>>()?;
2115    for track in &clip.tracks {
2116        match track.property {
2117            Property::Scale => continue,
2118            Property::Translation => match crate::sample_track(track, time) {
2119                TrackSample::Vec3(value) => {
2120                    let target = poses
2121                        .get_mut(track.bone)
2122                        .ok_or(TransitionPoseReasonV1::UnsupportedSampling)?;
2123                    target.translation =
2124                        finite_vec3(value).ok_or(TransitionPoseReasonV1::UnsupportedSampling)?;
2125                }
2126                _ => return Err(TransitionPoseReasonV1::UnsupportedSampling),
2127            },
2128            Property::Rotation => match crate::sample_track(track, time) {
2129                TrackSample::Quat(value) => {
2130                    let target = poses
2131                        .get_mut(track.bone)
2132                        .ok_or(TransitionPoseReasonV1::UnsupportedSampling)?;
2133                    target.rotation = canonical_quaternion(value, track.bone)
2134                        .map_err(|_| TransitionPoseReasonV1::UnsupportedSampling)?;
2135                }
2136                _ => return Err(TransitionPoseReasonV1::UnsupportedSampling),
2137            },
2138        }
2139    }
2140    Ok(poses)
2141}
2142
2143fn compare_pairs(
2144    endpoints: &[Endpoints],
2145    boundary: TransitionFamilyBoundaryV1,
2146    tolerances: TransitionFamilyTolerancesV1,
2147    skeleton: &Skeleton,
2148) -> Result<Vec<TransitionPosePairEvaluationV1>, TransitionPoseReasonV1> {
2149    let boundaries: &[TransitionFamilyBoundaryV1] = match boundary {
2150        TransitionFamilyBoundaryV1::Entry => &[TransitionFamilyBoundaryV1::Entry],
2151        TransitionFamilyBoundaryV1::Exit => &[TransitionFamilyBoundaryV1::Exit],
2152        TransitionFamilyBoundaryV1::Both => &[
2153            TransitionFamilyBoundaryV1::Entry,
2154            TransitionFamilyBoundaryV1::Exit,
2155        ],
2156    };
2157    let mut output = Vec::new();
2158    for left in 0..endpoints.len() {
2159        for right in left + 1..endpoints.len() {
2160            for &boundary in boundaries {
2161                let (left_pose, right_pose) = if boundary == TransitionFamilyBoundaryV1::Entry {
2162                    (
2163                        endpoints[left].entry.as_deref(),
2164                        endpoints[right].entry.as_deref(),
2165                    )
2166                } else {
2167                    (
2168                        endpoints[left].exit.as_deref(),
2169                        endpoints[right].exit.as_deref(),
2170                    )
2171                };
2172                let (left_pose, right_pose) = match (left_pose, right_pose) {
2173                    (Some(left_pose), Some(right_pose)) => (left_pose, right_pose),
2174                    _ => return Err(TransitionPoseReasonV1::UnsupportedSampling),
2175                };
2176                output.push(compare_one_pair(
2177                    [left, right],
2178                    boundary,
2179                    left_pose,
2180                    right_pose,
2181                    tolerances,
2182                    skeleton,
2183                ));
2184            }
2185        }
2186    }
2187    Ok(output)
2188}
2189
2190fn compare_one_pair(
2191    member_indices: [usize; 2],
2192    boundary: TransitionFamilyBoundaryV1,
2193    left: &[EndpointPose],
2194    right: &[EndpointPose],
2195    tolerances: TransitionFamilyTolerancesV1,
2196    skeleton: &Skeleton,
2197) -> TransitionPosePairEvaluationV1 {
2198    let mut max_translation_delta_m = 0.0f64;
2199    let mut max_rotation_delta_deg = 0.0f64;
2200    let mut translation_candidates =
2201        Vec::with_capacity(TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS);
2202    let mut rotation_candidates =
2203        Vec::with_capacity(TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS);
2204    for (ordinal, (left, right)) in left.iter().zip(right).enumerate() {
2205        let translation = translation_delta(left.translation, right.translation);
2206        let rotation = rotation_delta_deg(left.rotation, right.rotation);
2207        max_translation_delta_m = max_translation_delta_m.max(translation);
2208        max_rotation_delta_deg = max_rotation_delta_deg.max(rotation);
2209        if translation > tolerances.translation_m() {
2210            retain_top_candidate(
2211                &mut translation_candidates,
2212                ordinal,
2213                translation,
2214                TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS,
2215            );
2216        }
2217        if rotation > tolerances.rotation_deg() {
2218            retain_top_candidate(
2219                &mut rotation_candidates,
2220                ordinal,
2221                rotation,
2222                TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS,
2223            );
2224        }
2225    }
2226    sort_top_candidates(&mut translation_candidates);
2227    sort_top_candidates(&mut rotation_candidates);
2228    let translation_offenders = translation_candidates
2229        .into_iter()
2230        .map(
2231            |(bone_ordinal, delta_m)| TransitionPoseTranslationOffenderV1 {
2232                bone_ordinal,
2233                bone_name: skeleton.bones[bone_ordinal].name.clone(),
2234                delta_m,
2235            },
2236        )
2237        .collect();
2238    let rotation_offenders = rotation_candidates
2239        .into_iter()
2240        .map(
2241            |(bone_ordinal, delta_deg)| TransitionPoseRotationOffenderV1 {
2242                bone_ordinal,
2243                bone_name: skeleton.bones[bone_ordinal].name.clone(),
2244                delta_deg,
2245            },
2246        )
2247        .collect();
2248    TransitionPosePairEvaluationV1 {
2249        member_indices,
2250        boundary,
2251        max_translation_delta_m,
2252        max_rotation_delta_deg,
2253        translation_tolerance_m: tolerances.translation_m(),
2254        rotation_tolerance_deg: tolerances.rotation_deg(),
2255        translation_offenders,
2256        rotation_offenders,
2257    }
2258}
2259
2260/// Retain a candidate only when it belongs in the bounded final ordering.
2261/// Ordinals are unique within one skeleton, so the public tertiary bone-name
2262/// tie-break is unreachable after the ordinal tie-break and no name is needed
2263/// while selecting.
2264fn retain_top_candidate(
2265    candidates: &mut Vec<(usize, f64)>,
2266    ordinal: usize,
2267    delta: f64,
2268    cap: usize,
2269) {
2270    let candidate = (ordinal, delta);
2271    if candidates.len() < cap {
2272        candidates.push(candidate);
2273        return;
2274    }
2275    let worst = candidates
2276        .iter()
2277        .enumerate()
2278        .reduce(|worst, current| {
2279            if candidate_precedes(worst.1, current.1) {
2280                current
2281            } else {
2282                worst
2283            }
2284        })
2285        .map(|(index, _)| index);
2286    if let Some(worst) = worst
2287        && candidate_precedes(&candidate, &candidates[worst])
2288    {
2289        candidates[worst] = candidate;
2290    }
2291}
2292
2293fn sort_top_candidates(candidates: &mut [(usize, f64)]) {
2294    candidates.sort_by(candidate_order);
2295}
2296
2297fn candidate_precedes(left: &(usize, f64), right: &(usize, f64)) -> bool {
2298    candidate_order(left, right).is_lt()
2299}
2300
2301fn candidate_order(left: &(usize, f64), right: &(usize, f64)) -> std::cmp::Ordering {
2302    right
2303        .1
2304        .total_cmp(&left.1)
2305        .then_with(|| left.0.cmp(&right.0))
2306}
2307
2308fn pair_has_finding(pair: &TransitionPosePairEvaluationV1) -> bool {
2309    !pair.translation_offenders.is_empty() || !pair.rotation_offenders.is_empty()
2310}
2311
2312fn derive_result_state(result: &mut TransitionPoseEvaluationV1) {
2313    if result
2314        .families
2315        .iter()
2316        .any(|family| family.status == TransitionPoseStatusV1::Incomplete)
2317    {
2318        result.status = TransitionPoseStatusV1::Incomplete;
2319        result.decision = TransitionPoseDecisionV1::NotEvaluated;
2320        result.reason = None;
2321    } else if result
2322        .families
2323        .iter()
2324        .any(|family| family.decision == TransitionPoseDecisionV1::Finding)
2325    {
2326        result.status = TransitionPoseStatusV1::Complete;
2327        result.decision = TransitionPoseDecisionV1::Finding;
2328        result.reason = None;
2329    } else {
2330        result.status = TransitionPoseStatusV1::Complete;
2331        result.decision = TransitionPoseDecisionV1::Pass;
2332        result.reason = None;
2333    }
2334}
2335
2336fn finite_translation(bone: &Bone, ordinal: usize) -> Result<[f64; 3], SkeletonBasisError> {
2337    finite_vec3(bone.rest.translation).ok_or(SkeletonBasisError::InvalidRest { ordinal })
2338}
2339
2340fn finite_vec3(value: crate::glam::Vec3) -> Option<[f64; 3]> {
2341    if !value.is_finite() {
2342        return None;
2343    }
2344    Some([
2345        canonical_zero(f64::from(value.x)),
2346        canonical_zero(f64::from(value.y)),
2347        canonical_zero(f64::from(value.z)),
2348    ])
2349}
2350
2351fn canonical_quaternion(
2352    value: crate::glam::Quat,
2353    ordinal: usize,
2354) -> Result<[f64; 4], SkeletonBasisError> {
2355    let mut q = [
2356        f64::from(value.x),
2357        f64::from(value.y),
2358        f64::from(value.z),
2359        f64::from(value.w),
2360    ];
2361    if q.iter().any(|value| !value.is_finite()) {
2362        return Err(SkeletonBasisError::InvalidRest { ordinal });
2363    }
2364    let norm_squared = q.iter().map(|value| value * value).sum::<f64>();
2365    if !norm_squared.is_finite() || norm_squared == 0.0 {
2366        return Err(SkeletonBasisError::InvalidRest { ordinal });
2367    }
2368    let norm = norm_squared.sqrt();
2369    if !norm.is_finite() || norm == 0.0 {
2370        return Err(SkeletonBasisError::InvalidRest { ordinal });
2371    }
2372    for component in &mut q {
2373        *component /= norm;
2374    }
2375    if q.iter().any(|value| !value.is_finite()) {
2376        return Err(SkeletonBasisError::InvalidRest { ordinal });
2377    }
2378    if hemisphere_negative(q) {
2379        for component in &mut q {
2380            *component = -*component;
2381        }
2382    }
2383    for component in &mut q {
2384        *component = canonical_zero(*component);
2385    }
2386    Ok(q)
2387}
2388
2389fn hemisphere_negative(q: [f64; 4]) -> bool {
2390    for index in [3usize, 0, 1, 2] {
2391        if q[index] < 0.0 {
2392            return true;
2393        }
2394        if q[index] > 0.0 {
2395            return false;
2396        }
2397    }
2398    false
2399}
2400
2401fn canonical_zero(value: f64) -> f64 {
2402    if value == 0.0 { 0.0 } else { value }
2403}
2404
2405fn translation_delta(left: [f64; 3], right: [f64; 3]) -> f64 {
2406    let dx = left[0] - right[0];
2407    let dy = left[1] - right[1];
2408    let dz = left[2] - right[2];
2409    (dx * dx + dy * dy + dz * dz).sqrt()
2410}
2411
2412fn rotation_delta_deg(left: [f64; 4], right: [f64; 4]) -> f64 {
2413    if left == right
2414        || left
2415            .into_iter()
2416            .zip(right)
2417            .all(|(left, right)| left == -right)
2418    {
2419        return 0.0;
2420    }
2421    let dot = left
2422        .into_iter()
2423        .zip(right)
2424        .map(|(left, right)| left * right)
2425        .sum::<f64>();
2426    let right = if dot < 0.0 {
2427        right.map(|value| -value)
2428    } else {
2429        right
2430    };
2431    // `conjugate(left) * right`: atan2 is stable at identity, unlike acos of
2432    // a dot product rounded a hair below one after f32-to-f64 normalization.
2433    let vector = [
2434        left[3] * right[0] - left[0] * right[3] - left[1] * right[2] + left[2] * right[1],
2435        left[3] * right[1] + left[0] * right[2] - left[1] * right[3] - left[2] * right[0],
2436        left[3] * right[2] - left[0] * right[1] + left[1] * right[0] - left[2] * right[3],
2437    ];
2438    let vector_norm = vector
2439        .into_iter()
2440        .map(|value| value * value)
2441        .sum::<f64>()
2442        .sqrt();
2443    let scalar =
2444        (left[3] * right[3] + left[0] * right[0] + left[1] * right[1] + left[2] * right[2]).abs();
2445    (2.0 * vector_norm.atan2(scalar)).to_degrees()
2446}
2447
2448fn canonical_bytes(value: &impl Serialize, limit: usize) -> io::Result<Vec<u8>> {
2449    let mut writer = BoundedWriter {
2450        bytes: Vec::new(),
2451        limit,
2452        overflow: false,
2453    };
2454    serde_jcs::to_writer(&mut writer, value).map_err(io::Error::other)?;
2455    if writer.overflow {
2456        return Err(io::Error::other("bounded JCS result exceeded cap"));
2457    }
2458    Ok(writer.bytes)
2459}
2460
2461struct BoundedWriter {
2462    bytes: Vec<u8>,
2463    limit: usize,
2464    overflow: bool,
2465}
2466impl Write for BoundedWriter {
2467    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
2468        if self
2469            .bytes
2470            .len()
2471            .checked_add(buffer.len())
2472            .is_none_or(|length| length > self.limit)
2473        {
2474            self.overflow = true;
2475            return Err(io::Error::other("bounded JCS result exceeded cap"));
2476        }
2477        self.bytes.extend_from_slice(buffer);
2478        Ok(buffer.len())
2479    }
2480    fn flush(&mut self) -> io::Result<()> {
2481        Ok(())
2482    }
2483}
2484
2485#[cfg(test)]
2486mod tests {
2487    use super::*;
2488    use crate::{
2489        Bone, Clip, CollectionIdV1, CollectionLogicalIdV1, CollectionSourceKeyV1,
2490        CollectionTransitionFamilyMemberV1, CollectionTransitionFamilyV1,
2491        DependencyClosureBuilderV1, DependencyResourceKeyV1, Document,
2492        DocumentTransitionFamilyMemberV1, DocumentTransitionFamilyV1, Interpolation,
2493        RawSourceFactsBuilderV1, ResourceKeySyntaxV1, Skeleton, SourceFactDomainV1, SourceFormatV1,
2494        SourceResourceKindV1, SourceSetCoverageV1, SourceUnavailableReasonV1, Track, TrackValues,
2495        Transform, TransitionFamilyDeclarationV1, TransitionFamilyManifestIdentityV1,
2496    };
2497
2498    fn document(second_translation: Option<f32>) -> Document {
2499        let mut second = Clip {
2500            name: "Run".into(),
2501            duration_s: 1.0,
2502            tracks: Vec::new(),
2503        };
2504        if let Some(value) = second_translation {
2505            second.tracks.push(Track {
2506                bone: 0,
2507                property: Property::Translation,
2508                interpolation: Interpolation::Linear,
2509                times: vec![0.0, 1.0],
2510                values: TrackValues::Vec3s(vec![
2511                    crate::glam::Vec3::splat(value),
2512                    crate::glam::Vec3::splat(value),
2513                ]),
2514            });
2515        }
2516        Document {
2517            skeleton: Skeleton {
2518                bones: vec![Bone {
2519                    name: "root".into(),
2520                    parent: None,
2521                    rest: Transform::IDENTITY,
2522                    inverse_bind: None,
2523                }],
2524            },
2525            clips: vec![
2526                Clip {
2527                    name: "Walk".into(),
2528                    duration_s: 1.0,
2529                    tracks: Vec::new(),
2530                },
2531                second,
2532            ],
2533            ..Document::default()
2534        }
2535    }
2536
2537    fn declaration(
2538        boundary: TransitionFamilyBoundaryV1,
2539        translation_m: f64,
2540        time_normalized: f64,
2541    ) -> TransitionFamilyDeclarationInputV1 {
2542        let family = DocumentTransitionFamilyV1::new(
2543            "walk_to_run".into(),
2544            boundary,
2545            TransitionFamilyTolerancesV1::new(translation_m, 180.0, time_normalized).unwrap(),
2546            vec![
2547                DocumentTransitionFamilyMemberV1::new(0, "Walk".into()).unwrap(),
2548                DocumentTransitionFamilyMemberV1::new(1, "Run".into()).unwrap(),
2549            ],
2550        )
2551        .unwrap();
2552        TransitionFamilyDeclarationInputV1::new(
2553            TransitionFamilyDeclarationV1::document(vec![family]).unwrap(),
2554            b"declaration",
2555        )
2556        .unwrap()
2557    }
2558
2559    fn complete_closure(primary_input: InputIdentity) -> DependencyClosureV1 {
2560        DependencyClosureBuilderV1::new(primary_input, SourceSetCoverageV1::complete(), 0)
2561            .finish()
2562            .unwrap()
2563    }
2564
2565    fn loaded_source(document: Document, bytes: &[u8]) -> LoadedSource {
2566        let primary = InputIdentity::from_bytes(bytes);
2567        loaded_source_with_closure(document, primary.clone(), complete_closure(primary))
2568    }
2569
2570    fn loaded_source_with_closure(
2571        document: Document,
2572        primary: InputIdentity,
2573        closure: DependencyClosureV1,
2574    ) -> LoadedSource {
2575        let mut facts = RawSourceFactsBuilderV1::new(SourceFormatV1::Glb, primary.clone());
2576        for domain in [
2577            SourceFactDomainV1::Clips,
2578            SourceFactDomainV1::Constructs,
2579            SourceFactDomainV1::Resources,
2580        ] {
2581            facts.mark_complete(domain);
2582        }
2583        facts
2584            .finish_with_dependency_closure(document, closure)
2585            .unwrap()
2586    }
2587
2588    fn collection_declaration(
2589        families: Vec<CollectionTransitionFamilyV1>,
2590        manifest: TransitionFamilyManifestIdentityV1,
2591    ) -> TransitionFamilyDeclarationInputV1 {
2592        TransitionFamilyDeclarationInputV1::new(
2593            TransitionFamilyDeclarationV1::collection(manifest, families).unwrap(),
2594            b"collection",
2595        )
2596        .unwrap()
2597    }
2598
2599    fn collection_family(
2600        id: &str,
2601        members: Vec<CollectionTransitionFamilyMemberV1>,
2602    ) -> CollectionTransitionFamilyV1 {
2603        collection_family_with(id, TransitionFamilyBoundaryV1::Entry, 0.0, members)
2604    }
2605
2606    fn collection_family_with(
2607        id: &str,
2608        boundary: TransitionFamilyBoundaryV1,
2609        time_normalized: f64,
2610        members: Vec<CollectionTransitionFamilyMemberV1>,
2611    ) -> CollectionTransitionFamilyV1 {
2612        CollectionTransitionFamilyV1::new(
2613            CollectionLogicalIdV1::new(id).unwrap(),
2614            boundary,
2615            TransitionFamilyTolerancesV1::new(0.0, 0.0, time_normalized).unwrap(),
2616            members,
2617        )
2618        .unwrap()
2619    }
2620
2621    fn closure_with_external_buffer(
2622        primary_input: InputIdentity,
2623        buffer_bytes: &[u8],
2624    ) -> DependencyClosureV1 {
2625        let key =
2626            DependencyResourceKeyV1::from_source_str("animation.bin", ResourceKeySyntaxV1::GltfUri)
2627                .unwrap();
2628        let mut builder =
2629            DependencyClosureBuilderV1::new(primary_input, SourceSetCoverageV1::complete(), 1);
2630        assert!(builder.begin_reference("animation.bin".len(), 1));
2631        assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
2632        builder.record_external_open_attempt(&key).unwrap();
2633        assert!(
2634            builder
2635                .push_captured_external(
2636                    0,
2637                    SourceResourceKindV1::Buffer,
2638                    0,
2639                    key,
2640                    InputIdentity::from_bytes(buffer_bytes),
2641                )
2642                .unwrap()
2643        );
2644        builder.finish().unwrap()
2645    }
2646
2647    fn evaluate_document_transition_poses_v1(
2648        declaration: &TransitionFamilyDeclarationInputV1,
2649        subject_input: InputIdentity,
2650        document: &Document,
2651    ) -> Result<TransitionPoseEvaluationV1, TransitionPoseEvaluationControlError> {
2652        let closure = complete_closure(subject_input);
2653        super::evaluate_document_transition_poses_v1(declaration, &closure, document)
2654    }
2655
2656    #[test]
2657    fn collection_adapter_binds_manifest_and_routes_runtime_take_drift() {
2658        let collection_id = CollectionIdV1::new("collection").unwrap();
2659        let manifest = TransitionFamilyManifestIdentityV1::new(
2660            collection_id,
2661            InputIdentity::from_bytes(b"manifest"),
2662        )
2663        .unwrap();
2664        let logical_a = CollectionLogicalIdV1::new("collection/walk").unwrap();
2665        let logical_b = CollectionLogicalIdV1::new("collection/run").unwrap();
2666        let source_a = CollectionSourceKeyV1::new("walk").unwrap();
2667        let source_b = CollectionSourceKeyV1::new("run").unwrap();
2668        let family = collection_family(
2669            "collection/family",
2670            vec![
2671                CollectionTransitionFamilyMemberV1::new(
2672                    logical_a.clone(),
2673                    source_a.clone(),
2674                    0,
2675                    "Walk".into(),
2676                )
2677                .unwrap(),
2678                CollectionTransitionFamilyMemberV1::new(
2679                    logical_b.clone(),
2680                    source_b.clone(),
2681                    1,
2682                    "Run".into(),
2683                )
2684                .unwrap(),
2685            ],
2686        );
2687        let declaration = collection_declaration(vec![family], manifest.clone());
2688        let left = loaded_source(document(None), b"left");
2689        let right = loaded_source(document(None), b"right");
2690        let members = [
2691            CollectionTransitionPoseMemberInputV1::available(
2692                &logical_a, &source_a, 0, "Walk", &left,
2693            ),
2694            CollectionTransitionPoseMemberInputV1::available(
2695                &logical_b, &source_b, 1, "Run", &right,
2696            ),
2697        ];
2698        assert_eq!(
2699            evaluate_collection_transition_poses_v1(
2700                &declaration,
2701                &TransitionFamilyManifestIdentityV1::new(
2702                    CollectionIdV1::new("other").unwrap(),
2703                    InputIdentity::from_bytes(b"manifest"),
2704                )
2705                .unwrap(),
2706                &members,
2707            ),
2708            Err(TransitionPoseEvaluationControlError::InvalidCollectionManifestBinding)
2709        );
2710        assert_eq!(
2711            evaluate_collection_transition_poses_v1(&declaration, &manifest, &members)
2712                .unwrap()
2713                .decision(),
2714            TransitionPoseDecisionV1::Pass
2715        );
2716        let mut drift = document(None);
2717        drift.clips[1].name = "Renamed".into();
2718        let drift = loaded_source(drift, b"drift");
2719        let drift_members = [
2720            CollectionTransitionPoseMemberInputV1::available(
2721                &logical_a, &source_a, 0, "Walk", &left,
2722            ),
2723            CollectionTransitionPoseMemberInputV1::available(
2724                &logical_b, &source_b, 1, "Run", &drift,
2725            ),
2726        ];
2727        let result =
2728            evaluate_collection_transition_poses_v1(&declaration, &manifest, &drift_members)
2729                .unwrap();
2730        assert_eq!(result.status(), TransitionPoseStatusV1::Incomplete);
2731        assert_eq!(
2732            result.families()[0].reason(),
2733            Some(TransitionPoseReasonV1::MemberUnavailable)
2734        );
2735        assert!(result.families()[0].members()[1].source_input().is_some());
2736        assert!(result.families()[0].pairs().is_empty());
2737    }
2738
2739    #[test]
2740    fn collection_control_rejects_length_order_staleness_and_split_source_authority() {
2741        let manifest = TransitionFamilyManifestIdentityV1::new(
2742            CollectionIdV1::new("collection").unwrap(),
2743            InputIdentity::from_bytes(b"manifest"),
2744        )
2745        .unwrap();
2746        let logical_a = CollectionLogicalIdV1::new("collection/a").unwrap();
2747        let logical_b = CollectionLogicalIdV1::new("collection/b").unwrap();
2748        let source = CollectionSourceKeyV1::new("shared").unwrap();
2749        let declaration = collection_declaration(
2750            vec![collection_family(
2751                "collection/family",
2752                vec![
2753                    CollectionTransitionFamilyMemberV1::new(
2754                        logical_a.clone(),
2755                        source.clone(),
2756                        0,
2757                        "Walk".into(),
2758                    )
2759                    .unwrap(),
2760                    CollectionTransitionFamilyMemberV1::new(
2761                        logical_b.clone(),
2762                        source.clone(),
2763                        1,
2764                        "Run".into(),
2765                    )
2766                    .unwrap(),
2767                ],
2768            )],
2769            manifest.clone(),
2770        );
2771        let first = loaded_source(document(None), b"same");
2772        let second = loaded_source(document(None), b"same");
2773        let valid = [
2774            CollectionTransitionPoseMemberInputV1::available(
2775                &logical_a, &source, 0, "Walk", &first,
2776            ),
2777            CollectionTransitionPoseMemberInputV1::available(&logical_b, &source, 1, "Run", &first),
2778        ];
2779        assert_eq!(
2780            evaluate_collection_transition_poses_v1(&declaration, &manifest, &valid[..1]),
2781            Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2782        );
2783        assert_eq!(
2784            evaluate_collection_transition_poses_v1(&declaration, &manifest, &[valid[1], valid[0]],),
2785            Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2786        );
2787        let stale = [
2788            CollectionTransitionPoseMemberInputV1::available(
2789                &logical_a, &source, 0, "Walk", &first,
2790            ),
2791            CollectionTransitionPoseMemberInputV1::available(&logical_b, &source, 0, "Run", &first),
2792        ];
2793        assert_eq!(
2794            evaluate_collection_transition_poses_v1(&declaration, &manifest, &stale),
2795            Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2796        );
2797        let split_loaded = [
2798            CollectionTransitionPoseMemberInputV1::available(
2799                &logical_a, &source, 0, "Walk", &first,
2800            ),
2801            CollectionTransitionPoseMemberInputV1::available(
2802                &logical_b, &source, 1, "Run", &second,
2803            ),
2804        ];
2805        assert_eq!(
2806            evaluate_collection_transition_poses_v1(&declaration, &manifest, &split_loaded),
2807            Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2808        );
2809        let split_state = [
2810            CollectionTransitionPoseMemberInputV1::available(
2811                &logical_a, &source, 0, "Walk", &first,
2812            ),
2813            CollectionTransitionPoseMemberInputV1::unavailable(&logical_b, &source, 1, "Run"),
2814        ];
2815        assert_eq!(
2816            evaluate_collection_transition_poses_v1(&declaration, &manifest, &split_state),
2817            Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2818        );
2819        let unavailable_left = InputIdentity::from_bytes(b"missing-left");
2820        let unavailable_right = InputIdentity::from_bytes(b"missing-right");
2821        let split_unavailable_identity = [
2822            CollectionTransitionPoseMemberInputV1::unavailable_with_source_input(
2823                &logical_a,
2824                &source,
2825                0,
2826                "Walk",
2827                &unavailable_left,
2828            ),
2829            CollectionTransitionPoseMemberInputV1::unavailable_with_source_input(
2830                &logical_b,
2831                &source,
2832                1,
2833                "Run",
2834                &unavailable_right,
2835            ),
2836        ];
2837        assert_eq!(
2838            evaluate_collection_transition_poses_v1(
2839                &declaration,
2840                &manifest,
2841                &split_unavailable_identity,
2842            ),
2843            Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2844        );
2845    }
2846
2847    #[test]
2848    fn collection_control_rejects_false_incomplete_closure_state() {
2849        let manifest = TransitionFamilyManifestIdentityV1::new(
2850            CollectionIdV1::new("collection").unwrap(),
2851            InputIdentity::from_bytes(b"manifest"),
2852        )
2853        .unwrap();
2854        let logical = [
2855            CollectionLogicalIdV1::new("collection/a").unwrap(),
2856            CollectionLogicalIdV1::new("collection/b").unwrap(),
2857        ];
2858        let sources = [
2859            CollectionSourceKeyV1::new("a").unwrap(),
2860            CollectionSourceKeyV1::new("b").unwrap(),
2861        ];
2862        let declaration = collection_declaration(
2863            vec![collection_family(
2864                "collection/family",
2865                logical
2866                    .iter()
2867                    .zip(&sources)
2868                    .map(|(logical, source)| {
2869                        CollectionTransitionFamilyMemberV1::new(
2870                            logical.clone(),
2871                            source.clone(),
2872                            0,
2873                            "Walk".into(),
2874                        )
2875                        .unwrap()
2876                    })
2877                    .collect(),
2878            )],
2879            manifest.clone(),
2880        );
2881        let complete = loaded_source(document(None), b"complete");
2882        let members = [
2883            CollectionTransitionPoseMemberInputV1::available(
2884                &logical[0],
2885                &sources[0],
2886                0,
2887                "Walk",
2888                &complete,
2889            ),
2890            CollectionTransitionPoseMemberInputV1::dependency_closure_incomplete(
2891                &logical[1],
2892                &sources[1],
2893                0,
2894                "Walk",
2895                &complete,
2896            ),
2897        ];
2898
2899        assert_eq!(
2900            evaluate_collection_transition_poses_v1(&declaration, &manifest, &members),
2901            Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2902        );
2903    }
2904
2905    #[test]
2906    fn collection_available_partial_closure_normalizes_to_whole_family_gap() {
2907        let manifest = TransitionFamilyManifestIdentityV1::new(
2908            CollectionIdV1::new("collection").unwrap(),
2909            InputIdentity::from_bytes(b"manifest"),
2910        )
2911        .unwrap();
2912        let logical = [
2913            CollectionLogicalIdV1::new("collection/a").unwrap(),
2914            CollectionLogicalIdV1::new("collection/b").unwrap(),
2915        ];
2916        let source = CollectionSourceKeyV1::new("shared").unwrap();
2917        let declaration = collection_declaration(
2918            vec![collection_family(
2919                "collection/family",
2920                vec![
2921                    CollectionTransitionFamilyMemberV1::new(
2922                        logical[0].clone(),
2923                        source.clone(),
2924                        0,
2925                        "Walk".into(),
2926                    )
2927                    .unwrap(),
2928                    CollectionTransitionFamilyMemberV1::new(
2929                        logical[1].clone(),
2930                        source.clone(),
2931                        1,
2932                        "Run".into(),
2933                    )
2934                    .unwrap(),
2935                ],
2936            )],
2937            manifest.clone(),
2938        );
2939        let primary = InputIdentity::from_bytes(b"partial");
2940        let closure = DependencyClosureBuilderV1::new(
2941            primary.clone(),
2942            SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded),
2943            0,
2944        )
2945        .finish()
2946        .unwrap();
2947        let mut facts = RawSourceFactsBuilderV1::new(SourceFormatV1::Glb, primary.clone());
2948        facts.mark_complete(SourceFactDomainV1::Clips);
2949        facts.mark_complete(SourceFactDomainV1::Constructs);
2950        facts.mark_partial(
2951            SourceFactDomainV1::Resources,
2952            SourceUnavailableReasonV1::ProjectionBudgetExceeded,
2953        );
2954        let partial = facts
2955            .finish_with_dependency_closure(document(None), closure)
2956            .unwrap();
2957        let members = [
2958            CollectionTransitionPoseMemberInputV1::available(
2959                &logical[0],
2960                &source,
2961                0,
2962                "Walk",
2963                &partial,
2964            ),
2965            CollectionTransitionPoseMemberInputV1::available(
2966                &logical[1],
2967                &source,
2968                1,
2969                "Run",
2970                &partial,
2971            ),
2972        ];
2973
2974        let result =
2975            evaluate_collection_transition_poses_v1(&declaration, &manifest, &members).unwrap();
2976        let family = &result.families()[0];
2977        assert_eq!(result.status(), TransitionPoseStatusV1::Incomplete);
2978        assert_eq!(result.decision(), TransitionPoseDecisionV1::NotEvaluated);
2979        assert_eq!(
2980            family.reason(),
2981            Some(TransitionPoseReasonV1::DependencyClosureIncomplete)
2982        );
2983        assert!(family.skeleton_basis_input().is_none());
2984        assert!(family.pairs().is_empty());
2985        assert!(family.members().iter().all(|member| {
2986            member.source_input() == Some(&primary)
2987                && member.source_dependency_closure_identity().is_none()
2988        }));
2989    }
2990
2991    #[test]
2992    fn collection_runtime_gaps_preserve_priority_evidence_and_never_form_a_subset() {
2993        let manifest = TransitionFamilyManifestIdentityV1::new(
2994            CollectionIdV1::new("collection").unwrap(),
2995            InputIdentity::from_bytes(b"manifest"),
2996        )
2997        .unwrap();
2998        let logical = [
2999            CollectionLogicalIdV1::new("collection/a").unwrap(),
3000            CollectionLogicalIdV1::new("collection/b").unwrap(),
3001            CollectionLogicalIdV1::new("collection/c").unwrap(),
3002        ];
3003        let sources = [
3004            CollectionSourceKeyV1::new("a").unwrap(),
3005            CollectionSourceKeyV1::new("b").unwrap(),
3006            CollectionSourceKeyV1::new("c").unwrap(),
3007        ];
3008        let family = collection_family(
3009            "collection/family",
3010            logical
3011                .iter()
3012                .zip(&sources)
3013                .map(|(logical, source)| {
3014                    CollectionTransitionFamilyMemberV1::new(
3015                        logical.clone(),
3016                        source.clone(),
3017                        0,
3018                        "Walk".into(),
3019                    )
3020                    .unwrap()
3021                })
3022                .collect(),
3023        );
3024        let declaration = collection_declaration(vec![family], manifest.clone());
3025        let available = loaded_source(document(None), b"available");
3026        let partial_primary = InputIdentity::from_bytes(b"partial");
3027        let partial_closure = DependencyClosureBuilderV1::new(
3028            partial_primary.clone(),
3029            SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded),
3030            0,
3031        )
3032        .finish()
3033        .unwrap();
3034        let mut partial_facts =
3035            RawSourceFactsBuilderV1::new(SourceFormatV1::Glb, partial_primary.clone());
3036        partial_facts.mark_complete(SourceFactDomainV1::Clips);
3037        partial_facts.mark_complete(SourceFactDomainV1::Constructs);
3038        partial_facts.mark_partial(
3039            SourceFactDomainV1::Resources,
3040            SourceUnavailableReasonV1::ProjectionBudgetExceeded,
3041        );
3042        let partial = partial_facts
3043            .finish_with_dependency_closure(document(None), partial_closure)
3044            .unwrap();
3045        let missing_primary = InputIdentity::from_bytes(b"missing");
3046        let members = [
3047            CollectionTransitionPoseMemberInputV1::available(
3048                &logical[0],
3049                &sources[0],
3050                0,
3051                "Walk",
3052                &available,
3053            ),
3054            CollectionTransitionPoseMemberInputV1::dependency_closure_incomplete(
3055                &logical[1],
3056                &sources[1],
3057                0,
3058                "Walk",
3059                &partial,
3060            ),
3061            CollectionTransitionPoseMemberInputV1::unavailable_with_source_input(
3062                &logical[2],
3063                &sources[2],
3064                0,
3065                "Walk",
3066                &missing_primary,
3067            ),
3068        ];
3069        let result =
3070            evaluate_collection_transition_poses_v1(&declaration, &manifest, &members).unwrap();
3071        let row = &result.families()[0];
3072        assert_eq!(
3073            row.reason(),
3074            Some(TransitionPoseReasonV1::DependencyClosureIncomplete)
3075        );
3076        assert!(row.pairs().is_empty());
3077        assert!(row.skeleton_basis_input().is_none());
3078        assert_eq!(row.members()[1].source_input(), Some(&partial_primary));
3079        assert!(
3080            row.members()[1]
3081                .source_dependency_closure_identity()
3082                .is_none()
3083        );
3084        assert_eq!(row.members()[2].source_input(), Some(&missing_primary));
3085    }
3086
3087    #[test]
3088    fn collection_invalid_and_mismatched_skeletons_are_whole_family_gaps() {
3089        let manifest = TransitionFamilyManifestIdentityV1::new(
3090            CollectionIdV1::new("collection").unwrap(),
3091            InputIdentity::from_bytes(b"manifest"),
3092        )
3093        .unwrap();
3094        let logical = [
3095            CollectionLogicalIdV1::new("collection/a").unwrap(),
3096            CollectionLogicalIdV1::new("collection/b").unwrap(),
3097            CollectionLogicalIdV1::new("collection/c").unwrap(),
3098        ];
3099        let sources = [
3100            CollectionSourceKeyV1::new("a").unwrap(),
3101            CollectionSourceKeyV1::new("b").unwrap(),
3102            CollectionSourceKeyV1::new("c").unwrap(),
3103        ];
3104        let declaration = collection_declaration(
3105            vec![collection_family(
3106                "collection/family",
3107                logical
3108                    .iter()
3109                    .zip(&sources)
3110                    .map(|(logical, source)| {
3111                        CollectionTransitionFamilyMemberV1::new(
3112                            logical.clone(),
3113                            source.clone(),
3114                            0,
3115                            "Walk".into(),
3116                        )
3117                        .unwrap()
3118                    })
3119                    .collect(),
3120            )],
3121            manifest.clone(),
3122        );
3123        let same = loaded_source(document(None), b"same");
3124        let mut different_document = document(None);
3125        different_document.skeleton.bones[0].name = "different".into();
3126        let different = loaded_source(different_document, b"different");
3127        let mismatch = [
3128            CollectionTransitionPoseMemberInputV1::available(
3129                &logical[0],
3130                &sources[0],
3131                0,
3132                "Walk",
3133                &same,
3134            ),
3135            CollectionTransitionPoseMemberInputV1::available(
3136                &logical[1],
3137                &sources[1],
3138                0,
3139                "Walk",
3140                &same,
3141            ),
3142            CollectionTransitionPoseMemberInputV1::available(
3143                &logical[2],
3144                &sources[2],
3145                0,
3146                "Walk",
3147                &different,
3148            ),
3149        ];
3150        let result =
3151            evaluate_collection_transition_poses_v1(&declaration, &manifest, &mismatch).unwrap();
3152        assert_eq!(
3153            result.families()[0].reason(),
3154            Some(TransitionPoseReasonV1::SkeletonBasisMismatch)
3155        );
3156        assert!(result.families()[0].pairs().is_empty());
3157
3158        let mut invalid_document = document(None);
3159        invalid_document.skeleton.bones[0].rest.rotation =
3160            crate::glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
3161        let invalid = loaded_source(invalid_document, b"invalid");
3162        let invalid_members = [
3163            CollectionTransitionPoseMemberInputV1::available(
3164                &logical[0],
3165                &sources[0],
3166                0,
3167                "Walk",
3168                &invalid,
3169            ),
3170            CollectionTransitionPoseMemberInputV1::available(
3171                &logical[1],
3172                &sources[1],
3173                0,
3174                "Walk",
3175                &same,
3176            ),
3177            CollectionTransitionPoseMemberInputV1::available(
3178                &logical[2],
3179                &sources[2],
3180                0,
3181                "Walk",
3182                &different,
3183            ),
3184        ];
3185        let invalid_result =
3186            evaluate_collection_transition_poses_v1(&declaration, &manifest, &invalid_members)
3187                .unwrap();
3188        assert_eq!(
3189            invalid_result.families()[0].reason(),
3190            Some(TransitionPoseReasonV1::MemberUnavailable)
3191        );
3192        assert!(
3193            invalid_result.families()[0]
3194                .skeleton_basis_input()
3195                .is_none()
3196        );
3197        assert!(invalid_result.families()[0].pairs().is_empty());
3198    }
3199
3200    #[test]
3201    fn collection_repeated_families_cache_basis_and_track_admission_by_source_take() {
3202        let manifest = TransitionFamilyManifestIdentityV1::new(
3203            CollectionIdV1::new("collection").unwrap(),
3204            InputIdentity::from_bytes(b"manifest"),
3205        )
3206        .unwrap();
3207        let source = CollectionSourceKeyV1::new("shared").unwrap();
3208        let logical = [
3209            CollectionLogicalIdV1::new("collection/walk").unwrap(),
3210            CollectionLogicalIdV1::new("collection/run").unwrap(),
3211        ];
3212        let family_members = || {
3213            vec![
3214                CollectionTransitionFamilyMemberV1::new(
3215                    logical[0].clone(),
3216                    source.clone(),
3217                    0,
3218                    "Walk".into(),
3219                )
3220                .unwrap(),
3221                CollectionTransitionFamilyMemberV1::new(
3222                    logical[1].clone(),
3223                    source.clone(),
3224                    1,
3225                    "Run".into(),
3226                )
3227                .unwrap(),
3228            ]
3229        };
3230        let declaration = collection_declaration(
3231            vec![
3232                collection_family("collection/first", family_members()),
3233                collection_family("collection/second", family_members()),
3234            ],
3235            manifest.clone(),
3236        );
3237        let shared = loaded_source(document(Some(0.0)), b"shared");
3238        let members = [
3239            CollectionTransitionPoseMemberInputV1::available(
3240                &logical[0],
3241                &source,
3242                0,
3243                "Walk",
3244                &shared,
3245            ),
3246            CollectionTransitionPoseMemberInputV1::available(
3247                &logical[1],
3248                &source,
3249                1,
3250                "Run",
3251                &shared,
3252            ),
3253            CollectionTransitionPoseMemberInputV1::available(
3254                &logical[0],
3255                &source,
3256                0,
3257                "Walk",
3258                &shared,
3259            ),
3260            CollectionTransitionPoseMemberInputV1::available(
3261                &logical[1],
3262                &source,
3263                1,
3264                "Run",
3265                &shared,
3266            ),
3267        ];
3268        let mut basis_builds = 0usize;
3269        let mut track_visits = 0usize;
3270        let result = evaluate_collection_transition_poses_v1_with_probes(
3271            &declaration,
3272            &manifest,
3273            &members,
3274            |_| basis_builds += 1,
3275            |_| track_visits += 1,
3276        )
3277        .unwrap();
3278        assert_eq!(basis_builds, 1, "one source key builds one basis");
3279        assert_eq!(track_visits, 1, "one authored track is admitted once");
3280        assert!(
3281            result
3282                .families()
3283                .iter()
3284                .all(|family| family.status() == TransitionPoseStatusV1::Complete)
3285        );
3286    }
3287
3288    #[test]
3289    fn collection_distinct_family_bases_remain_independently_valid() {
3290        let manifest = TransitionFamilyManifestIdentityV1::new(
3291            CollectionIdV1::new("collection").unwrap(),
3292            InputIdentity::from_bytes(b"manifest"),
3293        )
3294        .unwrap();
3295        let logical = (0..4)
3296            .map(|index| CollectionLogicalIdV1::new(format!("collection/m{index}")).unwrap())
3297            .collect::<Vec<_>>();
3298        let sources = (0..4)
3299            .map(|index| CollectionSourceKeyV1::new(format!("s{index}")).unwrap())
3300            .collect::<Vec<_>>();
3301        let family = |id: &str, range: std::ops::Range<usize>| {
3302            collection_family(
3303                id,
3304                range
3305                    .map(|index| {
3306                        CollectionTransitionFamilyMemberV1::new(
3307                            logical[index].clone(),
3308                            sources[index].clone(),
3309                            0,
3310                            "Walk".into(),
3311                        )
3312                        .unwrap()
3313                    })
3314                    .collect(),
3315            )
3316        };
3317        let declaration = collection_declaration(
3318            vec![
3319                family("collection/first", 0..2),
3320                family("collection/second", 2..4),
3321            ],
3322            manifest.clone(),
3323        );
3324        let first = loaded_source(document(None), b"first");
3325        let mut second_document = document(None);
3326        second_document.skeleton.bones[0].name = "second-root".into();
3327        let second = loaded_source(second_document, b"second");
3328        let members = [
3329            CollectionTransitionPoseMemberInputV1::available(
3330                &logical[0],
3331                &sources[0],
3332                0,
3333                "Walk",
3334                &first,
3335            ),
3336            CollectionTransitionPoseMemberInputV1::available(
3337                &logical[1],
3338                &sources[1],
3339                0,
3340                "Walk",
3341                &first,
3342            ),
3343            CollectionTransitionPoseMemberInputV1::available(
3344                &logical[2],
3345                &sources[2],
3346                0,
3347                "Walk",
3348                &second,
3349            ),
3350            CollectionTransitionPoseMemberInputV1::available(
3351                &logical[3],
3352                &sources[3],
3353                0,
3354                "Walk",
3355                &second,
3356            ),
3357        ];
3358        let result =
3359            evaluate_collection_transition_poses_v1(&declaration, &manifest, &members).unwrap();
3360        assert_eq!(result.status(), TransitionPoseStatusV1::Complete);
3361        assert_ne!(
3362            result.families()[0].skeleton_basis_input(),
3363            result.families()[1].skeleton_basis_input()
3364        );
3365    }
3366
3367    #[test]
3368    fn collection_policy_and_pair_work_refusals_do_not_visit_skeletons_or_tracks() {
3369        let manifest = TransitionFamilyManifestIdentityV1::new(
3370            CollectionIdV1::new("collection").unwrap(),
3371            InputIdentity::from_bytes(b"manifest"),
3372        )
3373        .unwrap();
3374        let source = CollectionSourceKeyV1::new("shared").unwrap();
3375        let policy_logical = [
3376            CollectionLogicalIdV1::new("collection/policy-a").unwrap(),
3377            CollectionLogicalIdV1::new("collection/policy-b").unwrap(),
3378        ];
3379        let policy_family = collection_family_with(
3380            "collection/policy",
3381            TransitionFamilyBoundaryV1::Entry,
3382            0.1,
3383            policy_logical
3384                .iter()
3385                .map(|logical| {
3386                    CollectionTransitionFamilyMemberV1::new(
3387                        logical.clone(),
3388                        source.clone(),
3389                        0,
3390                        "Walk".into(),
3391                    )
3392                    .unwrap()
3393                })
3394                .collect(),
3395        );
3396        let work_logical = (0..92)
3397            .map(|index| CollectionLogicalIdV1::new(format!("collection/work-{index}")).unwrap())
3398            .collect::<Vec<_>>();
3399        let work_family = collection_family(
3400            "collection/work",
3401            work_logical
3402                .iter()
3403                .map(|logical| {
3404                    CollectionTransitionFamilyMemberV1::new(
3405                        logical.clone(),
3406                        source.clone(),
3407                        0,
3408                        "Walk".into(),
3409                    )
3410                    .unwrap()
3411                })
3412                .collect(),
3413        );
3414        let declaration =
3415            collection_declaration(vec![policy_family, work_family], manifest.clone());
3416        let mut poisoned = document(None);
3417        poisoned.skeleton.bones[0].rest.rotation = crate::glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
3418        poisoned.clips[0].tracks.push(Track {
3419            bone: usize::MAX,
3420            property: Property::Translation,
3421            interpolation: Interpolation::Linear,
3422            times: vec![],
3423            values: TrackValues::Vec3s(vec![]),
3424        });
3425        let shared = loaded_source(poisoned, b"shared");
3426        let mut members = policy_logical
3427            .iter()
3428            .map(|logical| {
3429                CollectionTransitionPoseMemberInputV1::available(
3430                    logical, &source, 0, "Walk", &shared,
3431                )
3432            })
3433            .collect::<Vec<_>>();
3434        members.extend(work_logical.iter().map(|logical| {
3435            CollectionTransitionPoseMemberInputV1::available(logical, &source, 0, "Walk", &shared)
3436        }));
3437        let mut basis_builds = 0usize;
3438        let mut track_visits = 0usize;
3439        let result = evaluate_collection_transition_poses_v1_with_probes(
3440            &declaration,
3441            &manifest,
3442            &members,
3443            |_| basis_builds += 1,
3444            |_| track_visits += 1,
3445        )
3446        .unwrap();
3447        assert_eq!(basis_builds, 0);
3448        assert_eq!(track_visits, 0);
3449        assert_eq!(
3450            result.families()[0].reason(),
3451            Some(TransitionPoseReasonV1::TimeToleranceUnsupported)
3452        );
3453        assert_eq!(
3454            result.families()[1].reason(),
3455            Some(TransitionPoseReasonV1::FamilyWorkLimit)
3456        );
3457    }
3458
3459    #[test]
3460    fn collection_raw_and_selected_track_caps_precede_endpoint_sampling() {
3461        let manifest = TransitionFamilyManifestIdentityV1::new(
3462            CollectionIdV1::new("collection").unwrap(),
3463            InputIdentity::from_bytes(b"manifest"),
3464        )
3465        .unwrap();
3466        let logical = [
3467            CollectionLogicalIdV1::new("collection/walk").unwrap(),
3468            CollectionLogicalIdV1::new("collection/run").unwrap(),
3469        ];
3470        let source = CollectionSourceKeyV1::new("shared").unwrap();
3471        let declaration = collection_declaration(
3472            vec![collection_family(
3473                "collection/family",
3474                vec![
3475                    CollectionTransitionFamilyMemberV1::new(
3476                        logical[0].clone(),
3477                        source.clone(),
3478                        0,
3479                        "Walk".into(),
3480                    )
3481                    .unwrap(),
3482                    CollectionTransitionFamilyMemberV1::new(
3483                        logical[1].clone(),
3484                        source.clone(),
3485                        1,
3486                        "Run".into(),
3487                    )
3488                    .unwrap(),
3489                ],
3490            )],
3491            manifest.clone(),
3492        );
3493        let scale = Track {
3494            bone: 0,
3495            property: Property::Scale,
3496            interpolation: Interpolation::Linear,
3497            times: vec![],
3498            values: TrackValues::Vec3s(vec![]),
3499        };
3500        let mut raw_document = document(None);
3501        raw_document.clips[1].tracks = vec![scale; 4];
3502        let raw = loaded_source(raw_document, b"raw");
3503        let raw_members = [
3504            CollectionTransitionPoseMemberInputV1::available(&logical[0], &source, 0, "Walk", &raw),
3505            CollectionTransitionPoseMemberInputV1::available(&logical[1], &source, 1, "Run", &raw),
3506        ];
3507        let mut raw_visits = 0usize;
3508        let raw_result = evaluate_collection_transition_poses_v1_with_probes(
3509            &declaration,
3510            &manifest,
3511            &raw_members,
3512            |_| {},
3513            |_| raw_visits += 1,
3514        )
3515        .unwrap();
3516        assert_eq!(raw_visits, 0, "raw length rejects without a track walk");
3517        assert_eq!(
3518            raw_result.families()[0].reason(),
3519            Some(TransitionPoseReasonV1::InputLimit)
3520        );
3521
3522        let selected_track = Track {
3523            bone: 0,
3524            property: Property::Translation,
3525            interpolation: Interpolation::Linear,
3526            times: vec![0.0],
3527            values: TrackValues::Vec3s(vec![crate::glam::Vec3::ZERO]),
3528        };
3529        let mut selected_document = document(None);
3530        selected_document.clips[1].tracks = vec![selected_track; 3];
3531        let selected = loaded_source(selected_document, b"selected");
3532        let selected_members = [
3533            CollectionTransitionPoseMemberInputV1::available(
3534                &logical[0],
3535                &source,
3536                0,
3537                "Walk",
3538                &selected,
3539            ),
3540            CollectionTransitionPoseMemberInputV1::available(
3541                &logical[1],
3542                &source,
3543                1,
3544                "Run",
3545                &selected,
3546            ),
3547        ];
3548        let mut selected_visits = 0usize;
3549        let selected_result = evaluate_collection_transition_poses_v1_with_probes(
3550            &declaration,
3551            &manifest,
3552            &selected_members,
3553            |_| {},
3554            |_| selected_visits += 1,
3555        )
3556        .unwrap();
3557        assert_eq!(selected_visits, 3);
3558        assert_eq!(
3559            selected_result.families()[0].reason(),
3560            Some(TransitionPoseReasonV1::InputLimit)
3561        );
3562    }
3563
3564    #[test]
3565    fn collection_aggregate_work_and_retention_plans_precede_rejected_family_traversal() {
3566        let manifest = TransitionFamilyManifestIdentityV1::new(
3567            CollectionIdV1::new("collection").unwrap(),
3568            InputIdentity::from_bytes(b"manifest"),
3569        )
3570        .unwrap();
3571        let source = CollectionSourceKeyV1::new("shared").unwrap();
3572        let logical = (0..64)
3573            .map(|index| CollectionLogicalIdV1::new(format!("collection/m{index}")).unwrap())
3574            .collect::<Vec<_>>();
3575        let family_members = || {
3576            logical
3577                .iter()
3578                .map(|logical| {
3579                    CollectionTransitionFamilyMemberV1::new(
3580                        logical.clone(),
3581                        source.clone(),
3582                        0,
3583                        "Walk".into(),
3584                    )
3585                    .unwrap()
3586                })
3587                .collect()
3588        };
3589
3590        let aggregate_declaration = collection_declaration(
3591            (0..17)
3592                .map(|index| {
3593                    collection_family_with(
3594                        &format!("collection/a{index}"),
3595                        TransitionFamilyBoundaryV1::Both,
3596                        0.0,
3597                        family_members(),
3598                    )
3599                })
3600                .collect(),
3601            manifest.clone(),
3602        );
3603        let mut aggregate_document = document(None);
3604        aggregate_document.skeleton.bones.clear();
3605        let aggregate_source = loaded_source(aggregate_document, b"aggregate");
3606        let aggregate_members = (0..17)
3607            .flat_map(|_| {
3608                logical.iter().map(|logical| {
3609                    CollectionTransitionPoseMemberInputV1::available(
3610                        logical,
3611                        &source,
3612                        0,
3613                        "Walk",
3614                        &aggregate_source,
3615                    )
3616                })
3617            })
3618            .collect::<Vec<_>>();
3619        let mut basis_builds = 0usize;
3620        let aggregate = evaluate_collection_transition_poses_v1_with_probes(
3621            &aggregate_declaration,
3622            &manifest,
3623            &aggregate_members,
3624            |_| basis_builds += 1,
3625            |_| panic!("aggregate-rejected families must not visit tracks"),
3626        )
3627        .unwrap();
3628        assert_eq!(
3629            basis_builds, 1,
3630            "only eligible source authority is built once"
3631        );
3632        assert_eq!(
3633            aggregate.families()[16].reason(),
3634            Some(TransitionPoseReasonV1::AggregateWorkLimit)
3635        );
3636
3637        let retention_declaration = collection_declaration(
3638            vec![
3639                collection_family("collection/r1", family_members()),
3640                collection_family("collection/r2", family_members()),
3641            ],
3642            manifest.clone(),
3643        );
3644        let mut retention_document = document(None);
3645        retention_document.skeleton.bones = (0..16)
3646            .map(|index| Bone {
3647                name: format!("b{index}"),
3648                parent: None,
3649                rest: Transform::IDENTITY,
3650                inverse_bind: None,
3651            })
3652            .collect();
3653        let retention_source = loaded_source(retention_document, b"retention");
3654        let retention_members = (0..2)
3655            .flat_map(|_| {
3656                logical.iter().map(|logical| {
3657                    CollectionTransitionPoseMemberInputV1::available(
3658                        logical,
3659                        &source,
3660                        0,
3661                        "Walk",
3662                        &retention_source,
3663                    )
3664                })
3665            })
3666            .collect::<Vec<_>>();
3667        let retention = evaluate_collection_transition_poses_v1(
3668            &retention_declaration,
3669            &manifest,
3670            &retention_members,
3671        )
3672        .unwrap();
3673        assert_eq!(
3674            retention.families()[1].reason(),
3675            Some(TransitionPoseReasonV1::RetentionLimit)
3676        );
3677    }
3678
3679    #[test]
3680    fn collection_result_budget_refusal_retains_basis_and_nullable_member_authority() {
3681        let manifest = TransitionFamilyManifestIdentityV1::new(
3682            CollectionIdV1::new("collection").unwrap(),
3683            InputIdentity::from_bytes(b"manifest"),
3684        )
3685        .unwrap();
3686        let source = CollectionSourceKeyV1::new("large").unwrap();
3687        let large_logical = (0..16)
3688            .map(|index| CollectionLogicalIdV1::new(format!("collection/large-{index}")).unwrap())
3689            .collect::<Vec<_>>();
3690        let missing_logical = [
3691            CollectionLogicalIdV1::new("collection/missing-a").unwrap(),
3692            CollectionLogicalIdV1::new("collection/missing-b").unwrap(),
3693        ];
3694        let missing_sources = [
3695            CollectionSourceKeyV1::new("missing-a").unwrap(),
3696            CollectionSourceKeyV1::new("missing-b").unwrap(),
3697        ];
3698        let declaration = collection_declaration(
3699            vec![
3700                collection_family(
3701                    "collection/large",
3702                    large_logical
3703                        .iter()
3704                        .map(|logical| {
3705                            CollectionTransitionFamilyMemberV1::new(
3706                                logical.clone(),
3707                                source.clone(),
3708                                0,
3709                                "Walk".into(),
3710                            )
3711                            .unwrap()
3712                        })
3713                        .collect(),
3714                ),
3715                collection_family(
3716                    "collection/missing",
3717                    missing_logical
3718                        .iter()
3719                        .zip(&missing_sources)
3720                        .map(|(logical, source)| {
3721                            CollectionTransitionFamilyMemberV1::new(
3722                                logical.clone(),
3723                                source.clone(),
3724                                0,
3725                                "Walk".into(),
3726                            )
3727                            .unwrap()
3728                        })
3729                        .collect(),
3730                ),
3731            ],
3732            manifest.clone(),
3733        );
3734        let mut large_document = document(None);
3735        large_document.skeleton.bones[0].name =
3736            "x".repeat(TRANSITION_POSE_EVALUATION_V1_MAX_BASIS_TEXT_BYTES);
3737        let large = loaded_source(large_document, b"large");
3738        let mut members = large_logical
3739            .iter()
3740            .map(|logical| {
3741                CollectionTransitionPoseMemberInputV1::available(
3742                    logical, &source, 0, "Walk", &large,
3743                )
3744            })
3745            .collect::<Vec<_>>();
3746        members.extend(
3747            missing_logical
3748                .iter()
3749                .zip(&missing_sources)
3750                .map(|(logical, source)| {
3751                    CollectionTransitionPoseMemberInputV1::unavailable(logical, source, 0, "Walk")
3752                }),
3753        );
3754        let result =
3755            evaluate_collection_transition_poses_v1(&declaration, &manifest, &members).unwrap();
3756        assert_eq!(
3757            result.families()[0].reason(),
3758            Some(TransitionPoseReasonV1::ResultLimit)
3759        );
3760        assert!(result.families()[0].skeleton_basis_input().is_some());
3761        assert_eq!(
3762            result.families()[1].reason(),
3763            Some(TransitionPoseReasonV1::MemberUnavailable)
3764        );
3765        assert!(result.families()[1].skeleton_basis_input().is_none());
3766        assert!(result.families()[1].members()[0].source_input().is_none());
3767        result.normalized_jcs().unwrap();
3768    }
3769
3770    #[test]
3771    fn basis_normalizes_quaternions_and_excludes_scale_bind_and_assets() {
3772        let first = document(None);
3773        let mut second = first.clone();
3774        second.skeleton.bones[0].rest.rotation = crate::glam::Quat::from_xyzw(0.0, -0.0, 0.0, -1.0);
3775        second.skeleton.bones[0].rest.scale = crate::glam::Vec3::splat(7.0);
3776        second.skeleton.bones[0].inverse_bind =
3777            Some(crate::glam::Mat4::from_scale(crate::glam::Vec3::splat(3.0)));
3778        let left = SkeletonBasisV1::from_skeleton(&first.skeleton).unwrap();
3779        let right = SkeletonBasisV1::from_skeleton(&second.skeleton).unwrap();
3780        assert_eq!(left.identity(), right.identity());
3781        second.skeleton.bones[0].name = "other".into();
3782        assert_ne!(
3783            left.identity(),
3784            SkeletonBasisV1::from_skeleton(&second.skeleton)
3785                .unwrap()
3786                .identity()
3787        );
3788        let parent_after_child = Skeleton {
3789            bones: vec![
3790                Bone {
3791                    name: "child".into(),
3792                    parent: Some(1),
3793                    rest: Transform::IDENTITY,
3794                    inverse_bind: None,
3795                },
3796                Bone {
3797                    name: "parent".into(),
3798                    parent: None,
3799                    rest: Transform::IDENTITY,
3800                    inverse_bind: None,
3801                },
3802            ],
3803        };
3804        assert_eq!(
3805            SkeletonBasisV1::from_skeleton(&parent_after_child),
3806            Err(SkeletonBasisError::InvalidParent { ordinal: 0 })
3807        );
3808    }
3809
3810    #[test]
3811    fn endpoint_comparison_is_inclusive_and_binds_raw_and_normalized_identities() {
3812        let declared = declaration(TransitionFamilyBoundaryV1::Both, 3.0_f64.sqrt(), 0.0);
3813        let document = document(Some(1.0));
3814        let raw = InputIdentity::from_bytes(b"document\r\n");
3815        let result =
3816            evaluate_document_transition_poses_v1(&declared, raw.clone(), &document).unwrap();
3817        assert_eq!(result.status(), TransitionPoseStatusV1::Complete);
3818        assert_eq!(result.decision(), TransitionPoseDecisionV1::Pass);
3819        assert_eq!(result.subject_input(), &raw);
3820        assert_eq!(result.declaration_input(), declared.source_identity());
3821        assert_eq!(
3822            result.declaration_normalized(),
3823            declared.normalized_identity()
3824        );
3825        assert_eq!(result.families()[0].pairs.len(), 2);
3826        assert!(
3827            result.families()[0]
3828                .pairs
3829                .iter()
3830                .all(|pair| pair.translation_offenders.is_empty())
3831        );
3832
3833        let finding = evaluate_document_transition_poses_v1(
3834            &declaration(TransitionFamilyBoundaryV1::Entry, 1.0, 0.0),
3835            raw,
3836            &document,
3837        )
3838        .unwrap();
3839        assert_eq!(finding.decision(), TransitionPoseDecisionV1::Finding);
3840        assert_eq!(
3841            finding.families()[0].pairs[0].translation_offenders.len(),
3842            1
3843        );
3844    }
3845
3846    #[test]
3847    fn complete_results_bind_external_dependency_changes_beyond_primary_bytes() {
3848        let declared = declaration(TransitionFamilyBoundaryV1::Entry, 100.0, 0.0);
3849        let primary = InputIdentity::from_bytes(br#"{"buffers":[{"uri":"animation.bin"}]}"#);
3850        let first_closure = closure_with_external_buffer(primary.clone(), b"first animation");
3851        let second_closure = closure_with_external_buffer(primary.clone(), b"second animation");
3852
3853        let first = super::evaluate_document_transition_poses_v1(
3854            &declared,
3855            &first_closure,
3856            &document(Some(1.0)),
3857        )
3858        .unwrap();
3859        let second = super::evaluate_document_transition_poses_v1(
3860            &declared,
3861            &second_closure,
3862            &document(Some(2.0)),
3863        )
3864        .unwrap();
3865
3866        assert_eq!(first.subject_input(), second.subject_input());
3867        assert_eq!(first.subject_input(), &primary);
3868        assert_ne!(
3869            first.subject_dependency_closure_identity(),
3870            second.subject_dependency_closure_identity()
3871        );
3872        assert_eq!(first.status(), TransitionPoseStatusV1::Complete);
3873        assert_eq!(second.status(), TransitionPoseStatusV1::Complete);
3874        assert_ne!(
3875            first.families()[0].pairs()[0].max_translation_delta_m(),
3876            second.families()[0].pairs()[0].max_translation_delta_m()
3877        );
3878        for result in [&first, &second] {
3879            let subject_closure = result
3880                .subject_dependency_closure_identity()
3881                .expect("complete result closure identity");
3882            assert!(result.families()[0].members().iter().all(|member| {
3883                member.source_input() == Some(result.subject_input())
3884                    && member.source_dependency_closure_identity() == Some(subject_closure)
3885            }));
3886        }
3887        assert_ne!(
3888            first.normalized_jcs().unwrap(),
3889            second.normalized_jcs().unwrap()
3890        );
3891    }
3892
3893    #[test]
3894    fn incomplete_dependency_closure_cannot_produce_a_complete_outcome() {
3895        let primary = InputIdentity::from_bytes(b"document");
3896        let closure = DependencyClosureV1::unavailable(primary.clone());
3897        assert!(!closure.coverage().is_complete());
3898        assert!(closure.identity().is_none());
3899
3900        let result = super::evaluate_document_transition_poses_v1(
3901            &declaration(TransitionFamilyBoundaryV1::Entry, 100.0, 0.0),
3902            &closure,
3903            &document(Some(1.0)),
3904        )
3905        .unwrap();
3906        assert_eq!(result.subject_input(), &primary);
3907        assert_eq!(result.status(), TransitionPoseStatusV1::Incomplete);
3908        assert_eq!(result.decision(), TransitionPoseDecisionV1::NotEvaluated);
3909        assert!(result.subject_dependency_closure_identity().is_none());
3910        assert_eq!(
3911            result.families()[0].reason(),
3912            Some(TransitionPoseReasonV1::DependencyClosureIncomplete)
3913        );
3914        assert!(result.families()[0].members().iter().all(|member| {
3915            member.source_input() == Some(&primary)
3916                && member.source_dependency_closure_identity().is_none()
3917        }));
3918        let serialized_len = result.normalized_jcs().unwrap().len();
3919        let exact = evaluate_document_transition_poses_v1_with_result_limit(
3920            &declaration(TransitionFamilyBoundaryV1::Entry, 100.0, 0.0),
3921            &closure,
3922            &document(Some(1.0)),
3923            serialized_len,
3924        )
3925        .unwrap();
3926        assert_eq!(
3927            exact.families()[0].reason(),
3928            Some(TransitionPoseReasonV1::DependencyClosureIncomplete)
3929        );
3930        assert_eq!(
3931            evaluate_document_transition_poses_v1_with_result_limit(
3932                &declaration(TransitionFamilyBoundaryV1::Entry, 100.0, 0.0),
3933                &closure,
3934                &document(Some(1.0)),
3935                serialized_len - 1,
3936            ),
3937            Err(TransitionPoseEvaluationControlError::ResultTooLarge)
3938        );
3939
3940        let empty = TransitionFamilyDeclarationInputV1::new(
3941            TransitionFamilyDeclarationV1::document(Vec::new()).unwrap(),
3942            b"empty",
3943        )
3944        .unwrap();
3945        let no_config =
3946            super::evaluate_document_transition_poses_v1(&empty, &closure, &document(None))
3947                .unwrap();
3948        assert_eq!(no_config.status(), TransitionPoseStatusV1::Complete);
3949        assert_eq!(no_config.decision(), TransitionPoseDecisionV1::Pass);
3950        assert_eq!(
3951            no_config.reason(),
3952            Some(TransitionPoseReasonV1::NoConfiguredFamilies)
3953        );
3954        assert!(no_config.subject_dependency_closure_identity().is_none());
3955    }
3956
3957    #[test]
3958    fn rotation_delta_is_reflexive_sign_invariant_and_has_the_right_angle() {
3959        let axis = crate::glam::Vec3::new(1.0, 2.0, 3.0).normalize();
3960        for angle in [0.1, 0.9, 1.7] {
3961            let rotation =
3962                canonical_quaternion(crate::glam::Quat::from_axis_angle(axis, angle), 0).unwrap();
3963            assert_eq!(rotation_delta_deg(rotation, rotation), 0.0);
3964            assert_eq!(
3965                rotation_delta_deg(rotation, rotation.map(|value| -value)),
3966                0.0
3967            );
3968        }
3969        let left = canonical_quaternion(
3970            crate::glam::Quat::from_axis_angle(axis, 40f32.to_radians()),
3971            0,
3972        )
3973        .unwrap();
3974        let right = canonical_quaternion(
3975            crate::glam::Quat::from_axis_angle(axis, 130f32.to_radians()),
3976            0,
3977        )
3978        .unwrap();
3979        assert!((rotation_delta_deg(left, right) - 90.0).abs() < 1e-5);
3980
3981        let mut document = document(None);
3982        let rotation = crate::glam::Quat::from_axis_angle(axis, 0.9);
3983        for clip in &mut document.clips {
3984            clip.tracks.push(Track {
3985                bone: 0,
3986                property: Property::Rotation,
3987                interpolation: Interpolation::Linear,
3988                times: vec![0.0],
3989                values: TrackValues::Quats(vec![rotation]),
3990            });
3991        }
3992        let result = evaluate_document_transition_poses_v1(
3993            &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
3994            InputIdentity::from_bytes(b"document"),
3995            &document,
3996        )
3997        .unwrap();
3998        assert_eq!(result.decision(), TransitionPoseDecisionV1::Pass);
3999        assert_eq!(
4000            result.families()[0].pairs()[0].max_rotation_delta_deg(),
4001            0.0
4002        );
4003        document.clips[1].tracks[0].values = TrackValues::Quats(vec![-rotation]);
4004        let sign_equivalent = evaluate_document_transition_poses_v1(
4005            &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4006            InputIdentity::from_bytes(b"document"),
4007            &document,
4008        )
4009        .unwrap();
4010        assert_eq!(sign_equivalent.decision(), TransitionPoseDecisionV1::Pass);
4011        assert_eq!(
4012            sign_equivalent.families()[0].pairs()[0].max_rotation_delta_deg(),
4013            0.0
4014        );
4015    }
4016
4017    #[test]
4018    fn scale_tracks_are_wholly_ignored_by_v1_admission_validation_and_sampling() {
4019        let declared = declaration(TransitionFamilyBoundaryV1::Both, 0.0, 0.0);
4020        let raw = InputIdentity::from_bytes(b"document");
4021        let baseline =
4022            evaluate_document_transition_poses_v1(&declared, raw.clone(), &document(Some(1.0)))
4023                .unwrap();
4024        let mut with_scale_noise = document(Some(1.0));
4025        let malformed_scale = Track {
4026            bone: usize::MAX,
4027            property: Property::Scale,
4028            interpolation: Interpolation::Linear,
4029            times: vec![f32::NAN],
4030            values: TrackValues::Vec3s(Vec::new()),
4031        };
4032        // One selected translation plus two malformed scale rows is exactly
4033        // the raw 3*bones admission cap. The scale rows remain semantically
4034        // invisible once that resource boundary is admitted.
4035        with_scale_noise.clips[1]
4036            .tracks
4037            .extend(std::iter::repeat_n(malformed_scale.clone(), 2));
4038        let with_scale_noise =
4039            evaluate_document_transition_poses_v1(&declared, raw.clone(), &with_scale_noise)
4040                .unwrap();
4041        assert_eq!(with_scale_noise, baseline);
4042
4043        let mut one_too_many = document(Some(1.0));
4044        one_too_many.clips[1]
4045            .tracks
4046            .extend(std::iter::repeat_n(malformed_scale, 3));
4047        let one_too_many =
4048            evaluate_document_transition_poses_v1(&declared, raw, &one_too_many).unwrap();
4049        assert_eq!(
4050            one_too_many.families()[0].reason(),
4051            Some(TransitionPoseReasonV1::InputLimit)
4052        );
4053    }
4054
4055    #[test]
4056    fn selected_track_validation_is_linear_and_rejects_duplicate_tr_channels() {
4057        let mut tracks =
4058            Vec::with_capacity(TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACKS_PER_CLIP);
4059        for bone in 0..TRANSITION_POSE_EVALUATION_V1_MAX_BONES {
4060            tracks.push(Track {
4061                bone,
4062                property: Property::Translation,
4063                interpolation: Interpolation::Linear,
4064                times: vec![0.0],
4065                values: TrackValues::Vec3s(vec![crate::glam::Vec3::ZERO]),
4066            });
4067            tracks.push(Track {
4068                bone,
4069                property: Property::Rotation,
4070                interpolation: Interpolation::Linear,
4071                times: vec![0.0],
4072                values: TrackValues::Quats(vec![crate::glam::Quat::IDENTITY]),
4073            });
4074        }
4075        let mut clip = Clip {
4076            name: "all-tr".into(),
4077            duration_s: 1.0,
4078            tracks,
4079        };
4080        let mut visits = 0usize;
4081        assert!(selected_tracks_are_strict_with(
4082            &clip,
4083            TRANSITION_POSE_EVALUATION_V1_MAX_BONES,
4084            |_| visits += 1,
4085        ));
4086        assert_eq!(
4087            visits,
4088            TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACKS_PER_CLIP
4089        );
4090
4091        clip.tracks.push(clip.tracks[0].clone());
4092        assert!(!selected_tracks_are_strict(
4093            &clip,
4094            TRANSITION_POSE_EVALUATION_V1_MAX_BONES
4095        ));
4096    }
4097
4098    #[test]
4099    fn time_and_duration_refusals_are_complete_family_unavailability() {
4100        let document = document(None);
4101        let time = evaluate_document_transition_poses_v1(
4102            &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.01),
4103            InputIdentity::from_bytes(b"document"),
4104            &document,
4105        )
4106        .unwrap();
4107        assert_eq!(time.status(), TransitionPoseStatusV1::Incomplete);
4108        assert_eq!(
4109            time.families()[0].reason,
4110            Some(TransitionPoseReasonV1::TimeToleranceUnsupported)
4111        );
4112
4113        let mut zero = document;
4114        zero.clips[1].duration_s = 0.0;
4115        let duration = evaluate_document_transition_poses_v1(
4116            &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4117            InputIdentity::from_bytes(b"document"),
4118            &zero,
4119        )
4120        .unwrap();
4121        assert_eq!(
4122            duration.families()[0].reason,
4123            Some(TransitionPoseReasonV1::ZeroDuration)
4124        );
4125    }
4126
4127    #[test]
4128    fn no_configured_families_does_not_traverse_mutable_document_payloads() {
4129        let mut document = document(None);
4130        document.skeleton.bones[0].parent = Some(0);
4131        let empty = TransitionFamilyDeclarationInputV1::new(
4132            TransitionFamilyDeclarationV1::document(Vec::new()).unwrap(),
4133            b"empty",
4134        )
4135        .unwrap();
4136        let result = evaluate_document_transition_poses_v1(
4137            &empty,
4138            InputIdentity::from_bytes(b"document"),
4139            &document,
4140        )
4141        .unwrap();
4142        assert_eq!(
4143            result.reason(),
4144            Some(TransitionPoseReasonV1::NoConfiguredFamilies)
4145        );
4146    }
4147
4148    #[test]
4149    fn admission_limits_precede_selected_allocation_but_not_witness_control() {
4150        let mut oversized = document(None);
4151        oversized.skeleton.bones = (0..TRANSITION_POSE_EVALUATION_V1_MAX_BONES + 1)
4152            .map(|ordinal| Bone {
4153                name: format!("bone-{ordinal}"),
4154                parent: None,
4155                rest: Transform::IDENTITY,
4156                inverse_bind: None,
4157            })
4158            .collect();
4159        let invalid = DocumentTransitionFamilyV1::new(
4160            "invalid".into(),
4161            TransitionFamilyBoundaryV1::Entry,
4162            TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4163            vec![
4164                DocumentTransitionFamilyMemberV1::new(2, "missing-a".into()).unwrap(),
4165                DocumentTransitionFamilyMemberV1::new(3, "missing-b".into()).unwrap(),
4166            ],
4167        )
4168        .unwrap();
4169        let invalid = TransitionFamilyDeclarationInputV1::new(
4170            TransitionFamilyDeclarationV1::document(vec![invalid]).unwrap(),
4171            b"declaration",
4172        )
4173        .unwrap();
4174        assert_eq!(
4175            evaluate_document_transition_poses_v1(
4176                &invalid,
4177                InputIdentity::from_bytes(b"document"),
4178                &oversized,
4179            ),
4180            Err(TransitionPoseEvaluationControlError::InvalidMemberWitness)
4181        );
4182
4183        let mut selected = document(None);
4184        let track = Track {
4185            bone: 0,
4186            property: Property::Translation,
4187            interpolation: Interpolation::Linear,
4188            times: vec![0.0],
4189            values: TrackValues::Vec3s(vec![crate::glam::Vec3::ZERO]),
4190        };
4191        selected.clips[1].tracks =
4192            vec![track.clone(); TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACKS_PER_CLIP + 1];
4193        let limited = evaluate_document_transition_poses_v1(
4194            &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4195            InputIdentity::from_bytes(b"document"),
4196            &selected,
4197        )
4198        .unwrap();
4199        assert_eq!(
4200            limited.families()[0].reason(),
4201            Some(TransitionPoseReasonV1::InputLimit)
4202        );
4203
4204        let small_selected = document(Some(1.0));
4205        let selected_clip = &small_selected.clips[1];
4206        assert_eq!(
4207            plan_selected_track_input_limits_with(&[vec![selected_clip]], 1, 1, 1, &[false]),
4208            vec![true]
4209        );
4210        let oversized_tracks = Clip {
4211            name: "many".into(),
4212            duration_s: 1.0,
4213            tracks: vec![track.clone(); 2],
4214        };
4215        let small = Clip {
4216            name: "small".into(),
4217            duration_s: 1.0,
4218            tracks: vec![track.clone()],
4219        };
4220        assert_eq!(
4221            plan_selected_track_input_limits_with(
4222                &[vec![&oversized_tracks], vec![&small]],
4223                1,
4224                1,
4225                8,
4226                &[false, false],
4227            ),
4228            vec![true, false]
4229        );
4230        assert_eq!(
4231            plan_selected_track_input_limits_with(
4232                &[vec![&small], vec![&oversized_tracks], vec![&small]],
4233                3,
4234                3,
4235                5,
4236                &[false, false, false],
4237            ),
4238            vec![false, true, false]
4239        );
4240        let huge_unsupported = Clip {
4241            name: "policy-only".into(),
4242            duration_s: 1.0,
4243            tracks: vec![track.clone(); 4],
4244        };
4245        assert_eq!(
4246            plan_selected_track_input_limits_with(
4247                &[vec![&huge_unsupported], vec![&small]],
4248                3,
4249                3,
4250                5,
4251                &[true, false],
4252            ),
4253            vec![false, false]
4254        );
4255        let named = Skeleton {
4256            bones: vec![Bone {
4257                name: "x".repeat(16),
4258                parent: None,
4259                rest: Transform::IDENTITY,
4260                inverse_bind: None,
4261            }],
4262        };
4263        assert!(!skeleton_input_is_within_limits_with(&named, 15));
4264        assert_eq!(
4265            TRANSITION_POSE_EVALUATION_V1_MAX_RAW_TRACK_ROWS_PER_CLIP,
4266            TRANSITION_POSE_EVALUATION_V1_MAX_BONES * 3
4267        );
4268        let oversized_name = Skeleton {
4269            bones: vec![Bone {
4270                name: "x".repeat(TRANSITION_POSE_EVALUATION_V1_MAX_BASIS_TEXT_BYTES + 1),
4271                parent: None,
4272                rest: Transform::IDENTITY,
4273                inverse_bind: None,
4274            }],
4275        };
4276        assert_eq!(
4277            SkeletonBasisV1::from_skeleton(&oversized_name),
4278            Err(SkeletonBasisError::TooMuchText)
4279        );
4280
4281        let four_tracks = Clip {
4282            name: "four".into(),
4283            duration_s: 1.0,
4284            tracks: vec![track.clone(); 4],
4285        };
4286        assert_eq!(
4287            plan_selected_track_input_limits(&[vec![&four_tracks]], 1, &[false]),
4288            vec![true]
4289        );
4290
4291        let mut scaled = document(None);
4292        scaled.skeleton.bones[0].rest.scale = crate::glam::Vec3::NAN;
4293        assert_eq!(
4294            evaluate_document_transition_poses_v1(
4295                &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4296                InputIdentity::from_bytes(b"document"),
4297                &scaled,
4298            )
4299            .unwrap()
4300            .status(),
4301            TransitionPoseStatusV1::Complete
4302        );
4303    }
4304
4305    #[test]
4306    fn clip_admission_bounds_global_witness_lookup_after_direct_witness_checks() {
4307        let declared = declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0);
4308        let mut over_cap = document(None);
4309        for ordinal in over_cap.clips.len()..TRANSITION_POSE_EVALUATION_V1_MAX_DOCUMENT_CLIPS {
4310            over_cap.clips.push(Clip {
4311                name: format!("extra-{ordinal}"),
4312                duration_s: 1.0,
4313                tracks: Vec::new(),
4314            });
4315        }
4316        // The first excess row duplicates a declared name. It deliberately is
4317        // not globally scanned once the document leaves the admitted domain.
4318        over_cap.clips.push(Clip {
4319            name: "Walk".into(),
4320            duration_s: 1.0,
4321            tracks: Vec::new(),
4322        });
4323        let limited = evaluate_document_transition_poses_v1(
4324            &declared,
4325            InputIdentity::from_bytes(b"document"),
4326            &over_cap,
4327        )
4328        .unwrap();
4329        assert_eq!(
4330            limited.families()[0].reason(),
4331            Some(TransitionPoseReasonV1::InputLimit)
4332        );
4333
4334        over_cap.clips[0].name = "stale".into();
4335        assert_eq!(
4336            evaluate_document_transition_poses_v1(
4337                &declared,
4338                InputIdentity::from_bytes(b"document"),
4339                &over_cap,
4340            ),
4341            Err(TransitionPoseEvaluationControlError::InvalidMemberWitness)
4342        );
4343    }
4344
4345    #[test]
4346    fn unrelated_assets_and_clips_do_not_block_transition_pose() {
4347        let mut document = document(None);
4348        document.assets.instances.push(crate::model::MeshInstance {
4349            node: 99,
4350            ..crate::model::MeshInstance::default()
4351        });
4352        let track = Track {
4353            bone: 99,
4354            property: Property::Translation,
4355            interpolation: Interpolation::Linear,
4356            times: Vec::new(),
4357            values: TrackValues::Vec3s(Vec::new()),
4358        };
4359        document.clips.push(Clip {
4360            name: "Unselected".into(),
4361            duration_s: 1.0,
4362            tracks: vec![track; TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACKS_PER_CLIP + 1],
4363        });
4364        assert_eq!(
4365            evaluate_document_transition_poses_v1(
4366                &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4367                InputIdentity::from_bytes(b"document"),
4368                &document,
4369            )
4370            .unwrap()
4371            .status(),
4372            TransitionPoseStatusV1::Complete
4373        );
4374    }
4375
4376    #[test]
4377    fn public_evaluator_rejects_wrong_scope_and_oversized_basis() {
4378        let collection_id = CollectionIdV1::new("collection").unwrap();
4379        let manifest = TransitionFamilyManifestIdentityV1::new(
4380            collection_id.clone(),
4381            InputIdentity::from_bytes(b"manifest"),
4382        )
4383        .unwrap();
4384        let members = ["one", "two"]
4385            .into_iter()
4386            .map(|suffix| {
4387                CollectionTransitionFamilyMemberV1::new(
4388                    CollectionLogicalIdV1::new(format!("collection/{suffix}")).unwrap(),
4389                    CollectionSourceKeyV1::new("source").unwrap(),
4390                    0,
4391                    suffix.into(),
4392                )
4393                .unwrap()
4394            })
4395            .collect();
4396        let collection = TransitionFamilyDeclarationV1::collection(
4397            manifest,
4398            vec![
4399                CollectionTransitionFamilyV1::new(
4400                    CollectionLogicalIdV1::new("collection/family").unwrap(),
4401                    TransitionFamilyBoundaryV1::Entry,
4402                    TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4403                    members,
4404                )
4405                .unwrap(),
4406            ],
4407        )
4408        .unwrap();
4409        let collection =
4410            TransitionFamilyDeclarationInputV1::new(collection, b"collection").unwrap();
4411        assert_eq!(
4412            evaluate_document_transition_poses_v1(
4413                &collection,
4414                InputIdentity::from_bytes(b"document"),
4415                &Document::default(),
4416            ),
4417            Err(TransitionPoseEvaluationControlError::WrongDeclarationScope)
4418        );
4419
4420        let mut oversized = document(None);
4421        oversized.skeleton.bones = (0..TRANSITION_POSE_EVALUATION_V1_MAX_BONES + 1)
4422            .map(|ordinal| Bone {
4423                name: format!("bone-{ordinal}"),
4424                parent: None,
4425                rest: Transform::IDENTITY,
4426                inverse_bind: None,
4427            })
4428            .collect();
4429        let limited = evaluate_document_transition_poses_v1(
4430            &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4431            InputIdentity::from_bytes(b"document"),
4432            &oversized,
4433        )
4434        .unwrap();
4435        assert_eq!(limited.status(), TransitionPoseStatusV1::Incomplete);
4436        assert_eq!(
4437            limited.families()[0].reason(),
4438            Some(TransitionPoseReasonV1::InputLimit)
4439        );
4440        assert_eq!(limited.families()[0].skeleton_basis_input(), None);
4441    }
4442
4443    #[test]
4444    fn selected_track_shape_gaps_are_incomplete_but_unselected_tracks_are_ignored() {
4445        let declaration = declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0);
4446        let mut variants = Vec::new();
4447
4448        let mut empty = document(Some(1.0));
4449        empty.clips[1].tracks[0].times.clear();
4450        variants.push(empty);
4451
4452        let mut nonfinite_time = document(Some(1.0));
4453        nonfinite_time.clips[1].tracks[0].times[0] = f32::NAN;
4454        variants.push(nonfinite_time);
4455
4456        let mut non_increasing = document(Some(1.0));
4457        non_increasing.clips[1].tracks[0].times[1] = 0.0;
4458        variants.push(non_increasing);
4459
4460        let mut wrong_cardinality = document(Some(1.0));
4461        wrong_cardinality.clips[1].tracks[0].values = TrackValues::Vec3s(Vec::new());
4462        variants.push(wrong_cardinality);
4463
4464        let mut wrong_type = document(Some(1.0));
4465        wrong_type.clips[1].tracks[0].values = TrackValues::Quats(vec![
4466            crate::glam::Quat::IDENTITY,
4467            crate::glam::Quat::IDENTITY,
4468        ]);
4469        variants.push(wrong_type);
4470
4471        let mut nonfinite_value = document(Some(1.0));
4472        nonfinite_value.clips[1].tracks[0].values =
4473            TrackValues::Vec3s(vec![crate::glam::Vec3::NAN, crate::glam::Vec3::ZERO]);
4474        variants.push(nonfinite_value);
4475
4476        let mut duplicate = document(Some(1.0));
4477        let duplicate_track = duplicate.clips[1].tracks[0].clone();
4478        duplicate.clips[1].tracks.push(duplicate_track);
4479        variants.push(duplicate);
4480
4481        let mut out_of_range = document(Some(1.0));
4482        out_of_range.clips[1].tracks[0].bone = 1;
4483        variants.push(out_of_range);
4484
4485        for document in variants {
4486            let result = evaluate_document_transition_poses_v1(
4487                &declaration,
4488                InputIdentity::from_bytes(b"document"),
4489                &document,
4490            )
4491            .expect("selected track gaps are result data, not control errors");
4492            assert_eq!(result.status(), TransitionPoseStatusV1::Incomplete);
4493            assert_eq!(
4494                result.families()[0].reason,
4495                Some(TransitionPoseReasonV1::UnsupportedSampling)
4496            );
4497        }
4498
4499        let mut unrelated = document(None);
4500        unrelated.clips.push(Clip {
4501            name: "Unselected".into(),
4502            duration_s: 1.0,
4503            tracks: vec![Track {
4504                bone: 99,
4505                property: Property::Translation,
4506                interpolation: Interpolation::Linear,
4507                times: Vec::new(),
4508                values: TrackValues::Vec3s(Vec::new()),
4509            }],
4510        });
4511        let result = evaluate_document_transition_poses_v1(
4512            &declaration,
4513            InputIdentity::from_bytes(b"document"),
4514            &unrelated,
4515        )
4516        .unwrap();
4517        assert_eq!(result.status(), TransitionPoseStatusV1::Complete);
4518    }
4519
4520    #[test]
4521    fn endpoint_sampling_does_not_consume_the_unconfigured_boundary() {
4522        let mut exit_only_gap = document(Some(0.0));
4523        exit_only_gap.clips[1].tracks.push(Track {
4524            bone: 0,
4525            property: Property::Rotation,
4526            interpolation: Interpolation::Linear,
4527            times: vec![0.0, 1.0],
4528            values: TrackValues::Quats(vec![
4529                crate::glam::Quat::IDENTITY,
4530                crate::glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0),
4531            ]),
4532        });
4533        let entry = evaluate_document_transition_poses_v1(
4534            &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4535            InputIdentity::from_bytes(b"document"),
4536            &exit_only_gap,
4537        )
4538        .unwrap();
4539        assert_eq!(entry.status(), TransitionPoseStatusV1::Complete);
4540        let exit = evaluate_document_transition_poses_v1(
4541            &declaration(TransitionFamilyBoundaryV1::Exit, 0.0, 0.0),
4542            InputIdentity::from_bytes(b"document"),
4543            &exit_only_gap,
4544        )
4545        .unwrap();
4546        assert_eq!(exit.status(), TransitionPoseStatusV1::Incomplete);
4547        assert_eq!(
4548            exit.families()[0].reason,
4549            Some(TransitionPoseReasonV1::UnsupportedSampling)
4550        );
4551
4552        let mut entry_only_gap = document(Some(0.0));
4553        entry_only_gap.clips[1].tracks.push(Track {
4554            bone: 0,
4555            property: Property::Rotation,
4556            interpolation: Interpolation::Linear,
4557            times: vec![0.0, 1.0],
4558            values: TrackValues::Quats(vec![
4559                crate::glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0),
4560                crate::glam::Quat::IDENTITY,
4561            ]),
4562        });
4563        let entry = evaluate_document_transition_poses_v1(
4564            &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4565            InputIdentity::from_bytes(b"document"),
4566            &entry_only_gap,
4567        )
4568        .unwrap();
4569        assert_eq!(entry.status(), TransitionPoseStatusV1::Incomplete);
4570        let exit = evaluate_document_transition_poses_v1(
4571            &declaration(TransitionFamilyBoundaryV1::Exit, 0.0, 0.0),
4572            InputIdentity::from_bytes(b"document"),
4573            &entry_only_gap,
4574        )
4575        .unwrap();
4576        assert_eq!(exit.status(), TransitionPoseStatusV1::Complete);
4577
4578        let mut nonrepresentable_duration = document(None);
4579        nonrepresentable_duration.clips[1].duration_s = 0.1;
4580        let entry = evaluate_document_transition_poses_v1(
4581            &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4582            InputIdentity::from_bytes(b"document"),
4583            &nonrepresentable_duration,
4584        )
4585        .unwrap();
4586        assert_eq!(entry.status(), TransitionPoseStatusV1::Complete);
4587        let exit = evaluate_document_transition_poses_v1(
4588            &declaration(TransitionFamilyBoundaryV1::Exit, 0.0, 0.0),
4589            InputIdentity::from_bytes(b"document"),
4590            &nonrepresentable_duration,
4591        )
4592        .unwrap();
4593        assert_eq!(exit.status(), TransitionPoseStatusV1::Incomplete);
4594        assert_eq!(
4595            exit.families()[0].reason,
4596            Some(TransitionPoseReasonV1::UnsupportedSampling)
4597        );
4598    }
4599
4600    #[test]
4601    fn multi_bone_pairs_and_independent_offender_caps_are_canonical() {
4602        let bones = (0..17)
4603            .map(|ordinal| Bone {
4604                name: format!("bone-{ordinal:02}"),
4605                parent: None,
4606                rest: Transform::IDENTITY,
4607                inverse_bind: None,
4608            })
4609            .collect::<Vec<_>>();
4610        let animated = |name: &str, translation_multiplier: f32| Clip {
4611            name: name.into(),
4612            duration_s: 1.0,
4613            tracks: (0..17)
4614                .flat_map(|ordinal| {
4615                    [
4616                        Track {
4617                            bone: ordinal,
4618                            property: Property::Translation,
4619                            interpolation: Interpolation::Linear,
4620                            times: vec![0.0, 1.0],
4621                            values: TrackValues::Vec3s(vec![
4622                                crate::glam::Vec3::X
4623                                    * ((ordinal + 1) as f32
4624                                        * translation_multiplier);
4625                                2
4626                            ]),
4627                        },
4628                        Track {
4629                            bone: ordinal,
4630                            property: Property::Rotation,
4631                            interpolation: Interpolation::Linear,
4632                            times: vec![0.0, 1.0],
4633                            values: TrackValues::Quats(vec![
4634                                crate::glam::Quat::from_rotation_x(
4635                                    std::f32::consts::FRAC_PI_2
4636                                );
4637                                2
4638                            ]),
4639                        },
4640                    ]
4641                })
4642                .collect(),
4643        };
4644        let document = Document {
4645            skeleton: Skeleton { bones },
4646            clips: vec![
4647                Clip {
4648                    name: "A".into(),
4649                    duration_s: 1.0,
4650                    tracks: Vec::new(),
4651                },
4652                animated("B", 1.0),
4653                animated("C", 2.0),
4654            ],
4655            ..Document::default()
4656        };
4657        let family = DocumentTransitionFamilyV1::new(
4658            "three_way".into(),
4659            TransitionFamilyBoundaryV1::Entry,
4660            TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4661            ["A", "B", "C"]
4662                .into_iter()
4663                .enumerate()
4664                .map(|(index, name)| {
4665                    DocumentTransitionFamilyMemberV1::new(index as u64, name.into())
4666                })
4667                .collect::<Result<Vec<_>, _>>()
4668                .unwrap(),
4669        )
4670        .unwrap();
4671        let declaration = TransitionFamilyDeclarationInputV1::new(
4672            TransitionFamilyDeclarationV1::document(vec![family]).unwrap(),
4673            b"declaration",
4674        )
4675        .unwrap();
4676        let result = evaluate_document_transition_poses_v1(
4677            &declaration,
4678            InputIdentity::from_bytes(b"document"),
4679            &document,
4680        )
4681        .unwrap();
4682        let pairs = result.families()[0].pairs();
4683        assert_eq!(
4684            pairs
4685                .iter()
4686                .map(TransitionPosePairEvaluationV1::member_indices)
4687                .collect::<Vec<_>>(),
4688            vec![[0, 1], [0, 2], [1, 2]]
4689        );
4690        let first = &pairs[0];
4691        assert_eq!(first.translation_offenders().len(), 16);
4692        assert_eq!(first.rotation_offenders().len(), 16);
4693        assert_eq!(
4694            first
4695                .translation_offenders()
4696                .iter()
4697                .map(TransitionPoseTranslationOffenderV1::bone_ordinal)
4698                .collect::<Vec<_>>(),
4699            (1..17).rev().collect::<Vec<_>>()
4700        );
4701        assert_eq!(
4702            first
4703                .rotation_offenders()
4704                .iter()
4705                .map(TransitionPoseRotationOffenderV1::bone_ordinal)
4706                .collect::<Vec<_>>(),
4707            (0..16).collect::<Vec<_>>()
4708        );
4709        assert!(
4710            first
4711                .translation_offenders()
4712                .iter()
4713                .any(|offender| offender.bone_ordinal() == 1)
4714        );
4715        assert!(
4716            first
4717                .rotation_offenders()
4718                .iter()
4719                .any(|offender| offender.bone_ordinal() == 1)
4720        );
4721    }
4722
4723    #[test]
4724    fn offender_selection_never_retains_more_than_its_final_cap() {
4725        let mut descending = Vec::new();
4726        for ordinal in 0..TRANSITION_POSE_EVALUATION_V1_MAX_BONES {
4727            retain_top_candidate(
4728                &mut descending,
4729                ordinal,
4730                ordinal as f64,
4731                TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS,
4732            );
4733            assert!(descending.len() <= TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS);
4734        }
4735        sort_top_candidates(&mut descending);
4736        assert_eq!(
4737            descending
4738                .iter()
4739                .map(|(ordinal, _)| *ordinal)
4740                .collect::<Vec<_>>(),
4741            (TRANSITION_POSE_EVALUATION_V1_MAX_BONES
4742                - TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS
4743                ..TRANSITION_POSE_EVALUATION_V1_MAX_BONES)
4744                .rev()
4745                .collect::<Vec<_>>()
4746        );
4747
4748        let mut tied = Vec::new();
4749        for ordinal in (0..TRANSITION_POSE_EVALUATION_V1_MAX_BONES).rev() {
4750            retain_top_candidate(
4751                &mut tied,
4752                ordinal,
4753                1.0,
4754                TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS,
4755            );
4756            assert!(tied.len() <= TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS);
4757        }
4758        sort_top_candidates(&mut tied);
4759        assert_eq!(
4760            tied.iter().map(|(ordinal, _)| *ordinal).collect::<Vec<_>>(),
4761            (0..TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS).collect::<Vec<_>>()
4762        );
4763    }
4764
4765    fn numbered_members(count: u64) -> Vec<DocumentTransitionFamilyMemberV1> {
4766        (0..count)
4767            .map(|index| DocumentTransitionFamilyMemberV1::new(index, format!("clip-{index}")))
4768            .collect::<Result<Vec<_>, _>>()
4769            .unwrap()
4770    }
4771
4772    fn numbered_family(
4773        family_id: String,
4774        count: u64,
4775        boundary: TransitionFamilyBoundaryV1,
4776    ) -> DocumentTransitionFamilyV1 {
4777        DocumentTransitionFamilyV1::new(
4778            family_id,
4779            boundary,
4780            TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4781            numbered_members(count),
4782        )
4783        .unwrap()
4784    }
4785
4786    fn numbered_document(clips: u64, bones: usize) -> Document {
4787        Document {
4788            skeleton: Skeleton {
4789                bones: (0..bones)
4790                    .map(|ordinal| Bone {
4791                        name: format!("bone-{ordinal}"),
4792                        parent: None,
4793                        rest: Transform::IDENTITY,
4794                        inverse_bind: None,
4795                    })
4796                    .collect(),
4797            },
4798            clips: (0..clips)
4799                .map(|index| Clip {
4800                    name: format!("clip-{index}"),
4801                    duration_s: 1.0,
4802                    tracks: Vec::new(),
4803                })
4804                .collect(),
4805            ..Document::default()
4806        }
4807    }
4808
4809    #[test]
4810    fn member_witnesses_are_control_preflight_before_all_unavailability_reasons() {
4811        let input = |families| {
4812            TransitionFamilyDeclarationInputV1::new(
4813                TransitionFamilyDeclarationV1::document(families).unwrap(),
4814                b"declaration",
4815            )
4816            .unwrap()
4817        };
4818        let assert_invalid_witness = |input: &TransitionFamilyDeclarationInputV1,
4819                                      document: &Document| {
4820            assert_eq!(
4821                evaluate_document_transition_poses_v1(
4822                    input,
4823                    InputIdentity::from_bytes(b"document"),
4824                    document,
4825                ),
4826                Err(TransitionPoseEvaluationControlError::InvalidMemberWitness)
4827            );
4828        };
4829
4830        let time_family = DocumentTransitionFamilyV1::new(
4831            "time".into(),
4832            TransitionFamilyBoundaryV1::Entry,
4833            TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.1).unwrap(),
4834            vec![
4835                DocumentTransitionFamilyMemberV1::new(2, "missing-a".into()).unwrap(),
4836                DocumentTransitionFamilyMemberV1::new(3, "missing-b".into()).unwrap(),
4837            ],
4838        )
4839        .unwrap();
4840        assert_invalid_witness(&input(vec![time_family]), &document(None));
4841
4842        let family_work =
4843            numbered_family("family_work".into(), 65, TransitionFamilyBoundaryV1::Both);
4844        assert_eq!(
4845            plan_families(std::slice::from_ref(&family_work), 1, &[false])[0].reason,
4846            Some(TransitionPoseReasonV1::FamilyWorkLimit)
4847        );
4848        assert_invalid_witness(&input(vec![family_work]), &Document::default());
4849
4850        let aggregate = (0..33)
4851            .map(|ordinal| {
4852                numbered_family(
4853                    format!("aggregate-{ordinal}"),
4854                    65,
4855                    TransitionFamilyBoundaryV1::Entry,
4856                )
4857            })
4858            .collect::<Vec<_>>();
4859        assert!(
4860            plan_families(&aggregate, 0, &vec![false; aggregate.len()])
4861                .iter()
4862                .any(|plan| plan.reason == Some(TransitionPoseReasonV1::AggregateWorkLimit))
4863        );
4864        assert_invalid_witness(&input(aggregate), &Document::default());
4865
4866        let retention = vec![
4867            numbered_family("retention-a".into(), 64, TransitionFamilyBoundaryV1::Entry),
4868            numbered_family("retention-b".into(), 64, TransitionFamilyBoundaryV1::Entry),
4869        ];
4870        assert_eq!(
4871            plan_families(&retention, 16, &[false, false])[1].reason,
4872            Some(TransitionPoseReasonV1::RetentionLimit)
4873        );
4874        assert_invalid_witness(&input(retention), &numbered_document(0, 16));
4875
4876        let earlier_time = DocumentTransitionFamilyV1::new(
4877            "a-time".into(),
4878            TransitionFamilyBoundaryV1::Entry,
4879            TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.1).unwrap(),
4880            vec![
4881                DocumentTransitionFamilyMemberV1::new(0, "Walk".into()).unwrap(),
4882                DocumentTransitionFamilyMemberV1::new(1, "Run".into()).unwrap(),
4883            ],
4884        )
4885        .unwrap();
4886        let later_invalid = DocumentTransitionFamilyV1::new(
4887            "z-invalid".into(),
4888            TransitionFamilyBoundaryV1::Entry,
4889            TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4890            vec![
4891                DocumentTransitionFamilyMemberV1::new(2, "missing-a".into()).unwrap(),
4892                DocumentTransitionFamilyMemberV1::new(3, "missing-b".into()).unwrap(),
4893            ],
4894        )
4895        .unwrap();
4896        assert_invalid_witness(&input(vec![earlier_time, later_invalid]), &document(None));
4897    }
4898
4899    #[test]
4900    fn pair_work_math_is_checked_before_sampling() {
4901        let family = numbered_family(
4902            "too_many_pairs".into(),
4903            65,
4904            TransitionFamilyBoundaryV1::Both,
4905        );
4906        let input = TransitionFamilyDeclarationInputV1::new(
4907            TransitionFamilyDeclarationV1::document(vec![family]).unwrap(),
4908            b"declaration",
4909        )
4910        .unwrap();
4911        let result = evaluate_document_transition_poses_v1(
4912            &input,
4913            InputIdentity::from_bytes(b"document"),
4914            &numbered_document(65, 1),
4915        )
4916        .unwrap();
4917        assert_eq!(
4918            result.families()[0].reason,
4919            Some(TransitionPoseReasonV1::FamilyWorkLimit)
4920        );
4921        assert_eq!(checked_pair_count(64), Some(2_016));
4922        assert_eq!(checked_pair_count(usize::MAX), None);
4923    }
4924
4925    #[test]
4926    fn retention_preflight_uses_the_actual_bone_bounded_offender_capacity() {
4927        let members = (0..64)
4928            .map(|index| DocumentTransitionFamilyMemberV1::new(index, format!("clip-{index}")))
4929            .collect::<Result<Vec<_>, _>>()
4930            .unwrap();
4931        let family = DocumentTransitionFamilyV1::new(
4932            "near_retention_limit".into(),
4933            TransitionFamilyBoundaryV1::Entry,
4934            TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4935            members,
4936        )
4937        .unwrap();
4938
4939        // 16 * C(64, 2) * (1 translation + 1 rotation) = 64,512, so a
4940        // one-bone document remains below the aggregate 65,536 record cap.
4941        let low_bone_families = vec![family.clone(); 16];
4942        assert!(
4943            plan_families(&low_bone_families, 1, &vec![false; low_bone_families.len()])
4944                .iter()
4945                .all(|plan| plan.reason.is_none())
4946        );
4947
4948        // With at least 16 bones each pair/boundary can retain 16 records per
4949        // channel. The second family crosses the same aggregate cap.
4950        let high_bone_plans = plan_families(&[family.clone(), family], 16, &[false, false]);
4951        assert_eq!(high_bone_plans[0].reason, None);
4952        assert_eq!(
4953            high_bone_plans[1].reason,
4954            Some(TransitionPoseReasonV1::RetentionLimit)
4955        );
4956    }
4957
4958    #[test]
4959    fn long_bone_names_hit_result_limit_before_offender_name_cloning() {
4960        let family = numbered_family("long_names".into(), 64, TransitionFamilyBoundaryV1::Entry);
4961        let input = TransitionFamilyDeclarationInputV1::new(
4962            TransitionFamilyDeclarationV1::document(vec![family]).unwrap(),
4963            b"declaration",
4964        )
4965        .unwrap();
4966        let mut document = numbered_document(64, 1);
4967        document.skeleton.bones[0].name = "x".repeat(200_000);
4968        let result = evaluate_document_transition_poses_v1(
4969            &input,
4970            InputIdentity::from_bytes(b"document"),
4971            &document,
4972        )
4973        .unwrap();
4974        assert_eq!(
4975            result.families()[0].reason(),
4976            Some(TransitionPoseReasonV1::ResultLimit)
4977        );
4978        assert!(result.families()[0].pairs().is_empty());
4979    }
4980
4981    #[test]
4982    fn detailed_reservation_subtracts_pair_free_authority_before_name_rows() {
4983        let declared = declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0);
4984        let families = declared.declaration().document_families().unwrap();
4985        let mut document = document(Some(1.0));
4986        document.skeleton.bones[0].name = "n".repeat(128);
4987        let subject = InputIdentity::from_bytes(b"document");
4988        let closure = complete_closure(subject.clone());
4989        let result =
4990            evaluate_document_transition_poses_v1(&declared, subject.clone(), &document).unwrap();
4991        let basis = SkeletonBasisV1::from_skeleton(&document.skeleton).unwrap();
4992        let remaining = detailed_result_budget_after_base(
4993            &result,
4994            families,
4995            &closure,
4996            basis.identity(),
4997            TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES,
4998        )
4999        .unwrap();
5000        let base_bytes = TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES - remaining;
5001        let mut reservation = remaining;
5002        assert!(reserve_detailed_name_budget(
5003            &mut reservation,
5004            &families[0],
5005            &document.skeleton,
5006        ));
5007        let detailed_bytes = remaining - reservation;
5008        let cap = base_bytes + detailed_bytes - 1;
5009        let result = evaluate_document_transition_poses_v1_with_result_limit(
5010            &declared, &closure, &document, cap,
5011        )
5012        .unwrap();
5013        assert_eq!(
5014            result.families()[0].reason(),
5015            Some(TransitionPoseReasonV1::ResultLimit)
5016        );
5017        assert!(result.families()[0].pairs().is_empty());
5018    }
5019
5020    #[test]
5021    fn result_limit_retry_retains_bindings_and_terminates_at_a_tiny_seam() {
5022        let declaration = declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0);
5023        let mut result = evaluate_document_transition_poses_v1(
5024            &declaration,
5025            InputIdentity::from_bytes(b"document"),
5026            &document(Some(1.0)),
5027        )
5028        .unwrap();
5029        let before_members = result.families()[0].members().to_vec();
5030        let before_basis = result.families()[0].skeleton_basis_input().cloned();
5031        let before_declaration = result.declaration_input().clone();
5032        let before_normalized = result.declaration_normalized().clone();
5033        let before_subject = result.subject_input().clone();
5034        let before_subject_closure = result.subject_dependency_closure_identity().cloned();
5035        let before_member_closures = result.families()[0]
5036            .members()
5037            .iter()
5038            .map(|member| member.source_dependency_closure_identity().cloned())
5039            .collect::<Vec<_>>();
5040        let detailed_bytes =
5041            canonical_bytes(&result, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)
5042                .unwrap()
5043                .len();
5044
5045        let mut degraded = result.clone();
5046        for family in &mut degraded.families {
5047            family.status = TransitionPoseStatusV1::Incomplete;
5048            family.decision = TransitionPoseDecisionV1::NotEvaluated;
5049            family.reason = Some(TransitionPoseReasonV1::ResultLimit);
5050            family.pairs.clear();
5051        }
5052        derive_result_state(&mut degraded);
5053        let degraded_bytes =
5054            canonical_bytes(&degraded, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)
5055                .unwrap()
5056                .len();
5057        assert!(degraded_bytes < detailed_bytes);
5058
5059        enforce_result_limit(&mut result, degraded_bytes).unwrap();
5060        assert_eq!(result.status(), TransitionPoseStatusV1::Incomplete);
5061        assert_eq!(result.decision(), TransitionPoseDecisionV1::NotEvaluated);
5062        assert_eq!(result.declaration_input(), &before_declaration);
5063        assert_eq!(result.declaration_normalized(), &before_normalized);
5064        assert_eq!(result.subject_input(), &before_subject);
5065        assert_eq!(
5066            result.subject_dependency_closure_identity(),
5067            before_subject_closure.as_ref()
5068        );
5069        assert_eq!(result.families()[0].members(), before_members);
5070        assert_eq!(
5071            result.families()[0]
5072                .members()
5073                .iter()
5074                .map(|member| member.source_dependency_closure_identity().cloned())
5075                .collect::<Vec<_>>(),
5076            before_member_closures
5077        );
5078        assert_eq!(
5079            result.families()[0].skeleton_basis_input(),
5080            before_basis.as_ref()
5081        );
5082        assert_eq!(result.families()[0].pairs(), &[]);
5083        assert_eq!(
5084            result.families()[0].reason(),
5085            Some(TransitionPoseReasonV1::ResultLimit)
5086        );
5087        assert!(canonical_bytes(&result, degraded_bytes).is_ok());
5088    }
5089
5090    #[test]
5091    fn bounded_writer_refuses_its_first_excess_byte() {
5092        assert!(canonical_bytes(&"1234", 6).is_ok());
5093        assert!(canonical_bytes(&"1234", 5).is_err());
5094    }
5095
5096    #[test]
5097    fn result_wire_covers_the_closed_no_config_pass_finding_and_incomplete_matrix() {
5098        let empty = TransitionFamilyDeclarationInputV1::new(
5099            TransitionFamilyDeclarationV1::document(Vec::new()).unwrap(),
5100            b"empty",
5101        )
5102        .unwrap();
5103        let pass = evaluate_document_transition_poses_v1(
5104            &empty,
5105            InputIdentity::from_bytes(b"document"),
5106            &document(None),
5107        )
5108        .unwrap();
5109        let pass_wire = serde_json::to_value(&pass).unwrap();
5110        assert_eq!(pass_wire["schema"], TRANSITION_POSE_EVALUATION_V1_ID);
5111        assert_eq!(pass_wire["status"], "complete");
5112        assert_eq!(pass_wire["decision"], "pass");
5113        assert_eq!(pass_wire["reason"], "no_configured_families");
5114
5115        let finding = evaluate_document_transition_poses_v1(
5116            &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
5117            InputIdentity::from_bytes(b"document"),
5118            &document(Some(1.0)),
5119        )
5120        .unwrap();
5121        let finding_wire = serde_json::to_value(&finding).unwrap();
5122        assert_eq!(finding_wire["status"], "complete");
5123        assert_eq!(finding_wire["decision"], "finding");
5124        assert!(finding_wire.get("reason").is_none());
5125
5126        let incomplete = evaluate_document_transition_poses_v1(
5127            &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.1),
5128            InputIdentity::from_bytes(b"document"),
5129            &document(None),
5130        )
5131        .unwrap();
5132        let incomplete_wire = serde_json::to_value(&incomplete).unwrap();
5133        assert_eq!(incomplete_wire["status"], "incomplete");
5134        assert_eq!(incomplete_wire["decision"], "not_evaluated");
5135        assert_eq!(
5136            incomplete_wire["families"][0]["reason"],
5137            "time_tolerance_unsupported"
5138        );
5139    }
5140}