Skip to main content

animsmith_engine/
addressability_v2.rs

1//! Rich, bounded glTF addressability for the exact Bevy 0.19.0 loader.
2//!
3//! This module is a successor to the animation-only V1 report.  It keeps that
4//! inventory intact and adds a separately versioned Bevy rule bundle for scene,
5//! skin, named-map, and animation-target projections.  None of the projections
6//! certify that Bevy loaded, spawned, or played the source asset at runtime.
7
8use crate::{
9    BevyAnimationAssetLabelV1, GltfAnimationAddressabilityInventoryV1, GltfAnimationObservationV1,
10    ResolvedProfileSettingsV2, ResolvedSettingOriginV2, SettingIdV2, SettingValueV2,
11};
12use animsmith_core::{
13    Applicability, Check, CheckCtx, CheckEvaluation, CheckOutput, CheckSelection,
14    ConfigurationState, EnginePredictionBasisV4, EnginePredictionFacetStateV1,
15    EnginePredictionFacetV4, EnginePredictionV4, EngineSettingIdV2, EngineSettingValueOriginV3,
16    EngineSettingValueV2, EvaluationScope, EvaluationScopeCode, EvaluationState, InputIdentity,
17    LoadedSource, PREDICTION_V1_MAX_FACETS_PER_FILE, PredictionBasisReferenceV1,
18    PredictionBasisReferenceV2, PredictionBasisReferenceV4, PredictionProvenanceV4,
19    PredictionUnavailableReasonV2, RawGltfAddressabilityInventoryV1,
20    RawGltfDefaultSceneObservationV1, RawSourceBasisReferenceV1, RawSourceDomainV1,
21    RawSourceFieldIdV1, RawSourceKeyV1, ResolvedSettingLocationV1, SelectionState, ToolInfo,
22    evaluate_checks,
23};
24use serde::{Deserialize, Serialize};
25use serde_json::value::RawValue;
26use std::collections::{BTreeMap, BTreeSet};
27use std::fmt::Write as _;
28use std::io::Read;
29
30/// Version of the rich standalone glTF addressability envelope.
31pub const GLTF_ADDRESSABILITY_V2_SCHEMA_VERSION: u32 = 2;
32/// Immutable identity of the rich standalone glTF addressability contract.
33pub const GLTF_ADDRESSABILITY_V2_ID: &str = "urn:animsmith:schema:gltf-addressability:2";
34/// Maximum serialized bytes accepted by the rich report reader.
35pub const GLTF_ADDRESSABILITY_V2_MAX_REPORT_BYTES: u64 = 256 * 1024 * 1024;
36
37/// Maximum rows retained independently for each rich addressability domain.
38pub const GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS: usize = 4_096;
39/// Maximum aggregate structural references retained by the new rich
40/// projections (the two embedded sealed inventories enforce their own bounds).
41pub const GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES: usize = 65_536;
42/// Maximum completed check scopes implied by all bounded V2 row domains.
43pub const GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES: usize =
44    5 * GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS + 5;
45/// Maximum UTF-8 bytes retained by one source name or path segment.
46pub const GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES: usize = 1_024;
47/// Maximum UTF-8 bytes retained by one slash-delimited target path.
48pub const GLTF_ADDRESSABILITY_V2_MAX_PATH_BYTES: usize = 4_096;
49/// Maximum segments retained by one target path.
50pub const GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS: usize = 256;
51/// Maximum aggregate text retained by the new rich projections (excluding the
52/// two embedded sealed inventories, which enforce their own text bounds).
53pub const GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES: usize = 1024 * 1024;
54
55const BEVY_TAG: &str = "v0.19.0";
56const BEVY_COMMIT: &str = "c6f634ca9f406d68ba5109d921247b654cb42c10";
57const BEVY_GLTF_CRATE: &str = "bevy_gltf 0.19.0";
58const GLTF_CRATE: &str = "gltf 1.4.1";
59const BEVY_RULE_BUNDLE_ID: &str = "urn:animsmith:bevy-gltf-addressability-rules:1";
60const BEVY_PROFILE_REVISION: u32 = 3;
61const FACET_BUDGET_SCOPE: &str = "engine-addressability:facet-budget";
62const ANIMATION_TARGET_NAMESPACE: [u8; 16] = [
63    0x31, 0x79, 0xf5, 0x19, 0xd9, 0x27, 0x4f, 0xf2, 0xb5, 0x96, 0x6f, 0xd0, 0x77, 0x02, 0x39, 0x11,
64];
65
66/// Explicit target pointer width used by Bevy's target-id preimage.
67///
68/// Bevy hashes every path segment's byte length using
69/// `usize::to_le_bytes()`.  The width is therefore an input to the exact
70/// prediction and is never inferred from the host running AnimSmith.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum TargetPointerWidth {
74    /// Four-byte little-endian segment lengths.
75    Bits32,
76    /// Eight-byte little-endian segment lengths.
77    Bits64,
78}
79
80impl TargetPointerWidth {
81    const fn bytes(self) -> usize {
82        match self {
83            Self::Bits32 => 4,
84            Self::Bits64 => 8,
85        }
86    }
87}
88
89/// Named-map duplicate policy in the pinned Bevy loader.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum BevyNamedMapDuplicatePolicyV1 {
93    /// Later insertions replace an earlier value with the same exact name.
94    LastWriteWins,
95}
96
97/// Immutable primary-source authority for one Bevy addressability rule.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct BevyAddressabilityRuleSourceV1 {
101    id: String,
102    url: String,
103}
104
105impl BevyAddressabilityRuleSourceV1 {
106    /// Stable source identifier.
107    pub fn id(&self) -> &str {
108        &self.id
109    }
110
111    /// Commit-pinned primary-source URL.
112    pub fn url(&self) -> &str {
113        &self.url
114    }
115}
116
117/// Separately versioned exact Bevy glTF addressability authority.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(deny_unknown_fields)]
120pub struct BevyGltfAddressabilityRulesV1 {
121    schema: String,
122    profile_revision: u32,
123    bevy_tag: String,
124    bevy_commit: String,
125    bevy_gltf_crate: String,
126    locked_gltf_crate: String,
127    bevy_animation_feature_required_for_targets: bool,
128    load_animations_required_for_targets: bool,
129    target_pointer_width: Option<TargetPointerWidth>,
130    named_scene_policy: BevyNamedMapDuplicatePolicyV1,
131    named_animation_policy: BevyNamedMapDuplicatePolicyV1,
132    named_skin_policy: BevyNamedMapDuplicatePolicyV1,
133    sources: Vec<BevyAddressabilityRuleSourceV1>,
134}
135
136impl BevyGltfAddressabilityRulesV1 {
137    /// Materialize the frozen revision-1 rules for the exact revision-3 Bevy
138    /// profile and an optional explicit target width.
139    pub fn frozen(target_pointer_width: Option<TargetPointerWidth>) -> Self {
140        let pinned =
141            |path: &str| format!("https://github.com/bevyengine/bevy/blob/{BEVY_COMMIT}/{path}");
142        let mut sources = vec![
143            BevyAddressabilityRuleSourceV1 {
144                id: "bevy-animation-target-id-0.19.0-c6f634ca".into(),
145                url: pinned("crates/bevy_animation/src/lib.rs"),
146            },
147            BevyAddressabilityRuleSourceV1 {
148                id: "bevy-gltf-labels-0.19.0-c6f634ca".into(),
149                url: pinned("crates/bevy_gltf/src/label.rs"),
150            },
151            BevyAddressabilityRuleSourceV1 {
152                id: "bevy-gltf-loader-0.19.0-c6f634ca".into(),
153                url: pinned("crates/bevy_gltf/src/loader/mod.rs"),
154            },
155            BevyAddressabilityRuleSourceV1 {
156                id: "bevy-gltf-node-path-0.19.0-c6f634ca".into(),
157                url: pinned("crates/bevy_gltf/src/loader/gltf_ext/scene.rs"),
158            },
159            BevyAddressabilityRuleSourceV1 {
160                id: "bevy-gltf-manifest-0.19.0-c6f634ca".into(),
161                url: pinned("crates/bevy_gltf/Cargo.toml"),
162            },
163            BevyAddressabilityRuleSourceV1 {
164                id: "bevy-lockfile-0.19.0-c6f634ca".into(),
165                url: pinned("Cargo.lock"),
166            },
167        ];
168        sources.sort_by(|left, right| left.id.cmp(&right.id));
169        Self {
170            schema: BEVY_RULE_BUNDLE_ID.into(),
171            profile_revision: BEVY_PROFILE_REVISION,
172            bevy_tag: BEVY_TAG.into(),
173            bevy_commit: BEVY_COMMIT.into(),
174            bevy_gltf_crate: BEVY_GLTF_CRATE.into(),
175            locked_gltf_crate: GLTF_CRATE.into(),
176            bevy_animation_feature_required_for_targets: true,
177            load_animations_required_for_targets: true,
178            target_pointer_width,
179            named_scene_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
180            named_animation_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
181            named_skin_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
182            sources,
183        }
184    }
185
186    /// Immutable rule-bundle schema identity.
187    pub fn contract_id(&self) -> &str {
188        &self.schema
189    }
190
191    /// Exact profile revision this separate rule bundle may accompany.
192    pub const fn profile_revision(&self) -> u32 {
193        self.profile_revision
194    }
195
196    /// Explicit target width, absent when exact target IDs are unavailable.
197    pub const fn target_pointer_width(&self) -> Option<TargetPointerWidth> {
198        self.target_pointer_width
199    }
200
201    /// Commit-pinned primary sources in canonical identifier order.
202    pub fn sources(&self) -> &[BevyAddressabilityRuleSourceV1] {
203        &self.sources
204    }
205
206    fn validate(&self) -> bool {
207        self == &Self::frozen(self.target_pointer_width)
208    }
209}
210
211/// Origin of one exact setting consumed by the rich Bevy projection.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
213#[serde(rename_all = "snake_case")]
214pub enum GltfAddressabilitySettingOriginV2 {
215    /// The caller explicitly selected the value.
216    ExplicitConfig,
217    /// The frozen profile supplied its verified default.
218    ProfileDefault,
219}
220
221/// Exact revision-3 settings consumed by the addressability rule bundle.
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(deny_unknown_fields)]
224pub struct BevyGltfAddressabilitySettingsV2 {
225    bevy_animation_feature: bool,
226    bevy_animation_feature_origin: GltfAddressabilitySettingOriginV2,
227    load_animations: bool,
228    load_animations_origin: GltfAddressabilitySettingOriginV2,
229}
230
231impl BevyGltfAddressabilitySettingsV2 {
232    fn from_resolved(
233        resolved: &ResolvedProfileSettingsV2,
234    ) -> Result<Self, GltfAddressabilityV2Error> {
235        let selection = resolved.profile().selection();
236        if selection.family() != "bevy"
237            || selection.profile_revision() != BEVY_PROFILE_REVISION
238            || selection.engine_version() != "0.19.0"
239            || selection.importer() != "gltf-asset-loader"
240        {
241            return Err(GltfAddressabilityV2Error::InvalidBevyProfile);
242        }
243        let boolean = |id| {
244            let setting = resolved
245                .document_settings()
246                .get(&id)
247                .ok_or(GltfAddressabilityV2Error::MissingRequiredSetting)?;
248            let SettingValueV2::Boolean(value) = setting.value() else {
249                return Err(GltfAddressabilityV2Error::MissingRequiredSetting);
250            };
251            Ok((*value, setting_origin(setting.origin())))
252        };
253        let (bevy_animation_feature, bevy_animation_feature_origin) =
254            boolean(SettingIdV2::BevyAnimationFeature)?;
255        let (load_animations, load_animations_origin) = boolean(SettingIdV2::LoadAnimations)?;
256        Ok(Self {
257            bevy_animation_feature,
258            bevy_animation_feature_origin,
259            load_animations,
260            load_animations_origin,
261        })
262    }
263
264    /// Whether Bevy's compile-time animation feature is present.
265    pub const fn bevy_animation_feature(&self) -> bool {
266        self.bevy_animation_feature
267    }
268
269    /// Whether the feature value was explicit or supplied by the profile.
270    pub const fn bevy_animation_feature_origin(&self) -> GltfAddressabilitySettingOriginV2 {
271        self.bevy_animation_feature_origin
272    }
273
274    /// Exact loader animation toggle.
275    pub const fn load_animations(&self) -> bool {
276        self.load_animations
277    }
278
279    /// Whether the loader toggle was explicit or supplied by the profile.
280    pub const fn load_animations_origin(&self) -> GltfAddressabilitySettingOriginV2 {
281        self.load_animations_origin
282    }
283}
284
285const fn setting_origin(value: ResolvedSettingOriginV2) -> GltfAddressabilitySettingOriginV2 {
286    match value {
287        ResolvedSettingOriginV2::ExplicitConfig => {
288            GltfAddressabilitySettingOriginV2::ExplicitConfig
289        }
290        ResolvedSettingOriginV2::ProfileDefault => {
291            GltfAddressabilitySettingOriginV2::ProfileDefault
292        }
293    }
294}
295
296/// Reproduce Bevy 0.19.0's `AnimationTargetId::from_names` exactly.
297///
298/// The returned string is the lower-case hyphenated UUID spelling Bevy emits.
299/// Segment byte lengths are encoded at the explicitly supplied target width.
300///
301/// # Errors
302///
303/// Returns [`BevyAnimationTargetIdError`] when a segment or complete path is
304/// outside the V2 bounds, or a segment length cannot fit the chosen width.
305pub fn bevy_animation_target_id_v1<'a>(
306    segments: impl IntoIterator<Item = &'a str>,
307    target_pointer_width: TargetPointerWidth,
308) -> Result<String, BevyAnimationTargetIdError> {
309    let mut hasher = blake3::Hasher::new();
310    hasher.update(&ANIMATION_TARGET_NAMESPACE);
311    let mut segment_count = 0usize;
312    let mut path_bytes = 0usize;
313    for segment in segments {
314        segment_count = segment_count
315            .checked_add(1)
316            .ok_or(BevyAnimationTargetIdError::ArithmeticOverflow)?;
317        if segment_count > GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS {
318            return Err(BevyAnimationTargetIdError::TooManySegments {
319                found: segment_count,
320                limit: GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS,
321            });
322        }
323        if segment.len() > GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES {
324            return Err(BevyAnimationTargetIdError::SegmentTooLong {
325                found: segment.len(),
326                limit: GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES,
327            });
328        }
329        path_bytes = path_bytes
330            .checked_add(segment.len())
331            .and_then(|value| value.checked_add(usize::from(segment_count > 1)))
332            .ok_or(BevyAnimationTargetIdError::ArithmeticOverflow)?;
333        if path_bytes > GLTF_ADDRESSABILITY_V2_MAX_PATH_BYTES {
334            return Err(BevyAnimationTargetIdError::PathTooLong {
335                found: path_bytes,
336                limit: GLTF_ADDRESSABILITY_V2_MAX_PATH_BYTES,
337            });
338        }
339        match target_pointer_width.bytes() {
340            4 => hasher.update(
341                &u32::try_from(segment.len())
342                    .map_err(|_| BevyAnimationTargetIdError::SegmentLengthOverflow)?
343                    .to_le_bytes(),
344            ),
345            8 => hasher.update(
346                &u64::try_from(segment.len())
347                    .map_err(|_| BevyAnimationTargetIdError::SegmentLengthOverflow)?
348                    .to_le_bytes(),
349            ),
350            _ => unreachable!("closed target pointer width"),
351        };
352        hasher.update(segment.as_bytes());
353    }
354    let mut uuid = [0u8; 16];
355    uuid.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
356    // `uuid::Builder::from_sha1_bytes`: retain the supplied bytes, then set
357    // RFC 4122 variant and version 5 bits.  Bevy intentionally uses this UUID
358    // builder even though the input bytes are the BLAKE3 prefix.
359    uuid[6] = (uuid[6] & 0x0f) | 0x50;
360    uuid[8] = (uuid[8] & 0x3f) | 0x80;
361    let mut value = String::with_capacity(36);
362    for (index, byte) in uuid.iter().enumerate() {
363        if matches!(index, 4 | 6 | 8 | 10) {
364            value.push('-');
365        }
366        write!(&mut value, "{byte:02x}").expect("writing to String cannot fail");
367    }
368    Ok(value)
369}
370
371/// A bounded Bevy animation-target UUID could not be reproduced.
372#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
373#[non_exhaustive]
374pub enum BevyAnimationTargetIdError {
375    /// A path has more than 256 segments.
376    #[error("target path has {found} segments; limit is {limit}")]
377    TooManySegments {
378        /// Observed segment count.
379        found: usize,
380        /// Inclusive segment limit.
381        limit: usize,
382    },
383    /// One segment exceeds 1,024 UTF-8 bytes.
384    #[error("target path segment has {found} bytes; limit is {limit}")]
385    SegmentTooLong {
386        /// Observed byte count.
387        found: usize,
388        /// Inclusive byte limit.
389        limit: usize,
390    },
391    /// The complete slash-delimited path exceeds 4,096 bytes.
392    #[error("target path has {found} bytes; limit is {limit}")]
393    PathTooLong {
394        /// Observed byte count.
395        found: usize,
396        /// Inclusive byte limit.
397        limit: usize,
398    },
399    /// A segment length cannot be represented at the selected target width.
400    #[error("target path segment length does not fit the selected pointer width")]
401    SegmentLengthOverflow,
402    /// Checked bound arithmetic overflowed.
403    #[error("target path bound arithmetic overflowed")]
404    ArithmeticOverflow,
405}
406
407/// Stable reason one exact rich Bevy projection is required-unavailable.
408#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
409#[serde(rename_all = "snake_case")]
410pub enum GltfAddressabilityUnavailableReasonV2 {
411    /// One or more raw source domains are not exhaustive.
412    RawSourceIncomplete,
413    /// The same-load dependency closure is not complete.
414    DependencyClosureIncomplete,
415    /// The source target has no scene-root path.
416    UnreachableTarget,
417    /// A target has multiple distinct all-scene path candidates.
418    MultipleCandidatePaths,
419    /// Different source target nodes have the same full Bevy name path.
420    DuplicateFullPath,
421    /// Different full paths reproduce the same Bevy target UUID.
422    TargetIdCollision,
423    /// The exact target pointer width was not declared.
424    TargetPointerWidthMissing,
425    /// Bevy was not declared with the `bevy_animation` feature enabled.
426    BevyAnimationFeatureDisabled,
427    /// Bevy's glTF `load_animations` setting is disabled.
428    LoadAnimationsDisabled,
429    /// A bounded path cannot be represented by this contract.
430    PathBoundsExceeded,
431    /// The named map cannot be proven exhaustive.
432    NamedMapIncomplete,
433    /// The bounded target domain retained only a canonical prefix.
434    TargetDomainTruncated,
435    /// A new rich projection exceeded its independently declared bound.
436    ProjectionBoundsExceeded,
437}
438
439/// Exact projection state for one rich addressability value.
440#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
441#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
442pub enum GltfAddressabilityProjectionV2<T> {
443    /// An exact value is available.
444    Available {
445        /// Exact projected value.
446        value: T,
447    },
448    /// Complete evidence proves that this conditional route/label is absent.
449    ProvenAbsent,
450    /// Exact prediction was required but one or more prerequisites were absent.
451    RequiredUnavailable {
452        /// Canonical typed reasons.
453        reasons: Vec<GltfAddressabilityUnavailableReasonV2>,
454    },
455}
456
457impl<T> GltfAddressabilityProjectionV2<T> {
458    fn unavailable(mut reasons: Vec<GltfAddressabilityUnavailableReasonV2>) -> Self {
459        reasons.sort();
460        reasons.dedup();
461        debug_assert!(!reasons.is_empty());
462        Self::RequiredUnavailable { reasons }
463    }
464
465    fn is_unavailable(&self) -> bool {
466        matches!(self, Self::RequiredUnavailable { .. })
467    }
468}
469
470/// One exact typed Bevy scene label.
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(deny_unknown_fields)]
473pub struct GltfAddressabilitySceneV2 {
474    source_scene_index: u64,
475    label: String,
476}
477
478impl GltfAddressabilitySceneV2 {
479    /// Exact source scene-array index.
480    pub const fn source_scene_index(&self) -> u64 {
481        self.source_scene_index
482    }
483
484    /// Exact typed label `Scene{i}`.
485    pub fn label(&self) -> &str {
486        &self.label
487    }
488}
489
490/// One source skin's distinct conditional skin and eager inverse-bind labels.
491#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
492#[serde(deny_unknown_fields)]
493pub struct GltfAddressabilitySkinV2 {
494    source_skin_index: u64,
495    explicit_skeleton_root_node_index: Option<u64>,
496    skin_label: GltfAddressabilityProjectionV2<String>,
497    inverse_bind_matrices_label: String,
498}
499
500impl GltfAddressabilitySkinV2 {
501    /// Exact source skin-array index.
502    pub const fn source_skin_index(&self) -> u64 {
503        self.source_skin_index
504    }
505
506    /// Exact authored `skin.skeleton`, preserved without a Bevy inferred-root claim.
507    pub const fn explicit_skeleton_root_node_index(&self) -> Option<u64> {
508        self.explicit_skeleton_root_node_index
509    }
510
511    /// Conditional `Skin{i}` label projection.
512    pub const fn skin_label(&self) -> &GltfAddressabilityProjectionV2<String> {
513        &self.skin_label
514    }
515
516    /// Eager `Skin{i}/InverseBindMatrices` label, including identity fallback.
517    pub fn inverse_bind_matrices_label(&self) -> &str {
518        &self.inverse_bind_matrices_label
519    }
520}
521
522/// One Bevy named-map domain.
523#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
524#[serde(rename_all = "snake_case")]
525pub enum GltfAddressabilityNamedMapKindV2 {
526    /// `Gltf.named_scenes`.
527    Scene,
528    /// `Gltf.named_animations`.
529    Animation,
530    /// `Gltf.named_skins`.
531    Skin,
532}
533
534/// One exact named-map winner, separate from typed index labels.
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
536#[serde(deny_unknown_fields)]
537pub struct GltfAddressabilityNamedMapWinnerV2 {
538    name: String,
539    source_index: u64,
540    typed_label: String,
541}
542
543impl GltfAddressabilityNamedMapWinnerV2 {
544    /// Exact authored map key.
545    pub fn name(&self) -> &str {
546        &self.name
547    }
548
549    /// Exact source-array identity selected by the winner.
550    pub const fn source_index(&self) -> u64 {
551        self.source_index
552    }
553
554    /// Exact typed label of the selected source row.
555    pub fn typed_label(&self) -> &str {
556        &self.typed_label
557    }
558}
559
560/// One independently coverage-qualified named-map projection.
561#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
562#[serde(deny_unknown_fields)]
563pub struct GltfAddressabilityNamedMapV2 {
564    kind: GltfAddressabilityNamedMapKindV2,
565    duplicate_policy: BevyNamedMapDuplicatePolicyV1,
566    winners: GltfAddressabilityProjectionV2<Vec<GltfAddressabilityNamedMapWinnerV2>>,
567}
568
569impl GltfAddressabilityNamedMapV2 {
570    /// Runtime named-map domain represented by this row.
571    pub const fn kind(&self) -> GltfAddressabilityNamedMapKindV2 {
572        self.kind
573    }
574
575    /// Exact duplicate-name winner policy used for this map.
576    pub const fn duplicate_policy(&self) -> BevyNamedMapDuplicatePolicyV1 {
577        self.duplicate_policy
578    }
579
580    /// Exact winners, proven empty state, or typed unavailability.
581    pub const fn winners(
582        &self,
583    ) -> &GltfAddressabilityProjectionV2<Vec<GltfAddressabilityNamedMapWinnerV2>> {
584        &self.winners
585    }
586}
587
588/// One source animation channel contributing to a unique target node.
589#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
590#[serde(deny_unknown_fields)]
591pub struct GltfAddressabilityTargetChannelV2 {
592    source_animation_index: u64,
593    source_channel_index: u64,
594}
595
596impl GltfAddressabilityTargetChannelV2 {
597    /// Exact source animation-array index.
598    pub const fn source_animation_index(&self) -> u64 {
599        self.source_animation_index
600    }
601
602    /// Exact source channel index within that animation.
603    pub const fn source_channel_index(&self) -> u64 {
604        self.source_channel_index
605    }
606}
607
608/// Exact Bevy target path and reproduced UUID.
609#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
610#[serde(deny_unknown_fields)]
611pub struct GltfAddressabilityTargetValueV2 {
612    segments: Vec<String>,
613    path: String,
614    uuid: String,
615}
616
617impl GltfAddressabilityTargetValueV2 {
618    /// Authored-or-fallback path segments, excluding the scene world root.
619    pub fn segments(&self) -> &[String] {
620        &self.segments
621    }
622
623    /// Slash-delimited display path.
624    pub fn path(&self) -> &str {
625        &self.path
626    }
627
628    /// Exact lower-case Bevy `AnimationTargetId` UUID.
629    pub fn uuid(&self) -> &str {
630        &self.uuid
631    }
632}
633
634/// One unique source animation target node and all contributing channels.
635#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
636#[serde(deny_unknown_fields)]
637pub struct GltfAddressabilityTargetV2 {
638    source_node_index: u64,
639    contributing_channels: Vec<GltfAddressabilityTargetChannelV2>,
640    projection: GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2>,
641}
642
643impl GltfAddressabilityTargetV2 {
644    /// Exact source node-array identity.
645    pub const fn source_node_index(&self) -> u64 {
646        self.source_node_index
647    }
648
649    /// Canonical source animation/channel contributors.
650    pub fn contributing_channels(&self) -> &[GltfAddressabilityTargetChannelV2] {
651        &self.contributing_channels
652    }
653
654    /// Exact path/UUID or typed required-unavailable evidence.
655    pub const fn projection(
656        &self,
657    ) -> &GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2> {
658        &self.projection
659    }
660}
661
662/// Structured rich projections paired with the one engine-addressability check.
663#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
664#[serde(deny_unknown_fields)]
665pub struct BevyGltfAddressabilityProjectionV2 {
666    scenes: Vec<GltfAddressabilitySceneV2>,
667    default_scene_route: GltfAddressabilityProjectionV2<String>,
668    skins: Vec<GltfAddressabilitySkinV2>,
669    named_maps: Vec<GltfAddressabilityNamedMapV2>,
670    target_coverage: GltfAddressabilityProjectionV2<()>,
671    targets: Vec<GltfAddressabilityTargetV2>,
672}
673
674impl BevyGltfAddressabilityProjectionV2 {
675    /// Project rich Bevy values from the immutable raw sidecar and unchanged
676    /// animation inventory.
677    pub fn from_inventories(
678        raw: &RawGltfAddressabilityInventoryV1,
679        animations: &GltfAnimationAddressabilityInventoryV1,
680        bevy_animation_enabled: bool,
681        load_animations: bool,
682        pointer_width: Option<TargetPointerWidth>,
683    ) -> Result<Self, GltfAddressabilityV2Error> {
684        validate_inventory_binding(raw, animations)?;
685        let scenes = raw
686            .scenes()
687            .iter()
688            .map(|scene| GltfAddressabilitySceneV2 {
689                source_scene_index: scene.source_scene_index(),
690                label: format!("Scene{}", scene.source_scene_index()),
691            })
692            .collect();
693        let default_scene_route = match raw.default_scene() {
694            RawGltfDefaultSceneObservationV1::Absent => {
695                GltfAddressabilityProjectionV2::ProvenAbsent
696            }
697            RawGltfDefaultSceneObservationV1::Selected { source_scene_index }
698                if raw
699                    .scenes()
700                    .get(source_scene_index as usize)
701                    .is_some_and(|scene| scene.source_scene_index() == source_scene_index) =>
702            {
703                GltfAddressabilityProjectionV2::Available {
704                    value: format!("Scene{source_scene_index}"),
705                }
706            }
707            RawGltfDefaultSceneObservationV1::Selected { .. }
708            | RawGltfDefaultSceneObservationV1::Unavailable { .. } => {
709                GltfAddressabilityProjectionV2::unavailable(vec![
710                    GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
711                ])
712            }
713        };
714
715        let attached = raw
716            .attachments()
717            .iter()
718            .map(|row| row.source_skin_index())
719            .collect::<BTreeSet<_>>();
720        let skins = raw
721            .skins()
722            .iter()
723            .map(|skin| {
724                let source_skin_index = skin.source_skin_index();
725                let skin_label = if attached.contains(&source_skin_index) {
726                    GltfAddressabilityProjectionV2::Available {
727                        value: format!("Skin{source_skin_index}"),
728                    }
729                } else if raw.attachment_coverage().is_complete() {
730                    GltfAddressabilityProjectionV2::ProvenAbsent
731                } else {
732                    GltfAddressabilityProjectionV2::unavailable(vec![
733                        GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
734                    ])
735                };
736                GltfAddressabilitySkinV2 {
737                    source_skin_index,
738                    explicit_skeleton_root_node_index: skin.skeleton_root_node_index(),
739                    skin_label,
740                    inverse_bind_matrices_label: format!(
741                        "Skin{source_skin_index}/InverseBindMatrices"
742                    ),
743                }
744            })
745            .collect();
746
747        let named_maps = project_named_maps(raw, animations);
748        let (target_coverage, targets) = project_targets(
749            raw,
750            animations,
751            bevy_animation_enabled,
752            load_animations,
753            pointer_width,
754        )?;
755        let mut projection = Self {
756            scenes,
757            default_scene_route,
758            skins,
759            named_maps,
760            target_coverage,
761            targets,
762        };
763        normalize_projection_bounds(&mut projection);
764        Ok(projection)
765    }
766
767    /// Exact typed scene-label rows.
768    pub fn scenes(&self) -> &[GltfAddressabilitySceneV2] {
769        &self.scenes
770    }
771
772    /// Optional route to one existing `Scene{i}`.
773    pub const fn default_scene_route(&self) -> &GltfAddressabilityProjectionV2<String> {
774        &self.default_scene_route
775    }
776
777    /// Exact source-skin rows with distinct conditional/eager labels.
778    pub fn skins(&self) -> &[GltfAddressabilitySkinV2] {
779        &self.skins
780    }
781
782    /// Scene, animation, and skin named-map projections.
783    pub fn named_maps(&self) -> &[GltfAddressabilityNamedMapV2] {
784        &self.named_maps
785    }
786
787    /// Unique source animation target-node projections.
788    pub fn targets(&self) -> &[GltfAddressabilityTargetV2] {
789        &self.targets
790    }
791
792    /// Whether the unique-target collection is exhaustive.
793    pub const fn target_coverage(&self) -> &GltfAddressabilityProjectionV2<()> {
794        &self.target_coverage
795    }
796
797    /// Whether any rich prediction is required-unavailable.
798    pub fn has_required_unavailable(&self) -> bool {
799        self.default_scene_route.is_unavailable()
800            || self.skins.iter().any(|row| row.skin_label.is_unavailable())
801            || self
802                .named_maps
803                .iter()
804                .any(|row| row.winners.is_unavailable())
805            || self
806                .targets
807                .iter()
808                .any(|row| row.projection.is_unavailable())
809            || self.target_coverage.is_unavailable()
810    }
811}
812
813/// Rich engine-neutral inventory adapter that reuses the immutable V1
814/// animation inventory beside the raw scene/node/skin sidecar.
815#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
816#[serde(deny_unknown_fields)]
817pub struct GltfAddressabilityInventoryV2 {
818    schema: String,
819    raw: RawGltfAddressabilityInventoryV1,
820    animations: GltfAnimationAddressabilityInventoryV1,
821}
822
823impl GltfAddressabilityInventoryV2 {
824    /// Bind the two immutable same-load inventories.
825    pub fn new(
826        raw: RawGltfAddressabilityInventoryV1,
827        animations: GltfAnimationAddressabilityInventoryV1,
828    ) -> Result<Self, GltfAddressabilityV2Error> {
829        validate_inventory_binding(&raw, &animations)?;
830        Ok(Self {
831            schema: GLTF_ADDRESSABILITY_V2_ID.into(),
832            raw,
833            animations,
834        })
835    }
836
837    /// Immutable rich inventory schema identity.
838    pub fn contract_id(&self) -> &str {
839        &self.schema
840    }
841
842    /// Raw scene/node/skin/path sidecar.
843    pub const fn raw(&self) -> &RawGltfAddressabilityInventoryV1 {
844        &self.raw
845    }
846
847    /// Unchanged V1 animation inventory.
848    pub const fn animations(&self) -> &GltfAnimationAddressabilityInventoryV1 {
849        &self.animations
850    }
851
852    fn validate(&self) -> Result<(), GltfAddressabilityV2Error> {
853        if self.schema != GLTF_ADDRESSABILITY_V2_ID {
854            return Err(GltfAddressabilityV2Error::InvalidSchema);
855        }
856        validate_inventory_binding(&self.raw, &self.animations)
857    }
858}
859
860/// Exact Bevy adapter paired with one real `engine-addressability` evaluation.
861#[derive(Debug, Clone, Serialize)]
862#[serde(deny_unknown_fields)]
863pub struct GltfAddressabilityBevyAdapterV2 {
864    rules: BevyGltfAddressabilityRulesV1,
865    settings: BevyGltfAddressabilitySettingsV2,
866    prediction_provenance: PredictionProvenanceV4,
867    check: CheckEvaluation,
868    projection: BevyGltfAddressabilityProjectionV2,
869}
870
871impl GltfAddressabilityBevyAdapterV2 {
872    /// Frozen separately versioned rule authority.
873    pub const fn rules(&self) -> &BevyGltfAddressabilityRulesV1 {
874        &self.rules
875    }
876
877    /// Exact consumed revision-3 settings and origins.
878    pub const fn settings(&self) -> &BevyGltfAddressabilitySettingsV2 {
879        &self.settings
880    }
881
882    /// Same-load V4 prediction provenance.
883    pub const fn prediction_provenance(&self) -> &PredictionProvenanceV4 {
884        &self.prediction_provenance
885    }
886
887    /// The one shared check lifecycle evaluation.
888    pub const fn check(&self) -> &CheckEvaluation {
889        &self.check
890    }
891
892    /// Structured projections validated against the check facets.
893    pub const fn projection(&self) -> &BevyGltfAddressabilityProjectionV2 {
894        &self.projection
895    }
896}
897
898/// Standalone producer envelope for rich glTF addressability.
899#[derive(Debug, Clone, Serialize)]
900#[serde(deny_unknown_fields)]
901pub struct GltfAddressabilityV2 {
902    schema_version: u32,
903    schema: &'static str,
904    tool: ToolInfo,
905    command: &'static str,
906    input: InputIdentity,
907    inventory: GltfAddressabilityInventoryV2,
908    bevy: Option<GltfAddressabilityBevyAdapterV2>,
909}
910
911impl GltfAddressabilityV2 {
912    /// Construct one validated rich report.
913    pub fn new(
914        tool: ToolInfo,
915        raw: RawGltfAddressabilityInventoryV1,
916        animations: GltfAnimationAddressabilityInventoryV1,
917        bevy: Option<GltfAddressabilityBevyAdapterV2>,
918    ) -> Result<Self, GltfAddressabilityV2Error> {
919        let inventory = GltfAddressabilityInventoryV2::new(raw, animations)?;
920        if let Some(adapter) = &bevy {
921            validate_adapter(adapter, &inventory)?;
922        }
923        Ok(Self {
924            schema_version: GLTF_ADDRESSABILITY_V2_SCHEMA_VERSION,
925            schema: GLTF_ADDRESSABILITY_V2_ID,
926            tool,
927            command: crate::GLTF_ANIMATION_ADDRESSABILITY_COMMAND,
928            input: inventory.raw.primary_input().clone(),
929            inventory,
930            bevy,
931        })
932    }
933
934    /// Exact primary input identity.
935    pub const fn input(&self) -> &InputIdentity {
936        &self.input
937    }
938
939    /// Rich engine-neutral inventory adapter.
940    pub const fn inventory(&self) -> &GltfAddressabilityInventoryV2 {
941        &self.inventory
942    }
943
944    /// Exact richer Bevy adapter when revision 3 was selected.
945    pub const fn bevy(&self) -> Option<&GltfAddressabilityBevyAdapterV2> {
946        self.bevy.as_ref()
947    }
948
949    /// Whether the real embedded check carries required-unavailable work.
950    pub fn has_required_prediction_unavailable(&self) -> bool {
951        self.bevy
952            .as_ref()
953            .is_some_and(|adapter| adapter.check.has_required_prediction_unavailable())
954    }
955
956    /// Strictly read and cross-validate one V2 report through the 256 MiB cap.
957    pub fn read_from(
958        reader: impl Read,
959    ) -> Result<GltfAddressabilityReadbackV2, GltfAddressabilityReadErrorV2> {
960        Self::read_from_with_limit(reader, GLTF_ADDRESSABILITY_V2_MAX_REPORT_BYTES)
961    }
962
963    fn read_from_with_limit(
964        reader: impl Read,
965        limit: u64,
966    ) -> Result<GltfAddressabilityReadbackV2, GltfAddressabilityReadErrorV2> {
967        let mut bounded = reader.take(limit + 1);
968        let mut bytes = Vec::new();
969        bounded
970            .read_to_end(&mut bytes)
971            .map_err(|source| GltfAddressabilityReadErrorV2::Io { source })?;
972        if bytes.len() as u64 > limit {
973            return Err(GltfAddressabilityReadErrorV2::ReportTooLarge { limit });
974        }
975        let wire: GltfAddressabilityWireV2 = serde_json::from_slice(&bytes)
976            .map_err(|source| GltfAddressabilityReadErrorV2::InvalidJson { source })?;
977        GltfAddressabilityReadbackV2::from_wire(wire)
978            .map_err(GltfAddressabilityReadErrorV2::Contract)
979    }
980}
981
982/// Build the optional exact revision-3 Bevy adapter through one check run.
983pub fn build_bevy_addressability_adapter_v2(
984    source: &LoadedSource,
985    raw: &RawGltfAddressabilityInventoryV1,
986    animations: &GltfAnimationAddressabilityInventoryV1,
987    resolved: &ResolvedProfileSettingsV2,
988    prediction_provenance: PredictionProvenanceV4,
989    pointer_width: Option<TargetPointerWidth>,
990    ctx: &CheckCtx<'_>,
991) -> Result<Option<GltfAddressabilityBevyAdapterV2>, GltfAddressabilityV2Error> {
992    validate_inventory_binding(raw, animations)?;
993    let settings = BevyGltfAddressabilitySettingsV2::from_resolved(resolved)?;
994    validate_provenance(source, raw, &prediction_provenance)?;
995    let rules = BevyGltfAddressabilityRulesV1::frozen(pointer_width);
996    let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
997        raw,
998        animations,
999        settings.bevy_animation_feature,
1000        settings.load_animations,
1001        pointer_width,
1002    )?;
1003    let output = build_check_output(source, raw, animations, &prediction_provenance, &projection)?;
1004    let check = RichAddressabilityCheck { output };
1005    let checks: [Box<dyn Check>; 1] = [Box::new(check)];
1006    let evaluation = evaluate_checks(ctx, &checks, CheckSelection::All)
1007        .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?
1008        .pop()
1009        .expect("one static check produces one evaluation");
1010    let adapter = GltfAddressabilityBevyAdapterV2 {
1011        rules,
1012        settings,
1013        prediction_provenance,
1014        check: evaluation,
1015        projection,
1016    };
1017    validate_adapter(
1018        &adapter,
1019        &GltfAddressabilityInventoryV2::new(raw.clone(), animations.clone())?,
1020    )?;
1021    Ok(Some(adapter))
1022}
1023
1024struct RichAddressabilityCheck {
1025    output: CheckOutput,
1026}
1027
1028impl Check for RichAddressabilityCheck {
1029    fn id(&self) -> &'static str {
1030        crate::ENGINE_ADDRESSABILITY_CHECK_ID
1031    }
1032
1033    fn applicability(&self, _ctx: &CheckCtx<'_>) -> Applicability {
1034        Applicability::Applicable
1035    }
1036
1037    fn evaluate(&self, _ctx: &CheckCtx<'_>) -> CheckOutput {
1038        self.output.clone()
1039    }
1040}
1041
1042fn build_check_output(
1043    source: &LoadedSource,
1044    raw: &RawGltfAddressabilityInventoryV1,
1045    animations: &GltfAnimationAddressabilityInventoryV1,
1046    provenance: &PredictionProvenanceV4,
1047    projection: &BevyGltfAddressabilityProjectionV2,
1048) -> Result<CheckOutput, GltfAddressabilityV2Error> {
1049    let facts = source.source_facts();
1050    let mut scopes = Vec::new();
1051    let mut facets = Vec::new();
1052    let mut add = |scope: EvaluationScope, available: bool, reasons: Vec<_>, clip_index| {
1053        if available {
1054            scopes.push(scope.clone());
1055            return Ok::<(), GltfAddressabilityV2Error>(());
1056        }
1057        let basis = rich_addressability_basis_v4(facts, clip_index);
1058        let facet = EnginePredictionFacetV4::required_unavailable(scope, basis, reasons)
1059            .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1060        facets.push(facet);
1061        Ok::<(), GltfAddressabilityV2Error>(())
1062    };
1063
1064    if matches!(
1065        animations.animations().coverage().state(),
1066        crate::GltfAnimationCoverageStateV1::Complete
1067    ) {
1068        for animation in animations.animations().rows() {
1069            let label = BevyAnimationAssetLabelV1::new(animation.source_clip_index() as usize)
1070                .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1071            add(
1072                EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL)
1073                    .subject(label.as_str().to_owned()),
1074                true,
1075                Vec::new(),
1076                Some(animation.source_clip_index() as usize),
1077            )?;
1078        }
1079    } else {
1080        add(
1081            EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
1082            false,
1083            vec![PredictionUnavailableReasonV2::RawSourceIncomplete],
1084            None,
1085        )?;
1086    }
1087
1088    for scene in &projection.scenes {
1089        add(
1090            EvaluationScope::new(EvaluationScopeCode::SCENE_ASSET_LABEL)
1091                .subject(scene.label.clone()),
1092            true,
1093            Vec::new(),
1094            None,
1095        )?;
1096    }
1097    add_projection_facet(
1098        &mut add,
1099        EvaluationScope::new(EvaluationScopeCode::DEFAULT_SCENE_ROUTE).subject("default_scene"),
1100        &projection.default_scene_route,
1101    )?;
1102    for skin in &projection.skins {
1103        add_projection_facet(
1104            &mut add,
1105            EvaluationScope::new(EvaluationScopeCode::SKIN_ASSET_LABEL)
1106                .subject(format!("Skin{}", skin.source_skin_index)),
1107            &skin.skin_label,
1108        )?;
1109        add(
1110            EvaluationScope::new(EvaluationScopeCode::INVERSE_BIND_MATRICES_ASSET_LABEL)
1111                .subject(skin.inverse_bind_matrices_label.clone()),
1112            true,
1113            Vec::new(),
1114            None,
1115        )?;
1116    }
1117    for map in &projection.named_maps {
1118        add_projection_facet(
1119            &mut add,
1120            EvaluationScope::new(EvaluationScopeCode::NAMED_ADDRESSABILITY_MAP).subject(match map
1121                .kind
1122            {
1123                GltfAddressabilityNamedMapKindV2::Scene => "scene",
1124                GltfAddressabilityNamedMapKindV2::Animation => "animation",
1125                GltfAddressabilityNamedMapKindV2::Skin => "skin",
1126            }),
1127            &map.winners,
1128        )?;
1129    }
1130    for target in &projection.targets {
1131        add_projection_facet(
1132            &mut add,
1133            EvaluationScope::new(EvaluationScopeCode::ANIMATION_TARGET_ID)
1134                .subject(format!("Node{}", target.source_node_index)),
1135            &target.projection,
1136        )?;
1137    }
1138    if !raw.scene_coverage().is_complete()
1139        || !raw.node_coverage().is_complete()
1140        || !raw.skin_coverage().is_complete()
1141        || !raw.attachment_coverage().is_complete()
1142        || !raw.path_candidate_coverage().is_complete()
1143        || animation_inventory_incomplete(animations)
1144        || projection.target_coverage.is_unavailable()
1145    {
1146        add(
1147            EvaluationScope::new(EvaluationScopeCode::GLTF_ADDRESSABILITY_INVENTORY),
1148            false,
1149            vec![PredictionUnavailableReasonV2::RawSourceIncomplete],
1150            None,
1151        )?;
1152    }
1153    if facets.len() > PREDICTION_V1_MAX_FACETS_PER_FILE {
1154        // V4 reserves `FacetBudgetExceeded` for this exact check-owned scope;
1155        // using the otherwise natural glTF-inventory scope would be rejected
1156        // by the shared CheckEvaluation lifecycle validator.
1157        let summary_scope = EvaluationScope::new(EvaluationScopeCode::custom(FACET_BUDGET_SCOPE));
1158        facets = vec![
1159            EnginePredictionFacetV4::required_unavailable(
1160                summary_scope,
1161                rich_addressability_basis_v4(facts, None),
1162                vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
1163            )
1164            .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?,
1165        ];
1166    }
1167    let output = CheckOutput::from_coverage(Vec::new(), scopes, Vec::new());
1168    if facets.is_empty() {
1169        Ok(output)
1170    } else {
1171        let prediction = EnginePredictionV4::new(provenance.identity().clone(), facets)
1172            .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1173        Ok(output.with_engine_prediction_v4(prediction))
1174    }
1175}
1176
1177fn rich_addressability_basis_v4(
1178    facts: animsmith_core::SourceFactsViewV1<'_>,
1179    source_animation_index: Option<usize>,
1180) -> EnginePredictionBasisV4 {
1181    let mut references = vec![PredictionBasisReferenceV4::v2(
1182        PredictionBasisReferenceV2::v1(
1183            PredictionBasisReferenceV1::primary_source("bevy-gltf-loader-0.19.0-c6f634ca")
1184                .expect("static primary-source id is valid"),
1185        ),
1186    )];
1187    for setting_id in [
1188        EngineSettingIdV2::BevyAnimationFeature,
1189        EngineSettingIdV2::LoadAnimations,
1190    ] {
1191        references.push(PredictionBasisReferenceV4::v2(
1192            PredictionBasisReferenceV2::v1(
1193                PredictionBasisReferenceV1::resolved_setting(
1194                    ResolvedSettingLocationV1::Document,
1195                    setting_id.as_str(),
1196                )
1197                .expect("static setting id is valid"),
1198            ),
1199        ));
1200    }
1201    if let Some(source_animation_index) = source_animation_index {
1202        let source_name = RawSourceBasisReferenceV1::from_source(
1203            RawSourceDomainV1::Clip,
1204            RawSourceKeyV1::Clip {
1205                source_clip_index: source_animation_index as u64,
1206            },
1207            RawSourceFieldIdV1::new("source_name.state").expect("static field is valid"),
1208            facts,
1209        )
1210        .expect("retained animation row resolves its same-load source witness");
1211        references.push(PredictionBasisReferenceV4::v2(
1212            PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::raw_source(source_name)),
1213        ));
1214    }
1215    EnginePredictionBasisV4::new(references).expect("static rich addressability basis is canonical")
1216}
1217
1218fn add_projection_facet<T>(
1219    add: &mut impl FnMut(
1220        EvaluationScope,
1221        bool,
1222        Vec<PredictionUnavailableReasonV2>,
1223        Option<usize>,
1224    ) -> Result<(), GltfAddressabilityV2Error>,
1225    scope: EvaluationScope,
1226    projection: &GltfAddressabilityProjectionV2<T>,
1227) -> Result<(), GltfAddressabilityV2Error> {
1228    match projection {
1229        GltfAddressabilityProjectionV2::Available { .. }
1230        | GltfAddressabilityProjectionV2::ProvenAbsent => add(scope, true, Vec::new(), None),
1231        GltfAddressabilityProjectionV2::RequiredUnavailable { reasons } => add(
1232            scope,
1233            false,
1234            reasons.iter().map(prediction_reason).collect(),
1235            None,
1236        ),
1237    }
1238}
1239
1240fn prediction_reason(
1241    reason: &GltfAddressabilityUnavailableReasonV2,
1242) -> PredictionUnavailableReasonV2 {
1243    match reason {
1244        GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete
1245        | GltfAddressabilityUnavailableReasonV2::NamedMapIncomplete
1246        | GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated => {
1247            PredictionUnavailableReasonV2::RawSourceIncomplete
1248        }
1249        GltfAddressabilityUnavailableReasonV2::DependencyClosureIncomplete => {
1250            PredictionUnavailableReasonV2::DependencyClosureIncomplete
1251        }
1252        GltfAddressabilityUnavailableReasonV2::BevyAnimationFeatureDisabled
1253        | GltfAddressabilityUnavailableReasonV2::LoadAnimationsDisabled
1254        | GltfAddressabilityUnavailableReasonV2::TargetPointerWidthMissing => {
1255            PredictionUnavailableReasonV2::ProjectIntentUnavailable
1256        }
1257        GltfAddressabilityUnavailableReasonV2::UnreachableTarget => {
1258            PredictionUnavailableReasonV2::SourceSelectorNoMatch
1259        }
1260        GltfAddressabilityUnavailableReasonV2::MultipleCandidatePaths
1261        | GltfAddressabilityUnavailableReasonV2::DuplicateFullPath
1262        | GltfAddressabilityUnavailableReasonV2::TargetIdCollision => {
1263            PredictionUnavailableReasonV2::SourceSelectorAmbiguous
1264        }
1265        GltfAddressabilityUnavailableReasonV2::PathBoundsExceeded
1266        | GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded => {
1267            PredictionUnavailableReasonV2::MeasurementUnavailable
1268        }
1269    }
1270}
1271
1272fn validate_provenance(
1273    source: &LoadedSource,
1274    raw: &RawGltfAddressabilityInventoryV1,
1275    provenance: &PredictionProvenanceV4,
1276) -> Result<(), GltfAddressabilityV2Error> {
1277    provenance
1278        .validate()
1279        .map_err(|_| GltfAddressabilityV2Error::InvalidPredictionProvenance)?;
1280    let selection = provenance.profile().selection();
1281    if selection.family() != "bevy"
1282        || selection.profile_revision() != BEVY_PROFILE_REVISION
1283        || selection.engine_version() != "0.19.0"
1284        || selection.importer() != "gltf-asset-loader"
1285    {
1286        return Err(GltfAddressabilityV2Error::InvalidBevyProfile);
1287    }
1288    if provenance.raw_source().primary_input() != raw.primary_input()
1289        || provenance.dependency_closure() != raw.dependency_closure()
1290        || source.dependency_closure() != raw.dependency_closure()
1291        || source.source_facts().primary_identity() != raw.primary_input()
1292    {
1293        return Err(GltfAddressabilityV2Error::InventoryBindingMismatch);
1294    }
1295    Ok(())
1296}
1297
1298fn validate_adapter(
1299    adapter: &GltfAddressabilityBevyAdapterV2,
1300    inventory: &GltfAddressabilityInventoryV2,
1301) -> Result<(), GltfAddressabilityV2Error> {
1302    inventory.validate()?;
1303    if !adapter.rules.validate() {
1304        return Err(GltfAddressabilityV2Error::InvalidRuleBundle);
1305    }
1306    adapter
1307        .prediction_provenance
1308        .validate()
1309        .map_err(|_| GltfAddressabilityV2Error::InvalidPredictionProvenance)?;
1310    let selection = adapter.prediction_provenance.profile().selection();
1311    if selection.family() != "bevy"
1312        || selection.profile_revision() != BEVY_PROFILE_REVISION
1313        || selection.engine_version() != "0.19.0"
1314        || selection.importer() != "gltf-asset-loader"
1315        || adapter.prediction_provenance.raw_source().primary_input()
1316            != inventory.raw.primary_input()
1317        || adapter.prediction_provenance.dependency_closure() != inventory.raw.dependency_closure()
1318    {
1319        return Err(GltfAddressabilityV2Error::InvalidPredictionProvenance);
1320    }
1321    validate_settings_binding(&adapter.settings, &adapter.prediction_provenance)?;
1322    let expected = BevyGltfAddressabilityProjectionV2::from_inventories(
1323        &inventory.raw,
1324        &inventory.animations,
1325        adapter.settings.bevy_animation_feature,
1326        adapter.settings.load_animations,
1327        adapter.rules.target_pointer_width,
1328    )?;
1329    if expected != adapter.projection
1330        || adapter.check.check_id() != crate::ENGINE_ADDRESSABILITY_CHECK_ID
1331        || adapter.check.selection() != SelectionState::Selected
1332        || adapter.check.configuration() != ConfigurationState::Enabled
1333        || adapter.check.applicability() != Applicability::Applicable
1334        || !adapter.check.findings().is_empty()
1335        || !adapter.check.gaps().is_empty()
1336    {
1337        return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1338    }
1339    let prediction = adapter.check.engine_prediction_v4();
1340    if let Some(prediction) = prediction {
1341        prediction
1342            .validate_against_provenance(&adapter.prediction_provenance)
1343            .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1344    }
1345    validate_facet_states(
1346        prediction,
1347        adapter.check.evaluated_scopes(),
1348        adapter.check.evaluation(),
1349        &expected_facet_specs(inventory, &adapter.projection),
1350    )
1351}
1352
1353fn validate_settings_binding(
1354    settings: &BevyGltfAddressabilitySettingsV2,
1355    provenance: &PredictionProvenanceV4,
1356) -> Result<(), GltfAddressabilityV2Error> {
1357    let matches = |id, expected_value, expected_origin| {
1358        provenance
1359            .settings()
1360            .document_setting(id)
1361            .is_some_and(|row| {
1362                row.value() == &EngineSettingValueV2::Boolean(expected_value)
1363                    && row.value_origin()
1364                        == match expected_origin {
1365                            GltfAddressabilitySettingOriginV2::ExplicitConfig => {
1366                                EngineSettingValueOriginV3::ExplicitConfig
1367                            }
1368                            GltfAddressabilitySettingOriginV2::ProfileDefault => {
1369                                EngineSettingValueOriginV3::ProfileDefault
1370                            }
1371                        }
1372            })
1373    };
1374    if !matches(
1375        EngineSettingIdV2::BevyAnimationFeature,
1376        settings.bevy_animation_feature,
1377        settings.bevy_animation_feature_origin,
1378    ) || !matches(
1379        EngineSettingIdV2::LoadAnimations,
1380        settings.load_animations,
1381        settings.load_animations_origin,
1382    ) {
1383        return Err(GltfAddressabilityV2Error::InvalidPredictionProvenance);
1384    }
1385    Ok(())
1386}
1387
1388#[derive(Debug, Clone)]
1389struct ExpectedFacetV2 {
1390    scope: EvaluationScope,
1391    state: EnginePredictionFacetStateV1,
1392    reasons: Vec<PredictionUnavailableReasonV2>,
1393}
1394
1395fn validate_facet_states(
1396    prediction: Option<&EnginePredictionV4>,
1397    evaluated_scopes: &[EvaluationScope],
1398    evaluation: EvaluationState,
1399    expected: &[ExpectedFacetV2],
1400) -> Result<(), GltfAddressabilityV2Error> {
1401    if evaluated_scopes.len() > GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES {
1402        return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1403    }
1404    let expected_available = expected
1405        .iter()
1406        .filter(|facet| facet.state == EnginePredictionFacetStateV1::Available)
1407        .collect::<Vec<_>>();
1408    let expected_unavailable = expected
1409        .iter()
1410        .filter(|facet| facet.state == EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
1411        .collect::<Vec<_>>();
1412    let actual_facets = prediction.map_or(&[][..], EnginePredictionV4::facets);
1413    let compacted = expected_unavailable.len() > PREDICTION_V1_MAX_FACETS_PER_FILE;
1414    let evaluated_set = evaluated_scopes.iter().collect::<BTreeSet<_>>();
1415    let expected_available_set = expected_available
1416        .iter()
1417        .map(|facet| &facet.scope)
1418        .collect::<BTreeSet<_>>();
1419    if evaluated_set.len() != evaluated_scopes.len()
1420        || expected_available_set.len() != expected_available.len()
1421    {
1422        return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1423    }
1424
1425    if compacted {
1426        if actual_facets.len() != 1
1427            || actual_facets[0].scope().code.as_str() != FACET_BUDGET_SCOPE
1428            || actual_facets[0].scope().subject.is_some()
1429            || actual_facets[0].state()
1430                != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
1431            || actual_facets[0].result().is_some()
1432            || actual_facets[0].reasons() != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
1433            || evaluated_set != expected_available_set
1434        {
1435            return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1436        }
1437        let expected_evaluation = if expected_available.is_empty() {
1438            EvaluationState::NotEvaluated
1439        } else {
1440            EvaluationState::Partial
1441        };
1442        return if evaluation == expected_evaluation {
1443            Ok(())
1444        } else {
1445            Err(GltfAddressabilityV2Error::InvalidCheckEvaluation)
1446        };
1447    }
1448
1449    let expected_unavailable_set = expected_unavailable
1450        .iter()
1451        .map(|facet| &facet.scope)
1452        .collect::<BTreeSet<_>>();
1453    let actual_by_scope = actual_facets
1454        .iter()
1455        .map(|facet| (facet.scope(), facet))
1456        .collect::<BTreeMap<_, _>>();
1457    if expected_unavailable_set.len() != expected_unavailable.len()
1458        || actual_by_scope.len() != actual_facets.len()
1459        || evaluated_set != expected_available_set
1460        || actual_by_scope.keys().copied().collect::<BTreeSet<_>>() != expected_unavailable_set
1461        || !evaluated_set.is_disjoint(&expected_unavailable_set)
1462    {
1463        return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1464    }
1465
1466    for expected in &expected_unavailable {
1467        let Some(actual) = actual_by_scope.get(&expected.scope) else {
1468            return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1469        };
1470        if actual.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
1471            || actual.result().is_some()
1472            || actual.reasons() != expected.reasons
1473        {
1474            return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1475        }
1476    }
1477
1478    if actual_facets
1479        .iter()
1480        .any(|facet| facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable)
1481    {
1482        return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1483    }
1484
1485    let expected_evaluation = if expected_unavailable.is_empty() {
1486        EvaluationState::Complete
1487    } else if expected_available.is_empty() {
1488        EvaluationState::NotEvaluated
1489    } else {
1490        EvaluationState::Partial
1491    };
1492    if evaluation != expected_evaluation {
1493        return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1494    }
1495    Ok(())
1496}
1497
1498fn expected_facet_specs(
1499    inventory: &GltfAddressabilityInventoryV2,
1500    projection: &BevyGltfAddressabilityProjectionV2,
1501) -> Vec<ExpectedFacetV2> {
1502    let mut expected = Vec::new();
1503    if matches!(
1504        inventory.animations.animations().coverage().state(),
1505        crate::GltfAnimationCoverageStateV1::Complete
1506    ) {
1507        for animation in inventory.animations.animations().rows() {
1508            expected.push(ExpectedFacetV2 {
1509                scope: EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL)
1510                    .subject(format!("Animation{}", animation.source_clip_index())),
1511                state: EnginePredictionFacetStateV1::Available,
1512                reasons: Vec::new(),
1513            });
1514        }
1515    } else {
1516        expected.push(ExpectedFacetV2 {
1517            scope: EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
1518            state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
1519            reasons: vec![PredictionUnavailableReasonV2::RawSourceIncomplete],
1520        });
1521    }
1522    for scene in &projection.scenes {
1523        expected.push(ExpectedFacetV2 {
1524            scope: EvaluationScope::new(EvaluationScopeCode::SCENE_ASSET_LABEL)
1525                .subject(scene.label.clone()),
1526            state: EnginePredictionFacetStateV1::Available,
1527            reasons: Vec::new(),
1528        });
1529    }
1530    push_expected_projection(
1531        &mut expected,
1532        EvaluationScope::new(EvaluationScopeCode::DEFAULT_SCENE_ROUTE).subject("default_scene"),
1533        &projection.default_scene_route,
1534    );
1535    for skin in &projection.skins {
1536        push_expected_projection(
1537            &mut expected,
1538            EvaluationScope::new(EvaluationScopeCode::SKIN_ASSET_LABEL)
1539                .subject(format!("Skin{}", skin.source_skin_index)),
1540            &skin.skin_label,
1541        );
1542        expected.push(ExpectedFacetV2 {
1543            scope: EvaluationScope::new(EvaluationScopeCode::INVERSE_BIND_MATRICES_ASSET_LABEL)
1544                .subject(skin.inverse_bind_matrices_label.clone()),
1545            state: EnginePredictionFacetStateV1::Available,
1546            reasons: Vec::new(),
1547        });
1548    }
1549    for map in &projection.named_maps {
1550        let subject = match map.kind {
1551            GltfAddressabilityNamedMapKindV2::Scene => "scene",
1552            GltfAddressabilityNamedMapKindV2::Animation => "animation",
1553            GltfAddressabilityNamedMapKindV2::Skin => "skin",
1554        };
1555        push_expected_projection(
1556            &mut expected,
1557            EvaluationScope::new(EvaluationScopeCode::NAMED_ADDRESSABILITY_MAP).subject(subject),
1558            &map.winners,
1559        );
1560    }
1561    for target in &projection.targets {
1562        push_expected_projection(
1563            &mut expected,
1564            EvaluationScope::new(EvaluationScopeCode::ANIMATION_TARGET_ID)
1565                .subject(format!("Node{}", target.source_node_index)),
1566            &target.projection,
1567        );
1568    }
1569    if !inventory.raw.scene_coverage().is_complete()
1570        || !inventory.raw.node_coverage().is_complete()
1571        || !inventory.raw.skin_coverage().is_complete()
1572        || !inventory.raw.attachment_coverage().is_complete()
1573        || !inventory.raw.path_candidate_coverage().is_complete()
1574        || animation_inventory_incomplete(&inventory.animations)
1575        || projection.target_coverage.is_unavailable()
1576    {
1577        expected.push(ExpectedFacetV2 {
1578            scope: EvaluationScope::new(EvaluationScopeCode::GLTF_ADDRESSABILITY_INVENTORY),
1579            state: EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
1580            reasons: vec![PredictionUnavailableReasonV2::RawSourceIncomplete],
1581        });
1582    }
1583    expected
1584}
1585
1586fn animation_inventory_incomplete(animations: &GltfAnimationAddressabilityInventoryV1) -> bool {
1587    !matches!(
1588        animations.animations().coverage().state(),
1589        crate::GltfAnimationCoverageStateV1::Complete
1590    ) || animations.animations().rows().iter().any(|animation| {
1591        !matches!(
1592            animation.channels().coverage().state(),
1593            crate::GltfAnimationCoverageStateV1::Complete
1594        )
1595    })
1596}
1597
1598fn push_expected_projection<T>(
1599    expected: &mut Vec<ExpectedFacetV2>,
1600    scope: EvaluationScope,
1601    projection: &GltfAddressabilityProjectionV2<T>,
1602) {
1603    let (state, mut reasons) = match projection {
1604        GltfAddressabilityProjectionV2::Available { .. }
1605        | GltfAddressabilityProjectionV2::ProvenAbsent => {
1606            (EnginePredictionFacetStateV1::Available, Vec::new())
1607        }
1608        GltfAddressabilityProjectionV2::RequiredUnavailable { reasons } => (
1609            EnginePredictionFacetStateV1::RequiredPredictionUnavailable,
1610            reasons.iter().map(prediction_reason).collect(),
1611        ),
1612    };
1613    reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
1614    reasons.dedup();
1615    expected.push(ExpectedFacetV2 {
1616        scope,
1617        state,
1618        reasons,
1619    });
1620}
1621
1622#[derive(Debug, Deserialize)]
1623#[serde(deny_unknown_fields)]
1624struct ToolSourceWireV2 {
1625    revision: Option<String>,
1626    dirty: Option<bool>,
1627}
1628
1629#[derive(Debug, Deserialize)]
1630#[serde(deny_unknown_fields)]
1631struct ToolWireV2 {
1632    name: String,
1633    version: String,
1634    source: ToolSourceWireV2,
1635}
1636
1637#[derive(Debug, Deserialize)]
1638#[serde(deny_unknown_fields)]
1639struct AdapterWireV2 {
1640    rules: BevyGltfAddressabilityRulesV1,
1641    settings: BevyGltfAddressabilitySettingsV2,
1642    prediction_provenance: PredictionProvenanceV4,
1643    check: Box<RawValue>,
1644    projection: BevyGltfAddressabilityProjectionV2,
1645}
1646
1647#[derive(Debug, Deserialize)]
1648#[serde(deny_unknown_fields)]
1649struct GltfAddressabilityWireV2 {
1650    schema_version: u32,
1651    schema: String,
1652    tool: ToolWireV2,
1653    command: String,
1654    input: InputIdentity,
1655    inventory: GltfAddressabilityInventoryV2,
1656    bevy: Option<AdapterWireV2>,
1657}
1658
1659#[derive(Debug, Deserialize)]
1660#[serde(deny_unknown_fields)]
1661struct CheckWireV2 {
1662    check_id: String,
1663    selection: SelectionState,
1664    configuration: ConfigurationState,
1665    applicability: Applicability,
1666    evaluation: EvaluationState,
1667    findings: Vec<serde_json::Value>,
1668    #[serde(default)]
1669    evaluated_scopes: Vec<EvaluationScope>,
1670    #[serde(default)]
1671    gaps: Vec<serde_json::Value>,
1672    #[serde(default)]
1673    prediction: Option<EnginePredictionV4>,
1674}
1675
1676/// Strict read-side representation of the one rich check evaluation.
1677#[derive(Debug, Clone)]
1678pub struct GltfAddressabilityCheckReadbackV2 {
1679    check_id: String,
1680    selection: SelectionState,
1681    configuration: ConfigurationState,
1682    applicability: Applicability,
1683    evaluation: EvaluationState,
1684    evaluated_scopes: Vec<EvaluationScope>,
1685    prediction: Option<EnginePredictionV4>,
1686}
1687
1688impl GltfAddressabilityCheckReadbackV2 {
1689    /// Stable check id, validated as `engine-addressability`.
1690    pub fn check_id(&self) -> &str {
1691        &self.check_id
1692    }
1693
1694    /// Serialized selection state.
1695    pub const fn selection(&self) -> SelectionState {
1696        self.selection
1697    }
1698
1699    /// Serialized configuration state.
1700    pub const fn configuration(&self) -> ConfigurationState {
1701        self.configuration
1702    }
1703
1704    /// Serialized applicability state.
1705    pub const fn applicability(&self) -> Applicability {
1706        self.applicability
1707    }
1708
1709    /// Derived serialized evaluation state.
1710    pub const fn evaluation(&self) -> EvaluationState {
1711        self.evaluation
1712    }
1713
1714    /// Strictly decoded V4 prediction attachment.
1715    pub const fn prediction(&self) -> Option<&EnginePredictionV4> {
1716        self.prediction.as_ref()
1717    }
1718
1719    /// Exact completed scopes.
1720    pub fn evaluated_scopes(&self) -> &[EvaluationScope] {
1721        &self.evaluated_scopes
1722    }
1723}
1724
1725/// Strict read-side exact Bevy adapter.
1726#[derive(Debug, Clone)]
1727pub struct GltfAddressabilityBevyReadbackV2 {
1728    rules: BevyGltfAddressabilityRulesV1,
1729    settings: BevyGltfAddressabilitySettingsV2,
1730    prediction_provenance: PredictionProvenanceV4,
1731    check: GltfAddressabilityCheckReadbackV2,
1732    projection: BevyGltfAddressabilityProjectionV2,
1733}
1734
1735impl GltfAddressabilityBevyReadbackV2 {
1736    /// Frozen rule bundle.
1737    pub const fn rules(&self) -> &BevyGltfAddressabilityRulesV1 {
1738        &self.rules
1739    }
1740
1741    /// Exact consumed settings.
1742    pub const fn settings(&self) -> &BevyGltfAddressabilitySettingsV2 {
1743        &self.settings
1744    }
1745
1746    /// Strict same-load provenance.
1747    pub const fn prediction_provenance(&self) -> &PredictionProvenanceV4 {
1748        &self.prediction_provenance
1749    }
1750
1751    /// Strict real-check readback.
1752    pub const fn check(&self) -> &GltfAddressabilityCheckReadbackV2 {
1753        &self.check
1754    }
1755
1756    /// Structured rich projections.
1757    pub const fn projection(&self) -> &BevyGltfAddressabilityProjectionV2 {
1758        &self.projection
1759    }
1760}
1761
1762/// Fully validated read-side V2 addressability report.
1763#[derive(Debug, Clone)]
1764pub struct GltfAddressabilityReadbackV2 {
1765    input: InputIdentity,
1766    inventory: GltfAddressabilityInventoryV2,
1767    bevy: Option<GltfAddressabilityBevyReadbackV2>,
1768}
1769
1770impl GltfAddressabilityReadbackV2 {
1771    fn from_wire(wire: GltfAddressabilityWireV2) -> Result<Self, GltfAddressabilityV2Error> {
1772        if wire.schema_version != GLTF_ADDRESSABILITY_V2_SCHEMA_VERSION
1773            || wire.schema != GLTF_ADDRESSABILITY_V2_ID
1774            || wire.command != crate::GLTF_ANIMATION_ADDRESSABILITY_COMMAND
1775            || wire.tool.name != "animsmith"
1776            || !valid_semver(&wire.tool.version)
1777            || wire.tool.source.revision.as_ref().is_some_and(|revision| {
1778                revision.len() != 40 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit())
1779            })
1780        {
1781            return Err(GltfAddressabilityV2Error::InvalidSchema);
1782        }
1783        let _dirty = wire.tool.source.dirty;
1784        wire.inventory.validate()?;
1785        if &wire.input != wire.inventory.raw.primary_input() {
1786            return Err(GltfAddressabilityV2Error::InventoryBindingMismatch);
1787        }
1788        let bevy = wire
1789            .bevy
1790            .map(|adapter| read_adapter(adapter, &wire.inventory))
1791            .transpose()?;
1792        Ok(Self {
1793            input: wire.input,
1794            inventory: wire.inventory,
1795            bevy,
1796        })
1797    }
1798
1799    /// Exact primary input identity.
1800    pub const fn input(&self) -> &InputIdentity {
1801        &self.input
1802    }
1803
1804    /// Strict rich inventory.
1805    pub const fn inventory(&self) -> &GltfAddressabilityInventoryV2 {
1806        &self.inventory
1807    }
1808
1809    /// Strict exact Bevy adapter when present.
1810    pub const fn bevy(&self) -> Option<&GltfAddressabilityBevyReadbackV2> {
1811        self.bevy.as_ref()
1812    }
1813}
1814
1815fn read_adapter(
1816    wire: AdapterWireV2,
1817    inventory: &GltfAddressabilityInventoryV2,
1818) -> Result<GltfAddressabilityBevyReadbackV2, GltfAddressabilityV2Error> {
1819    if !wire.rules.validate() {
1820        return Err(GltfAddressabilityV2Error::InvalidRuleBundle);
1821    }
1822    wire.prediction_provenance
1823        .validate()
1824        .map_err(|_| GltfAddressabilityV2Error::InvalidPredictionProvenance)?;
1825    let selection = wire.prediction_provenance.profile().selection();
1826    if selection.family() != "bevy"
1827        || selection.profile_revision() != BEVY_PROFILE_REVISION
1828        || selection.engine_version() != "0.19.0"
1829        || selection.importer() != "gltf-asset-loader"
1830        || wire.prediction_provenance.raw_source().primary_input() != inventory.raw.primary_input()
1831        || wire.prediction_provenance.dependency_closure() != inventory.raw.dependency_closure()
1832    {
1833        return Err(GltfAddressabilityV2Error::InvalidPredictionProvenance);
1834    }
1835    validate_settings_binding(&wire.settings, &wire.prediction_provenance)?;
1836    let expected = BevyGltfAddressabilityProjectionV2::from_inventories(
1837        &inventory.raw,
1838        &inventory.animations,
1839        wire.settings.bevy_animation_feature,
1840        wire.settings.load_animations,
1841        wire.rules.target_pointer_width,
1842    )?;
1843    if expected != wire.projection {
1844        return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1845    }
1846    let check: CheckWireV2 = serde_json::from_str(wire.check.get())
1847        .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1848    if check.check_id != crate::ENGINE_ADDRESSABILITY_CHECK_ID
1849        || check.selection != SelectionState::Selected
1850        || check.configuration != ConfigurationState::Enabled
1851        || check.applicability != Applicability::Applicable
1852        || !check.findings.is_empty()
1853        || !check.gaps.is_empty()
1854    {
1855        return Err(GltfAddressabilityV2Error::InvalidCheckEvaluation);
1856    }
1857    if let Some(prediction) = &check.prediction {
1858        prediction
1859            .validate_against_provenance(&wire.prediction_provenance)
1860            .map_err(|_| GltfAddressabilityV2Error::InvalidCheckEvaluation)?;
1861    }
1862    validate_facet_states(
1863        check.prediction.as_ref(),
1864        &check.evaluated_scopes,
1865        check.evaluation,
1866        &expected_facet_specs(inventory, &wire.projection),
1867    )?;
1868    Ok(GltfAddressabilityBevyReadbackV2 {
1869        rules: wire.rules,
1870        settings: wire.settings,
1871        prediction_provenance: wire.prediction_provenance,
1872        check: GltfAddressabilityCheckReadbackV2 {
1873            check_id: check.check_id,
1874            selection: check.selection,
1875            configuration: check.configuration,
1876            applicability: check.applicability,
1877            evaluation: check.evaluation,
1878            evaluated_scopes: check.evaluated_scopes,
1879            prediction: check.prediction,
1880        },
1881        projection: wire.projection,
1882    })
1883}
1884
1885fn valid_semver(value: &str) -> bool {
1886    let core = value
1887        .split_once('+')
1888        .map_or(value, |(core, _)| core)
1889        .split_once('-')
1890        .map_or_else(
1891            || value.split_once('+').map_or(value, |(core, _)| core),
1892            |(core, _)| core,
1893        );
1894    let mut parts = core.split('.');
1895    (0..3).all(|_| {
1896        parts
1897            .next()
1898            .is_some_and(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
1899    }) && parts.next().is_none()
1900}
1901
1902/// Bounded rich report reader failure.
1903#[derive(Debug, thiserror::Error)]
1904#[non_exhaustive]
1905pub enum GltfAddressabilityReadErrorV2 {
1906    /// Reading the bounded input failed.
1907    #[error("cannot read rich addressability report: {source}")]
1908    Io {
1909        /// Underlying reader error.
1910        source: std::io::Error,
1911    },
1912    /// The serialized input exceeded 256 MiB.
1913    #[error("rich addressability report exceeds byte limit {limit}")]
1914    ReportTooLarge {
1915        /// Immutable byte ceiling.
1916        limit: u64,
1917    },
1918    /// Strict JSON decoding failed.
1919    #[error("invalid rich addressability JSON: {source}")]
1920    InvalidJson {
1921        /// Decoder diagnostic.
1922        source: serde_json::Error,
1923    },
1924    /// Cross-field or identity validation failed.
1925    #[error(transparent)]
1926    Contract(#[from] GltfAddressabilityV2Error),
1927}
1928
1929fn validate_inventory_binding(
1930    raw: &RawGltfAddressabilityInventoryV1,
1931    animations: &GltfAnimationAddressabilityInventoryV1,
1932) -> Result<(), GltfAddressabilityV2Error> {
1933    raw.validate()
1934        .map_err(|_| GltfAddressabilityV2Error::InvalidRawInventory)?;
1935    if raw.primary_input() != animations.primary_input()
1936        || raw.dependency_closure() != animations.dependency_closure()
1937    {
1938        return Err(GltfAddressabilityV2Error::InventoryBindingMismatch);
1939    }
1940    Ok(())
1941}
1942
1943fn project_named_maps(
1944    raw: &RawGltfAddressabilityInventoryV1,
1945    animations: &GltfAnimationAddressabilityInventoryV1,
1946) -> Vec<GltfAddressabilityNamedMapV2> {
1947    let mut scene_winners = BTreeMap::new();
1948    for row in raw.scenes() {
1949        if let Some(name) = row.name() {
1950            scene_winners.insert(
1951                name.to_owned(),
1952                GltfAddressabilityNamedMapWinnerV2 {
1953                    name: name.to_owned(),
1954                    source_index: row.source_scene_index(),
1955                    typed_label: format!("Scene{}", row.source_scene_index()),
1956                },
1957            );
1958        }
1959    }
1960    let scenes = named_map(
1961        GltfAddressabilityNamedMapKindV2::Scene,
1962        raw.scene_coverage().is_complete(),
1963        scene_winners,
1964    );
1965
1966    let mut animation_winners = BTreeMap::new();
1967    for row in animations.animations().rows() {
1968        if let GltfAnimationObservationV1::Observed { value: name } = row.source_name() {
1969            animation_winners.insert(
1970                name.clone(),
1971                GltfAddressabilityNamedMapWinnerV2 {
1972                    name: name.clone(),
1973                    source_index: row.source_clip_index(),
1974                    typed_label: format!("Animation{}", row.source_clip_index()),
1975                },
1976            );
1977        }
1978    }
1979    let animations = named_map(
1980        GltfAddressabilityNamedMapKindV2::Animation,
1981        matches!(
1982            animations.animations().coverage().state(),
1983            crate::GltfAnimationCoverageStateV1::Complete
1984        ),
1985        animation_winners,
1986    );
1987
1988    // Skin handles are lazily created in first-reference order, and the named
1989    // map is last-write-wins over that order.  Unreferenced skins never enter
1990    // this map even though their inverse-bind assets are created eagerly.
1991    let skins_by_index = raw
1992        .skins()
1993        .iter()
1994        .map(|skin| (skin.source_skin_index(), skin))
1995        .collect::<BTreeMap<_, _>>();
1996    let mut seen = BTreeSet::new();
1997    let mut skin_winners = BTreeMap::new();
1998    for attachment in raw.attachments() {
1999        let index = attachment.source_skin_index();
2000        if seen.insert(index)
2001            && let Some(skin) = skins_by_index.get(&index)
2002            && let Some(name) = skin.name()
2003        {
2004            skin_winners.insert(
2005                name.to_owned(),
2006                GltfAddressabilityNamedMapWinnerV2 {
2007                    name: name.to_owned(),
2008                    source_index: index,
2009                    typed_label: format!("Skin{index}"),
2010                },
2011            );
2012        }
2013    }
2014    let skins = named_map(
2015        GltfAddressabilityNamedMapKindV2::Skin,
2016        raw.skin_coverage().is_complete() && raw.attachment_coverage().is_complete(),
2017        skin_winners,
2018    );
2019    vec![scenes, animations, skins]
2020}
2021
2022fn named_map(
2023    kind: GltfAddressabilityNamedMapKindV2,
2024    complete: bool,
2025    winners: BTreeMap<String, GltfAddressabilityNamedMapWinnerV2>,
2026) -> GltfAddressabilityNamedMapV2 {
2027    let keys_bounded = winners
2028        .keys()
2029        .all(|name| name.len() <= GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES);
2030    GltfAddressabilityNamedMapV2 {
2031        kind,
2032        duplicate_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
2033        winners: if !keys_bounded {
2034            GltfAddressabilityProjectionV2::unavailable(vec![
2035                GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2036            ])
2037        } else if complete {
2038            GltfAddressabilityProjectionV2::Available {
2039                value: winners.into_values().collect(),
2040            }
2041        } else {
2042            GltfAddressabilityProjectionV2::unavailable(vec![
2043                GltfAddressabilityUnavailableReasonV2::NamedMapIncomplete,
2044            ])
2045        },
2046    }
2047}
2048
2049fn normalize_projection_bounds(projection: &mut BevyGltfAddressabilityProjectionV2) {
2050    if projection_structural_references(projection)
2051        > GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES
2052    {
2053        make_named_maps_bounds_unavailable(projection);
2054        let mut retained_references = 0usize;
2055        let mut retained = 0usize;
2056        for target in &projection.targets {
2057            let target_references = target.contributing_channels.len()
2058                + match &target.projection {
2059                    GltfAddressabilityProjectionV2::Available { value } => value.segments.len(),
2060                    GltfAddressabilityProjectionV2::ProvenAbsent
2061                    | GltfAddressabilityProjectionV2::RequiredUnavailable { .. } => 0,
2062                };
2063            if retained_references.saturating_add(target_references)
2064                > GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES
2065            {
2066                break;
2067            }
2068            retained_references += target_references;
2069            retained += 1;
2070        }
2071        if retained < projection.targets.len() {
2072            projection.targets.truncate(retained);
2073            projection.target_coverage = GltfAddressabilityProjectionV2::unavailable(vec![
2074                GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2075            ]);
2076        }
2077        // Any omitted structure can hide a path or UUID collision.
2078        if projection.target_coverage.is_unavailable() {
2079            make_targets_bounds_unavailable(projection);
2080        }
2081    }
2082    if projection_text_bytes(projection) > GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES {
2083        make_named_maps_bounds_unavailable(projection);
2084        make_targets_bounds_unavailable(projection);
2085    }
2086    debug_assert!(
2087        projection_structural_references(projection)
2088            <= GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES
2089    );
2090    debug_assert!(projection_text_bytes(projection) <= GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES);
2091}
2092
2093fn make_named_maps_bounds_unavailable(projection: &mut BevyGltfAddressabilityProjectionV2) {
2094    for map in &mut projection.named_maps {
2095        map.winners = GltfAddressabilityProjectionV2::unavailable(vec![
2096            GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2097        ]);
2098    }
2099}
2100
2101fn make_targets_bounds_unavailable(projection: &mut BevyGltfAddressabilityProjectionV2) {
2102    projection.target_coverage = GltfAddressabilityProjectionV2::unavailable(vec![
2103        GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2104    ]);
2105    for target in &mut projection.targets {
2106        target.projection = GltfAddressabilityProjectionV2::unavailable(vec![
2107            GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2108        ]);
2109    }
2110}
2111
2112fn projection_structural_references(projection: &BevyGltfAddressabilityProjectionV2) -> usize {
2113    let named = projection
2114        .named_maps
2115        .iter()
2116        .map(|map| match &map.winners {
2117            GltfAddressabilityProjectionV2::Available { value } => value.len(),
2118            GltfAddressabilityProjectionV2::ProvenAbsent
2119            | GltfAddressabilityProjectionV2::RequiredUnavailable { .. } => 0,
2120        })
2121        .sum::<usize>();
2122    named
2123        + projection
2124            .targets
2125            .iter()
2126            .map(|target| {
2127                target.contributing_channels.len()
2128                    + match &target.projection {
2129                        GltfAddressabilityProjectionV2::Available { value } => value.segments.len(),
2130                        GltfAddressabilityProjectionV2::ProvenAbsent
2131                        | GltfAddressabilityProjectionV2::RequiredUnavailable { .. } => 0,
2132                    }
2133            })
2134            .sum::<usize>()
2135}
2136
2137fn projection_text_bytes(projection: &BevyGltfAddressabilityProjectionV2) -> usize {
2138    let mut bytes = projection
2139        .scenes
2140        .iter()
2141        .map(|row| row.label.len())
2142        .sum::<usize>();
2143    if let GltfAddressabilityProjectionV2::Available { value } = &projection.default_scene_route {
2144        bytes = bytes.saturating_add(value.len());
2145    }
2146    for skin in &projection.skins {
2147        bytes = bytes.saturating_add(skin.inverse_bind_matrices_label.len());
2148        if let GltfAddressabilityProjectionV2::Available { value } = &skin.skin_label {
2149            bytes = bytes.saturating_add(value.len());
2150        }
2151    }
2152    for map in &projection.named_maps {
2153        if let GltfAddressabilityProjectionV2::Available { value } = &map.winners {
2154            for winner in value {
2155                bytes = bytes
2156                    .saturating_add(winner.name.len())
2157                    .saturating_add(winner.typed_label.len());
2158            }
2159        }
2160    }
2161    for target in &projection.targets {
2162        if let GltfAddressabilityProjectionV2::Available { value } = &target.projection {
2163            bytes = bytes
2164                .saturating_add(value.path.len())
2165                .saturating_add(value.uuid.len())
2166                .saturating_add(value.segments.iter().map(String::len).sum::<usize>());
2167        }
2168    }
2169    bytes
2170}
2171
2172fn project_targets(
2173    raw: &RawGltfAddressabilityInventoryV1,
2174    animations: &GltfAnimationAddressabilityInventoryV1,
2175    bevy_animation_enabled: bool,
2176    load_animations: bool,
2177    pointer_width: Option<TargetPointerWidth>,
2178) -> Result<
2179    (
2180        GltfAddressabilityProjectionV2<()>,
2181        Vec<GltfAddressabilityTargetV2>,
2182    ),
2183    GltfAddressabilityV2Error,
2184> {
2185    let mut contributors = BTreeMap::<u64, Vec<GltfAddressabilityTargetChannelV2>>::new();
2186    for animation in animations.animations().rows() {
2187        for channel in animation.channels().rows() {
2188            contributors
2189                .entry(channel.target().index())
2190                .or_default()
2191                .push(GltfAddressabilityTargetChannelV2 {
2192                    source_animation_index: animation.source_clip_index(),
2193                    source_channel_index: channel.source_channel_index(),
2194                });
2195        }
2196    }
2197    let contributor_count = contributors.len();
2198    let truncated = contributor_count > GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS;
2199    let target_domain_complete = target_domain_evidence_complete(raw, animations);
2200    let mut candidate_paths = BTreeMap::<u64, BTreeSet<Vec<u64>>>::new();
2201    for candidate in raw.path_candidates() {
2202        if let Some(target) = candidate.target_node_index() {
2203            candidate_paths
2204                .entry(target)
2205                .or_default()
2206                .insert(candidate.source_node_indices().to_vec());
2207        }
2208    }
2209    let nodes = raw
2210        .nodes()
2211        .iter()
2212        .map(|node| (node.source_node_index(), node))
2213        .collect::<BTreeMap<_, _>>();
2214    let base_reasons = target_base_reasons(
2215        raw,
2216        animations,
2217        bevy_animation_enabled,
2218        load_animations,
2219        pointer_width,
2220    );
2221    let collision_index = if base_reasons.is_empty() {
2222        let contributing_nodes = contributors.keys().copied().collect::<BTreeSet<_>>();
2223        Some(build_candidate_collision_index(
2224            &candidate_paths,
2225            &contributing_nodes,
2226            &nodes,
2227            pointer_width.expect("complete target prerequisites include pointer width"),
2228        )?)
2229    } else {
2230        None
2231    };
2232    let mut targets = Vec::with_capacity(
2233        contributors
2234            .len()
2235            .min(GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS),
2236    );
2237    for (source_node_index, contributing_channels) in contributors
2238        .into_iter()
2239        .take(GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS)
2240    {
2241        let mut reasons = base_reasons.clone();
2242        let paths = candidate_paths.get(&source_node_index);
2243        let unique_path = match paths {
2244            None => {
2245                reasons.push(GltfAddressabilityUnavailableReasonV2::UnreachableTarget);
2246                None
2247            }
2248            Some(paths) if paths.len() != 1 => {
2249                reasons.push(GltfAddressabilityUnavailableReasonV2::MultipleCandidatePaths);
2250                None
2251            }
2252            Some(paths) => paths.first(),
2253        };
2254        let projection = if reasons.is_empty() {
2255            let node_path = unique_path.expect("one path exists without path reasons");
2256            let segments = node_path
2257                .iter()
2258                .map(|index| {
2259                    nodes
2260                        .get(index)
2261                        .map(|node| {
2262                            node.name()
2263                                .map_or_else(|| format!("GltfNode{index}"), str::to_owned)
2264                        })
2265                        .ok_or(GltfAddressabilityV2Error::InventoryBindingMismatch)
2266                })
2267                .collect::<Result<Vec<_>, _>>()?;
2268            let width = pointer_width.expect("width is present without base reason");
2269            project_target_segments(segments, width)
2270        } else {
2271            GltfAddressabilityProjectionV2::unavailable(reasons)
2272        };
2273        targets.push(GltfAddressabilityTargetV2 {
2274            source_node_index,
2275            contributing_channels,
2276            projection,
2277        });
2278    }
2279    if truncated {
2280        // Collision freedom is a whole-domain property. Once the canonical
2281        // tail is omitted, no retained path/UUID is exact even if it is unique
2282        // within the retained prefix.
2283        for target in &mut targets {
2284            let mut reasons = vec![GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated];
2285            if !target_domain_complete {
2286                reasons.push(GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete);
2287            }
2288            target.projection = GltfAddressabilityProjectionV2::unavailable(reasons);
2289        }
2290    } else if collision_index
2291        .as_ref()
2292        .is_some_and(|index| !index.complete)
2293    {
2294        // A path outside this contract's hashing bound can still participate
2295        // in Bevy's global UUID domain, so collision freedom is unavailable
2296        // for every otherwise exact retained target.
2297        for target in &mut targets {
2298            if matches!(
2299                target.projection,
2300                GltfAddressabilityProjectionV2::Available { .. }
2301            ) {
2302                target.projection = GltfAddressabilityProjectionV2::unavailable(vec![
2303                    GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded,
2304                ]);
2305            }
2306        }
2307    } else {
2308        if let Some(index) = &collision_index {
2309            invalidate_target_collisions_with_index(&mut targets, index);
2310        }
2311    }
2312    let coverage = target_coverage_projection(target_domain_complete, contributor_count);
2313    Ok((coverage, targets))
2314}
2315
2316fn project_target_segments(
2317    segments: Vec<String>,
2318    pointer_width: TargetPointerWidth,
2319) -> GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2> {
2320    let path = segments.join("/");
2321    match bevy_animation_target_id_v1(segments.iter().map(String::as_str), pointer_width) {
2322        Ok(uuid) => GltfAddressabilityProjectionV2::Available {
2323            value: GltfAddressabilityTargetValueV2 {
2324                segments,
2325                path,
2326                uuid,
2327            },
2328        },
2329        Err(_) => GltfAddressabilityProjectionV2::unavailable(vec![
2330            GltfAddressabilityUnavailableReasonV2::PathBoundsExceeded,
2331        ]),
2332    }
2333}
2334
2335fn target_coverage_projection(
2336    target_domain_complete: bool,
2337    contributor_count: usize,
2338) -> GltfAddressabilityProjectionV2<()> {
2339    let mut coverage_reasons = Vec::new();
2340    if !target_domain_complete {
2341        coverage_reasons.push(GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete);
2342    }
2343    if contributor_count > GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS {
2344        coverage_reasons.push(GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated);
2345    }
2346    if coverage_reasons.is_empty() {
2347        GltfAddressabilityProjectionV2::Available { value: () }
2348    } else {
2349        GltfAddressabilityProjectionV2::unavailable(coverage_reasons)
2350    }
2351}
2352
2353fn target_domain_evidence_complete(
2354    raw: &RawGltfAddressabilityInventoryV1,
2355    animations: &GltfAnimationAddressabilityInventoryV1,
2356) -> bool {
2357    raw.node_coverage().is_complete()
2358        && raw.scene_coverage().is_complete()
2359        && raw.path_candidate_coverage().is_complete()
2360        && !animation_inventory_incomplete(animations)
2361}
2362
2363fn target_base_reasons(
2364    raw: &RawGltfAddressabilityInventoryV1,
2365    animations: &GltfAnimationAddressabilityInventoryV1,
2366    bevy_animation_enabled: bool,
2367    load_animations: bool,
2368    pointer_width: Option<TargetPointerWidth>,
2369) -> Vec<GltfAddressabilityUnavailableReasonV2> {
2370    let mut reasons = Vec::new();
2371    if !raw.node_coverage().is_complete()
2372        || !raw.scene_coverage().is_complete()
2373        || !raw.path_candidate_coverage().is_complete()
2374        || !matches!(
2375            animations.animations().coverage().state(),
2376            crate::GltfAnimationCoverageStateV1::Complete
2377        )
2378        || animations.animations().rows().iter().any(|animation| {
2379            !matches!(
2380                animation.channels().coverage().state(),
2381                crate::GltfAnimationCoverageStateV1::Complete
2382            )
2383        })
2384    {
2385        reasons.push(GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete);
2386    }
2387    if !raw.dependency_closure().coverage().is_complete() {
2388        reasons.push(GltfAddressabilityUnavailableReasonV2::DependencyClosureIncomplete);
2389    }
2390    if !bevy_animation_enabled {
2391        reasons.push(GltfAddressabilityUnavailableReasonV2::BevyAnimationFeatureDisabled);
2392    }
2393    if !load_animations {
2394        reasons.push(GltfAddressabilityUnavailableReasonV2::LoadAnimationsDisabled);
2395    }
2396    if pointer_width.is_none() {
2397        reasons.push(GltfAddressabilityUnavailableReasonV2::TargetPointerWidthMissing);
2398    }
2399    reasons
2400}
2401
2402struct CandidateCollisionIndexV2 {
2403    paths: BTreeMap<Vec<String>, BTreeSet<u64>>,
2404    uuids: BTreeMap<String, BTreeSet<u64>>,
2405    complete: bool,
2406}
2407
2408fn build_candidate_collision_index(
2409    candidate_paths: &BTreeMap<u64, BTreeSet<Vec<u64>>>,
2410    contributing_nodes: &BTreeSet<u64>,
2411    nodes: &BTreeMap<u64, &animsmith_core::RawGltfNodeRowV1>,
2412    pointer_width: TargetPointerWidth,
2413) -> Result<CandidateCollisionIndexV2, GltfAddressabilityV2Error> {
2414    let mut index = CandidateCollisionIndexV2 {
2415        paths: BTreeMap::new(),
2416        uuids: BTreeMap::new(),
2417        complete: true,
2418    };
2419    for source_node_index in contributing_nodes {
2420        let Some(paths) = candidate_paths.get(source_node_index) else {
2421            continue;
2422        };
2423        for node_path in paths {
2424            let segments = node_path
2425                .iter()
2426                .map(|node_index| {
2427                    nodes
2428                        .get(node_index)
2429                        .map(|node| {
2430                            node.name()
2431                                .map_or_else(|| format!("GltfNode{node_index}"), str::to_owned)
2432                        })
2433                        .ok_or(GltfAddressabilityV2Error::InventoryBindingMismatch)
2434                })
2435                .collect::<Result<Vec<_>, _>>()?;
2436            match bevy_animation_target_id_v1(segments.iter().map(String::as_str), pointer_width) {
2437                Ok(uuid) => {
2438                    index
2439                        .paths
2440                        .entry(segments)
2441                        .or_default()
2442                        .insert(*source_node_index);
2443                    index
2444                        .uuids
2445                        .entry(uuid)
2446                        .or_default()
2447                        .insert(*source_node_index);
2448                }
2449                Err(_) => index.complete = false,
2450            }
2451        }
2452    }
2453    Ok(index)
2454}
2455
2456#[cfg(test)]
2457fn invalidate_target_collisions(targets: &mut [GltfAddressabilityTargetV2]) {
2458    let mut index = CandidateCollisionIndexV2 {
2459        paths: BTreeMap::new(),
2460        uuids: BTreeMap::new(),
2461        complete: true,
2462    };
2463    for target in targets.iter() {
2464        if let GltfAddressabilityProjectionV2::Available { value } = &target.projection {
2465            index
2466                .paths
2467                .entry(value.segments.clone())
2468                .or_default()
2469                .insert(target.source_node_index);
2470            index
2471                .uuids
2472                .entry(value.uuid.clone())
2473                .or_default()
2474                .insert(target.source_node_index);
2475        }
2476    }
2477    invalidate_target_collisions_with_index(targets, &index);
2478}
2479
2480fn invalidate_target_collisions_with_index(
2481    targets: &mut [GltfAddressabilityTargetV2],
2482    index: &CandidateCollisionIndexV2,
2483) {
2484    for target in targets.iter_mut() {
2485        let GltfAddressabilityProjectionV2::Available { value } = &target.projection else {
2486            continue;
2487        };
2488        let mut reasons = Vec::new();
2489        if index
2490            .paths
2491            .get(&value.segments)
2492            .is_some_and(|owners| owners.len() > 1)
2493        {
2494            reasons.push(GltfAddressabilityUnavailableReasonV2::DuplicateFullPath);
2495        }
2496        if index
2497            .uuids
2498            .get(&value.uuid)
2499            .is_some_and(|owners| owners.len() > 1)
2500        {
2501            reasons.push(GltfAddressabilityUnavailableReasonV2::TargetIdCollision);
2502        }
2503        if !reasons.is_empty() {
2504            target.projection = GltfAddressabilityProjectionV2::unavailable(reasons);
2505        }
2506    }
2507}
2508
2509/// Invalid rich glTF addressability input or adapter evidence.
2510#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2511#[non_exhaustive]
2512pub enum GltfAddressabilityV2Error {
2513    /// The rich report or inventory schema/header is not immutable V2.
2514    #[error("invalid rich glTF addressability schema or header")]
2515    InvalidSchema,
2516    /// The raw sidecar did not pass its own strict validation.
2517    #[error("invalid raw glTF addressability inventory")]
2518    InvalidRawInventory,
2519    /// Raw and animation inventories do not identify the same primary/closure.
2520    #[error("raw and animation addressability inventories are not from the same load")]
2521    InventoryBindingMismatch,
2522    /// The selected profile is not exact Bevy 0.19.0 revision 3.
2523    #[error("rich addressability requires exact Bevy 0.19.0 profile revision 3")]
2524    InvalidBevyProfile,
2525    /// One of the two required Bevy animation settings was not materialized.
2526    #[error("rich addressability is missing a required Bevy setting")]
2527    MissingRequiredSetting,
2528    /// Prediction provenance is invalid or does not bind this exact inventory.
2529    #[error("invalid rich addressability prediction provenance")]
2530    InvalidPredictionProvenance,
2531    /// The separately versioned Bevy rule bundle was mutated.
2532    #[error("invalid Bevy glTF addressability rule bundle")]
2533    InvalidRuleBundle,
2534    /// The real check evaluation does not correspond one-to-one to projections.
2535    #[error("invalid rich engine-addressability check evaluation")]
2536    InvalidCheckEvaluation,
2537}
2538
2539#[cfg(test)]
2540mod tests {
2541    use super::*;
2542    use animsmith_core::{
2543        Clip, Config, DependencyClosureBuilderV1, Document, MetricGrids,
2544        RawGltfAddressabilityCoverageV1, RawGltfAddressabilityInventoryInputV1,
2545        RawGltfInverseBindMatricesObservationV1, RawGltfNodeRowV1, RawGltfScenePathCandidateRowV1,
2546        RawGltfSceneRowV1, RawGltfSkinAttachmentRowV1, RawGltfSkinRowV1, RawSourceFactsBuilderV1,
2547        ResolvedRoles, SourceFactDomainV1, SourceFactSetV1, SourceFormatV1,
2548        SourceLoaderDispositionV1, SourceObservationV1, SourceProvenanceV1, SourceTargetKindV1,
2549        SourceTargetV1, SourceUnavailableReasonV1, ToolSource,
2550    };
2551    use std::io::Cursor;
2552
2553    fn empty_loaded_source() -> LoadedSource {
2554        let primary = InputIdentity::from_bytes(b"rich-addressability");
2555        let mut facts = RawSourceFactsBuilderV1::new(SourceFormatV1::GltfJson, primary.clone());
2556        facts.mark_complete(SourceFactDomainV1::Clips);
2557        let closure = DependencyClosureBuilderV1::new(
2558            primary,
2559            facts.resource_coverage(),
2560            facts.resource_rows().len(),
2561        )
2562        .finish()
2563        .unwrap();
2564        facts
2565            .finish_with_dependency_closure(Document::default(), closure)
2566            .unwrap()
2567    }
2568
2569    fn loaded_source_with_targets(
2570        target_node_indices: impl IntoIterator<Item = u64>,
2571        channels_complete: bool,
2572    ) -> LoadedSource {
2573        loaded_source_with_targets_and_closure(target_node_indices, channels_complete, true)
2574    }
2575
2576    fn loaded_source_with_targets_and_closure(
2577        target_node_indices: impl IntoIterator<Item = u64>,
2578        channels_complete: bool,
2579        closure_complete: bool,
2580    ) -> LoadedSource {
2581        let primary = InputIdentity::from_bytes(b"rich-addressability-targets");
2582        let mut facts = RawSourceFactsBuilderV1::new(SourceFormatV1::GltfJson, primary.clone());
2583        let channels = target_node_indices
2584            .into_iter()
2585            .enumerate()
2586            .map(|(channel_index, target_node_index)| {
2587                animsmith_core::SourceChannelFactV1::new(
2588                    channel_index,
2589                    SourceTargetV1::new(SourceTargetKindV1::Node, target_node_index),
2590                    animsmith_core::SourceChannelPropertyV1::Translation,
2591                    animsmith_core::SourceComponentMaskV1::new(true, true, true),
2592                    SourceObservationV1::proven_absent(SourceProvenanceV1::format_defined()),
2593                    SourceLoaderDispositionV1::Preserved,
2594                    SourceProvenanceV1::format_defined(),
2595                )
2596                .with_accessors(channel_index * 2, channel_index * 2 + 1)
2597            })
2598            .collect::<Vec<_>>();
2599        let channel_set = if channels_complete {
2600            SourceFactSetV1::complete(channels)
2601        } else {
2602            SourceFactSetV1::partial(
2603                channels,
2604                SourceUnavailableReasonV1::ProjectionBudgetExceeded,
2605            )
2606        };
2607        assert!(facts.push_clip(animsmith_core::SourceClipFactV1::new(
2608            0,
2609            SourceObservationV1::proven_absent(SourceProvenanceV1::format_defined()),
2610            SourceObservationV1::observed(
2611                0,
2612                SourceProvenanceV1::format_defined(),
2613                SourceLoaderDispositionV1::Normalized,
2614            ),
2615            SourceObservationV1::proven_absent(SourceProvenanceV1::format_defined()),
2616            SourceObservationV1::proven_absent(SourceProvenanceV1::format_defined()),
2617            channel_set,
2618        )));
2619        facts.mark_complete(SourceFactDomainV1::Clips);
2620        facts.mark_complete(SourceFactDomainV1::Constructs);
2621        facts.mark_complete(SourceFactDomainV1::Resources);
2622        let closure = DependencyClosureBuilderV1::new(
2623            primary,
2624            facts.resource_coverage(),
2625            facts.resource_rows().len(),
2626        )
2627        .finish()
2628        .unwrap();
2629        let document = Document {
2630            clips: vec![Clip {
2631                name: "clip-0".into(),
2632                duration_s: 0.0,
2633                tracks: Vec::new(),
2634            }],
2635            ..Document::default()
2636        };
2637        if closure_complete {
2638            facts
2639                .finish_with_dependency_closure(document, closure)
2640                .unwrap()
2641        } else {
2642            facts.finish(document).unwrap()
2643        }
2644    }
2645
2646    fn raw_for_graph(
2647        source: &LoadedSource,
2648        scenes: Vec<RawGltfSceneRowV1>,
2649        scene_coverage: RawGltfAddressabilityCoverageV1,
2650        nodes: Vec<RawGltfNodeRowV1>,
2651        node_coverage: RawGltfAddressabilityCoverageV1,
2652        path_candidates: Vec<RawGltfScenePathCandidateRowV1>,
2653        path_coverage: RawGltfAddressabilityCoverageV1,
2654    ) -> RawGltfAddressabilityInventoryV1 {
2655        RawGltfAddressabilityInventoryV1::new(
2656            source.source_facts().primary_identity().clone(),
2657            source.dependency_closure().clone(),
2658            RawGltfAddressabilityInventoryInputV1 {
2659                default_scene: RawGltfDefaultSceneObservationV1::Absent,
2660                scene_coverage,
2661                scenes,
2662                node_coverage,
2663                nodes,
2664                skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
2665                skins: Vec::new(),
2666                attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
2667                attachments: Vec::new(),
2668                path_candidate_coverage: path_coverage,
2669                path_candidates,
2670            },
2671        )
2672        .unwrap()
2673    }
2674
2675    fn simple_target_raw(
2676        source: &LoadedSource,
2677        target_name: Option<String>,
2678    ) -> RawGltfAddressabilityInventoryV1 {
2679        raw_for_graph(
2680            source,
2681            vec![RawGltfSceneRowV1::new(0, None, vec![0])],
2682            RawGltfAddressabilityCoverageV1::Complete,
2683            vec![RawGltfNodeRowV1::new(0, target_name, None, Vec::new())],
2684            RawGltfAddressabilityCoverageV1::Complete,
2685            vec![RawGltfScenePathCandidateRowV1::new(0, 0, vec![0])],
2686            RawGltfAddressabilityCoverageV1::Complete,
2687        )
2688    }
2689
2690    fn wide_target_raw(
2691        source: &LoadedSource,
2692        retained_target_count: usize,
2693        target_domain_complete: bool,
2694    ) -> RawGltfAddressabilityInventoryV1 {
2695        let retained_indices = (0..retained_target_count as u64).collect::<Vec<_>>();
2696        let target_coverage = if target_domain_complete {
2697            RawGltfAddressabilityCoverageV1::Complete
2698        } else {
2699            RawGltfAddressabilityCoverageV1::budget_exceeded()
2700        };
2701        raw_for_graph(
2702            source,
2703            vec![RawGltfSceneRowV1::new(0, None, retained_indices.clone())],
2704            RawGltfAddressabilityCoverageV1::Complete,
2705            retained_indices
2706                .iter()
2707                .map(|index| RawGltfNodeRowV1::new(*index, None, None, Vec::new()))
2708                .collect(),
2709            target_coverage,
2710            retained_indices
2711                .iter()
2712                .enumerate()
2713                .map(|(ordinal, index)| {
2714                    RawGltfScenePathCandidateRowV1::new(ordinal as u64, 0, vec![*index])
2715                })
2716                .collect(),
2717            target_coverage,
2718        )
2719    }
2720
2721    fn adapter_report_json(pointer_width: Option<TargetPointerWidth>) -> serde_json::Value {
2722        let source = loaded_source_with_targets([0], true);
2723        let raw = simple_target_raw(&source, Some("target".into()));
2724        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
2725        let resolved = crate::resolve_static_v2(crate::EngineDeclarationV2 {
2726            selection: Some(crate::ProfileSelection::new(
2727                "bevy",
2728                3,
2729                "0.19.0",
2730                "gltf-asset-loader",
2731            )),
2732            document_settings: Some(BTreeMap::from([
2733                (
2734                    "bevy_animation_feature".into(),
2735                    crate::SettingValueV2::Boolean(true),
2736                ),
2737                (
2738                    "extension_handler_environment".into(),
2739                    crate::SettingValueV2::HandlerEnvironment(
2740                        crate::BevyGltfHandlerEnvironmentV2::BareEmpty,
2741                    ),
2742                ),
2743            ])),
2744            ..crate::EngineDeclarationV2::default()
2745        })
2746        .unwrap()
2747        .unwrap()
2748        .resolve_input(SourceFormatV1::GltfJson)
2749        .unwrap();
2750        let provenance =
2751            crate::project_prediction_provenance_v4(&resolved, &source, Vec::new()).unwrap();
2752        let grids = MetricGrids::new(source.document());
2753        let roles = ResolvedRoles::default();
2754        let config = Config::default();
2755        let adapter = build_bevy_addressability_adapter_v2(
2756            &source,
2757            &raw,
2758            &animations,
2759            &resolved,
2760            provenance,
2761            pointer_width,
2762            &CheckCtx::new(&grids, &roles, &config),
2763        )
2764        .unwrap()
2765        .unwrap();
2766        let report = GltfAddressabilityV2::new(
2767            ToolInfo::animsmith(ToolSource::new(None, None)),
2768            raw,
2769            animations,
2770            Some(adapter),
2771        )
2772        .unwrap();
2773        let bytes = serde_json::to_vec(&report).unwrap();
2774        GltfAddressabilityV2::read_from(Cursor::new(&bytes)).unwrap();
2775        serde_json::from_slice(&bytes).unwrap()
2776    }
2777
2778    fn prediction_provenance_fixture() -> PredictionProvenanceV4 {
2779        serde_json::from_value(
2780            adapter_report_json(Some(TargetPointerWidth::Bits64))["bevy"]["prediction_provenance"]
2781                .clone(),
2782        )
2783        .unwrap()
2784    }
2785
2786    fn analytic_raw(source: &LoadedSource) -> RawGltfAddressabilityInventoryV1 {
2787        RawGltfAddressabilityInventoryV1::new(
2788            source.source_facts().primary_identity().clone(),
2789            source.dependency_closure().clone(),
2790            RawGltfAddressabilityInventoryInputV1 {
2791                default_scene: RawGltfDefaultSceneObservationV1::Selected {
2792                    source_scene_index: 0,
2793                },
2794                scene_coverage: RawGltfAddressabilityCoverageV1::Complete,
2795                scenes: vec![
2796                    RawGltfSceneRowV1::new(0, Some("World".into()), vec![0]),
2797                    RawGltfSceneRowV1::new(1, Some("World".into()), vec![0]),
2798                ],
2799                node_coverage: RawGltfAddressabilityCoverageV1::Complete,
2800                nodes: vec![RawGltfNodeRowV1::new(0, None, None, Vec::new())],
2801                skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
2802                skins: vec![
2803                    RawGltfSkinRowV1::new(
2804                        0,
2805                        Some("Rig".into()),
2806                        vec![0],
2807                        Some(0),
2808                        RawGltfInverseBindMatricesObservationV1::Absent,
2809                    ),
2810                    RawGltfSkinRowV1::new(
2811                        1,
2812                        Some("Rig".into()),
2813                        vec![0],
2814                        None,
2815                        RawGltfInverseBindMatricesObservationV1::Absent,
2816                    ),
2817                ],
2818                attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
2819                attachments: vec![RawGltfSkinAttachmentRowV1::new(0, 0)],
2820                path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
2821                path_candidates: vec![
2822                    RawGltfScenePathCandidateRowV1::new(0, 0, vec![0]),
2823                    RawGltfScenePathCandidateRowV1::new(1, 1, vec![0]),
2824                ],
2825            },
2826        )
2827        .unwrap()
2828    }
2829
2830    #[test]
2831    fn frozen_rule_bundle_is_strict_and_commit_pinned() {
2832        for width in [
2833            None,
2834            Some(TargetPointerWidth::Bits32),
2835            Some(TargetPointerWidth::Bits64),
2836        ] {
2837            let bundle = BevyGltfAddressabilityRulesV1::frozen(width);
2838            assert!(bundle.validate());
2839            assert_eq!(bundle.profile_revision(), 3);
2840            assert_eq!(bundle.sources().len(), 6);
2841            assert!(
2842                bundle
2843                    .sources()
2844                    .windows(2)
2845                    .all(|pair| pair[0].id() < pair[1].id())
2846            );
2847            assert!(
2848                bundle
2849                    .sources()
2850                    .iter()
2851                    .all(|source| source.url().contains(BEVY_COMMIT))
2852            );
2853        }
2854    }
2855
2856    #[test]
2857    fn target_id_golden_vectors_cover_both_pointer_widths_and_boundaries() {
2858        // Goldens are independently obtained from Bevy 0.19.0's exact
2859        // `AnimationTargetId::from_iter` implementation on 32- and 64-bit
2860        // targets.  They intentionally differ because Bevy hashes `usize`.
2861        let paths: &[&[&str]] = &[
2862            &[],
2863            &["Root"],
2864            &["Root", "Hips", "GltfNode7"],
2865            &["ab", "c"],
2866            &["a", "bc"],
2867        ];
2868        let expected_32 = [
2869            "77da024c-dd93-5d43-8f9a-2af3d4b17c5b",
2870            "2514296d-dbd8-5ea8-a8cc-1fadaf961d2f",
2871            "15c3912b-7127-55c1-b5ff-83950e3d0f9a",
2872            "fd324118-d34f-53d0-9787-d5a1594985e7",
2873            "2ceb26c6-8d62-53c7-b4c5-ee3b9b81784e",
2874        ];
2875        let expected_64 = [
2876            "77da024c-dd93-5d43-8f9a-2af3d4b17c5b",
2877            "05c24be8-bacd-5ee9-9a65-f8215983900e",
2878            "90fed833-c803-5800-883d-adee90aab446",
2879            "0d54b499-ea4e-5a49-afec-0a87e56da473",
2880            "c22eb6e6-9f59-509b-9cfc-79d424c0d445",
2881        ];
2882        for ((path, expected_32), expected_64) in paths.iter().zip(expected_32).zip(expected_64) {
2883            assert_eq!(
2884                bevy_animation_target_id_v1(path.iter().copied(), TargetPointerWidth::Bits32)
2885                    .unwrap(),
2886                expected_32
2887            );
2888            assert_eq!(
2889                bevy_animation_target_id_v1(path.iter().copied(), TargetPointerWidth::Bits64)
2890                    .unwrap(),
2891                expected_64
2892            );
2893        }
2894
2895        let max_segment = "x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES);
2896        assert!(
2897            bevy_animation_target_id_v1([max_segment.as_str()], TargetPointerWidth::Bits64).is_ok()
2898        );
2899        let over_segment = format!("{max_segment}x");
2900        assert!(matches!(
2901            bevy_animation_target_id_v1([over_segment.as_str()], TargetPointerWidth::Bits64),
2902            Err(BevyAnimationTargetIdError::SegmentTooLong { .. })
2903        ));
2904
2905        let at_path_limit = [
2906            "a".repeat(1024),
2907            "b".repeat(1024),
2908            "c".repeat(1024),
2909            "d".repeat(1021),
2910        ];
2911        assert!(
2912            bevy_animation_target_id_v1(
2913                at_path_limit.iter().map(String::as_str),
2914                TargetPointerWidth::Bits64,
2915            )
2916            .is_ok()
2917        );
2918        let over_path_limit = [
2919            "a".repeat(1024),
2920            "b".repeat(1024),
2921            "c".repeat(1024),
2922            "d".repeat(1022),
2923        ];
2924        assert!(matches!(
2925            bevy_animation_target_id_v1(
2926                over_path_limit.iter().map(String::as_str),
2927                TargetPointerWidth::Bits64,
2928            ),
2929            Err(BevyAnimationTargetIdError::PathTooLong {
2930                found: 4097,
2931                limit: 4096
2932            })
2933        ));
2934        assert!(
2935            bevy_animation_target_id_v1(
2936                std::iter::repeat_n("", GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS),
2937                TargetPointerWidth::Bits64,
2938            )
2939            .is_ok()
2940        );
2941        assert!(matches!(
2942            bevy_animation_target_id_v1(
2943                std::iter::repeat_n("", GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS + 1),
2944                TargetPointerWidth::Bits64,
2945            ),
2946            Err(BevyAnimationTargetIdError::TooManySegments {
2947                found: 257,
2948                limit: 256
2949            })
2950        ));
2951    }
2952
2953    #[test]
2954    fn analytic_labels_default_route_and_named_winners_follow_bevy_rules() {
2955        let source = empty_loaded_source();
2956        let raw = analytic_raw(&source);
2957        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
2958        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
2959            &raw,
2960            &animations,
2961            true,
2962            true,
2963            Some(TargetPointerWidth::Bits64),
2964        )
2965        .unwrap();
2966        assert_eq!(
2967            projection
2968                .scenes()
2969                .iter()
2970                .map(GltfAddressabilitySceneV2::label)
2971                .collect::<Vec<_>>(),
2972            ["Scene0", "Scene1"]
2973        );
2974        assert_eq!(
2975            projection.default_scene_route(),
2976            &GltfAddressabilityProjectionV2::Available {
2977                value: "Scene0".into()
2978            }
2979        );
2980        assert!(matches!(
2981            projection.skins()[0].skin_label(),
2982            GltfAddressabilityProjectionV2::Available { value } if value == "Skin0"
2983        ));
2984        assert!(matches!(
2985            projection.skins()[1].skin_label(),
2986            GltfAddressabilityProjectionV2::ProvenAbsent
2987        ));
2988        assert_eq!(
2989            projection.skins()[1].inverse_bind_matrices_label(),
2990            "Skin1/InverseBindMatrices"
2991        );
2992        let scene_map = &projection.named_maps()[0];
2993        assert!(matches!(
2994            &scene_map.winners,
2995            GltfAddressabilityProjectionV2::Available { value }
2996                if value.len() == 1 && value[0].source_index() == 1
2997        ));
2998        let skin_map = &projection.named_maps()[2];
2999        assert!(matches!(
3000            &skin_map.winners,
3001            GltfAddressabilityProjectionV2::Available { value }
3002                if value.len() == 1 && value[0].source_index() == 0
3003        ));
3004    }
3005
3006    #[test]
3007    fn retained_default_scene_is_exact_under_partial_tail_coverage() {
3008        let source = empty_loaded_source();
3009        let raw = RawGltfAddressabilityInventoryV1::new(
3010            source.source_facts().primary_identity().clone(),
3011            source.dependency_closure().clone(),
3012            RawGltfAddressabilityInventoryInputV1 {
3013                default_scene: RawGltfDefaultSceneObservationV1::Selected {
3014                    source_scene_index: 0,
3015                },
3016                scene_coverage: RawGltfAddressabilityCoverageV1::budget_exceeded(),
3017                scenes: vec![RawGltfSceneRowV1::new(0, None, Vec::new())],
3018                node_coverage: RawGltfAddressabilityCoverageV1::Complete,
3019                nodes: Vec::new(),
3020                skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
3021                skins: Vec::new(),
3022                attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
3023                attachments: Vec::new(),
3024                path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
3025                path_candidates: Vec::new(),
3026            },
3027        )
3028        .unwrap();
3029        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3030        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3031            &raw,
3032            &animations,
3033            true,
3034            true,
3035            Some(TargetPointerWidth::Bits64),
3036        )
3037        .unwrap();
3038        assert_eq!(
3039            projection.default_scene_route(),
3040            &GltfAddressabilityProjectionV2::Available {
3041                value: "Scene0".into()
3042            }
3043        );
3044    }
3045
3046    #[test]
3047    fn named_map_keys_enforce_exact_segment_bound() {
3048        let winner = |name: String| {
3049            let mut winners = BTreeMap::new();
3050            winners.insert(
3051                name.clone(),
3052                GltfAddressabilityNamedMapWinnerV2 {
3053                    name,
3054                    source_index: 0,
3055                    typed_label: "Scene0".into(),
3056                },
3057            );
3058            named_map(GltfAddressabilityNamedMapKindV2::Scene, true, winners)
3059        };
3060        assert!(matches!(
3061            winner("x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES)).winners(),
3062            GltfAddressabilityProjectionV2::Available { .. }
3063        ));
3064        assert!(matches!(
3065            winner("x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES + 1)).winners(),
3066            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3067                if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3068        ));
3069    }
3070
3071    #[test]
3072    fn incomplete_target_evidence_never_proves_complete_empty_or_prefix_coverage() {
3073        for targets in [Vec::new(), vec![0]] {
3074            let source = loaded_source_with_targets(targets, false);
3075            let raw = if source.source_facts().clips().rows()[0]
3076                .channels()
3077                .rows()
3078                .is_empty()
3079            {
3080                raw_for_graph(
3081                    &source,
3082                    Vec::new(),
3083                    RawGltfAddressabilityCoverageV1::Complete,
3084                    Vec::new(),
3085                    RawGltfAddressabilityCoverageV1::Complete,
3086                    Vec::new(),
3087                    RawGltfAddressabilityCoverageV1::Complete,
3088                )
3089            } else {
3090                simple_target_raw(&source, Some("target".into()))
3091            };
3092            let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3093            let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3094                &raw,
3095                &animations,
3096                true,
3097                true,
3098                Some(TargetPointerWidth::Bits64),
3099            )
3100            .unwrap();
3101            assert!(matches!(
3102                projection.target_coverage(),
3103                GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3104                    if reasons == &[GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3105            ));
3106        }
3107
3108        assert!(matches!(
3109            target_coverage_projection(true, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS),
3110            GltfAddressabilityProjectionV2::Available { .. }
3111        ));
3112        assert!(matches!(
3113            target_coverage_projection(true, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS + 1),
3114            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3115                if reasons == [GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated]
3116        ));
3117        assert!(matches!(
3118            target_coverage_projection(false, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS + 1),
3119            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3120                if reasons == [
3121                    GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
3122                    GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated,
3123                ]
3124        ));
3125    }
3126
3127    #[test]
3128    fn production_target_projection_retains_exact_n_and_truncates_n_plus_one() {
3129        let exact_count = GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS;
3130        let exact_source = loaded_source_with_targets(0..exact_count as u64, true);
3131        let exact_raw = wide_target_raw(&exact_source, exact_count, true);
3132        let exact_animations =
3133            GltfAnimationAddressabilityInventoryV1::from_source(&exact_source).unwrap();
3134        let exact = BevyGltfAddressabilityProjectionV2::from_inventories(
3135            &exact_raw,
3136            &exact_animations,
3137            true,
3138            true,
3139            Some(TargetPointerWidth::Bits64),
3140        )
3141        .unwrap();
3142        assert!(matches!(
3143            exact.target_coverage(),
3144            GltfAddressabilityProjectionV2::Available { .. }
3145        ));
3146        assert_eq!(exact.targets().len(), exact_count);
3147        assert_eq!(
3148            exact
3149                .targets()
3150                .iter()
3151                .map(GltfAddressabilityTargetV2::source_node_index)
3152                .collect::<Vec<_>>(),
3153            (0..exact_count as u64).collect::<Vec<_>>()
3154        );
3155        assert!(exact.targets().iter().all(|target| matches!(
3156            target.projection(),
3157            GltfAddressabilityProjectionV2::Available { .. }
3158        )));
3159
3160        let over_count = exact_count + 1;
3161        let over_source = loaded_source_with_targets(0..over_count as u64, true);
3162        // The raw sidecar retains its canonical prefix and marks the omitted
3163        // target-domain evidence partial. `project_targets` must independently
3164        // count the complete animation contributors and exercise its `.take(N)`.
3165        let over_raw = wide_target_raw(&over_source, exact_count, false);
3166        let over_animations =
3167            GltfAnimationAddressabilityInventoryV1::from_source(&over_source).unwrap();
3168        let over = BevyGltfAddressabilityProjectionV2::from_inventories(
3169            &over_raw,
3170            &over_animations,
3171            true,
3172            true,
3173            Some(TargetPointerWidth::Bits64),
3174        )
3175        .unwrap();
3176        assert_eq!(over.targets().len(), exact_count);
3177        assert_eq!(
3178            over.targets()
3179                .iter()
3180                .map(GltfAddressabilityTargetV2::source_node_index)
3181                .collect::<Vec<_>>(),
3182            (0..exact_count as u64).collect::<Vec<_>>()
3183        );
3184        let expected_reasons = [
3185            GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
3186            GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated,
3187        ];
3188        assert!(matches!(
3189            over.target_coverage(),
3190            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3191                if reasons.as_slice() == expected_reasons
3192        ));
3193        assert!(over.targets().iter().all(|target| matches!(
3194            target.projection(),
3195            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3196                if reasons.as_slice() == expected_reasons
3197        )));
3198        assert_eq!(
3199            over.targets().last().unwrap().contributing_channels(),
3200            &[GltfAddressabilityTargetChannelV2 {
3201                source_animation_index: 0,
3202                source_channel_index: (exact_count - 1) as u64,
3203            }]
3204        );
3205    }
3206
3207    #[test]
3208    fn unnamed_target_nodes_use_bevys_exact_source_index_fallback() {
3209        let source = loaded_source_with_targets([7], true);
3210        let raw = raw_for_graph(
3211            &source,
3212            vec![RawGltfSceneRowV1::new(0, None, vec![7])],
3213            RawGltfAddressabilityCoverageV1::Complete,
3214            (0..=7)
3215                .map(|index| RawGltfNodeRowV1::new(index, None, None, Vec::new()))
3216                .collect(),
3217            RawGltfAddressabilityCoverageV1::Complete,
3218            vec![RawGltfScenePathCandidateRowV1::new(0, 0, vec![7])],
3219            RawGltfAddressabilityCoverageV1::Complete,
3220        );
3221        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3222        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3223            &raw,
3224            &animations,
3225            true,
3226            true,
3227            Some(TargetPointerWidth::Bits64),
3228        )
3229        .unwrap();
3230        let GltfAddressabilityProjectionV2::Available { value } =
3231            projection.targets()[0].projection()
3232        else {
3233            panic!("complete unnamed target must be exactly projectable");
3234        };
3235        assert_eq!(value.segments(), ["GltfNode7"]);
3236        assert_eq!(value.path(), "GltfNode7");
3237        assert_eq!(
3238            value.uuid(),
3239            bevy_animation_target_id_v1(["GltfNode7"], TargetPointerWidth::Bits64).unwrap()
3240        );
3241    }
3242
3243    #[test]
3244    fn each_raw_target_domain_and_dependency_closure_fail_closed_with_causal_reason() {
3245        for partial_domain in ["scene", "node", "path"] {
3246            let source = loaded_source_with_targets([0], true);
3247            let partial = RawGltfAddressabilityCoverageV1::budget_exceeded();
3248            let complete = RawGltfAddressabilityCoverageV1::Complete;
3249            let raw = raw_for_graph(
3250                &source,
3251                vec![RawGltfSceneRowV1::new(0, None, vec![0])],
3252                if partial_domain == "scene" {
3253                    partial
3254                } else {
3255                    complete
3256                },
3257                vec![RawGltfNodeRowV1::new(
3258                    0,
3259                    Some("target".into()),
3260                    None,
3261                    Vec::new(),
3262                )],
3263                if partial_domain == "node" {
3264                    partial
3265                } else {
3266                    complete
3267                },
3268                vec![RawGltfScenePathCandidateRowV1::new(0, 0, vec![0])],
3269                if partial_domain == "path" {
3270                    partial
3271                } else {
3272                    complete
3273                },
3274            );
3275            let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3276            let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3277                &raw,
3278                &animations,
3279                true,
3280                true,
3281                Some(TargetPointerWidth::Bits64),
3282            )
3283            .unwrap();
3284            assert!(
3285                matches!(
3286                projection.target_coverage(),
3287                GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3288                    if reasons.as_slice()
3289                        == [GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3290                ),
3291                "partial {partial_domain} coverage must invalidate target coverage"
3292            );
3293            assert!(
3294                matches!(
3295                projection.targets()[0].projection(),
3296                GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3297                    if reasons.as_slice()
3298                        == [GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3299                ),
3300                "partial {partial_domain} coverage must invalidate every target row"
3301            );
3302        }
3303
3304        let source = loaded_source_with_targets_and_closure([0], true, false);
3305        assert!(!source.dependency_closure().coverage().is_complete());
3306        let raw = simple_target_raw(&source, Some("target".into()));
3307        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3308        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3309            &raw,
3310            &animations,
3311            true,
3312            true,
3313            Some(TargetPointerWidth::Bits64),
3314        )
3315        .unwrap();
3316        assert!(matches!(
3317            projection.target_coverage(),
3318            GltfAddressabilityProjectionV2::Available { .. }
3319        ));
3320        assert!(matches!(
3321            projection.targets()[0].projection(),
3322            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3323                if reasons.as_slice() == [
3324                    GltfAddressabilityUnavailableReasonV2::DependencyClosureIncomplete,
3325                ]
3326        ));
3327    }
3328
3329    #[test]
3330    fn rich_unavailable_facets_compact_only_after_the_exact_file_limit() {
3331        let source = loaded_source_with_targets([0], true);
3332        let raw = simple_target_raw(&source, Some("target".into()));
3333        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3334        let provenance = prediction_provenance_fixture();
3335        let inventory =
3336            GltfAddressabilityInventoryV2::new(raw.clone(), animations.clone()).unwrap();
3337        let base = BevyGltfAddressabilityProjectionV2::from_inventories(
3338            &raw,
3339            &animations,
3340            true,
3341            true,
3342            Some(TargetPointerWidth::Bits64),
3343        )
3344        .unwrap();
3345
3346        for count in [
3347            PREDICTION_V1_MAX_FACETS_PER_FILE,
3348            PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3349        ] {
3350            let mut projection = base.clone();
3351            projection.targets = (0..count as u64)
3352                .map(|source_node_index| GltfAddressabilityTargetV2 {
3353                    source_node_index,
3354                    contributing_channels: vec![GltfAddressabilityTargetChannelV2 {
3355                        source_animation_index: 0,
3356                        source_channel_index: source_node_index,
3357                    }],
3358                    projection: GltfAddressabilityProjectionV2::unavailable(vec![
3359                        GltfAddressabilityUnavailableReasonV2::TargetPointerWidthMissing,
3360                    ]),
3361                })
3362                .collect();
3363            let output =
3364                build_check_output(&source, &raw, &animations, &provenance, &projection).unwrap();
3365            let check =
3366                CheckEvaluation::evaluated(crate::ENGINE_ADDRESSABILITY_CHECK_ID, output).unwrap();
3367            let expected = expected_facet_specs(&inventory, &projection);
3368            validate_facet_states(
3369                check.engine_prediction_v4(),
3370                check.evaluated_scopes(),
3371                check.evaluation(),
3372                &expected,
3373            )
3374            .unwrap();
3375
3376            let facets = check.engine_prediction_v4().unwrap().facets();
3377            if count == PREDICTION_V1_MAX_FACETS_PER_FILE {
3378                assert_eq!(facets.len(), PREDICTION_V1_MAX_FACETS_PER_FILE);
3379                assert!(facets.iter().all(|facet| {
3380                    facet.scope().code == EvaluationScopeCode::ANIMATION_TARGET_ID
3381                        && facet.reasons()
3382                            == [PredictionUnavailableReasonV2::ProjectIntentUnavailable]
3383                }));
3384            } else {
3385                assert_eq!(facets.len(), 1);
3386                assert_eq!(facets[0].scope().code.as_str(), FACET_BUDGET_SCOPE);
3387                assert!(facets[0].scope().subject.is_none());
3388                assert_eq!(
3389                    facets[0].reasons(),
3390                    [PredictionUnavailableReasonV2::FacetBudgetExceeded]
3391                );
3392            }
3393        }
3394    }
3395
3396    #[test]
3397    fn evaluated_scope_guard_accepts_exact_n_and_rejects_n_plus_one() {
3398        let scopes = (0..=GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES)
3399            .map(|index| {
3400                EvaluationScope::new(EvaluationScopeCode::SCENE_ASSET_LABEL)
3401                    .subject(format!("Scene{index}"))
3402            })
3403            .collect::<Vec<_>>();
3404        let expected = scopes
3405            .iter()
3406            .cloned()
3407            .map(|scope| ExpectedFacetV2 {
3408                scope,
3409                state: EnginePredictionFacetStateV1::Available,
3410                reasons: Vec::new(),
3411            })
3412            .collect::<Vec<_>>();
3413        validate_facet_states(
3414            None,
3415            &scopes[..GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES],
3416            EvaluationState::Complete,
3417            &expected[..GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES],
3418        )
3419        .unwrap();
3420        assert_eq!(
3421            validate_facet_states(None, &scopes, EvaluationState::Complete, &expected),
3422            Err(GltfAddressabilityV2Error::InvalidCheckEvaluation)
3423        );
3424    }
3425
3426    #[test]
3427    fn feature_and_loader_settings_are_independently_required_for_targets() {
3428        let source = loaded_source_with_targets([0], true);
3429        let raw = simple_target_raw(&source, Some("target".into()));
3430        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3431        for (feature, load, expected) in [
3432            (
3433                false,
3434                true,
3435                GltfAddressabilityUnavailableReasonV2::BevyAnimationFeatureDisabled,
3436            ),
3437            (
3438                true,
3439                false,
3440                GltfAddressabilityUnavailableReasonV2::LoadAnimationsDisabled,
3441            ),
3442        ] {
3443            let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3444                &raw,
3445                &animations,
3446                feature,
3447                load,
3448                Some(TargetPointerWidth::Bits64),
3449            )
3450            .unwrap();
3451            assert!(matches!(
3452                projection.targets()[0].projection(),
3453                GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3454                    if reasons.contains(&expected)
3455            ));
3456        }
3457    }
3458
3459    #[test]
3460    fn overlong_runtime_path_maps_to_typed_unavailable_after_the_exact_boundary() {
3461        let segments = vec![
3462            "a".repeat(1024),
3463            "b".repeat(1024),
3464            "c".repeat(1024),
3465            "d".repeat(1022),
3466        ];
3467        let projection = project_target_segments(segments, TargetPointerWidth::Bits64);
3468        assert!(matches!(
3469            projection,
3470            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3471                if reasons == [GltfAddressabilityUnavailableReasonV2::PathBoundsExceeded]
3472        ));
3473    }
3474
3475    #[test]
3476    fn slash_joined_display_paths_do_not_alias_segment_vectors() {
3477        let target = |source_node_index, segments: Vec<&str>, width| {
3478            let owned = segments
3479                .iter()
3480                .map(|segment| (*segment).to_owned())
3481                .collect::<Vec<_>>();
3482            GltfAddressabilityTargetV2 {
3483                source_node_index,
3484                contributing_channels: vec![GltfAddressabilityTargetChannelV2 {
3485                    source_animation_index: 0,
3486                    source_channel_index: source_node_index,
3487                }],
3488                projection: GltfAddressabilityProjectionV2::Available {
3489                    value: GltfAddressabilityTargetValueV2 {
3490                        path: owned.join("/"),
3491                        uuid: bevy_animation_target_id_v1(segments, width).unwrap(),
3492                        segments: owned,
3493                    },
3494                },
3495            }
3496        };
3497        let mut targets = vec![
3498            target(0, vec!["a/b", "c"], TargetPointerWidth::Bits64),
3499            target(1, vec!["a", "b/c"], TargetPointerWidth::Bits64),
3500        ];
3501        invalidate_target_collisions(&mut targets);
3502        assert!(targets.iter().all(|target| matches!(
3503            target.projection(),
3504            GltfAddressabilityProjectionV2::Available { .. }
3505        )));
3506    }
3507
3508    #[test]
3509    fn multi_path_candidates_participate_in_global_collision_analysis() {
3510        let source = loaded_source_with_targets([1, 3], true);
3511        let raw = raw_for_graph(
3512            &source,
3513            vec![
3514                RawGltfSceneRowV1::new(0, None, vec![0]),
3515                RawGltfSceneRowV1::new(1, None, vec![1]),
3516                RawGltfSceneRowV1::new(2, None, vec![2]),
3517            ],
3518            RawGltfAddressabilityCoverageV1::Complete,
3519            vec![
3520                RawGltfNodeRowV1::new(0, Some("a".into()), None, vec![1]),
3521                RawGltfNodeRowV1::new(1, Some("b".into()), Some(0), Vec::new()),
3522                RawGltfNodeRowV1::new(2, Some("a".into()), None, vec![3]),
3523                RawGltfNodeRowV1::new(3, Some("b".into()), Some(2), Vec::new()),
3524            ],
3525            RawGltfAddressabilityCoverageV1::Complete,
3526            vec![
3527                RawGltfScenePathCandidateRowV1::new(0, 0, vec![0]),
3528                RawGltfScenePathCandidateRowV1::new(1, 0, vec![0, 1]),
3529                RawGltfScenePathCandidateRowV1::new(2, 1, vec![1]),
3530                RawGltfScenePathCandidateRowV1::new(3, 2, vec![2]),
3531                RawGltfScenePathCandidateRowV1::new(4, 2, vec![2, 3]),
3532            ],
3533            RawGltfAddressabilityCoverageV1::Complete,
3534        );
3535        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3536        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3537            &raw,
3538            &animations,
3539            true,
3540            true,
3541            Some(TargetPointerWidth::Bits64),
3542        )
3543        .unwrap();
3544        assert!(matches!(
3545            projection.targets()[0].projection(),
3546            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3547                if reasons.contains(&GltfAddressabilityUnavailableReasonV2::MultipleCandidatePaths)
3548        ));
3549        assert!(matches!(
3550            projection.targets()[1].projection(),
3551            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3552                if reasons.contains(&GltfAddressabilityUnavailableReasonV2::DuplicateFullPath)
3553                    && reasons.contains(&GltfAddressabilityUnavailableReasonV2::TargetIdCollision)
3554        ));
3555    }
3556
3557    #[test]
3558    fn target_id_collision_helper_invalidates_distinct_paths() {
3559        let mut targets = vec![
3560            GltfAddressabilityTargetV2 {
3561                source_node_index: 0,
3562                contributing_channels: Vec::new(),
3563                projection: GltfAddressabilityProjectionV2::Available {
3564                    value: GltfAddressabilityTargetValueV2 {
3565                        segments: vec!["left".into()],
3566                        path: "left".into(),
3567                        uuid: "00000000-0000-5000-8000-000000000000".into(),
3568                    },
3569                },
3570            },
3571            GltfAddressabilityTargetV2 {
3572                source_node_index: 1,
3573                contributing_channels: Vec::new(),
3574                projection: GltfAddressabilityProjectionV2::Available {
3575                    value: GltfAddressabilityTargetValueV2 {
3576                        segments: vec!["right".into()],
3577                        path: "right".into(),
3578                        uuid: "00000000-0000-5000-8000-000000000000".into(),
3579                    },
3580                },
3581            },
3582        ];
3583        invalidate_target_collisions(&mut targets);
3584        assert!(targets.iter().all(|target| matches!(
3585            target.projection(),
3586            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3587                if reasons == &[GltfAddressabilityUnavailableReasonV2::TargetIdCollision]
3588        )));
3589    }
3590
3591    #[test]
3592    fn named_map_incomplete_and_projection_reference_bounds_are_typed() {
3593        let source = empty_loaded_source();
3594        let raw = raw_for_graph(
3595            &source,
3596            vec![RawGltfSceneRowV1::new(
3597                0,
3598                Some("retained".into()),
3599                Vec::new(),
3600            )],
3601            RawGltfAddressabilityCoverageV1::budget_exceeded(),
3602            Vec::new(),
3603            RawGltfAddressabilityCoverageV1::Complete,
3604            Vec::new(),
3605            RawGltfAddressabilityCoverageV1::Complete,
3606        );
3607        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3608        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3609            &raw,
3610            &animations,
3611            true,
3612            true,
3613            Some(TargetPointerWidth::Bits64),
3614        )
3615        .unwrap();
3616        assert!(matches!(
3617            projection.named_maps()[0].winners(),
3618            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3619                if reasons == &[GltfAddressabilityUnavailableReasonV2::NamedMapIncomplete]
3620        ));
3621
3622        let bounded_projection = |channel_count| BevyGltfAddressabilityProjectionV2 {
3623            scenes: Vec::new(),
3624            default_scene_route: GltfAddressabilityProjectionV2::ProvenAbsent,
3625            skins: Vec::new(),
3626            named_maps: Vec::new(),
3627            target_coverage: GltfAddressabilityProjectionV2::Available { value: () },
3628            targets: vec![GltfAddressabilityTargetV2 {
3629                source_node_index: 0,
3630                contributing_channels: vec![
3631                    GltfAddressabilityTargetChannelV2 {
3632                        source_animation_index: 0,
3633                        source_channel_index: 0,
3634                    };
3635                    channel_count
3636                ],
3637                projection: GltfAddressabilityProjectionV2::Available {
3638                    value: GltfAddressabilityTargetValueV2 {
3639                        segments: vec!["target".into()],
3640                        path: "target".into(),
3641                        uuid: "00000000-0000-5000-8000-000000000000".into(),
3642                    },
3643                },
3644            }],
3645        };
3646        let mut exact = bounded_projection(GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES - 1);
3647        normalize_projection_bounds(&mut exact);
3648        assert!(matches!(
3649            exact.targets()[0].projection(),
3650            GltfAddressabilityProjectionV2::Available { .. }
3651        ));
3652        let mut over = bounded_projection(GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES);
3653        normalize_projection_bounds(&mut over);
3654        assert!(matches!(
3655            over.target_coverage(),
3656            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3657                if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3658        ));
3659    }
3660
3661    #[test]
3662    fn rich_projection_dynamic_text_accepts_one_mib_and_refuses_one_mib_plus_one() {
3663        let projection_with_first_name = |first_name_bytes: usize| {
3664            let winners = (0..1024)
3665                .map(|index| GltfAddressabilityNamedMapWinnerV2 {
3666                    name: "x".repeat(if index == 0 { first_name_bytes } else { 1023 }),
3667                    source_index: index,
3668                    typed_label: "x".into(),
3669                })
3670                .collect();
3671            BevyGltfAddressabilityProjectionV2 {
3672                scenes: Vec::new(),
3673                default_scene_route: GltfAddressabilityProjectionV2::ProvenAbsent,
3674                skins: Vec::new(),
3675                named_maps: vec![GltfAddressabilityNamedMapV2 {
3676                    kind: GltfAddressabilityNamedMapKindV2::Scene,
3677                    duplicate_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
3678                    winners: GltfAddressabilityProjectionV2::Available { value: winners },
3679                }],
3680                target_coverage: GltfAddressabilityProjectionV2::Available { value: () },
3681                targets: Vec::new(),
3682            }
3683        };
3684        let mut exact = projection_with_first_name(1023);
3685        assert_eq!(
3686            projection_text_bytes(&exact),
3687            GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES
3688        );
3689        normalize_projection_bounds(&mut exact);
3690        assert!(matches!(
3691            exact.named_maps()[0].winners(),
3692            GltfAddressabilityProjectionV2::Available { .. }
3693        ));
3694
3695        let mut over = projection_with_first_name(1024);
3696        assert_eq!(
3697            projection_text_bytes(&over),
3698            GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES + 1
3699        );
3700        normalize_projection_bounds(&mut over);
3701        assert!(matches!(
3702            over.named_maps()[0].winners(),
3703            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3704                if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3705        ));
3706    }
3707
3708    #[test]
3709    fn empty_authored_node_name_is_an_exact_bevy_segment_and_round_trips() {
3710        let source = loaded_source_with_targets([0], true);
3711        let raw = simple_target_raw(&source, Some(String::new()));
3712        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3713        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3714            &raw,
3715            &animations,
3716            true,
3717            true,
3718            Some(TargetPointerWidth::Bits64),
3719        )
3720        .unwrap();
3721        let target = projection.targets()[0].projection();
3722        assert!(matches!(
3723            target,
3724            GltfAddressabilityProjectionV2::Available { value }
3725                if value.segments() == [String::new()] && value.path().is_empty()
3726        ));
3727        let encoded = serde_json::to_vec(target).unwrap();
3728        let decoded: GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2> =
3729            serde_json::from_slice(&encoded).unwrap();
3730        assert_eq!(&decoded, target);
3731        let mut non_strict = serde_json::to_value(target).unwrap();
3732        non_strict["value"]["invented"] = serde_json::json!(true);
3733        assert!(
3734            serde_json::from_value::<
3735                GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2>,
3736            >(non_strict)
3737            .is_err()
3738        );
3739        assert_ne!(
3740            bevy_animation_target_id_v1([""], TargetPointerWidth::Bits64).unwrap(),
3741            bevy_animation_target_id_v1(std::iter::empty(), TargetPointerWidth::Bits64).unwrap()
3742        );
3743    }
3744
3745    #[test]
3746    fn standalone_inventory_round_trip_is_strict_and_identity_bound() {
3747        let source = empty_loaded_source();
3748        let raw = analytic_raw(&source);
3749        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3750        let report = GltfAddressabilityV2::new(
3751            ToolInfo::animsmith(ToolSource::new(None, None)),
3752            raw,
3753            animations,
3754            None,
3755        )
3756        .unwrap();
3757        let json = serde_json::to_value(&report).unwrap();
3758        let readback =
3759            GltfAddressabilityV2::read_from(Cursor::new(serde_json::to_vec(&json).unwrap()))
3760                .unwrap();
3761        assert_eq!(readback.input(), report.input());
3762        assert_eq!(readback.inventory(), report.inventory());
3763        assert!(readback.bevy().is_none());
3764
3765        let mut wrong_schema = json.clone();
3766        wrong_schema["schema"] = serde_json::json!("urn:animsmith:schema:gltf-addressability:1");
3767        assert!(
3768            GltfAddressabilityV2::read_from(Cursor::new(
3769                serde_json::to_vec(&wrong_schema).unwrap()
3770            ))
3771            .is_err()
3772        );
3773
3774        let mut wrong_binding = json;
3775        wrong_binding["input"]["bytes"] = serde_json::json!(999);
3776        assert!(
3777            GltfAddressabilityV2::read_from(Cursor::new(
3778                serde_json::to_vec(&wrong_binding).unwrap()
3779            ))
3780            .is_err()
3781        );
3782    }
3783
3784    #[test]
3785    fn adapter_strict_readback_rejects_each_authority_projection_and_check_mutation() {
3786        let complete = adapter_report_json(Some(TargetPointerWidth::Bits64));
3787        let reject = |value: serde_json::Value| {
3788            let bytes = serde_json::to_vec(&value).unwrap();
3789            assert!(GltfAddressabilityV2::read_from(Cursor::new(bytes)).is_err());
3790        };
3791
3792        let mut rules = complete.clone();
3793        rules["bevy"]["rules"]["bevy_commit"] = serde_json::json!("0".repeat(40));
3794        reject(rules);
3795
3796        let mut setting_value = complete.clone();
3797        setting_value["bevy"]["settings"]["load_animations"] = serde_json::json!(false);
3798        reject(setting_value);
3799
3800        let mut setting_origin = complete.clone();
3801        setting_origin["bevy"]["settings"]["bevy_animation_feature_origin"] =
3802            serde_json::json!("profile_default");
3803        reject(setting_origin);
3804
3805        let mut provenance = complete.clone();
3806        provenance["bevy"]["prediction_provenance"]["identity"]["bytes"] = serde_json::json!(1);
3807        reject(provenance);
3808
3809        let mut projection = complete.clone();
3810        projection["bevy"]["projection"]["targets"][0]["projection"]["value"]["path"] =
3811            serde_json::json!("mutated");
3812        reject(projection);
3813
3814        let mut scopes = complete.clone();
3815        let duplicate = scopes["bevy"]["check"]["evaluated_scopes"][0].clone();
3816        scopes["bevy"]["check"]["evaluated_scopes"]
3817            .as_array_mut()
3818            .unwrap()
3819            .push(duplicate);
3820        reject(scopes);
3821
3822        let mut evaluation = complete;
3823        evaluation["bevy"]["check"]["evaluation"] = serde_json::json!("partial");
3824        reject(evaluation);
3825
3826        let mut reasons = adapter_report_json(None);
3827        let target_facet = reasons["bevy"]["check"]["prediction"]["facets"]
3828            .as_array_mut()
3829            .unwrap()
3830            .iter_mut()
3831            .find(|facet| facet["scope"]["code"] == "animation_target_id")
3832            .unwrap();
3833        target_facet["reasons"] = serde_json::json!(["raw_source_incomplete"]);
3834        reject(reasons);
3835    }
3836
3837    #[test]
3838    fn staged_v2_reader_accepts_n_and_rejects_n_plus_one_before_json_decode() {
3839        assert_eq!(GLTF_ADDRESSABILITY_V2_MAX_REPORT_BYTES, 256 * 1024 * 1024);
3840        let exact = GltfAddressabilityV2::read_from_with_limit(Cursor::new(b"null"), 4);
3841        assert!(matches!(
3842            exact,
3843            Err(GltfAddressabilityReadErrorV2::InvalidJson { .. })
3844                | Err(GltfAddressabilityReadErrorV2::Contract(_))
3845        ));
3846        let over = GltfAddressabilityV2::read_from_with_limit(Cursor::new(b"null "), 4);
3847        assert!(matches!(
3848            over,
3849            Err(GltfAddressabilityReadErrorV2::ReportTooLarge { limit: 4 })
3850        ));
3851    }
3852
3853    #[test]
3854    fn public_schema_caps_each_raw_path_candidate_at_256_segments() {
3855        let schema: serde_json::Value = serde_json::from_str(include_str!(
3856            "../../../docs/schemas/gltf-addressability-v2.schema.json"
3857        ))
3858        .unwrap();
3859        let candidate_schema =
3860            schema["$defs"]["path_candidate_row"]["properties"]["source_node_indices"].clone();
3861        assert_eq!(candidate_schema["maxItems"], 256);
3862        let standalone = serde_json::json!({
3863            "$defs": {"u64": schema["$defs"]["u64"].clone()},
3864            "allOf": [candidate_schema.clone()]
3865        });
3866        let validator = jsonschema::options().build(&standalone).unwrap();
3867        assert!(validator.is_valid(&serde_json::json!(vec![0; 256])));
3868        assert!(!validator.is_valid(&serde_json::json!(vec![0; 257])));
3869
3870        let mut weakened = candidate_schema;
3871        weakened["maxItems"] = serde_json::json!(257);
3872        let weakened = serde_json::json!({
3873            "$defs": {"u64": schema["$defs"]["u64"].clone()},
3874            "allOf": [weakened]
3875        });
3876        let weakened = jsonschema::options().build(&weakened).unwrap();
3877        assert!(weakened.is_valid(&serde_json::json!(vec![0; 257])));
3878    }
3879}