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)]
2540pub(crate) mod 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    pub(crate) fn adapter_report_json(
2722        pointer_width: Option<TargetPointerWidth>,
2723    ) -> serde_json::Value {
2724        let source = loaded_source_with_targets([0], true);
2725        let raw = simple_target_raw(&source, Some("target".into()));
2726        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
2727        let resolved = crate::resolve_static_v2(crate::EngineDeclarationV2 {
2728            selection: Some(crate::ProfileSelection::new(
2729                "bevy",
2730                3,
2731                "0.19.0",
2732                "gltf-asset-loader",
2733            )),
2734            document_settings: Some(BTreeMap::from([
2735                (
2736                    "bevy_animation_feature".into(),
2737                    crate::SettingValueV2::Boolean(true),
2738                ),
2739                (
2740                    "extension_handler_environment".into(),
2741                    crate::SettingValueV2::HandlerEnvironment(
2742                        crate::BevyGltfHandlerEnvironmentV2::BareEmpty,
2743                    ),
2744                ),
2745            ])),
2746            ..crate::EngineDeclarationV2::default()
2747        })
2748        .unwrap()
2749        .unwrap()
2750        .resolve_input(SourceFormatV1::GltfJson)
2751        .unwrap();
2752        let provenance =
2753            crate::project_prediction_provenance_v4(&resolved, &source, Vec::new()).unwrap();
2754        let grids = MetricGrids::new(source.document());
2755        let roles = ResolvedRoles::default();
2756        let config = Config::default();
2757        let adapter = build_bevy_addressability_adapter_v2(
2758            &source,
2759            &raw,
2760            &animations,
2761            &resolved,
2762            provenance,
2763            pointer_width,
2764            &CheckCtx::new(&grids, &roles, &config),
2765        )
2766        .unwrap()
2767        .unwrap();
2768        let report = GltfAddressabilityV2::new(
2769            ToolInfo::animsmith(ToolSource::new(None, None)),
2770            raw,
2771            animations,
2772            Some(adapter),
2773        )
2774        .unwrap();
2775        let bytes = serde_json::to_vec(&report).unwrap();
2776        GltfAddressabilityV2::read_from(Cursor::new(&bytes)).unwrap();
2777        serde_json::from_slice(&bytes).unwrap()
2778    }
2779
2780    fn prediction_provenance_fixture() -> PredictionProvenanceV4 {
2781        serde_json::from_value(
2782            adapter_report_json(Some(TargetPointerWidth::Bits64))["bevy"]["prediction_provenance"]
2783                .clone(),
2784        )
2785        .unwrap()
2786    }
2787
2788    fn analytic_raw(source: &LoadedSource) -> RawGltfAddressabilityInventoryV1 {
2789        RawGltfAddressabilityInventoryV1::new(
2790            source.source_facts().primary_identity().clone(),
2791            source.dependency_closure().clone(),
2792            RawGltfAddressabilityInventoryInputV1 {
2793                default_scene: RawGltfDefaultSceneObservationV1::Selected {
2794                    source_scene_index: 0,
2795                },
2796                scene_coverage: RawGltfAddressabilityCoverageV1::Complete,
2797                scenes: vec![
2798                    RawGltfSceneRowV1::new(0, Some("World".into()), vec![0]),
2799                    RawGltfSceneRowV1::new(1, Some("World".into()), vec![0]),
2800                ],
2801                node_coverage: RawGltfAddressabilityCoverageV1::Complete,
2802                nodes: vec![RawGltfNodeRowV1::new(0, None, None, Vec::new())],
2803                skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
2804                skins: vec![
2805                    RawGltfSkinRowV1::new(
2806                        0,
2807                        Some("Rig".into()),
2808                        vec![0],
2809                        Some(0),
2810                        RawGltfInverseBindMatricesObservationV1::Absent,
2811                    ),
2812                    RawGltfSkinRowV1::new(
2813                        1,
2814                        Some("Rig".into()),
2815                        vec![0],
2816                        None,
2817                        RawGltfInverseBindMatricesObservationV1::Absent,
2818                    ),
2819                ],
2820                attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
2821                attachments: vec![RawGltfSkinAttachmentRowV1::new(0, 0)],
2822                path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
2823                path_candidates: vec![
2824                    RawGltfScenePathCandidateRowV1::new(0, 0, vec![0]),
2825                    RawGltfScenePathCandidateRowV1::new(1, 1, vec![0]),
2826                ],
2827            },
2828        )
2829        .unwrap()
2830    }
2831
2832    #[test]
2833    fn frozen_rule_bundle_is_strict_and_commit_pinned() {
2834        for width in [
2835            None,
2836            Some(TargetPointerWidth::Bits32),
2837            Some(TargetPointerWidth::Bits64),
2838        ] {
2839            let bundle = BevyGltfAddressabilityRulesV1::frozen(width);
2840            assert!(bundle.validate());
2841            assert_eq!(bundle.profile_revision(), 3);
2842            assert_eq!(bundle.sources().len(), 6);
2843            assert!(
2844                bundle
2845                    .sources()
2846                    .windows(2)
2847                    .all(|pair| pair[0].id() < pair[1].id())
2848            );
2849            assert!(
2850                bundle
2851                    .sources()
2852                    .iter()
2853                    .all(|source| source.url().contains(BEVY_COMMIT))
2854            );
2855        }
2856    }
2857
2858    #[test]
2859    fn target_id_golden_vectors_cover_both_pointer_widths_and_boundaries() {
2860        // Goldens are independently obtained from Bevy 0.19.0's exact
2861        // `AnimationTargetId::from_iter` implementation on 32- and 64-bit
2862        // targets.  They intentionally differ because Bevy hashes `usize`.
2863        let paths: &[&[&str]] = &[
2864            &[],
2865            &["Root"],
2866            &["Root", "Hips", "GltfNode7"],
2867            &["ab", "c"],
2868            &["a", "bc"],
2869        ];
2870        let expected_32 = [
2871            "77da024c-dd93-5d43-8f9a-2af3d4b17c5b",
2872            "2514296d-dbd8-5ea8-a8cc-1fadaf961d2f",
2873            "15c3912b-7127-55c1-b5ff-83950e3d0f9a",
2874            "fd324118-d34f-53d0-9787-d5a1594985e7",
2875            "2ceb26c6-8d62-53c7-b4c5-ee3b9b81784e",
2876        ];
2877        let expected_64 = [
2878            "77da024c-dd93-5d43-8f9a-2af3d4b17c5b",
2879            "05c24be8-bacd-5ee9-9a65-f8215983900e",
2880            "90fed833-c803-5800-883d-adee90aab446",
2881            "0d54b499-ea4e-5a49-afec-0a87e56da473",
2882            "c22eb6e6-9f59-509b-9cfc-79d424c0d445",
2883        ];
2884        for ((path, expected_32), expected_64) in paths.iter().zip(expected_32).zip(expected_64) {
2885            assert_eq!(
2886                bevy_animation_target_id_v1(path.iter().copied(), TargetPointerWidth::Bits32)
2887                    .unwrap(),
2888                expected_32
2889            );
2890            assert_eq!(
2891                bevy_animation_target_id_v1(path.iter().copied(), TargetPointerWidth::Bits64)
2892                    .unwrap(),
2893                expected_64
2894            );
2895        }
2896
2897        let max_segment = "x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES);
2898        assert!(
2899            bevy_animation_target_id_v1([max_segment.as_str()], TargetPointerWidth::Bits64).is_ok()
2900        );
2901        let over_segment = format!("{max_segment}x");
2902        assert!(matches!(
2903            bevy_animation_target_id_v1([over_segment.as_str()], TargetPointerWidth::Bits64),
2904            Err(BevyAnimationTargetIdError::SegmentTooLong { .. })
2905        ));
2906
2907        let at_path_limit = [
2908            "a".repeat(1024),
2909            "b".repeat(1024),
2910            "c".repeat(1024),
2911            "d".repeat(1021),
2912        ];
2913        assert!(
2914            bevy_animation_target_id_v1(
2915                at_path_limit.iter().map(String::as_str),
2916                TargetPointerWidth::Bits64,
2917            )
2918            .is_ok()
2919        );
2920        let over_path_limit = [
2921            "a".repeat(1024),
2922            "b".repeat(1024),
2923            "c".repeat(1024),
2924            "d".repeat(1022),
2925        ];
2926        assert!(matches!(
2927            bevy_animation_target_id_v1(
2928                over_path_limit.iter().map(String::as_str),
2929                TargetPointerWidth::Bits64,
2930            ),
2931            Err(BevyAnimationTargetIdError::PathTooLong {
2932                found: 4097,
2933                limit: 4096
2934            })
2935        ));
2936        assert!(
2937            bevy_animation_target_id_v1(
2938                std::iter::repeat_n("", GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS),
2939                TargetPointerWidth::Bits64,
2940            )
2941            .is_ok()
2942        );
2943        assert!(matches!(
2944            bevy_animation_target_id_v1(
2945                std::iter::repeat_n("", GLTF_ADDRESSABILITY_V2_MAX_PATH_SEGMENTS + 1),
2946                TargetPointerWidth::Bits64,
2947            ),
2948            Err(BevyAnimationTargetIdError::TooManySegments {
2949                found: 257,
2950                limit: 256
2951            })
2952        ));
2953    }
2954
2955    #[test]
2956    fn analytic_labels_default_route_and_named_winners_follow_bevy_rules() {
2957        let source = empty_loaded_source();
2958        let raw = analytic_raw(&source);
2959        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
2960        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
2961            &raw,
2962            &animations,
2963            true,
2964            true,
2965            Some(TargetPointerWidth::Bits64),
2966        )
2967        .unwrap();
2968        assert_eq!(
2969            projection
2970                .scenes()
2971                .iter()
2972                .map(GltfAddressabilitySceneV2::label)
2973                .collect::<Vec<_>>(),
2974            ["Scene0", "Scene1"]
2975        );
2976        assert_eq!(
2977            projection.default_scene_route(),
2978            &GltfAddressabilityProjectionV2::Available {
2979                value: "Scene0".into()
2980            }
2981        );
2982        assert!(matches!(
2983            projection.skins()[0].skin_label(),
2984            GltfAddressabilityProjectionV2::Available { value } if value == "Skin0"
2985        ));
2986        assert!(matches!(
2987            projection.skins()[1].skin_label(),
2988            GltfAddressabilityProjectionV2::ProvenAbsent
2989        ));
2990        assert_eq!(
2991            projection.skins()[1].inverse_bind_matrices_label(),
2992            "Skin1/InverseBindMatrices"
2993        );
2994        let scene_map = &projection.named_maps()[0];
2995        assert!(matches!(
2996            &scene_map.winners,
2997            GltfAddressabilityProjectionV2::Available { value }
2998                if value.len() == 1 && value[0].source_index() == 1
2999        ));
3000        let skin_map = &projection.named_maps()[2];
3001        assert!(matches!(
3002            &skin_map.winners,
3003            GltfAddressabilityProjectionV2::Available { value }
3004                if value.len() == 1 && value[0].source_index() == 0
3005        ));
3006    }
3007
3008    #[test]
3009    fn retained_default_scene_is_exact_under_partial_tail_coverage() {
3010        let source = empty_loaded_source();
3011        let raw = RawGltfAddressabilityInventoryV1::new(
3012            source.source_facts().primary_identity().clone(),
3013            source.dependency_closure().clone(),
3014            RawGltfAddressabilityInventoryInputV1 {
3015                default_scene: RawGltfDefaultSceneObservationV1::Selected {
3016                    source_scene_index: 0,
3017                },
3018                scene_coverage: RawGltfAddressabilityCoverageV1::budget_exceeded(),
3019                scenes: vec![RawGltfSceneRowV1::new(0, None, Vec::new())],
3020                node_coverage: RawGltfAddressabilityCoverageV1::Complete,
3021                nodes: Vec::new(),
3022                skin_coverage: RawGltfAddressabilityCoverageV1::Complete,
3023                skins: Vec::new(),
3024                attachment_coverage: RawGltfAddressabilityCoverageV1::Complete,
3025                attachments: Vec::new(),
3026                path_candidate_coverage: RawGltfAddressabilityCoverageV1::Complete,
3027                path_candidates: Vec::new(),
3028            },
3029        )
3030        .unwrap();
3031        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3032        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3033            &raw,
3034            &animations,
3035            true,
3036            true,
3037            Some(TargetPointerWidth::Bits64),
3038        )
3039        .unwrap();
3040        assert_eq!(
3041            projection.default_scene_route(),
3042            &GltfAddressabilityProjectionV2::Available {
3043                value: "Scene0".into()
3044            }
3045        );
3046    }
3047
3048    #[test]
3049    fn named_map_keys_enforce_exact_segment_bound() {
3050        let winner = |name: String| {
3051            let mut winners = BTreeMap::new();
3052            winners.insert(
3053                name.clone(),
3054                GltfAddressabilityNamedMapWinnerV2 {
3055                    name,
3056                    source_index: 0,
3057                    typed_label: "Scene0".into(),
3058                },
3059            );
3060            named_map(GltfAddressabilityNamedMapKindV2::Scene, true, winners)
3061        };
3062        assert!(matches!(
3063            winner("x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES)).winners(),
3064            GltfAddressabilityProjectionV2::Available { .. }
3065        ));
3066        assert!(matches!(
3067            winner("x".repeat(GLTF_ADDRESSABILITY_V2_MAX_SEGMENT_BYTES + 1)).winners(),
3068            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3069                if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3070        ));
3071    }
3072
3073    #[test]
3074    fn incomplete_target_evidence_never_proves_complete_empty_or_prefix_coverage() {
3075        for targets in [Vec::new(), vec![0]] {
3076            let source = loaded_source_with_targets(targets, false);
3077            let raw = if source.source_facts().clips().rows()[0]
3078                .channels()
3079                .rows()
3080                .is_empty()
3081            {
3082                raw_for_graph(
3083                    &source,
3084                    Vec::new(),
3085                    RawGltfAddressabilityCoverageV1::Complete,
3086                    Vec::new(),
3087                    RawGltfAddressabilityCoverageV1::Complete,
3088                    Vec::new(),
3089                    RawGltfAddressabilityCoverageV1::Complete,
3090                )
3091            } else {
3092                simple_target_raw(&source, Some("target".into()))
3093            };
3094            let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3095            let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3096                &raw,
3097                &animations,
3098                true,
3099                true,
3100                Some(TargetPointerWidth::Bits64),
3101            )
3102            .unwrap();
3103            assert!(matches!(
3104                projection.target_coverage(),
3105                GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3106                    if reasons == &[GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3107            ));
3108        }
3109
3110        assert!(matches!(
3111            target_coverage_projection(true, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS),
3112            GltfAddressabilityProjectionV2::Available { .. }
3113        ));
3114        assert!(matches!(
3115            target_coverage_projection(true, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS + 1),
3116            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3117                if reasons == [GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated]
3118        ));
3119        assert!(matches!(
3120            target_coverage_projection(false, GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS + 1),
3121            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3122                if reasons == [
3123                    GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
3124                    GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated,
3125                ]
3126        ));
3127    }
3128
3129    #[test]
3130    fn production_target_projection_retains_exact_n_and_truncates_n_plus_one() {
3131        let exact_count = GLTF_ADDRESSABILITY_V2_MAX_DOMAIN_ROWS;
3132        let exact_source = loaded_source_with_targets(0..exact_count as u64, true);
3133        let exact_raw = wide_target_raw(&exact_source, exact_count, true);
3134        let exact_animations =
3135            GltfAnimationAddressabilityInventoryV1::from_source(&exact_source).unwrap();
3136        let exact = BevyGltfAddressabilityProjectionV2::from_inventories(
3137            &exact_raw,
3138            &exact_animations,
3139            true,
3140            true,
3141            Some(TargetPointerWidth::Bits64),
3142        )
3143        .unwrap();
3144        assert!(matches!(
3145            exact.target_coverage(),
3146            GltfAddressabilityProjectionV2::Available { .. }
3147        ));
3148        assert_eq!(exact.targets().len(), exact_count);
3149        assert_eq!(
3150            exact
3151                .targets()
3152                .iter()
3153                .map(GltfAddressabilityTargetV2::source_node_index)
3154                .collect::<Vec<_>>(),
3155            (0..exact_count as u64).collect::<Vec<_>>()
3156        );
3157        assert!(exact.targets().iter().all(|target| matches!(
3158            target.projection(),
3159            GltfAddressabilityProjectionV2::Available { .. }
3160        )));
3161
3162        let over_count = exact_count + 1;
3163        let over_source = loaded_source_with_targets(0..over_count as u64, true);
3164        // The raw sidecar retains its canonical prefix and marks the omitted
3165        // target-domain evidence partial. `project_targets` must independently
3166        // count the complete animation contributors and exercise its `.take(N)`.
3167        let over_raw = wide_target_raw(&over_source, exact_count, false);
3168        let over_animations =
3169            GltfAnimationAddressabilityInventoryV1::from_source(&over_source).unwrap();
3170        let over = BevyGltfAddressabilityProjectionV2::from_inventories(
3171            &over_raw,
3172            &over_animations,
3173            true,
3174            true,
3175            Some(TargetPointerWidth::Bits64),
3176        )
3177        .unwrap();
3178        assert_eq!(over.targets().len(), exact_count);
3179        assert_eq!(
3180            over.targets()
3181                .iter()
3182                .map(GltfAddressabilityTargetV2::source_node_index)
3183                .collect::<Vec<_>>(),
3184            (0..exact_count as u64).collect::<Vec<_>>()
3185        );
3186        let expected_reasons = [
3187            GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete,
3188            GltfAddressabilityUnavailableReasonV2::TargetDomainTruncated,
3189        ];
3190        assert!(matches!(
3191            over.target_coverage(),
3192            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3193                if reasons.as_slice() == expected_reasons
3194        ));
3195        assert!(over.targets().iter().all(|target| matches!(
3196            target.projection(),
3197            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3198                if reasons.as_slice() == expected_reasons
3199        )));
3200        assert_eq!(
3201            over.targets().last().unwrap().contributing_channels(),
3202            &[GltfAddressabilityTargetChannelV2 {
3203                source_animation_index: 0,
3204                source_channel_index: (exact_count - 1) as u64,
3205            }]
3206        );
3207    }
3208
3209    #[test]
3210    fn unnamed_target_nodes_use_bevys_exact_source_index_fallback() {
3211        let source = loaded_source_with_targets([7], true);
3212        let raw = raw_for_graph(
3213            &source,
3214            vec![RawGltfSceneRowV1::new(0, None, vec![7])],
3215            RawGltfAddressabilityCoverageV1::Complete,
3216            (0..=7)
3217                .map(|index| RawGltfNodeRowV1::new(index, None, None, Vec::new()))
3218                .collect(),
3219            RawGltfAddressabilityCoverageV1::Complete,
3220            vec![RawGltfScenePathCandidateRowV1::new(0, 0, vec![7])],
3221            RawGltfAddressabilityCoverageV1::Complete,
3222        );
3223        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3224        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3225            &raw,
3226            &animations,
3227            true,
3228            true,
3229            Some(TargetPointerWidth::Bits64),
3230        )
3231        .unwrap();
3232        let GltfAddressabilityProjectionV2::Available { value } =
3233            projection.targets()[0].projection()
3234        else {
3235            panic!("complete unnamed target must be exactly projectable");
3236        };
3237        assert_eq!(value.segments(), ["GltfNode7"]);
3238        assert_eq!(value.path(), "GltfNode7");
3239        assert_eq!(
3240            value.uuid(),
3241            bevy_animation_target_id_v1(["GltfNode7"], TargetPointerWidth::Bits64).unwrap()
3242        );
3243    }
3244
3245    #[test]
3246    fn each_raw_target_domain_and_dependency_closure_fail_closed_with_causal_reason() {
3247        for partial_domain in ["scene", "node", "path"] {
3248            let source = loaded_source_with_targets([0], true);
3249            let partial = RawGltfAddressabilityCoverageV1::budget_exceeded();
3250            let complete = RawGltfAddressabilityCoverageV1::Complete;
3251            let raw = raw_for_graph(
3252                &source,
3253                vec![RawGltfSceneRowV1::new(0, None, vec![0])],
3254                if partial_domain == "scene" {
3255                    partial
3256                } else {
3257                    complete
3258                },
3259                vec![RawGltfNodeRowV1::new(
3260                    0,
3261                    Some("target".into()),
3262                    None,
3263                    Vec::new(),
3264                )],
3265                if partial_domain == "node" {
3266                    partial
3267                } else {
3268                    complete
3269                },
3270                vec![RawGltfScenePathCandidateRowV1::new(0, 0, vec![0])],
3271                if partial_domain == "path" {
3272                    partial
3273                } else {
3274                    complete
3275                },
3276            );
3277            let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3278            let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3279                &raw,
3280                &animations,
3281                true,
3282                true,
3283                Some(TargetPointerWidth::Bits64),
3284            )
3285            .unwrap();
3286            assert!(
3287                matches!(
3288                projection.target_coverage(),
3289                GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3290                    if reasons.as_slice()
3291                        == [GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3292                ),
3293                "partial {partial_domain} coverage must invalidate target coverage"
3294            );
3295            assert!(
3296                matches!(
3297                projection.targets()[0].projection(),
3298                GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3299                    if reasons.as_slice()
3300                        == [GltfAddressabilityUnavailableReasonV2::RawSourceIncomplete]
3301                ),
3302                "partial {partial_domain} coverage must invalidate every target row"
3303            );
3304        }
3305
3306        let source = loaded_source_with_targets_and_closure([0], true, false);
3307        assert!(!source.dependency_closure().coverage().is_complete());
3308        let raw = simple_target_raw(&source, Some("target".into()));
3309        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3310        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3311            &raw,
3312            &animations,
3313            true,
3314            true,
3315            Some(TargetPointerWidth::Bits64),
3316        )
3317        .unwrap();
3318        assert!(matches!(
3319            projection.target_coverage(),
3320            GltfAddressabilityProjectionV2::Available { .. }
3321        ));
3322        assert!(matches!(
3323            projection.targets()[0].projection(),
3324            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3325                if reasons.as_slice() == [
3326                    GltfAddressabilityUnavailableReasonV2::DependencyClosureIncomplete,
3327                ]
3328        ));
3329    }
3330
3331    #[test]
3332    fn rich_unavailable_facets_compact_only_after_the_exact_file_limit() {
3333        let source = loaded_source_with_targets([0], true);
3334        let raw = simple_target_raw(&source, Some("target".into()));
3335        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3336        let provenance = prediction_provenance_fixture();
3337        let inventory =
3338            GltfAddressabilityInventoryV2::new(raw.clone(), animations.clone()).unwrap();
3339        let base = BevyGltfAddressabilityProjectionV2::from_inventories(
3340            &raw,
3341            &animations,
3342            true,
3343            true,
3344            Some(TargetPointerWidth::Bits64),
3345        )
3346        .unwrap();
3347
3348        for count in [
3349            PREDICTION_V1_MAX_FACETS_PER_FILE,
3350            PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3351        ] {
3352            let mut projection = base.clone();
3353            projection.targets = (0..count as u64)
3354                .map(|source_node_index| GltfAddressabilityTargetV2 {
3355                    source_node_index,
3356                    contributing_channels: vec![GltfAddressabilityTargetChannelV2 {
3357                        source_animation_index: 0,
3358                        source_channel_index: source_node_index,
3359                    }],
3360                    projection: GltfAddressabilityProjectionV2::unavailable(vec![
3361                        GltfAddressabilityUnavailableReasonV2::TargetPointerWidthMissing,
3362                    ]),
3363                })
3364                .collect();
3365            let output =
3366                build_check_output(&source, &raw, &animations, &provenance, &projection).unwrap();
3367            let check =
3368                CheckEvaluation::evaluated(crate::ENGINE_ADDRESSABILITY_CHECK_ID, output).unwrap();
3369            let expected = expected_facet_specs(&inventory, &projection);
3370            validate_facet_states(
3371                check.engine_prediction_v4(),
3372                check.evaluated_scopes(),
3373                check.evaluation(),
3374                &expected,
3375            )
3376            .unwrap();
3377
3378            let facets = check.engine_prediction_v4().unwrap().facets();
3379            if count == PREDICTION_V1_MAX_FACETS_PER_FILE {
3380                assert_eq!(facets.len(), PREDICTION_V1_MAX_FACETS_PER_FILE);
3381                assert!(facets.iter().all(|facet| {
3382                    facet.scope().code == EvaluationScopeCode::ANIMATION_TARGET_ID
3383                        && facet.reasons()
3384                            == [PredictionUnavailableReasonV2::ProjectIntentUnavailable]
3385                }));
3386            } else {
3387                assert_eq!(facets.len(), 1);
3388                assert_eq!(facets[0].scope().code.as_str(), FACET_BUDGET_SCOPE);
3389                assert!(facets[0].scope().subject.is_none());
3390                assert_eq!(
3391                    facets[0].reasons(),
3392                    [PredictionUnavailableReasonV2::FacetBudgetExceeded]
3393                );
3394            }
3395        }
3396    }
3397
3398    #[test]
3399    fn evaluated_scope_guard_accepts_exact_n_and_rejects_n_plus_one() {
3400        let scopes = (0..=GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES)
3401            .map(|index| {
3402                EvaluationScope::new(EvaluationScopeCode::SCENE_ASSET_LABEL)
3403                    .subject(format!("Scene{index}"))
3404            })
3405            .collect::<Vec<_>>();
3406        let expected = scopes
3407            .iter()
3408            .cloned()
3409            .map(|scope| ExpectedFacetV2 {
3410                scope,
3411                state: EnginePredictionFacetStateV1::Available,
3412                reasons: Vec::new(),
3413            })
3414            .collect::<Vec<_>>();
3415        validate_facet_states(
3416            None,
3417            &scopes[..GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES],
3418            EvaluationState::Complete,
3419            &expected[..GLTF_ADDRESSABILITY_V2_MAX_EVALUATED_SCOPES],
3420        )
3421        .unwrap();
3422        assert_eq!(
3423            validate_facet_states(None, &scopes, EvaluationState::Complete, &expected),
3424            Err(GltfAddressabilityV2Error::InvalidCheckEvaluation)
3425        );
3426    }
3427
3428    #[test]
3429    fn feature_and_loader_settings_are_independently_required_for_targets() {
3430        let source = loaded_source_with_targets([0], true);
3431        let raw = simple_target_raw(&source, Some("target".into()));
3432        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3433        for (feature, load, expected) in [
3434            (
3435                false,
3436                true,
3437                GltfAddressabilityUnavailableReasonV2::BevyAnimationFeatureDisabled,
3438            ),
3439            (
3440                true,
3441                false,
3442                GltfAddressabilityUnavailableReasonV2::LoadAnimationsDisabled,
3443            ),
3444        ] {
3445            let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3446                &raw,
3447                &animations,
3448                feature,
3449                load,
3450                Some(TargetPointerWidth::Bits64),
3451            )
3452            .unwrap();
3453            assert!(matches!(
3454                projection.targets()[0].projection(),
3455                GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3456                    if reasons.contains(&expected)
3457            ));
3458        }
3459    }
3460
3461    #[test]
3462    fn overlong_runtime_path_maps_to_typed_unavailable_after_the_exact_boundary() {
3463        let segments = vec![
3464            "a".repeat(1024),
3465            "b".repeat(1024),
3466            "c".repeat(1024),
3467            "d".repeat(1022),
3468        ];
3469        let projection = project_target_segments(segments, TargetPointerWidth::Bits64);
3470        assert!(matches!(
3471            projection,
3472            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3473                if reasons == [GltfAddressabilityUnavailableReasonV2::PathBoundsExceeded]
3474        ));
3475    }
3476
3477    #[test]
3478    fn slash_joined_display_paths_do_not_alias_segment_vectors() {
3479        let target = |source_node_index, segments: Vec<&str>, width| {
3480            let owned = segments
3481                .iter()
3482                .map(|segment| (*segment).to_owned())
3483                .collect::<Vec<_>>();
3484            GltfAddressabilityTargetV2 {
3485                source_node_index,
3486                contributing_channels: vec![GltfAddressabilityTargetChannelV2 {
3487                    source_animation_index: 0,
3488                    source_channel_index: source_node_index,
3489                }],
3490                projection: GltfAddressabilityProjectionV2::Available {
3491                    value: GltfAddressabilityTargetValueV2 {
3492                        path: owned.join("/"),
3493                        uuid: bevy_animation_target_id_v1(segments, width).unwrap(),
3494                        segments: owned,
3495                    },
3496                },
3497            }
3498        };
3499        let mut targets = vec![
3500            target(0, vec!["a/b", "c"], TargetPointerWidth::Bits64),
3501            target(1, vec!["a", "b/c"], TargetPointerWidth::Bits64),
3502        ];
3503        invalidate_target_collisions(&mut targets);
3504        assert!(targets.iter().all(|target| matches!(
3505            target.projection(),
3506            GltfAddressabilityProjectionV2::Available { .. }
3507        )));
3508    }
3509
3510    #[test]
3511    fn multi_path_candidates_participate_in_global_collision_analysis() {
3512        let source = loaded_source_with_targets([1, 3], true);
3513        let raw = raw_for_graph(
3514            &source,
3515            vec![
3516                RawGltfSceneRowV1::new(0, None, vec![0]),
3517                RawGltfSceneRowV1::new(1, None, vec![1]),
3518                RawGltfSceneRowV1::new(2, None, vec![2]),
3519            ],
3520            RawGltfAddressabilityCoverageV1::Complete,
3521            vec![
3522                RawGltfNodeRowV1::new(0, Some("a".into()), None, vec![1]),
3523                RawGltfNodeRowV1::new(1, Some("b".into()), Some(0), Vec::new()),
3524                RawGltfNodeRowV1::new(2, Some("a".into()), None, vec![3]),
3525                RawGltfNodeRowV1::new(3, Some("b".into()), Some(2), Vec::new()),
3526            ],
3527            RawGltfAddressabilityCoverageV1::Complete,
3528            vec![
3529                RawGltfScenePathCandidateRowV1::new(0, 0, vec![0]),
3530                RawGltfScenePathCandidateRowV1::new(1, 0, vec![0, 1]),
3531                RawGltfScenePathCandidateRowV1::new(2, 1, vec![1]),
3532                RawGltfScenePathCandidateRowV1::new(3, 2, vec![2]),
3533                RawGltfScenePathCandidateRowV1::new(4, 2, vec![2, 3]),
3534            ],
3535            RawGltfAddressabilityCoverageV1::Complete,
3536        );
3537        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3538        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3539            &raw,
3540            &animations,
3541            true,
3542            true,
3543            Some(TargetPointerWidth::Bits64),
3544        )
3545        .unwrap();
3546        assert!(matches!(
3547            projection.targets()[0].projection(),
3548            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3549                if reasons.contains(&GltfAddressabilityUnavailableReasonV2::MultipleCandidatePaths)
3550        ));
3551        assert!(matches!(
3552            projection.targets()[1].projection(),
3553            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3554                if reasons.contains(&GltfAddressabilityUnavailableReasonV2::DuplicateFullPath)
3555                    && reasons.contains(&GltfAddressabilityUnavailableReasonV2::TargetIdCollision)
3556        ));
3557    }
3558
3559    #[test]
3560    fn target_id_collision_helper_invalidates_distinct_paths() {
3561        let mut targets = vec![
3562            GltfAddressabilityTargetV2 {
3563                source_node_index: 0,
3564                contributing_channels: Vec::new(),
3565                projection: GltfAddressabilityProjectionV2::Available {
3566                    value: GltfAddressabilityTargetValueV2 {
3567                        segments: vec!["left".into()],
3568                        path: "left".into(),
3569                        uuid: "00000000-0000-5000-8000-000000000000".into(),
3570                    },
3571                },
3572            },
3573            GltfAddressabilityTargetV2 {
3574                source_node_index: 1,
3575                contributing_channels: Vec::new(),
3576                projection: GltfAddressabilityProjectionV2::Available {
3577                    value: GltfAddressabilityTargetValueV2 {
3578                        segments: vec!["right".into()],
3579                        path: "right".into(),
3580                        uuid: "00000000-0000-5000-8000-000000000000".into(),
3581                    },
3582                },
3583            },
3584        ];
3585        invalidate_target_collisions(&mut targets);
3586        assert!(targets.iter().all(|target| matches!(
3587            target.projection(),
3588            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3589                if reasons == &[GltfAddressabilityUnavailableReasonV2::TargetIdCollision]
3590        )));
3591    }
3592
3593    #[test]
3594    fn named_map_incomplete_and_projection_reference_bounds_are_typed() {
3595        let source = empty_loaded_source();
3596        let raw = raw_for_graph(
3597            &source,
3598            vec![RawGltfSceneRowV1::new(
3599                0,
3600                Some("retained".into()),
3601                Vec::new(),
3602            )],
3603            RawGltfAddressabilityCoverageV1::budget_exceeded(),
3604            Vec::new(),
3605            RawGltfAddressabilityCoverageV1::Complete,
3606            Vec::new(),
3607            RawGltfAddressabilityCoverageV1::Complete,
3608        );
3609        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3610        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3611            &raw,
3612            &animations,
3613            true,
3614            true,
3615            Some(TargetPointerWidth::Bits64),
3616        )
3617        .unwrap();
3618        assert!(matches!(
3619            projection.named_maps()[0].winners(),
3620            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3621                if reasons == &[GltfAddressabilityUnavailableReasonV2::NamedMapIncomplete]
3622        ));
3623
3624        let bounded_projection = |channel_count| BevyGltfAddressabilityProjectionV2 {
3625            scenes: Vec::new(),
3626            default_scene_route: GltfAddressabilityProjectionV2::ProvenAbsent,
3627            skins: Vec::new(),
3628            named_maps: Vec::new(),
3629            target_coverage: GltfAddressabilityProjectionV2::Available { value: () },
3630            targets: vec![GltfAddressabilityTargetV2 {
3631                source_node_index: 0,
3632                contributing_channels: vec![
3633                    GltfAddressabilityTargetChannelV2 {
3634                        source_animation_index: 0,
3635                        source_channel_index: 0,
3636                    };
3637                    channel_count
3638                ],
3639                projection: GltfAddressabilityProjectionV2::Available {
3640                    value: GltfAddressabilityTargetValueV2 {
3641                        segments: vec!["target".into()],
3642                        path: "target".into(),
3643                        uuid: "00000000-0000-5000-8000-000000000000".into(),
3644                    },
3645                },
3646            }],
3647        };
3648        let mut exact = bounded_projection(GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES - 1);
3649        normalize_projection_bounds(&mut exact);
3650        assert!(matches!(
3651            exact.targets()[0].projection(),
3652            GltfAddressabilityProjectionV2::Available { .. }
3653        ));
3654        let mut over = bounded_projection(GLTF_ADDRESSABILITY_V2_MAX_STRUCTURAL_REFERENCES);
3655        normalize_projection_bounds(&mut over);
3656        assert!(matches!(
3657            over.target_coverage(),
3658            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3659                if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3660        ));
3661    }
3662
3663    #[test]
3664    fn rich_projection_dynamic_text_accepts_one_mib_and_refuses_one_mib_plus_one() {
3665        let projection_with_first_name = |first_name_bytes: usize| {
3666            let winners = (0..1024)
3667                .map(|index| GltfAddressabilityNamedMapWinnerV2 {
3668                    name: "x".repeat(if index == 0 { first_name_bytes } else { 1023 }),
3669                    source_index: index,
3670                    typed_label: "x".into(),
3671                })
3672                .collect();
3673            BevyGltfAddressabilityProjectionV2 {
3674                scenes: Vec::new(),
3675                default_scene_route: GltfAddressabilityProjectionV2::ProvenAbsent,
3676                skins: Vec::new(),
3677                named_maps: vec![GltfAddressabilityNamedMapV2 {
3678                    kind: GltfAddressabilityNamedMapKindV2::Scene,
3679                    duplicate_policy: BevyNamedMapDuplicatePolicyV1::LastWriteWins,
3680                    winners: GltfAddressabilityProjectionV2::Available { value: winners },
3681                }],
3682                target_coverage: GltfAddressabilityProjectionV2::Available { value: () },
3683                targets: Vec::new(),
3684            }
3685        };
3686        let mut exact = projection_with_first_name(1023);
3687        assert_eq!(
3688            projection_text_bytes(&exact),
3689            GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES
3690        );
3691        normalize_projection_bounds(&mut exact);
3692        assert!(matches!(
3693            exact.named_maps()[0].winners(),
3694            GltfAddressabilityProjectionV2::Available { .. }
3695        ));
3696
3697        let mut over = projection_with_first_name(1024);
3698        assert_eq!(
3699            projection_text_bytes(&over),
3700            GLTF_ADDRESSABILITY_V2_MAX_TOTAL_TEXT_BYTES + 1
3701        );
3702        normalize_projection_bounds(&mut over);
3703        assert!(matches!(
3704            over.named_maps()[0].winners(),
3705            GltfAddressabilityProjectionV2::RequiredUnavailable { reasons }
3706                if reasons == &[GltfAddressabilityUnavailableReasonV2::ProjectionBoundsExceeded]
3707        ));
3708    }
3709
3710    #[test]
3711    fn empty_authored_node_name_is_an_exact_bevy_segment_and_round_trips() {
3712        let source = loaded_source_with_targets([0], true);
3713        let raw = simple_target_raw(&source, Some(String::new()));
3714        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3715        let projection = BevyGltfAddressabilityProjectionV2::from_inventories(
3716            &raw,
3717            &animations,
3718            true,
3719            true,
3720            Some(TargetPointerWidth::Bits64),
3721        )
3722        .unwrap();
3723        let target = projection.targets()[0].projection();
3724        assert!(matches!(
3725            target,
3726            GltfAddressabilityProjectionV2::Available { value }
3727                if value.segments() == [String::new()] && value.path().is_empty()
3728        ));
3729        let encoded = serde_json::to_vec(target).unwrap();
3730        let decoded: GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2> =
3731            serde_json::from_slice(&encoded).unwrap();
3732        assert_eq!(&decoded, target);
3733        let mut non_strict = serde_json::to_value(target).unwrap();
3734        non_strict["value"]["invented"] = serde_json::json!(true);
3735        assert!(
3736            serde_json::from_value::<
3737                GltfAddressabilityProjectionV2<GltfAddressabilityTargetValueV2>,
3738            >(non_strict)
3739            .is_err()
3740        );
3741        assert_ne!(
3742            bevy_animation_target_id_v1([""], TargetPointerWidth::Bits64).unwrap(),
3743            bevy_animation_target_id_v1(std::iter::empty(), TargetPointerWidth::Bits64).unwrap()
3744        );
3745    }
3746
3747    #[test]
3748    fn standalone_inventory_round_trip_is_strict_and_identity_bound() {
3749        let source = empty_loaded_source();
3750        let raw = analytic_raw(&source);
3751        let animations = GltfAnimationAddressabilityInventoryV1::from_source(&source).unwrap();
3752        let report = GltfAddressabilityV2::new(
3753            ToolInfo::animsmith(ToolSource::new(None, None)),
3754            raw,
3755            animations,
3756            None,
3757        )
3758        .unwrap();
3759        let json = serde_json::to_value(&report).unwrap();
3760        let readback =
3761            GltfAddressabilityV2::read_from(Cursor::new(serde_json::to_vec(&json).unwrap()))
3762                .unwrap();
3763        assert_eq!(readback.input(), report.input());
3764        assert_eq!(readback.inventory(), report.inventory());
3765        assert!(readback.bevy().is_none());
3766
3767        let mut wrong_schema = json.clone();
3768        wrong_schema["schema"] = serde_json::json!("urn:animsmith:schema:gltf-addressability:1");
3769        assert!(
3770            GltfAddressabilityV2::read_from(Cursor::new(
3771                serde_json::to_vec(&wrong_schema).unwrap()
3772            ))
3773            .is_err()
3774        );
3775
3776        let mut wrong_binding = json;
3777        wrong_binding["input"]["bytes"] = serde_json::json!(999);
3778        assert!(
3779            GltfAddressabilityV2::read_from(Cursor::new(
3780                serde_json::to_vec(&wrong_binding).unwrap()
3781            ))
3782            .is_err()
3783        );
3784    }
3785
3786    #[test]
3787    fn adapter_strict_readback_rejects_each_authority_projection_and_check_mutation() {
3788        let complete = adapter_report_json(Some(TargetPointerWidth::Bits64));
3789        assert!(
3790            crate::validate_bevy_readback_v1(serde_json::to_vec(&complete).unwrap().as_slice())
3791                .is_err()
3792        );
3793        let reject = |value: serde_json::Value| {
3794            let bytes = serde_json::to_vec(&value).unwrap();
3795            assert!(GltfAddressabilityV2::read_from(Cursor::new(bytes)).is_err());
3796        };
3797
3798        let mut rules = complete.clone();
3799        rules["bevy"]["rules"]["bevy_commit"] = serde_json::json!("0".repeat(40));
3800        reject(rules);
3801
3802        let mut setting_value = complete.clone();
3803        setting_value["bevy"]["settings"]["load_animations"] = serde_json::json!(false);
3804        reject(setting_value);
3805
3806        let mut setting_origin = complete.clone();
3807        setting_origin["bevy"]["settings"]["bevy_animation_feature_origin"] =
3808            serde_json::json!("profile_default");
3809        reject(setting_origin);
3810
3811        let mut provenance = complete.clone();
3812        provenance["bevy"]["prediction_provenance"]["identity"]["bytes"] = serde_json::json!(1);
3813        reject(provenance);
3814
3815        let mut projection = complete.clone();
3816        projection["bevy"]["projection"]["targets"][0]["projection"]["value"]["path"] =
3817            serde_json::json!("mutated");
3818        reject(projection);
3819
3820        let mut scopes = complete.clone();
3821        let duplicate = scopes["bevy"]["check"]["evaluated_scopes"][0].clone();
3822        scopes["bevy"]["check"]["evaluated_scopes"]
3823            .as_array_mut()
3824            .unwrap()
3825            .push(duplicate);
3826        reject(scopes);
3827
3828        let mut evaluation = complete;
3829        evaluation["bevy"]["check"]["evaluation"] = serde_json::json!("partial");
3830        reject(evaluation);
3831
3832        let mut reasons = adapter_report_json(None);
3833        let target_facet = reasons["bevy"]["check"]["prediction"]["facets"]
3834            .as_array_mut()
3835            .unwrap()
3836            .iter_mut()
3837            .find(|facet| facet["scope"]["code"] == "animation_target_id")
3838            .unwrap();
3839        target_facet["reasons"] = serde_json::json!(["raw_source_incomplete"]);
3840        reject(reasons);
3841    }
3842
3843    #[test]
3844    fn staged_v2_reader_accepts_n_and_rejects_n_plus_one_before_json_decode() {
3845        assert_eq!(GLTF_ADDRESSABILITY_V2_MAX_REPORT_BYTES, 256 * 1024 * 1024);
3846        let exact = GltfAddressabilityV2::read_from_with_limit(Cursor::new(b"null"), 4);
3847        assert!(matches!(
3848            exact,
3849            Err(GltfAddressabilityReadErrorV2::InvalidJson { .. })
3850                | Err(GltfAddressabilityReadErrorV2::Contract(_))
3851        ));
3852        let over = GltfAddressabilityV2::read_from_with_limit(Cursor::new(b"null "), 4);
3853        assert!(matches!(
3854            over,
3855            Err(GltfAddressabilityReadErrorV2::ReportTooLarge { limit: 4 })
3856        ));
3857    }
3858
3859    #[test]
3860    fn public_schema_caps_each_raw_path_candidate_at_256_segments() {
3861        let schema: serde_json::Value = serde_json::from_str(include_str!(
3862            "../../../docs/schemas/gltf-addressability-v2.schema.json"
3863        ))
3864        .unwrap();
3865        let candidate_schema =
3866            schema["$defs"]["path_candidate_row"]["properties"]["source_node_indices"].clone();
3867        assert_eq!(candidate_schema["maxItems"], 256);
3868        let standalone = serde_json::json!({
3869            "$defs": {"u64": schema["$defs"]["u64"].clone()},
3870            "allOf": [candidate_schema.clone()]
3871        });
3872        let validator = jsonschema::options().build(&standalone).unwrap();
3873        assert!(validator.is_valid(&serde_json::json!(vec![0; 256])));
3874        assert!(!validator.is_valid(&serde_json::json!(vec![0; 257])));
3875
3876        let mut weakened = candidate_schema;
3877        weakened["maxItems"] = serde_json::json!(257);
3878        let weakened = serde_json::json!({
3879            "$defs": {"u64": schema["$defs"]["u64"].clone()},
3880            "allOf": [weakened]
3881        });
3882        let weakened = jsonschema::options().build(&weakened).unwrap();
3883        assert!(weakened.is_valid(&serde_json::json!(vec![0; 257])));
3884    }
3885}