Skip to main content

animsmith_engine/
unit_scale.rs

1//! Exact Bevy 0.19 glTF unit and static/default-rest scale prediction.
2
3use crate::error::PredictionRuleError;
4use crate::{SettingIdV2, profiles_v2};
5use animsmith_core::engine_contract::{EngineSettingIdV2, EngineSettingValueV2};
6use animsmith_core::measure::{
7    AssetMeasurements, LinearTransformClassification, SkeletonNodeMeasurements, measure_assets,
8};
9use animsmith_core::prediction::{
10    EnginePredictionBasisV4, PredictionBasisReferenceV4, RawSceneAttachmentBasisDomainV1,
11    RawSceneAttachmentBasisReferenceV1,
12};
13use animsmith_core::{
14    Applicability, Check, CheckCtx, CheckOutput, DependencyClosureCoverageV1,
15    EngineMachineResultV1, EnginePredictionFacetV4, EnginePredictionV4, EvaluationScope,
16    EvaluationScopeCode, ImporterSubjectCreationV1, InventoryCoverageResultV1, LoadedSource,
17    MeasurementPointerV1, PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE, PredictionBasisReferenceV1,
18    PredictionBasisReferenceV2, PredictionFacetDemandV2, PredictionInventoryCoverageStateV1,
19    PredictionInventoryDomainV1, PredictionProvenanceV4, PredictionProvenanceV5,
20    PredictionRuleAllocationV2, PredictionScalarV1, PredictionUnavailableReasonV2,
21    RawSceneAttachmentCoverageV1, RawSceneAttachmentInventoryV1, RawSourceBasisReferenceV1,
22    RawSourceDomainV1, RawSourceFieldIdV1, RawSourceKeyV1, ResolvedSettingLocationV1,
23    SourceFormatV1, SourceNodeLocalRest, SourceSkeletonCoverage, SourceSkeletonRowKindV1,
24    TransformScaleDomainV1, TransformScaleResultV1, TransformScaleSubjectKindV1,
25    UnitMappingResultV1,
26};
27use std::collections::{BTreeMap, BTreeSet};
28
29/// Stable id for Bevy's exact glTF unit/static-scale prediction.
30pub const ENGINE_UNIT_SCALE_CHECK_ID: &str = "engine-unit-scale";
31
32const BEVY_FAMILY: &str = "bevy";
33const BEVY_PROFILE_REVISION: u32 = 2;
34const BEVY_ENGINE_VERSION: &str = "0.19.0";
35const BEVY_IMPORTER: &str = "gltf-asset-loader";
36
37const BEVY_LOADER_SOURCE: &str = "bevy-gltf-loader-0.19.0-c6f634ca";
38const BEVY_COORDINATE_SOURCE: &str = "bevy-gltf-coordinate-conversion-0.19.0-c6f634ca";
39const BEVY_RENDER_ASSET_SOURCE: &str = "bevy-render-asset-usages-0.19.0-c6f634ca";
40const GLTF_UNIT_SOURCE: &str = "khronos-gltf-2.0-coordinate-units";
41
42const FILE_SCOPE: &str = "engine-unit-scale:file-unit";
43const SCENE_SCOPE: &str = "engine-unit-scale:loader-scene-root";
44const SCENE_INVENTORY_SCOPE: &str = "engine-unit-scale:scene-inventory";
45const MESH_SCOPE: &str = "engine-unit-scale:loader-mesh-primitive";
46const MESH_INVENTORY_SCOPE: &str = "engine-unit-scale:mesh-inventory";
47const SELECTED_NODE_SCOPE: &str = "engine-unit-scale:selected-source-node";
48const SELECTED_REACHABILITY_UNAVAILABLE_REASON: &str =
49    "animsmith:selected_node_scene_reachability_unavailable";
50
51// Bound malformed or adversarial parent walks independently of the source
52// inventory size. Ordinary production rigs fit within 128 retained rows;
53// deeper ancestry is a typed unavailable result instead of an unbounded walk.
54const SELECTED_ANCESTRY_MAX_NODES: usize = 128;
55
56/// Engine-owned check for the exact Bevy revision-2 glTF/GLB profile.
57pub struct EngineUnitScaleCheck<'a> {
58    source: &'a LoadedSource,
59    provenance: Option<&'a PredictionProvenanceV4>,
60    successor_provenance: Option<&'a PredictionProvenanceV5>,
61}
62
63impl<'a> EngineUnitScaleCheck<'a> {
64    /// Bind one optional exact-profile V4 provenance record to its same load.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`PredictionRuleError`] when provenance is invalid, does not
69    /// reproduce the source's raw facts, inventory, or dependency closure, or
70    /// claims the frozen Bevy tuple with a non-registry profile record.
71    pub fn new(
72        source: &'a LoadedSource,
73        provenance: Option<&'a PredictionProvenanceV4>,
74    ) -> Result<Self, PredictionRuleError> {
75        if let Some(provenance) = provenance {
76            provenance
77                .validate()
78                .map_err(|_| PredictionRuleError::SourceProvenanceMismatch)?;
79            let raw_source = animsmith_core::RawSourceBindingV2::from_source(
80                source.source_facts(),
81                source.exact_source_timing(),
82            )
83            .map_err(|_| PredictionRuleError::SourceProvenanceMismatch)?;
84            let raw_inventory_matches = match (
85                provenance.raw_scene_attachment().inventory(),
86                source.raw_scene_attachment_inventory(),
87            ) {
88                (Some(left), Some(right)) => left == right,
89                (None, None) => true,
90                _ => false,
91            };
92            if &raw_source != provenance.raw_source()
93                || !raw_inventory_matches
94                || source.dependency_closure() != provenance.dependency_closure()
95            {
96                return Err(PredictionRuleError::SourceProvenanceMismatch);
97            }
98            if is_bevy_tuple(provenance) && !has_frozen_bevy_profile(provenance) {
99                return Err(PredictionRuleError::FrozenProfileMismatch);
100            }
101        }
102        Ok(Self {
103            source,
104            provenance,
105            successor_provenance: None,
106        })
107    }
108
109    /// Bind optional V5 provenance while retaining this rule's immutable V4
110    /// inputs and machine results inside an EnginePredictionV5 attachment.
111    pub fn new_v5(
112        source: &'a LoadedSource,
113        provenance: Option<&'a PredictionProvenanceV5>,
114    ) -> Result<Self, PredictionRuleError> {
115        let mut check = Self::new(source, provenance.map(PredictionProvenanceV5::base))?;
116        if let Some(provenance) = provenance {
117            provenance
118                .validate()
119                .map_err(|_| PredictionRuleError::SourceProvenanceMismatch)?;
120            if is_bevy_tuple_revision(provenance.base(), 3)
121                && !has_frozen_bevy_profile_revision(provenance.base(), 3)
122            {
123                return Err(PredictionRuleError::FrozenProfileMismatch);
124            }
125        }
126        check.successor_provenance = provenance;
127        Ok(check)
128    }
129
130    fn exact_applicable(&self) -> bool {
131        match (self.provenance, self.successor_provenance) {
132            (Some(provenance), Some(_)) => {
133                is_exact_bevy_gltf_revision(self.source, provenance, 2)
134                    || is_exact_bevy_gltf_revision(self.source, provenance, 3)
135            }
136            (Some(provenance), None) => is_exact_bevy_gltf(self.source, provenance),
137            (None, _) => false,
138        }
139    }
140}
141
142impl Check for EngineUnitScaleCheck<'_> {
143    fn id(&self) -> &'static str {
144        ENGINE_UNIT_SCALE_CHECK_ID
145    }
146
147    fn applicability(&self, _ctx: &CheckCtx<'_>) -> Applicability {
148        if self.exact_applicable() {
149            Applicability::Applicable
150        } else {
151            Applicability::NotApplicable
152        }
153    }
154
155    fn evaluate(&self, _ctx: &CheckCtx<'_>) -> CheckOutput {
156        let Some(provenance) = self.provenance else {
157            return empty_output();
158        };
159        if !self.exact_applicable() {
160            return empty_output();
161        }
162        let plan = plan(self.source, provenance);
163        let (capacity, summary_required) = match plan.demand {
164            PredictionFacetDemandV2::Exact(count) => (count, false),
165            PredictionFacetDemandV2::NPlusOne => (
166                PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_sub(1),
167                true,
168            ),
169        };
170        evaluate_allocated(
171            self.source,
172            provenance,
173            self.successor_provenance,
174            &plan,
175            capacity,
176            summary_required,
177        )
178    }
179
180    fn prediction_facet_demand_v2(&self, _ctx: &CheckCtx<'_>) -> PredictionFacetDemandV2 {
181        match self.provenance {
182            Some(provenance) if self.exact_applicable() => plan(self.source, provenance).demand,
183            _ => PredictionFacetDemandV2::Exact(0),
184        }
185    }
186
187    fn evaluate_with_prediction_allocation_v2(
188        &self,
189        _ctx: &CheckCtx<'_>,
190        allocation: PredictionRuleAllocationV2<'_>,
191    ) -> CheckOutput {
192        let Some(provenance) = self.provenance else {
193            return empty_output();
194        };
195        if !self.exact_applicable() {
196            return empty_output();
197        }
198        let plan = plan(self.source, provenance);
199        evaluate_allocated(
200            self.source,
201            provenance,
202            self.successor_provenance,
203            &plan,
204            allocation.candidate_capacity(),
205            allocation.summary_required(),
206        )
207    }
208}
209
210#[derive(Debug, Clone, Copy)]
211enum ScenePlan {
212    Detailed(usize),
213    Incomplete,
214}
215
216#[derive(Debug, Clone)]
217enum MeshPlan {
218    Detailed(Vec<MeshJoinRow>),
219    CompleteEmpty,
220    Incomplete,
221    JoinOverflow,
222}
223
224/// One deterministic atomic row of the bounded scene/attachment/primitive join.
225#[derive(Debug, Clone, Copy)]
226struct MeshJoinRow {
227    source_scene_index: u64,
228    source_root_ordinal: u64,
229    root_node_index: u64,
230    source_node_index: u64,
231    source_mesh_index: u64,
232    source_primitive_index: u64,
233}
234
235#[derive(Debug, Clone)]
236struct RulePlan {
237    demand: PredictionFacetDemandV2,
238    scenes: ScenePlan,
239    meshes: MeshPlan,
240    selectors: usize,
241}
242
243fn plan(source: &LoadedSource, provenance: &PredictionProvenanceV4) -> RulePlan {
244    let inventory = provenance.raw_scene_attachment().inventory();
245    let scenes = match inventory {
246        Some(inventory)
247            if inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete =>
248        {
249            ScenePlan::Detailed(inventory.scenes().rows().len())
250        }
251        _ => ScenePlan::Incomplete,
252    };
253    let meshes = plan_meshes(source, inventory);
254    let selectors = selected_facet_count(source, inventory, provenance);
255
256    let scene_facets = match scenes {
257        ScenePlan::Detailed(count) => count,
258        ScenePlan::Incomplete => 1,
259    };
260    let mesh_facets = match &meshes {
261        MeshPlan::Detailed(rows) => rows.len(),
262        MeshPlan::CompleteEmpty | MeshPlan::Incomplete | MeshPlan::JoinOverflow => 1,
263    };
264    let exact = 1usize
265        .checked_add(scene_facets)
266        .and_then(|count| count.checked_add(mesh_facets))
267        .and_then(|count| count.checked_add(selectors));
268    let demand = match exact {
269        Some(count) if count <= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE => {
270            PredictionFacetDemandV2::Exact(count)
271        }
272        _ => PredictionFacetDemandV2::NPlusOne,
273    };
274    RulePlan {
275        demand,
276        scenes,
277        meshes,
278        selectors,
279    }
280}
281
282fn plan_meshes(
283    source: &LoadedSource,
284    inventory: Option<&RawSceneAttachmentInventoryV1>,
285) -> MeshPlan {
286    let Some(inventory) = inventory else {
287        return MeshPlan::Incomplete;
288    };
289    if inventory.scenes().coverage() != RawSceneAttachmentCoverageV1::Complete
290        || inventory.node_mesh_attachments().coverage() != RawSceneAttachmentCoverageV1::Complete
291        || inventory.mesh_primitives().coverage() != RawSceneAttachmentCoverageV1::Complete
292        || inventory.source_skeleton().coverage() != RawSceneAttachmentCoverageV1::Complete
293        || source.document().assets.source_skeleton.coverage != SourceSkeletonCoverage::Complete
294    {
295        return MeshPlan::Incomplete;
296    }
297    checked_mesh_join(source, inventory)
298}
299
300fn selected_facet_count(
301    source: &LoadedSource,
302    inventory: Option<&RawSceneAttachmentInventoryV1>,
303    provenance: &PredictionProvenanceV4,
304) -> usize {
305    let selectors = provenance.rule_inputs().runtime_node_selectors();
306    if selectors.is_empty() {
307        return 0;
308    }
309    if source.document().assets.source_skeleton.coverage != SourceSkeletonCoverage::Complete {
310        return selectors.len();
311    }
312    let nodes = &source.document().assets.source_skeleton.nodes;
313    let parents = selected_parent_index(source);
314    let complete_scenes = inventory.filter(|inventory| {
315        inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
316            && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
317    });
318    let mut count = 0usize;
319    for selector in selectors {
320        let mut matches = nodes.iter().filter(|node| {
321            node.name
322                .as_deref()
323                .is_some_and(|name| animsmith_core::config::glob_match(selector, name))
324        });
325        let first = matches.next();
326        let ambiguous = matches.next().is_some();
327        let facets = match (first, ambiguous, complete_scenes) {
328            (Some(node), false, Some(inventory)) => {
329                let mut work = 0usize;
330                let mut reachable = 0usize;
331                for scene in inventory.scenes().rows() {
332                    match selected_scene_reachability(
333                        node.source_node_index as u64,
334                        scene.root_node_indices(),
335                        &parents,
336                        &mut work,
337                    ) {
338                        SelectedSceneReachability::Reachable(_, _) => {
339                            reachable = reachable.saturating_add(1);
340                        }
341                        SelectedSceneReachability::Unreachable => {}
342                        SelectedSceneReachability::Unavailable
343                        | SelectedSceneReachability::WorkBudgetExceeded => {
344                            reachable = 0;
345                            break;
346                        }
347                    }
348                }
349                reachable.max(1)
350            }
351            _ => 1,
352        };
353        count = count.saturating_add(facets);
354        if count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
355            return count;
356        }
357    }
358    count
359}
360
361/// Materialize the atomic scene×reachable-attachment×primitive join to N+1.
362///
363/// Each scene/attachment probe, parent step, and matching primitive expansion
364/// spends one deterministic work unit. That bounds adversarial unmatched
365/// cross-products and deep walks as well as emitted facets; an incomplete probe
366/// is a budget result, never a complete-empty inventory claim.
367fn checked_mesh_join(source: &LoadedSource, inventory: &RawSceneAttachmentInventoryV1) -> MeshPlan {
368    let parents = source
369        .document()
370        .assets
371        .source_skeleton
372        .nodes
373        .iter()
374        .map(|node| {
375            (
376                node.source_node_index as u64,
377                node.parent_source_node_index.map(|parent| parent as u64),
378            )
379        })
380        .collect::<BTreeMap<_, _>>();
381    let scene_roots = inventory
382        .scenes()
383        .rows()
384        .iter()
385        .map(|scene| {
386            (
387                scene.source_scene_index(),
388                scene
389                    .root_node_indices()
390                    .iter()
391                    .copied()
392                    .enumerate()
393                    .map(|(ordinal, node)| (node, ordinal as u64))
394                    .collect::<BTreeMap<_, _>>(),
395            )
396        })
397        .collect::<Vec<_>>();
398    let primitives_by_mesh = inventory.mesh_primitives().rows().iter().fold(
399        BTreeMap::<u64, Vec<u64>>::new(),
400        |mut grouped, primitive| {
401            grouped
402                .entry(primitive.source_mesh_index())
403                .or_default()
404                .push(primitive.source_primitive_index());
405            grouped
406        },
407    );
408    let mut work = 0usize;
409    let mut rows = Vec::new();
410    for (source_scene_index, roots) in scene_roots {
411        for attachment in inventory.node_mesh_attachments().rows() {
412            if join_budget_exceeded(&mut work) {
413                return MeshPlan::JoinOverflow;
414            }
415            let Ok(reachable) = reachable_scene_root_indexed(
416                attachment.source_node_index(),
417                &roots,
418                &parents,
419                &mut work,
420            ) else {
421                return MeshPlan::JoinOverflow;
422            };
423            let Some((source_root_ordinal, root_node_index)) = reachable else {
424                continue;
425            };
426            for &source_primitive_index in primitives_by_mesh
427                .get(&attachment.source_mesh_index())
428                .into_iter()
429                .flatten()
430            {
431                if join_budget_exceeded(&mut work) {
432                    return MeshPlan::JoinOverflow;
433                }
434                rows.push(MeshJoinRow {
435                    source_scene_index,
436                    source_root_ordinal,
437                    root_node_index,
438                    source_node_index: attachment.source_node_index(),
439                    source_mesh_index: attachment.source_mesh_index(),
440                    source_primitive_index,
441                });
442            }
443        }
444    }
445    if rows.is_empty() {
446        MeshPlan::CompleteEmpty
447    } else {
448        MeshPlan::Detailed(rows)
449    }
450}
451
452fn join_budget_exceeded(work: &mut usize) -> bool {
453    *work = work.saturating_add(1);
454    *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
455}
456
457fn evaluate_allocated(
458    source: &LoadedSource,
459    provenance: &PredictionProvenanceV4,
460    successor_provenance: Option<&PredictionProvenanceV5>,
461    plan: &RulePlan,
462    candidate_capacity: usize,
463    summary_required: bool,
464) -> CheckOutput {
465    let measurements = measure_assets(source.document());
466    let dependency_complete = matches!(
467        provenance.dependency_closure().coverage(),
468        DependencyClosureCoverageV1::Complete
469    );
470    let mut candidates = Vec::with_capacity(plan.demand.bounded_count());
471
472    if candidate_capacity != 0 {
473        candidates.push(file_facet(provenance, dependency_complete));
474    }
475    append_scene_facets(
476        &mut candidates,
477        provenance,
478        plan.scenes,
479        dependency_complete,
480        candidate_capacity,
481    );
482    append_mesh_facets(
483        &mut candidates,
484        provenance,
485        &plan.meshes,
486        dependency_complete,
487        candidate_capacity,
488    );
489    append_selected_node_facets(
490        &mut candidates,
491        plan.selectors,
492        SelectedEvaluation {
493            source,
494            provenance,
495            measurements: &measurements,
496            dependency_complete,
497            candidate_capacity,
498        },
499    );
500
501    let mut facets = candidates;
502    if summary_required {
503        facets.push(unavailable_facet(
504            budget_scope(),
505            static_basis(&[BEVY_LOADER_SOURCE]),
506            vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
507        ));
508    }
509    let evaluated_scopes = facets
510        .iter()
511        .filter(|facet| facet.result().is_some())
512        .map(|facet| facet.scope().clone())
513        .collect();
514    let prediction = EnginePredictionV4::new(provenance.identity().clone(), facets)
515        .expect("allocated unit/scale facets satisfy V4 bounds");
516    let output = CheckOutput::from_coverage(Vec::new(), evaluated_scopes, Vec::new());
517    match successor_provenance {
518        Some(successor) => output.with_engine_prediction_v5(
519            animsmith_core::EnginePredictionV5::new(successor, prediction)
520                .expect("unit-scale V5 prediction binds the same immutable V4 authority"),
521        ),
522        None => output.with_engine_prediction_v4(prediction),
523    }
524}
525
526fn file_facet(
527    _provenance: &PredictionProvenanceV4,
528    dependency_complete: bool,
529) -> EnginePredictionFacetV4 {
530    let scope = EvaluationScope::new(EvaluationScopeCode::custom(FILE_SCOPE));
531    let basis = unit_basis();
532    if !dependency_complete {
533        return unavailable_facet(
534            scope,
535            basis,
536            vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
537        );
538    }
539    EnginePredictionFacetV4::available(
540        scope,
541        basis,
542        EngineMachineResultV1::UnitMapping(UnitMappingResultV1::gltf_to_engine_world_length_unit()),
543    )
544    .expect("the frozen Bevy file mapping is valid")
545}
546
547fn append_scene_facets(
548    facets: &mut Vec<EnginePredictionFacetV4>,
549    provenance: &PredictionProvenanceV4,
550    plan: ScenePlan,
551    dependency_complete: bool,
552    candidate_capacity: usize,
553) {
554    if facets.len() >= candidate_capacity {
555        return;
556    }
557    let Some(inventory) = provenance.raw_scene_attachment().inventory() else {
558        facets.push(unavailable_facet(
559            EvaluationScope::new(EvaluationScopeCode::custom(SCENE_INVENTORY_SCOPE)),
560            scene_inventory_basis(None),
561            unavailable_with_dependency(
562                PredictionUnavailableReasonV2::RawSourceIncomplete,
563                dependency_complete,
564            ),
565        ));
566        return;
567    };
568    match plan {
569        ScenePlan::Incomplete => facets.push(unavailable_facet(
570            EvaluationScope::new(EvaluationScopeCode::custom(SCENE_INVENTORY_SCOPE)),
571            scene_inventory_basis(Some(inventory)),
572            unavailable_with_dependency(
573                PredictionUnavailableReasonV2::RawSourceIncomplete,
574                dependency_complete,
575            ),
576        )),
577        ScenePlan::Detailed(expected) => {
578            debug_assert_eq!(expected, inventory.scenes().rows().len());
579            for scene in inventory.scenes().rows() {
580                if facets.len() >= candidate_capacity {
581                    break;
582                }
583                let scope = EvaluationScope::new(EvaluationScopeCode::custom(SCENE_SCOPE))
584                    .subject(format!("source_scene:{}", scene.source_scene_index()));
585                let basis = scene_row_basis(scene.source_scene_index());
586                if dependency_complete {
587                    facets.push(available_transform(
588                        scope,
589                        basis,
590                        TransformScaleSubjectKindV1::LoaderSceneEntity,
591                        ImporterSubjectCreationV1::Created,
592                        TransformScaleDomainV1::Local,
593                        LinearTransformClassification::UnitOrthonormal,
594                    ));
595                } else {
596                    facets.push(unavailable_facet(
597                        scope,
598                        basis,
599                        vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
600                    ));
601                }
602            }
603        }
604    }
605}
606
607fn append_mesh_facets(
608    facets: &mut Vec<EnginePredictionFacetV4>,
609    provenance: &PredictionProvenanceV4,
610    plan: &MeshPlan,
611    dependency_complete: bool,
612    candidate_capacity: usize,
613) {
614    if facets.len() >= candidate_capacity {
615        return;
616    }
617    match plan {
618        MeshPlan::Incomplete => {
619            facets.push(unavailable_facet(
620                EvaluationScope::new(EvaluationScopeCode::custom(MESH_INVENTORY_SCOPE)),
621                mesh_inventory_basis(provenance.raw_scene_attachment().inventory()),
622                unavailable_with_dependency(
623                    PredictionUnavailableReasonV2::RawSourceIncomplete,
624                    dependency_complete,
625                ),
626            ));
627        }
628        MeshPlan::JoinOverflow => {
629            facets.push(unavailable_facet(
630                EvaluationScope::new(EvaluationScopeCode::custom(MESH_INVENTORY_SCOPE)),
631                mesh_inventory_basis(provenance.raw_scene_attachment().inventory()),
632                vec![custom_reason("animsmith:mesh_join_work_budget_exceeded")],
633            ));
634        }
635        MeshPlan::CompleteEmpty => {
636            let scope = EvaluationScope::new(EvaluationScopeCode::custom(MESH_INVENTORY_SCOPE));
637            let basis = mesh_inventory_basis(provenance.raw_scene_attachment().inventory());
638            if dependency_complete {
639                facets.push(
640                    EnginePredictionFacetV4::available(
641                        scope,
642                        basis,
643                        EngineMachineResultV1::InventoryCoverage(InventoryCoverageResultV1 {
644                            domain: PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects,
645                            coverage: PredictionInventoryCoverageStateV1::Complete,
646                            retained_rows: 0,
647                        }),
648                    )
649                    .expect("complete-empty joined mesh subjects form an available result"),
650                );
651            } else {
652                facets.push(unavailable_facet(
653                    scope,
654                    basis,
655                    vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
656                ));
657            }
658        }
659        MeshPlan::Detailed(rows) => {
660            for row in rows {
661                if facets.len() >= candidate_capacity {
662                    break;
663                }
664                let scope =
665                    EvaluationScope::new(EvaluationScopeCode::custom(MESH_SCOPE)).subject(format!(
666                        "source_scene:{}:source_node:{}:source_mesh:{}:source_primitive:{}",
667                        row.source_scene_index,
668                        row.source_node_index,
669                        row.source_mesh_index,
670                        row.source_primitive_index,
671                    ));
672                let basis = mesh_row_basis(
673                    row.source_scene_index,
674                    row.source_root_ordinal,
675                    row.root_node_index,
676                    row.source_node_index,
677                    row.source_mesh_index,
678                    row.source_primitive_index,
679                );
680                if dependency_complete {
681                    facets.push(available_transform(
682                        scope,
683                        basis,
684                        TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity,
685                        if load_meshes(provenance) {
686                            ImporterSubjectCreationV1::Created
687                        } else {
688                            ImporterSubjectCreationV1::SuppressedBySetting
689                        },
690                        TransformScaleDomainV1::Local,
691                        LinearTransformClassification::UnitOrthonormal,
692                    ));
693                } else {
694                    facets.push(unavailable_facet(
695                        scope,
696                        basis,
697                        vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
698                    ));
699                }
700            }
701        }
702    }
703}
704
705#[derive(Clone, Copy)]
706struct SelectedEvaluation<'a> {
707    source: &'a LoadedSource,
708    provenance: &'a PredictionProvenanceV4,
709    measurements: &'a AssetMeasurements,
710    dependency_complete: bool,
711    candidate_capacity: usize,
712}
713
714fn append_selected_node_facets(
715    facets: &mut Vec<EnginePredictionFacetV4>,
716    expected_facets: usize,
717    evaluation: SelectedEvaluation<'_>,
718) {
719    let SelectedEvaluation {
720        source,
721        provenance,
722        measurements: _,
723        dependency_complete,
724        candidate_capacity,
725    } = evaluation;
726    let selectors = provenance.rule_inputs().runtime_node_selectors();
727    if selectors.is_empty() {
728        debug_assert_eq!(expected_facets, 0);
729        return;
730    }
731    let inventory = provenance.raw_scene_attachment().inventory();
732    let inventory_complete = inventory.is_some_and(|inventory| {
733        inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
734            && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
735    });
736    let start_len = facets.len();
737    if source.document().assets.source_skeleton.coverage != SourceSkeletonCoverage::Complete
738        || !inventory_complete
739    {
740        for selector in selectors {
741            if facets.len() >= candidate_capacity {
742                break;
743            }
744            facets.push(unavailable_facet(
745                selected_scope(selector),
746                selected_resolution_basis(source, inventory, selector, &[]),
747                unavailable_with_dependency(
748                    PredictionUnavailableReasonV2::RawSourceIncomplete,
749                    dependency_complete,
750                ),
751            ));
752        }
753        return;
754    }
755
756    let inventory = inventory.expect("complete selected-node inventory exists");
757    for selector in selectors {
758        if facets.len() >= candidate_capacity {
759            break;
760        }
761        let mut matches = source
762            .document()
763            .assets
764            .source_skeleton
765            .nodes
766            .iter()
767            .filter(|node| {
768                node.name
769                    .as_deref()
770                    .is_some_and(|name| animsmith_core::config::glob_match(selector, name))
771            });
772        let first = matches.next();
773        let second = matches.next();
774        match (first, second) {
775            (None, _) => facets.push(unavailable_facet(
776                selected_scope(selector),
777                selected_resolution_basis(source, Some(inventory), selector, &[]),
778                unavailable_with_dependency(
779                    PredictionUnavailableReasonV2::SourceSelectorNoMatch,
780                    dependency_complete,
781                ),
782            )),
783            (Some(first), Some(second)) => facets.push(unavailable_facet(
784                selected_scope(selector),
785                selected_resolution_basis(
786                    source,
787                    Some(inventory),
788                    selector,
789                    &[first.source_node_index, second.source_node_index],
790                ),
791                unavailable_with_dependency(
792                    PredictionUnavailableReasonV2::SourceSelectorAmbiguous,
793                    dependency_complete,
794                ),
795            )),
796            (Some(node), None) => append_resolved_selected_node(
797                facets,
798                inventory,
799                selector,
800                node.source_node_index,
801                evaluation,
802            ),
803        }
804    }
805    if candidate_capacity >= start_len.saturating_add(expected_facets) {
806        debug_assert_eq!(facets.len() - start_len, expected_facets);
807    }
808}
809
810fn append_resolved_selected_node(
811    facets: &mut Vec<EnginePredictionFacetV4>,
812    inventory: &RawSceneAttachmentInventoryV1,
813    selector: &str,
814    node_index: usize,
815    evaluation: SelectedEvaluation<'_>,
816) {
817    let SelectedEvaluation {
818        source,
819        measurements,
820        dependency_complete,
821        candidate_capacity,
822        ..
823    } = evaluation;
824    let parents = selected_parent_index(source);
825    let mut reachable_scenes = Vec::new();
826    let mut work = 0usize;
827    for scene in inventory.scenes().rows() {
828        match selected_scene_reachability(
829            node_index as u64,
830            scene.root_node_indices(),
831            &parents,
832            &mut work,
833        ) {
834            SelectedSceneReachability::Reachable(root_ordinal, root_node_index) => {
835                reachable_scenes.push((scene, (root_ordinal, root_node_index)));
836            }
837            SelectedSceneReachability::Unreachable => {}
838            SelectedSceneReachability::Unavailable
839            | SelectedSceneReachability::WorkBudgetExceeded => {
840                facets.push(unavailable_facet(
841                    selected_scope(selector),
842                    selected_node_basis(
843                        source,
844                        Some(inventory),
845                        selector,
846                        node_index,
847                        &[],
848                        None,
849                        None,
850                    ),
851                    unavailable_with_dependency(
852                        custom_reason(SELECTED_REACHABILITY_UNAVAILABLE_REASON),
853                        dependency_complete,
854                    ),
855                ));
856                return;
857            }
858        }
859    }
860    let ancestry = selected_ancestry(source, node_index);
861    let measurement = measurements
862        .skeleton_nodes
863        .iter()
864        .enumerate()
865        .find(|(_, node)| node.node_index == node_index);
866    if reachable_scenes.is_empty() {
867        facets.push(unavailable_facet(
868            selected_scope(selector),
869            selected_node_basis(
870                source,
871                Some(inventory),
872                selector,
873                node_index,
874                &ancestry.rows,
875                None,
876                None,
877            ),
878            unavailable_with_dependency(
879                custom_reason("animsmith:selected_node_unreachable"),
880                dependency_complete,
881            ),
882        ));
883        return;
884    }
885
886    for (scene, (root_ordinal, root_node_index)) in reachable_scenes {
887        if facets.len() >= candidate_capacity {
888            break;
889        }
890        let scene_witness = Some(SelectedSceneWitness {
891            source_scene_index: scene.source_scene_index(),
892            source_root_ordinal: root_ordinal,
893            root_node_index,
894        });
895        let basis = selected_node_basis(
896            source,
897            Some(inventory),
898            selector,
899            node_index,
900            &ancestry.rows,
901            scene_witness,
902            measurement,
903        );
904        let scope = selected_scene_scope(selector, scene.source_scene_index(), node_index);
905        let unavailable_reason = match ancestry.representation {
906            SelectedRepresentation::AllTrs => measurement.and_then(|(_, node)| {
907                (node.rest_world_linear.classification == LinearTransformClassification::NonFinite
908                    || node.rest_world_matrix.is_none())
909                .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
910            }),
911            SelectedRepresentation::MatrixAuthored => Some(custom_reason(
912                "animsmith:matrix_authored_selected_node_or_ancestry",
913            )),
914            SelectedRepresentation::Unavailable => Some(custom_reason(
915                "animsmith:selected_node_ancestry_unavailable",
916            )),
917        }
918        .or_else(|| {
919            measurement
920                .is_none()
921                .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
922        });
923        if let Some(reason) = unavailable_reason {
924            facets.push(unavailable_facet(
925                scope,
926                basis,
927                unavailable_with_dependency(reason, dependency_complete),
928            ));
929        } else if dependency_complete {
930            let node = measurement
931                .expect("available selected node has a measurement")
932                .1;
933            // Bevy's optional loader-root 180-degree Y rotation is proper
934            // orthonormal pre-multiplication. Its two linear sign flips
935            // preserve the same-load affine classification exactly.
936            facets.push(available_transform(
937                scope,
938                basis,
939                TransformScaleSubjectKindV1::SelectedSourceNode,
940                ImporterSubjectCreationV1::Created,
941                TransformScaleDomainV1::LoaderRootToSubject,
942                node.rest_world_linear.classification,
943            ));
944        } else {
945            facets.push(unavailable_facet(
946                scope,
947                basis,
948                vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
949            ));
950        }
951    }
952}
953
954#[derive(Debug, Clone, Copy, PartialEq, Eq)]
955enum SelectedRepresentation {
956    AllTrs,
957    MatrixAuthored,
958    Unavailable,
959}
960
961struct SelectedAncestry {
962    representation: SelectedRepresentation,
963    rows: Vec<usize>,
964}
965
966fn selected_ancestry(source: &LoadedSource, start: usize) -> SelectedAncestry {
967    let nodes = source
968        .document()
969        .assets
970        .source_skeleton
971        .nodes
972        .iter()
973        .map(|node| (node.source_node_index, node))
974        .collect::<BTreeMap<_, _>>();
975    let mut seen = BTreeSet::new();
976    let mut current = start;
977    let mut rows = Vec::new();
978    for _ in 0..SELECTED_ANCESTRY_MAX_NODES {
979        let index = current;
980        if !seen.insert(index) {
981            return SelectedAncestry {
982                representation: SelectedRepresentation::Unavailable,
983                rows,
984            };
985        }
986        let Some(node) = nodes.get(&index) else {
987            return SelectedAncestry {
988                representation: SelectedRepresentation::Unavailable,
989                rows,
990            };
991        };
992        rows.push(index);
993        if matches!(node.local_rest, SourceNodeLocalRest::Matrix(_)) {
994            return SelectedAncestry {
995                representation: SelectedRepresentation::MatrixAuthored,
996                rows,
997            };
998        }
999        let Some(parent) = node.parent_source_node_index else {
1000            return SelectedAncestry {
1001                representation: SelectedRepresentation::AllTrs,
1002                rows,
1003            };
1004        };
1005        current = parent;
1006    }
1007    SelectedAncestry {
1008        representation: SelectedRepresentation::Unavailable,
1009        rows,
1010    }
1011}
1012
1013#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1014enum SelectedSceneReachability {
1015    Reachable(u64, u64),
1016    Unreachable,
1017    Unavailable,
1018    WorkBudgetExceeded,
1019}
1020
1021fn selected_reachability_budget_exceeded(work: &mut usize) -> bool {
1022    *work = work.saturating_add(1);
1023    *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
1024}
1025
1026fn selected_parent_index(source: &LoadedSource) -> BTreeMap<u64, Option<u64>> {
1027    source
1028        .document()
1029        .assets
1030        .source_skeleton
1031        .nodes
1032        .iter()
1033        .map(|node| {
1034            (
1035                node.source_node_index as u64,
1036                node.parent_source_node_index.map(|parent| parent as u64),
1037            )
1038        })
1039        .collect()
1040}
1041
1042fn selected_scene_reachability(
1043    start: u64,
1044    roots: &[u64],
1045    parents: &BTreeMap<u64, Option<u64>>,
1046    work: &mut usize,
1047) -> SelectedSceneReachability {
1048    if *work >= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
1049        return SelectedSceneReachability::WorkBudgetExceeded;
1050    }
1051    let roots = roots
1052        .iter()
1053        .copied()
1054        .enumerate()
1055        .map(|(ordinal, node)| (node, ordinal as u64))
1056        .collect::<BTreeMap<_, _>>();
1057    let mut seen = BTreeSet::new();
1058    let mut current = Some(start);
1059    for _ in 0..SELECTED_ANCESTRY_MAX_NODES {
1060        let Some(index) = current else {
1061            return SelectedSceneReachability::Unreachable;
1062        };
1063        if selected_reachability_budget_exceeded(work) {
1064            return SelectedSceneReachability::WorkBudgetExceeded;
1065        }
1066        if let Some(ordinal) = roots.get(&index) {
1067            return SelectedSceneReachability::Reachable(*ordinal, index);
1068        }
1069        if !seen.insert(index) {
1070            return SelectedSceneReachability::Unavailable;
1071        }
1072        current = match parents.get(&index) {
1073            Some(parent) => *parent,
1074            None => return SelectedSceneReachability::Unavailable,
1075        };
1076    }
1077    match current {
1078        Some(_) => SelectedSceneReachability::Unavailable,
1079        None => SelectedSceneReachability::Unreachable,
1080    }
1081}
1082
1083fn reachable_scene_root_indexed(
1084    start: u64,
1085    roots: &BTreeMap<u64, u64>,
1086    parents: &BTreeMap<u64, Option<u64>>,
1087    work: &mut usize,
1088) -> Result<Option<(u64, u64)>, ()> {
1089    let mut seen = BTreeSet::new();
1090    let mut current = Some(start);
1091    while let Some(index) = current {
1092        if join_budget_exceeded(work) {
1093            return Err(());
1094        }
1095        if let Some(ordinal) = roots.get(&index) {
1096            return Ok(Some((*ordinal, index)));
1097        }
1098        if !seen.insert(index) {
1099            return Ok(None);
1100        }
1101        current = match parents.get(&index) {
1102            Some(parent) => *parent,
1103            None => return Ok(None),
1104        };
1105    }
1106    Ok(None)
1107}
1108
1109fn available_transform(
1110    scope: EvaluationScope,
1111    basis: EnginePredictionBasisV4,
1112    subject_kind: TransformScaleSubjectKindV1,
1113    creation: ImporterSubjectCreationV1,
1114    domain: TransformScaleDomainV1,
1115    classification: LinearTransformClassification,
1116) -> EnginePredictionFacetV4 {
1117    EnginePredictionFacetV4::available(
1118        scope,
1119        basis,
1120        EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
1121            subject_kind,
1122            creation,
1123            domain,
1124            classification: (creation == ImporterSubjectCreationV1::Created)
1125                .then_some(classification),
1126        }),
1127    )
1128    .expect("the exact Bevy transform result is valid")
1129}
1130
1131fn unavailable_facet(
1132    scope: EvaluationScope,
1133    basis: EnginePredictionBasisV4,
1134    reasons: Vec<PredictionUnavailableReasonV2>,
1135) -> EnginePredictionFacetV4 {
1136    EnginePredictionFacetV4::required_unavailable(scope, basis, reasons)
1137        .expect("typed unit/scale unavailability is valid")
1138}
1139
1140fn unavailable_with_dependency(
1141    reason: PredictionUnavailableReasonV2,
1142    dependency_complete: bool,
1143) -> Vec<PredictionUnavailableReasonV2> {
1144    let mut reasons = vec![reason];
1145    if !dependency_complete {
1146        reasons.push(PredictionUnavailableReasonV2::DependencyClosureIncomplete);
1147    }
1148    reasons
1149}
1150
1151fn unit_basis() -> EnginePredictionBasisV4 {
1152    let mut references = [
1153        "application_world_unit_policy",
1154        "importer_scale_conversion",
1155        "physical_dimensions_preserved",
1156        "source_to_target_unit_mapping",
1157        "target_linear_unit",
1158    ]
1159    .into_iter()
1160    .map(|fact| {
1161        PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(
1162            PredictionBasisReferenceV1::profile_fact(fact).expect("static fact id is valid"),
1163        ))
1164    })
1165    .collect::<Vec<_>>();
1166    for source in [BEVY_LOADER_SOURCE, BEVY_COORDINATE_SOURCE, GLTF_UNIT_SOURCE] {
1167        references.push(primary_source_reference(source));
1168    }
1169    EnginePredictionBasisV4::new(references).expect("the frozen unit basis is valid")
1170}
1171
1172fn scene_inventory_basis(
1173    inventory: Option<&RawSceneAttachmentInventoryV1>,
1174) -> EnginePredictionBasisV4 {
1175    let mut references = static_references(&[BEVY_LOADER_SOURCE, BEVY_COORDINATE_SOURCE]);
1176    references.push(transform_profile_fact_reference());
1177    references.push(setting_reference(SettingIdV2::ExtensionHandlerEnvironment));
1178    references.push(setting_reference(SettingIdV2::RotateSceneEntity));
1179    if inventory.is_some() {
1180        references.push(raw_inventory_reference(
1181            RawSceneAttachmentBasisReferenceV1::Coverage {
1182                domain: RawSceneAttachmentBasisDomainV1::Scenes,
1183            },
1184        ));
1185    }
1186    EnginePredictionBasisV4::new(references).expect("the frozen scene basis is valid")
1187}
1188
1189fn scene_row_basis(source_scene_index: u64) -> EnginePredictionBasisV4 {
1190    let mut references = static_references(&[BEVY_LOADER_SOURCE, BEVY_COORDINATE_SOURCE]);
1191    references.push(transform_profile_fact_reference());
1192    references.push(setting_reference(SettingIdV2::ExtensionHandlerEnvironment));
1193    references.push(setting_reference(SettingIdV2::RotateSceneEntity));
1194    references.push(raw_inventory_reference(
1195        RawSceneAttachmentBasisReferenceV1::Coverage {
1196            domain: RawSceneAttachmentBasisDomainV1::Scenes,
1197        },
1198    ));
1199    references.push(raw_inventory_reference(
1200        RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
1201    ));
1202    EnginePredictionBasisV4::new(references).expect("the scene-row basis is valid")
1203}
1204
1205fn mesh_inventory_basis(
1206    inventory: Option<&RawSceneAttachmentInventoryV1>,
1207) -> EnginePredictionBasisV4 {
1208    let mut references = static_references(&[
1209        BEVY_LOADER_SOURCE,
1210        BEVY_COORDINATE_SOURCE,
1211        BEVY_RENDER_ASSET_SOURCE,
1212    ]);
1213    references.push(transform_profile_fact_reference());
1214    references.push(setting_reference(SettingIdV2::ExtensionHandlerEnvironment));
1215    references.push(setting_reference(SettingIdV2::LoadMeshes));
1216    references.push(setting_reference(SettingIdV2::RotateMeshes));
1217    if inventory.is_some() {
1218        for domain in [
1219            RawSceneAttachmentBasisDomainV1::SourceSkeleton,
1220            RawSceneAttachmentBasisDomainV1::Scenes,
1221            RawSceneAttachmentBasisDomainV1::NodeMeshAttachments,
1222            RawSceneAttachmentBasisDomainV1::MeshPrimitives,
1223        ] {
1224            references.push(raw_inventory_reference(
1225                RawSceneAttachmentBasisReferenceV1::Coverage { domain },
1226            ));
1227        }
1228    }
1229    EnginePredictionBasisV4::new(references).expect("the frozen mesh basis is valid")
1230}
1231
1232fn mesh_row_basis(
1233    source_scene_index: u64,
1234    source_root_ordinal: u64,
1235    root_node_index: u64,
1236    source_node_index: u64,
1237    source_mesh_index: u64,
1238    source_primitive_index: u64,
1239) -> EnginePredictionBasisV4 {
1240    let mut references = static_references(&[
1241        BEVY_LOADER_SOURCE,
1242        BEVY_COORDINATE_SOURCE,
1243        BEVY_RENDER_ASSET_SOURCE,
1244    ]);
1245    references.push(transform_profile_fact_reference());
1246    references.push(setting_reference(SettingIdV2::ExtensionHandlerEnvironment));
1247    references.push(setting_reference(SettingIdV2::LoadMeshes));
1248    references.push(setting_reference(SettingIdV2::RotateMeshes));
1249    for domain in [
1250        RawSceneAttachmentBasisDomainV1::SourceSkeleton,
1251        RawSceneAttachmentBasisDomainV1::Scenes,
1252        RawSceneAttachmentBasisDomainV1::NodeMeshAttachments,
1253        RawSceneAttachmentBasisDomainV1::MeshPrimitives,
1254    ] {
1255        references.push(raw_inventory_reference(
1256            RawSceneAttachmentBasisReferenceV1::Coverage { domain },
1257        ));
1258    }
1259    for reference in [
1260        RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
1261        RawSceneAttachmentBasisReferenceV1::SceneRoot {
1262            source_scene_index,
1263            source_root_ordinal,
1264            source_node_index: root_node_index,
1265        },
1266        RawSceneAttachmentBasisReferenceV1::NodeMeshAttachmentRow {
1267            source_node_index,
1268            source_mesh_index,
1269        },
1270        RawSceneAttachmentBasisReferenceV1::MeshPrimitiveRow {
1271            source_mesh_index,
1272            source_primitive_index,
1273        },
1274    ] {
1275        references.push(raw_inventory_reference(reference));
1276    }
1277    EnginePredictionBasisV4::new(references).expect("the mesh-row basis is valid")
1278}
1279
1280fn raw_inventory_reference(
1281    reference: RawSceneAttachmentBasisReferenceV1,
1282) -> PredictionBasisReferenceV4 {
1283    PredictionBasisReferenceV4::raw_scene_attachment(reference)
1284}
1285
1286fn selected_static_references(selector: &str) -> Vec<PredictionBasisReferenceV4> {
1287    let mut references = static_references(&[BEVY_LOADER_SOURCE, BEVY_COORDINATE_SOURCE]);
1288    references.push(transform_profile_fact_reference());
1289    references.push(setting_reference(SettingIdV2::ExtensionHandlerEnvironment));
1290    references.push(setting_reference(SettingIdV2::RotateSceneEntity));
1291    references.push(PredictionBasisReferenceV4::v2(
1292        PredictionBasisReferenceV2::v1(
1293            PredictionBasisReferenceV1::project_field(
1294                "runtime_nodes.selector",
1295                PredictionScalarV1::text(selector).expect("validated selector text is bounded"),
1296            )
1297            .expect("the runtime-node selector field id is valid"),
1298        ),
1299    ));
1300    references
1301}
1302
1303fn selected_resolution_basis(
1304    source: &LoadedSource,
1305    inventory: Option<&RawSceneAttachmentInventoryV1>,
1306    selector: &str,
1307    matched_node_indices: &[usize],
1308) -> EnginePredictionBasisV4 {
1309    let mut references = selected_static_references(selector);
1310    if inventory.is_some() {
1311        for domain in [
1312            RawSceneAttachmentBasisDomainV1::SourceSkeleton,
1313            RawSceneAttachmentBasisDomainV1::Scenes,
1314        ] {
1315            references.push(raw_inventory_reference(
1316                RawSceneAttachmentBasisReferenceV1::Coverage { domain },
1317            ));
1318        }
1319    }
1320    for &node_index in matched_node_indices {
1321        references.push(raw_source_node_reference(source, node_index, "name"));
1322    }
1323    EnginePredictionBasisV4::new(references).expect("the selected-node resolution basis is valid")
1324}
1325
1326#[derive(Debug, Clone, Copy)]
1327struct SelectedSceneWitness {
1328    source_scene_index: u64,
1329    source_root_ordinal: u64,
1330    root_node_index: u64,
1331}
1332
1333fn selected_node_basis(
1334    source: &LoadedSource,
1335    inventory: Option<&RawSceneAttachmentInventoryV1>,
1336    selector: &str,
1337    node_index: usize,
1338    ancestry_rows: &[usize],
1339    scene: Option<SelectedSceneWitness>,
1340    measurement: Option<(usize, &SkeletonNodeMeasurements)>,
1341) -> EnginePredictionBasisV4 {
1342    let mut references = selected_resolution_basis(source, inventory, selector, &[node_index])
1343        .references()
1344        .to_vec();
1345    for &ancestry_node_index in ancestry_rows {
1346        references.push(raw_source_node_reference(
1347            source,
1348            ancestry_node_index,
1349            "local_rest.kind",
1350        ));
1351        references.push(raw_source_node_reference(
1352            source,
1353            ancestry_node_index,
1354            "parent_source_node_index",
1355        ));
1356    }
1357    if let Some(scene) = scene {
1358        references.push(raw_inventory_reference(
1359            RawSceneAttachmentBasisReferenceV1::SceneRow {
1360                source_scene_index: scene.source_scene_index,
1361            },
1362        ));
1363        references.push(raw_inventory_reference(
1364            RawSceneAttachmentBasisReferenceV1::SceneRoot {
1365                source_scene_index: scene.source_scene_index,
1366                source_root_ordinal: scene.source_root_ordinal,
1367                source_node_index: scene.root_node_index,
1368            },
1369        ));
1370    }
1371    if let Some((measurement_ordinal, node)) = measurement {
1372        references.push(PredictionBasisReferenceV4::v2(
1373            PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::measurement_v16(
1374                MeasurementPointerV1::new(format!(
1375                    "/measurements/skeleton_nodes/{measurement_ordinal}/rest_world_linear/classification"
1376                ))
1377                .expect("static measurement path shape is valid"),
1378                PredictionScalarV1::token(classification_name(
1379                    node.rest_world_linear.classification,
1380                ))
1381                .expect("classification spelling is a token"),
1382            )),
1383        ));
1384    }
1385    EnginePredictionBasisV4::new(references).expect("the selected-node evidence basis is valid")
1386}
1387
1388fn raw_source_node_reference(
1389    source: &LoadedSource,
1390    node_index: usize,
1391    field: &'static str,
1392) -> PredictionBasisReferenceV4 {
1393    let raw = RawSourceBasisReferenceV1::from_source(
1394        RawSourceDomainV1::SourceNode,
1395        RawSourceKeyV1::SourceSkeleton {
1396            row_kind: SourceSkeletonRowKindV1::SourceNode,
1397            source_index: node_index as u64,
1398        },
1399        RawSourceFieldIdV1::new(field).expect("static raw field is valid"),
1400        source.source_facts(),
1401    )
1402    .expect("resolved selected source node has the cited raw field");
1403    PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(
1404        PredictionBasisReferenceV1::raw_source(raw),
1405    ))
1406}
1407
1408fn transform_profile_fact_reference() -> PredictionBasisReferenceV4 {
1409    PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(
1410        PredictionBasisReferenceV1::profile_fact("resulting_transform_scale")
1411            .expect("static profile fact id is valid"),
1412    ))
1413}
1414
1415fn static_basis(sources: &[&str]) -> EnginePredictionBasisV4 {
1416    EnginePredictionBasisV4::new(static_references(sources))
1417        .expect("the frozen static basis is valid")
1418}
1419
1420fn static_references(sources: &[&str]) -> Vec<PredictionBasisReferenceV4> {
1421    sources
1422        .iter()
1423        .map(|source| primary_source_reference(source))
1424        .collect()
1425}
1426
1427fn primary_source_reference(source: &str) -> PredictionBasisReferenceV4 {
1428    PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(
1429        PredictionBasisReferenceV1::primary_source(source)
1430            .expect("static primary-source id is valid"),
1431    ))
1432}
1433
1434fn setting_reference(setting: SettingIdV2) -> PredictionBasisReferenceV4 {
1435    PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(
1436        PredictionBasisReferenceV1::resolved_setting(
1437            ResolvedSettingLocationV1::Document,
1438            setting.as_str(),
1439        )
1440        .expect("static setting id is valid"),
1441    ))
1442}
1443
1444fn selected_scope(selector: &str) -> EvaluationScope {
1445    EvaluationScope::new(EvaluationScopeCode::custom(SELECTED_NODE_SCOPE))
1446        .subject(format!("selector:{selector}"))
1447}
1448
1449fn selected_scene_scope(
1450    selector: &str,
1451    source_scene_index: u64,
1452    node_index: usize,
1453) -> EvaluationScope {
1454    EvaluationScope::new(EvaluationScopeCode::custom(SELECTED_NODE_SCOPE)).subject(format!(
1455        "selector:{selector}:source_scene:{source_scene_index}:source_node:{node_index}"
1456    ))
1457}
1458
1459fn budget_scope() -> EvaluationScope {
1460    EvaluationScope::new(EvaluationScopeCode::custom(
1461        "engine-unit-scale:facet-budget",
1462    ))
1463}
1464
1465fn custom_reason(code: &'static str) -> PredictionUnavailableReasonV2 {
1466    PredictionUnavailableReasonV2::custom(code).expect("static reason code is valid")
1467}
1468
1469fn classification_name(classification: LinearTransformClassification) -> &'static str {
1470    match classification {
1471        LinearTransformClassification::UnitOrthonormal => "unit_orthonormal",
1472        LinearTransformClassification::UniformScaled => "uniform_scaled",
1473        LinearTransformClassification::NonUniform => "non_uniform",
1474        LinearTransformClassification::Sheared => "sheared",
1475        LinearTransformClassification::Reflected => "reflected",
1476        LinearTransformClassification::Singular => "singular",
1477        LinearTransformClassification::NonFinite => "non_finite",
1478        _ => "unknown",
1479    }
1480}
1481
1482fn load_meshes(provenance: &PredictionProvenanceV4) -> bool {
1483    matches!(
1484        provenance
1485            .settings()
1486            .document_setting(EngineSettingIdV2::LoadMeshes)
1487            .map(|setting| setting.value()),
1488        Some(EngineSettingValueV2::Token(value)) if value == "nonempty"
1489    )
1490}
1491
1492fn is_bevy_tuple(provenance: &PredictionProvenanceV4) -> bool {
1493    is_bevy_tuple_revision(provenance, BEVY_PROFILE_REVISION)
1494}
1495
1496fn is_bevy_tuple_revision(provenance: &PredictionProvenanceV4, revision: u32) -> bool {
1497    let selection = provenance.profile().selection();
1498    selection.family() == BEVY_FAMILY
1499        && selection.profile_revision() == revision
1500        && selection.engine_version() == BEVY_ENGINE_VERSION
1501        && selection.importer() == BEVY_IMPORTER
1502}
1503
1504fn has_frozen_bevy_profile(provenance: &PredictionProvenanceV4) -> bool {
1505    has_frozen_bevy_profile_revision(provenance, BEVY_PROFILE_REVISION)
1506}
1507
1508fn has_frozen_bevy_profile_revision(provenance: &PredictionProvenanceV4, revision: u32) -> bool {
1509    profiles_v2().iter().any(|profile| {
1510        let selection = profile.selection();
1511        selection.family() == BEVY_FAMILY
1512            && selection.profile_revision() == revision
1513            && selection.engine_version() == BEVY_ENGINE_VERSION
1514            && selection.importer() == BEVY_IMPORTER
1515            && crate::project_engine_profile_v2(profile)
1516                .is_ok_and(|projected| &projected == provenance.profile())
1517    })
1518}
1519
1520fn is_exact_bevy_gltf(source: &LoadedSource, provenance: &PredictionProvenanceV4) -> bool {
1521    is_exact_bevy_gltf_revision(source, provenance, BEVY_PROFILE_REVISION)
1522}
1523
1524fn is_exact_bevy_gltf_revision(
1525    source: &LoadedSource,
1526    provenance: &PredictionProvenanceV4,
1527    revision: u32,
1528) -> bool {
1529    matches!(
1530        source.source_facts().format(),
1531        SourceFormatV1::GltfJson | SourceFormatV1::Glb
1532    ) && source.source_facts().format() == provenance.source_format()
1533        && is_bevy_tuple_revision(provenance, revision)
1534        && has_frozen_bevy_profile_revision(provenance, revision)
1535}
1536
1537fn empty_output() -> CheckOutput {
1538    CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
1539}