1use std::collections::{BTreeMap, BTreeSet};
8use std::fmt::Write as _;
9use std::io::Read;
10
11use glam::Mat4;
12use serde::de::{DeserializeSeed, EnumAccess, MapAccess, SeqAccess, VariantAccess, Visitor};
13use serde::{Deserialize, Deserializer, Serialize};
14use serde_json::value::RawValue;
15use sha2::{Digest, Sha256};
16
17use crate::dependency_closure::DependencyClosureCoverageV1;
18use crate::diff::MetricDelta;
19use crate::engine_contract::{
20 EngineFactIdV1, EngineFactStateV1, EngineFactValueV1, EngineSettingIdV2, EngineSettingValueV2,
21};
22use crate::evaluation::{
23 Applicability, CheckEvaluation, CheckEvaluationGapRef, CheckEvaluationValidationInput,
24 ConfigurationState, EvaluationScope, EvaluationState, SelectionState,
25 validate_and_derive_check_evaluation,
26};
27use crate::measure::{
28 Aabb, AdditionalInfluenceSetMeasurements, AssetMeasurements, ClipMeasurements,
29 ImageMeasurements, LinearTransformClassification, LinearTransformMeasurements,
30 MaterialDefinitionMeasurements, MeasurementAvailability, MeshDefinitionMeasurements,
31 NodeInstanceMeasurements, PrimitiveMeasurements, SceneMeasurements,
32 SkeletonNodeLocalRestMeasurements, SkeletonRestWorldMatrixUnavailableReason,
33 SkinDerivedMatrixMeasurements, SkinDerivedMatrixUnavailableReason,
34 StaticNodeAabbUnavailableReason, TextureMeasurements, assess_inverse_bind,
35 measure_linear_transform, summarize_skin_bind_linear,
36};
37use crate::metrics::canonical_net_yaw_deg;
38use crate::model::{
39 DecodedImageColorType, MaterialResourceCoverage, SourceInverseBindAccessorStatus,
40 SourceSkeletonCoverage,
41};
42use crate::prediction::{
43 EngineMachineResultV1, EnginePredictionBasisV2, EnginePredictionBasisV4,
44 EnginePredictionFacetStateV1, EnginePredictionV1, EnginePredictionV4, EnginePredictionV5,
45 EnginePredictionV6, ExactSourceTimingBasisReferenceV1, ExactSourceTimingBindingV1,
46 ExactSourceTimingDomainV1, ExactSourceTimingKeyV1, ExactSourceTimingObservationStateWireV1,
47 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE, PREDICTION_V1_MAX_FACETS_PER_FILE,
48 PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE, PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE,
49 PredictionBasisReferenceV4, PredictionContractError, PredictionDecodeError,
50 PredictionProvenanceV4, PredictionProvenanceV5, PredictionProvenanceV6, PredictionScalarV1,
51 RawSceneAttachmentBasisDomainV1, RawSceneAttachmentBasisReferenceV1, RawSourceBasisReferenceV1,
52 ResolvedSettingLocationV1, TransformScaleDomainV1, TransformScaleResultV1,
53 TransformScaleSubjectKindV1, UnitMappingResultV1,
54 decode_engine_prediction_v1_with_measurement_schema,
55 decode_engine_prediction_v2_with_measurement_schema, decode_engine_prediction_v3,
56 decode_engine_prediction_v4, decode_prediction_provenance_v1_with_measurement_schema,
57 decode_prediction_provenance_v2_with_measurement_schema, decode_prediction_provenance_v3,
58 decode_prediction_provenance_v4, validate_measurement_references_batch,
59 validate_measurement_references_batch_v2, validate_measurement_references_batch_v3,
60 validate_measurement_references_batch_v4,
61};
62use crate::profile::ResolvedRoles;
63use crate::source_facts::SourceFormatV1;
64use crate::{Document, Severity};
65use crate::{
66 EnginePredictionV2, EnginePredictionV3, ImporterSubjectCreationV1, InventoryCoverageResultV1,
67 PredictionBasisReferenceV1, PredictionBasisReferenceV2, PredictionInventoryCoverageStateV1,
68 PredictionInventoryDomainV1, PredictionProvenanceV1, PredictionProvenanceV2,
69 PredictionProvenanceV3, PredictionUnavailableReasonV2, RawAnimationChannelInventoryV1,
70 RawSceneAttachmentCoverageV1, RawSourceDomainV1, RawSourceFieldIdV1, RawSourceKeyV1,
71 RawSourceSetCoverageStateV1, ResolvedEngineSettingsCoverageStateV2,
72 SourceImportDispositionResultV1, SourceImportDispositionV1, SourceImportSubjectKindV1,
73 SourceSkeletonRowKindV1,
74};
75
76pub const OUTPUT_SCHEMA_VERSION: u32 = 19;
78pub const OUTPUT_SCHEMA_ID: &str = "urn:animsmith:schema:output:19";
80pub const OUTPUT_V18_SCHEMA_ID: &str = "urn:animsmith:schema:output:18";
82pub const OUTPUT_V18_SCHEMA_VERSION: u32 = 18;
84pub const OUTPUT_V17_SCHEMA_ID: &str = "urn:animsmith:schema:output:17";
86pub const OUTPUT_V17_SCHEMA_VERSION: u32 = 17;
88pub const OUTPUT_V16_SCHEMA_ID: &str = "urn:animsmith:schema:output:16";
90pub const OUTPUT_V16_SCHEMA_VERSION: u32 = 16;
92pub const OUTPUT_V15_SCHEMA_ID: &str = "urn:animsmith:schema:output:15";
94pub const OUTPUT_V15_SCHEMA_VERSION: u32 = 15;
96pub const OUTPUT_V10_SCHEMA_ID: &str = "urn:animsmith:schema:output:10";
98pub const OUTPUT_V11_SCHEMA_ID: &str = "urn:animsmith:schema:output:11";
100pub const OUTPUT_V11_SCHEMA_VERSION: u32 = 11;
102pub const OUTPUT_V12_SCHEMA_ID: &str = "urn:animsmith:schema:output:12";
104pub const OUTPUT_V12_SCHEMA_VERSION: u32 = 12;
106pub const OUTPUT_V13_SCHEMA_ID: &str = "urn:animsmith:schema:output:13";
108pub const OUTPUT_V13_SCHEMA_VERSION: u32 = 13;
110pub const OUTPUT_V14_SCHEMA_ID: &str = "urn:animsmith:schema:output:14";
112pub const OUTPUT_V14_SCHEMA_VERSION: u32 = 14;
114pub const OUTPUT_V11_MAX_REPORT_BYTES: u64 = 256 * 1024 * 1024;
116pub const OUTPUT_V11_MAX_FILES: usize = 4_096;
118pub const OUTPUT_V11_MAX_CHECKS_PER_FILE: usize = 4_096;
120pub const MEASUREMENTS_SCHEMA_VERSION: u32 = 18;
122pub const MEASUREMENTS_SCHEMA_ID: &str = "urn:animsmith:schema:measurements:18";
124pub const MEASUREMENTS_V17_SCHEMA_ID: &str = "urn:animsmith:schema:measurements:17";
126pub const MEASUREMENTS_V17_SCHEMA_VERSION: u32 = 17;
128pub const MEASUREMENTS_V16_SCHEMA_ID: &str = "urn:animsmith:schema:measurements:16";
130pub const MEASUREMENTS_V16_SCHEMA_VERSION: u32 = 16;
132pub const MEASUREMENTS_V15_SCHEMA_ID: &str = "urn:animsmith:schema:measurements:15";
134pub const MEASUREMENTS_V15_SCHEMA_VERSION: u32 = 15;
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
139pub struct ToolSource {
140 revision: Option<String>,
141 dirty: Option<bool>,
142}
143
144impl ToolSource {
145 pub fn new(revision: Option<String>, dirty: Option<bool>) -> Self {
152 let revision = revision.filter(|revision| {
153 revision.len() == 40 && revision.bytes().all(|byte| byte.is_ascii_hexdigit())
154 });
155 Self { revision, dirty }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
161pub struct ToolInfo {
162 name: &'static str,
163 version: &'static str,
164 source: ToolSource,
165}
166
167impl ToolInfo {
168 pub fn animsmith(source: ToolSource) -> Self {
171 Self {
172 name: "animsmith",
173 version: env!("CARGO_PKG_VERSION"),
174 source,
175 }
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
184pub struct InputIdentity {
185 sha256: String,
186 bytes: u64,
187}
188
189#[must_use]
195pub fn sha256_hex(bytes: &[u8]) -> String {
196 sha256_digest_hex(Sha256::digest(bytes).into())
197}
198
199fn sha256_digest_hex(digest: [u8; 32]) -> String {
200 let mut hex = String::with_capacity(64);
201 for byte in digest {
202 let _ = write!(hex, "{byte:02x}");
203 }
204 hex
205}
206
207impl InputIdentity {
208 pub fn from_bytes(bytes: &[u8]) -> Self {
210 Self {
211 sha256: sha256_hex(bytes),
212 bytes: bytes.len() as u64,
213 }
214 }
215
216 pub fn from_sha256_digest(digest: [u8; 32], bytes: u64) -> Self {
221 Self {
222 sha256: sha256_digest_hex(digest),
223 bytes,
224 }
225 }
226
227 pub fn from_sha256_hex(sha256: &str, bytes: u64) -> Option<Self> {
235 let digits = sha256.as_bytes();
236 if digits.len() != 64
237 || !digits
238 .iter()
239 .all(|digit| digit.is_ascii_digit() || (b'a'..=b'f').contains(digit))
240 {
241 return None;
242 }
243 Some(Self {
244 sha256: sha256.to_owned(),
245 bytes,
246 })
247 }
248
249 pub fn sha256(&self) -> &str {
251 &self.sha256
252 }
253
254 pub fn bytes(&self) -> u64 {
256 self.bytes
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262#[serde(deny_unknown_fields)]
263pub struct RigInfo {
264 profile: String,
265 resolution_outcome: String,
266 resolved_roles: BTreeMap<String, String>,
267 resolved_role_policies: BTreeMap<String, String>,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
272#[non_exhaustive]
273pub enum RigInfoError {
274 #[error(
276 "resolved role {role:?} references bone {bone}, but the document has {bone_count} bones"
277 )]
278 InvalidBoneId {
279 role: &'static str,
281 bone: usize,
283 bone_count: usize,
285 },
286 #[error(
288 "resolved role {role:?} expected bone {bone} to be {expected:?}, but the document names it {found:?}"
289 )]
290 BoneNameMismatch {
291 role: &'static str,
293 bone: usize,
295 expected: String,
297 found: String,
299 },
300}
301
302impl RigInfo {
303 pub fn from_resolved(doc: &Document, roles: &ResolvedRoles) -> Result<Self, RigInfoError> {
312 let resolved = roles
313 .iter_with_details()
314 .map(|(role, bone, expected_name, policy)| {
315 let name = doc
316 .skeleton
317 .bones
318 .get(bone)
319 .ok_or(RigInfoError::InvalidBoneId {
320 role: role.as_str(),
321 bone,
322 bone_count: doc.skeleton.bones.len(),
323 })?;
324 if name.name != expected_name {
325 return Err(RigInfoError::BoneNameMismatch {
326 role: role.as_str(),
327 bone,
328 expected: expected_name.to_owned(),
329 found: name.name.clone(),
330 });
331 }
332 Ok((role.as_str(), (name.name.clone(), policy.as_str())))
333 })
334 .collect::<Result<BTreeMap<_, _>, _>>()?;
335 Ok(Self {
336 profile: roles.profile.clone(),
337 resolution_outcome: roles.outcome().as_str().to_owned(),
338 resolved_roles: resolved
339 .iter()
340 .map(|(&role, (name, _))| (role.to_owned(), name.clone()))
341 .collect(),
342 resolved_role_policies: resolved
343 .into_iter()
344 .map(|(role, (_, policy))| (role.to_owned(), policy.to_owned()))
345 .collect(),
346 })
347 }
348}
349
350#[derive(Debug, Clone, Serialize)]
353pub struct MeasurementContract {
354 schema_version: u32,
355 schema: &'static str,
356 clips: BTreeMap<String, ClipMeasurements>,
357 #[serde(flatten)]
358 assets: AssetMeasurements,
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
363#[non_exhaustive]
364pub enum MeasurementContractError {
365 #[error("measurement value {path} must be finite")]
367 NonFiniteValue {
368 path: String,
370 },
371 #[error("measurement structure {path} is invalid: {reason}")]
373 InvalidStructure {
374 path: String,
376 reason: String,
378 },
379}
380
381impl MeasurementContract {
382 pub fn new(
389 clips: BTreeMap<String, ClipMeasurements>,
390 assets: AssetMeasurements,
391 ) -> Result<Self, MeasurementContractError> {
392 validate_measurements(&clips, &assets, MeasurementRevision::V18)?;
393 Ok(Self {
394 schema_version: MEASUREMENTS_SCHEMA_VERSION,
395 schema: MEASUREMENTS_SCHEMA_ID,
396 clips,
397 assets,
398 })
399 }
400
401 pub(crate) fn historical_v15(
402 clips: BTreeMap<String, ClipMeasurements>,
403 assets: AssetMeasurements,
404 ) -> Result<Self, MeasurementContractError> {
405 validate_measurements(&clips, &assets, MeasurementRevision::V15)?;
406 Ok(Self {
407 schema_version: MEASUREMENTS_V15_SCHEMA_VERSION,
408 schema: MEASUREMENTS_V15_SCHEMA_ID,
409 clips,
410 assets,
411 })
412 }
413
414 pub(crate) fn historical_v16(
415 clips: BTreeMap<String, ClipMeasurements>,
416 assets: AssetMeasurements,
417 ) -> Result<Self, MeasurementContractError> {
418 validate_measurements(&clips, &assets, MeasurementRevision::V16)?;
419 Ok(Self {
420 schema_version: MEASUREMENTS_V16_SCHEMA_VERSION,
421 schema: MEASUREMENTS_V16_SCHEMA_ID,
422 clips,
423 assets,
424 })
425 }
426
427 pub(crate) fn historical_v17(
428 clips: BTreeMap<String, ClipMeasurements>,
429 assets: AssetMeasurements,
430 ) -> Result<Self, MeasurementContractError> {
431 validate_measurements(&clips, &assets, MeasurementRevision::V17)?;
432 Ok(Self {
433 schema_version: MEASUREMENTS_V17_SCHEMA_VERSION,
434 schema: MEASUREMENTS_V17_SCHEMA_ID,
435 clips,
436 assets,
437 })
438 }
439
440 fn prediction_v16_projection(&self) -> Result<Self, MeasurementContractError> {
447 let mut clips = self.clips.clone();
448 for clip in clips.values_mut() {
449 let has_unavailable_bone = clip.loop_continuity.as_ref().is_some_and(|continuity| {
450 continuity
451 .bones
452 .iter()
453 .any(|bone| bone.availability != MeasurementAvailability::Measured)
454 });
455 if has_unavailable_bone {
456 clip.loop_continuity = None;
457 clip.loop_continuity_availability = MeasurementAvailability::Unavailable;
458 } else if let Some(continuity) = &mut clip.loop_continuity {
459 for bone in &mut continuity.bones {
460 bone.availability_was_present = false;
461 }
462 }
463 }
464 let mut assets = self.assets.clone();
465 for node in &mut assets.skeleton_nodes {
466 node.rest_world_linear.rotation_xyzw = None;
467 }
468 for skin in &mut assets.skins {
469 for joint in &mut skin.joints {
470 if let Some(linear) = &mut joint.joint_bind_to_mesh.linear {
471 linear.rotation_xyzw = None;
472 }
473 if let Some(linear) = &mut joint.mesh_bind_world.linear {
474 linear.rotation_xyzw = None;
475 }
476 }
477 }
478 Self::historical_v16(clips, assets)
479 }
480
481 pub fn clips(&self) -> &BTreeMap<String, ClipMeasurements> {
483 &self.clips
484 }
485
486 pub fn assets(&self) -> &AssetMeasurements {
488 &self.assets
489 }
490
491 pub fn into_parts(self) -> (BTreeMap<String, ClipMeasurements>, AssetMeasurements) {
493 (self.clips, self.assets)
494 }
495}
496
497#[derive(Clone, Copy, PartialEq, Eq)]
498enum MeasurementRevision {
499 V15,
500 V16,
501 V17,
502 V18,
503}
504
505fn validate_measurements(
506 clips: &BTreeMap<String, ClipMeasurements>,
507 assets: &AssetMeasurements,
508 revision: MeasurementRevision,
509) -> Result<(), MeasurementContractError> {
510 let finite = |value: f64, path: String| {
511 value
512 .is_finite()
513 .then_some(())
514 .ok_or(MeasurementContractError::NonFiniteValue { path })
515 };
516 let permits_roundoff = |observed: f64, lower_bound: f64| {
517 let tolerance = 1.0e-9 * observed.abs().max(lower_bound.abs()).max(1.0);
518 observed + tolerance >= lower_bound
519 };
520 let check_availability = |value_present: bool,
521 availability: MeasurementAvailability,
522 path: String| {
523 match (value_present, availability) {
524 (true, MeasurementAvailability::Measured) => Ok(()),
525 (
526 false,
527 MeasurementAvailability::NotApplicable | MeasurementAvailability::Unavailable,
528 ) => Ok(()),
529 _ => Err(MeasurementContractError::InvalidStructure {
530 path,
531 reason: "value presence must match availability status".into(),
532 }),
533 }
534 };
535 for (clip_name, clip) in clips {
536 finite(clip.duration_s, format!("clips[{clip_name:?}].duration_s"))?;
537 let mut previous_bone_index = None;
538 let mut covered_bone_names = BTreeSet::new();
539 for (offset, bone) in clip.bone_channels.iter().enumerate() {
540 let path = format!("clips[{clip_name:?}].bone_channels[{offset}]");
541 if previous_bone_index.is_some_and(|previous| previous >= bone.bone_index) {
542 return Err(MeasurementContractError::InvalidStructure {
543 path: format!("{path}.bone_index"),
544 reason: "bone channel entries must use strictly increasing unique bone indices"
545 .into(),
546 });
547 }
548 previous_bone_index = Some(bone.bone_index);
549 if bone.properties.is_empty() {
550 return Err(MeasurementContractError::InvalidStructure {
551 path: format!("{path}.properties"),
552 reason: "bone channel coverage must contain at least one property".into(),
553 });
554 }
555 if bone
556 .properties
557 .windows(2)
558 .any(|properties| properties[0] >= properties[1])
559 {
560 return Err(MeasurementContractError::InvalidStructure {
561 path: format!("{path}.properties"),
562 reason:
563 "channel properties must be unique and ordered translation, rotation, scale"
564 .into(),
565 });
566 }
567 covered_bone_names.insert(bone.bone_name.clone());
568 }
569 let expected_animated_bones: Vec<_> = covered_bone_names.into_iter().collect();
570 if clip.animated_bones != expected_animated_bones {
571 return Err(MeasurementContractError::InvalidStructure {
572 path: format!("clips[{clip_name:?}].animated_bones"),
573 reason: "animated_bones must equal the sorted unique bone names in bone_channels"
574 .into(),
575 });
576 }
577 for (bone, value) in &clip.bone_rotation_range_deg {
578 if clip.animated_bones.binary_search(bone).is_err() {
579 return Err(MeasurementContractError::InvalidStructure {
580 path: format!("clips[{clip_name:?}].bone_rotation_range_deg[{bone:?}]"),
581 reason: "rotation-range bones must be present in animated_bones".into(),
582 });
583 }
584 finite(
585 *value,
586 format!("clips[{clip_name:?}].bone_rotation_range_deg[{bone:?}]"),
587 )?;
588 }
589 check_availability(
590 clip.loop_continuity.is_some(),
591 clip.loop_continuity_availability,
592 format!("clips[{clip_name:?}].loop_continuity"),
593 )?;
594 check_availability(
595 clip.loop_endpoint_mode.is_some(),
596 clip.loop_endpoint_mode_availability,
597 format!("clips[{clip_name:?}].loop_endpoint_mode"),
598 )?;
599 check_availability(
600 clip.frame_grid.is_some(),
601 clip.frame_grid_availability,
602 format!("clips[{clip_name:?}].frame_grid"),
603 )?;
604 check_availability(
605 clip.loop_seam_ratio.is_some(),
606 clip.loop_seam_ratio_availability,
607 format!("clips[{clip_name:?}].loop_seam_ratio"),
608 )?;
609 check_availability(
610 clip.gait.is_some(),
611 clip.gait_availability,
612 format!("clips[{clip_name:?}].gait"),
613 )?;
614 check_availability(
615 clip.root_trajectory.is_some(),
616 clip.root_trajectory_availability,
617 format!("clips[{clip_name:?}].root_trajectory"),
618 )?;
619 check_availability(
620 clip.speed_mps.is_some(),
621 clip.speed_mps_availability,
622 format!("clips[{clip_name:?}].speed_mps"),
623 )?;
624 if let Some(gait) = &clip.gait {
625 check_availability(
626 gait.phase.is_some(),
627 gait.phase_availability,
628 format!("clips[{clip_name:?}].gait.phase"),
629 )?;
630 }
631 if let Some(trajectory) = &clip.root_trajectory {
632 let path = format!("clips[{clip_name:?}].root_trajectory");
633 check_availability(
634 trajectory.translation.is_some(),
635 trajectory.translation_availability,
636 format!("{path}.translation"),
637 )?;
638 check_availability(
639 trajectory.yaw.is_some(),
640 trajectory.yaw_availability,
641 format!("{path}.yaw"),
642 )?;
643 if trajectory.translation_availability == MeasurementAvailability::NotApplicable {
644 return Err(MeasurementContractError::InvalidStructure {
645 path: format!("{path}.translation_availability"),
646 reason:
647 "translation remains applicable when a root-trajectory bone is selected"
648 .into(),
649 });
650 }
651 if trajectory.yaw_availability == MeasurementAvailability::NotApplicable {
652 return Err(MeasurementContractError::InvalidStructure {
653 path: format!("{path}.yaw_availability"),
654 reason: "yaw remains applicable when a root-trajectory bone is selected".into(),
655 });
656 }
657 if let Some(translation) = trajectory.translation {
658 for (field, value) in [
659 (
660 "horizontal_displacement_x_m",
661 translation.horizontal_displacement_x_m,
662 ),
663 (
664 "horizontal_displacement_z_m",
665 translation.horizontal_displacement_z_m,
666 ),
667 ("horizontal_travel_m", translation.horizontal_travel_m),
668 (
669 "vertical_displacement_m",
670 translation.vertical_displacement_m,
671 ),
672 (
673 "vertical_min_displacement_m",
674 translation.vertical_min_displacement_m,
675 ),
676 (
677 "vertical_max_displacement_m",
678 translation.vertical_max_displacement_m,
679 ),
680 ] {
681 finite(value, format!("{path}.translation.{field}"))?;
682 }
683 if translation.horizontal_travel_m < 0.0 {
684 return Err(MeasurementContractError::InvalidStructure {
685 path: format!("{path}.translation.horizontal_travel_m"),
686 reason: "sampled horizontal travel must be non-negative".into(),
687 });
688 }
689 let horizontal_displacement_m = translation
690 .horizontal_displacement_x_m
691 .hypot(translation.horizontal_displacement_z_m);
692 if !permits_roundoff(translation.horizontal_travel_m, horizontal_displacement_m) {
693 return Err(MeasurementContractError::InvalidStructure {
694 path: format!("{path}.translation.horizontal_travel_m"),
695 reason: "sampled horizontal travel must contain endpoint displacement"
696 .into(),
697 });
698 }
699 if translation.vertical_min_displacement_m > 0.0
700 || translation.vertical_max_displacement_m < 0.0
701 || translation.vertical_displacement_m < translation.vertical_min_displacement_m
702 || translation.vertical_displacement_m > translation.vertical_max_displacement_m
703 {
704 return Err(MeasurementContractError::InvalidStructure {
705 path: format!("{path}.translation"),
706 reason: "vertical extrema must include zero and the endpoint displacement"
707 .into(),
708 });
709 }
710 }
711 if let Some(yaw) = trajectory.yaw {
712 finite(yaw.net_yaw_deg, format!("{path}.yaw.net_yaw_deg"))?;
713 finite(
714 yaw.unwrapped_yaw_deg,
715 format!("{path}.yaw.unwrapped_yaw_deg"),
716 )?;
717 finite(yaw.yaw_travel_deg, format!("{path}.yaw.yaw_travel_deg"))?;
718 if !(-180.0..=180.0).contains(&yaw.net_yaw_deg) {
719 return Err(MeasurementContractError::InvalidStructure {
720 path: format!("{path}.yaw.net_yaw_deg"),
721 reason: "net yaw must be in the inclusive range [-180, 180]".into(),
722 });
723 }
724 if yaw.net_yaw_deg != canonical_net_yaw_deg(yaw.unwrapped_yaw_deg) {
725 return Err(MeasurementContractError::InvalidStructure {
726 path: format!("{path}.yaw.net_yaw_deg"),
727 reason: "net yaw must be the canonical endpoint-equivalent unwrapped yaw"
728 .into(),
729 });
730 }
731 if yaw.yaw_travel_deg < 0.0 {
732 return Err(MeasurementContractError::InvalidStructure {
733 path: format!("{path}.yaw.yaw_travel_deg"),
734 reason: "sampled yaw travel must be non-negative".into(),
735 });
736 }
737 if !permits_roundoff(yaw.yaw_travel_deg, yaw.unwrapped_yaw_deg.abs()) {
738 return Err(MeasurementContractError::InvalidStructure {
739 path: format!("{path}.yaw.yaw_travel_deg"),
740 reason: "sampled yaw travel must contain signed unwrapped yaw".into(),
741 });
742 }
743 }
744 }
745 if let Some(loop_continuity) = &clip.loop_continuity {
746 if loop_continuity.bones.is_empty() {
747 return Err(MeasurementContractError::InvalidStructure {
748 path: format!("clips[{clip_name:?}].loop_continuity.bones"),
749 reason: "present loop-continuity evidence must contain at least one bone"
750 .into(),
751 });
752 }
753 for (expected_index, bone) in loop_continuity.bones.iter().enumerate() {
754 let path = format!("clips[{clip_name:?}].loop_continuity.bones[{expected_index}]");
755 if usize::try_from(bone.bone_index) != Ok(expected_index) {
756 return Err(MeasurementContractError::InvalidStructure {
757 path: format!("{path}.bone_index"),
758 reason: format!(
759 "expected skeleton-order index {expected_index}, found {}",
760 bone.bone_index
761 ),
762 });
763 }
764 if matches!(
765 revision,
766 MeasurementRevision::V17 | MeasurementRevision::V18
767 ) && !bone.availability_was_present
768 {
769 return Err(MeasurementContractError::InvalidStructure {
770 path: format!("{path}.availability"),
771 reason: "measurements-v17 requires explicit per-bone availability".into(),
772 });
773 }
774 if !matches!(
775 revision,
776 MeasurementRevision::V17 | MeasurementRevision::V18
777 ) && bone.availability_was_present
778 {
779 return Err(MeasurementContractError::InvalidStructure {
780 path: format!("{path}.availability"),
781 reason:
782 "per-bone loop-continuity availability is exclusive to measurements-v17"
783 .into(),
784 });
785 }
786 if !matches!(
787 revision,
788 MeasurementRevision::V17 | MeasurementRevision::V18
789 ) && bone.availability != MeasurementAvailability::Measured
790 {
791 return Err(MeasurementContractError::InvalidStructure {
792 path: format!("{path}.availability"),
793 reason: "historical measurement contracts require every present loop-continuity row to be measured"
794 .into(),
795 });
796 }
797 if bone.availability == MeasurementAvailability::NotApplicable {
798 return Err(MeasurementContractError::InvalidStructure {
799 path: format!("{path}.availability"),
800 reason: "an existing bone's loop-continuity evidence remains applicable"
801 .into(),
802 });
803 }
804 let values = [
805 ("position_delta_m", bone.position_delta_m),
806 ("rotation_delta_deg", bone.rotation_delta_deg),
807 ("seam_velocity_delta_mps", bone.seam_velocity_delta_mps),
808 (
809 "seam_angular_velocity_delta_degps",
810 bone.seam_angular_velocity_delta_degps,
811 ),
812 ];
813 let values_present = values.iter().filter(|(_, value)| value.is_some()).count();
814 match (bone.availability, values_present) {
815 (MeasurementAvailability::Measured, 4)
816 | (MeasurementAvailability::Unavailable, 0) => {}
817 _ => {
818 return Err(MeasurementContractError::InvalidStructure {
819 path: path.clone(),
820 reason: "all loop-continuity values must be present exactly when the bone is measured"
821 .into(),
822 });
823 }
824 }
825 for (field, value) in values {
826 let Some(value) = value else { continue };
827 finite(value, format!("{path}.{field}"))?;
828 if value < 0.0 {
829 return Err(MeasurementContractError::InvalidStructure {
830 path: format!("{path}.{field}"),
831 reason: "loop-continuity deltas must be non-negative".into(),
832 });
833 }
834 }
835 }
836 }
837 if let Some(frame_grid) = &clip.frame_grid {
838 let path = format!("clips[{clip_name:?}].frame_grid");
839 finite(frame_grid.fps, format!("{path}.fps"))?;
840 if frame_grid.fps <= 0.0 {
841 return Err(MeasurementContractError::InvalidStructure {
842 path: format!("{path}.fps"),
843 reason: "declared frame-grid FPS must be positive".into(),
844 });
845 }
846 if frame_grid.frame_intervals == 0 {
847 return Err(MeasurementContractError::InvalidStructure {
848 path: format!("{path}.frame_intervals"),
849 reason: "declared frame-grid evidence must contain at least one interval"
850 .into(),
851 });
852 }
853 }
854 if let Some(value) = clip.loop_seam_ratio {
855 finite(value, format!("clips[{clip_name:?}].loop_seam_ratio"))?;
856 }
857 if let Some(gait) = &clip.gait {
858 if let Some(value) = gait.phase {
859 finite(value, format!("clips[{clip_name:?}].gait.phase"))?;
860 }
861 finite(
862 gait.lr_amplitude_m,
863 format!("clips[{clip_name:?}].gait.lr_amplitude_m"),
864 )?;
865 }
866 if let Some(value) = clip.speed_mps {
867 finite(value, format!("clips[{clip_name:?}].speed_mps"))?;
868 }
869 }
870 let invalid = |path: String, reason: &str| MeasurementContractError::InvalidStructure {
871 path,
872 reason: reason.to_owned(),
873 };
874 let finite_aabb = |aabb: &Aabb, path: &str| {
875 for (corner, values) in [("min", aabb.min), ("max", aabb.max)] {
876 for (axis, value) in values.into_iter().enumerate() {
877 finite(f64::from(value), format!("{path}.{corner}[{axis}]"))?;
878 }
879 }
880 for (axis, (min, max)) in aabb.min.into_iter().zip(aabb.max).enumerate() {
881 if min > max {
882 return Err(invalid(
883 format!("{path}.min[{axis}]"),
884 "AABB minimum cannot exceed maximum",
885 ));
886 }
887 }
888 Ok(())
889 };
890
891 let mut mesh_indices = BTreeSet::new();
892 for (index, mesh) in assets.mesh_definitions.iter().enumerate() {
893 if !mesh_indices.insert(mesh.mesh_index) {
894 return Err(invalid(
895 format!("mesh_definitions[{index}].mesh_index"),
896 "mesh_index must be unique",
897 ));
898 }
899 if let Some(aabb) = &mesh.geometry_aabb {
900 finite_aabb(aabb, &format!("mesh_definitions[{index}].geometry_aabb"))?;
901 }
902 if let Some(centroid) = mesh.geometry_centroid {
903 for (axis, value) in centroid.into_iter().enumerate() {
904 finite(
905 f64::from(value),
906 format!("mesh_definitions[{index}].geometry_centroid[{axis}]"),
907 )?;
908 }
909 }
910 if let Some(value) = mesh.weight_sum_min {
911 finite(value, format!("mesh_definitions[{index}].weight_sum_min"))?;
912 }
913 if let Some(value) = mesh.weight_sum_max {
914 finite(value, format!("mesh_definitions[{index}].weight_sum_max"))?;
915 }
916 if revision == MeasurementRevision::V15 && mesh.vertex_count > u64::from(u32::MAX) {
917 return Err(invalid(
918 format!("mesh_definitions[{index}].vertex_count"),
919 "measurements-v15 vertex_count cannot exceed its historical u32 maximum",
920 ));
921 }
922 match (&mesh.primitives, revision) {
923 (
924 None,
925 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
926 ) => {
927 return Err(invalid(
928 format!("mesh_definitions[{index}].primitives"),
929 "measurements-v16 requires per-primitive evidence",
930 ));
931 }
932 (Some(_), MeasurementRevision::V15) => {
933 return Err(invalid(
934 format!("mesh_definitions[{index}].primitives"),
935 "measurements-v15 cannot carry per-primitive evidence",
936 ));
937 }
938 (
939 Some(primitives),
940 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
941 ) => {
942 let mut summed_vertex_count = 0u64;
943 let mut summed_finite_vertex_count = 0u64;
944 let mut aggregate_min = [f32::INFINITY; 3];
945 let mut aggregate_max = [f32::NEG_INFINITY; 3];
946 let mut weighted_centroid_sum = [0.0f64; 3];
947 let mut previous_primitive_index = None;
948 for (primitive_offset, primitive) in primitives.iter().enumerate() {
949 let path = format!("mesh_definitions[{index}].primitives[{primitive_offset}]");
950 if previous_primitive_index
951 .is_some_and(|previous| previous >= primitive.primitive_index)
952 {
953 return Err(invalid(
954 format!("{path}.primitive_index"),
955 "primitive_index must be unique and strictly increasing in source order",
956 ));
957 }
958 previous_primitive_index = Some(primitive.primitive_index);
959 if primitive.finite_vertex_count > primitive.vertex_count {
960 return Err(invalid(
961 format!("{path}.finite_vertex_count"),
962 "finite_vertex_count cannot exceed vertex_count",
963 ));
964 }
965 match (
966 primitive.finite_vertex_count,
967 primitive.geometry_aabb.as_ref(),
968 primitive.geometry_centroid,
969 ) {
970 (0, None, None) => {}
971 (1.., Some(aabb), Some(centroid)) => {
972 finite_aabb(aabb, &format!("{path}.geometry_aabb"))?;
973 for (axis, value) in centroid.into_iter().enumerate() {
974 finite(
975 f64::from(value),
976 format!("{path}.geometry_centroid[{axis}]"),
977 )?;
978 if value < aabb.min[axis] || value > aabb.max[axis] {
979 return Err(invalid(
980 format!("{path}.geometry_centroid[{axis}]"),
981 "primitive centroid must lie inside its geometry AABB",
982 ));
983 }
984 aggregate_min[axis] = aggregate_min[axis].min(aabb.min[axis]);
985 aggregate_max[axis] = aggregate_max[axis].max(aabb.max[axis]);
986 weighted_centroid_sum[axis] +=
987 f64::from(value) * primitive.finite_vertex_count as f64;
988 }
989 }
990 (0, _, _) => {
991 return Err(invalid(
992 path,
993 "a primitive with no finite vertices cannot carry geometry facts",
994 ));
995 }
996 (1.., _, _) => {
997 return Err(invalid(
998 path,
999 "a primitive with finite vertices requires both geometry facts",
1000 ));
1001 }
1002 }
1003 if assets.material_resource_coverage == MaterialResourceCoverage::Complete
1004 && primitive.material_index.is_some_and(|material_index| {
1005 material_index >= assets.material_definitions.len()
1006 })
1007 {
1008 return Err(invalid(
1009 format!("{path}.material_index"),
1010 "material_index must reference a source material when material resource coverage is complete",
1011 ));
1012 }
1013 summed_vertex_count = summed_vertex_count
1014 .checked_add(primitive.vertex_count)
1015 .ok_or_else(|| {
1016 invalid(
1017 format!("mesh_definitions[{index}].vertex_count"),
1018 "primitive vertex-count sum overflows u64",
1019 )
1020 })?;
1021 summed_finite_vertex_count = summed_finite_vertex_count
1022 .checked_add(primitive.finite_vertex_count)
1023 .ok_or_else(|| {
1024 invalid(
1025 format!("mesh_definitions[{index}].primitives"),
1026 "primitive finite-vertex-count sum overflows u64",
1027 )
1028 })?;
1029 }
1030 if summed_vertex_count != mesh.vertex_count {
1031 return Err(invalid(
1032 format!("mesh_definitions[{index}].vertex_count"),
1033 "vertex_count must equal the checked sum of primitive vertex counts",
1034 ));
1035 }
1036 let expected_aabb = (summed_finite_vertex_count != 0).then_some(Aabb {
1037 min: aggregate_min,
1038 max: aggregate_max,
1039 });
1040 let expected_centroid = (summed_finite_vertex_count != 0).then(|| {
1041 let count = summed_finite_vertex_count as f64;
1042 weighted_centroid_sum.map(|sum| (sum / count) as f32)
1043 });
1044 match (
1045 summed_finite_vertex_count,
1046 mesh.geometry_aabb.as_ref(),
1047 mesh.geometry_centroid,
1048 ) {
1049 (0, None, None) | (1.., Some(_), Some(_)) => {}
1050 (0, _, _) => {
1051 return Err(invalid(
1052 format!("mesh_definitions[{index}]"),
1053 "a mesh with no finite primitive vertices cannot carry geometry facts",
1054 ));
1055 }
1056 (1.., _, _) => {
1057 return Err(invalid(
1058 format!("mesh_definitions[{index}]"),
1059 "a mesh with finite primitive vertices requires both geometry facts",
1060 ));
1061 }
1062 }
1063 if mesh.geometry_aabb != expected_aabb {
1064 return Err(invalid(
1065 format!("mesh_definitions[{index}].geometry_aabb"),
1066 "mesh AABB must equal the exact union of primitive AABBs",
1067 ));
1068 }
1069 if mesh.geometry_centroid != expected_centroid {
1070 return Err(invalid(
1071 format!("mesh_definitions[{index}].geometry_centroid"),
1072 "mesh centroid must equal the finite-count-weighted primitive centroids",
1073 ));
1074 }
1075 }
1076 (None, MeasurementRevision::V15) => {}
1077 }
1078 let mut previous_set_index = None;
1079 for (set_offset, set) in mesh.additional_influence_sets.iter().enumerate() {
1080 let path = format!(
1081 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].set_index"
1082 );
1083 if set.set_index == 0 {
1084 return Err(invalid(path, "set_index must be at least 1"));
1085 }
1086 if !set.joints_present && !set.weights_present {
1087 return Err(invalid(
1088 format!("mesh_definitions[{index}].additional_influence_sets[{set_offset}]"),
1089 "an additional influence set must declare joints, weights, or both",
1090 ));
1091 }
1092 if set.joints_without_weights_present && !set.joints_present {
1093 return Err(invalid(
1094 format!(
1095 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].joints_without_weights_present"
1096 ),
1097 "joints_without_weights_present requires joints_present",
1098 ));
1099 }
1100 if set.weights_without_joints_present && !set.weights_present {
1101 return Err(invalid(
1102 format!(
1103 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].weights_without_joints_present"
1104 ),
1105 "weights_without_joints_present requires weights_present",
1106 ));
1107 }
1108 if set.joints_present && !set.weights_present && !set.joints_without_weights_present {
1109 return Err(invalid(
1110 format!(
1111 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].joints_without_weights_present"
1112 ),
1113 "joints_without_weights_present is required when weights_present is false",
1114 ));
1115 }
1116 if set.weights_present && !set.joints_present && !set.weights_without_joints_present {
1117 return Err(invalid(
1118 format!(
1119 "mesh_definitions[{index}].additional_influence_sets[{set_offset}].weights_without_joints_present"
1120 ),
1121 "weights_without_joints_present is required when joints_present is false",
1122 ));
1123 }
1124 if previous_set_index.is_some_and(|previous| previous >= set.set_index) {
1125 return Err(invalid(
1126 path,
1127 "set_index values must be strictly increasing and unique",
1128 ));
1129 }
1130 previous_set_index = Some(set.set_index);
1131 }
1132 }
1133
1134 let mut node_indices = BTreeSet::new();
1135 for (index, instance) in assets.node_instances.iter().enumerate() {
1136 if !node_indices.insert(instance.node_index) {
1137 return Err(invalid(
1138 format!("node_instances[{index}].node_index"),
1139 "node_index must be unique",
1140 ));
1141 }
1142 if !mesh_indices.contains(&instance.mesh_index) {
1143 return Err(invalid(
1144 format!("node_instances[{index}].mesh_index"),
1145 "mesh_index must reference a mesh definition",
1146 ));
1147 }
1148 match (
1149 instance.static_node_world_aabb.as_ref(),
1150 instance.static_node_world_aabb_unavailable_reason,
1151 ) {
1152 (Some(aabb), None) => finite_aabb(
1153 aabb,
1154 &format!("node_instances[{index}].static_node_world_aabb"),
1155 )?,
1156 (None, Some(_)) => {}
1157 (Some(_), Some(_)) => {
1158 return Err(invalid(
1159 format!("node_instances[{index}]"),
1160 "an available static node AABB cannot have an unavailable reason",
1161 ));
1162 }
1163 (None, None) => {
1164 return Err(invalid(
1165 format!("node_instances[{index}]"),
1166 "a missing static node AABB requires an unavailable reason",
1167 ));
1168 }
1169 }
1170 }
1171
1172 let mut scene_indices = BTreeSet::new();
1173 for (index, scene) in assets.scenes.iter().enumerate() {
1174 if !scene_indices.insert(scene.scene_index) {
1175 return Err(invalid(
1176 format!("scenes[{index}].scene_index"),
1177 "scene_index must be unique",
1178 ));
1179 }
1180 if scene.excluded_instance_count > scene.instance_count {
1181 return Err(invalid(
1182 format!("scenes[{index}].excluded_instance_count"),
1183 "excluded_instance_count cannot exceed instance_count",
1184 ));
1185 }
1186 let available = scene.instance_count - scene.excluded_instance_count;
1187 match (&scene.static_scene_world_aabb, available) {
1188 (Some(aabb), 1..) => {
1189 finite_aabb(aabb, &format!("scenes[{index}].static_scene_world_aabb"))?
1190 }
1191 (None, 0) => {}
1192 (Some(_), 0) => {
1193 return Err(invalid(
1194 format!("scenes[{index}].static_scene_world_aabb"),
1195 "a scene with no available instances cannot have an AABB",
1196 ));
1197 }
1198 (None, _) => {
1199 return Err(invalid(
1200 format!("scenes[{index}].static_scene_world_aabb"),
1201 "a scene with available instances requires an AABB",
1202 ));
1203 }
1204 }
1205 }
1206 if let Some(default_scene_index) = assets.default_scene_index
1207 && !scene_indices.contains(&default_scene_index)
1208 {
1209 return Err(invalid(
1210 "default_scene_index".into(),
1211 "default_scene_index must reference a declared scene",
1212 ));
1213 }
1214 validate_skeleton_measurements(assets, revision, &invalid)?;
1215 validate_material_resources(assets, revision, &invalid)?;
1216 Ok(())
1217}
1218
1219fn validate_linear_transform_fields(
1220 linear: &LinearTransformMeasurements,
1221 path: &str,
1222 revision: MeasurementRevision,
1223 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1224) -> Result<(), MeasurementContractError> {
1225 let numeric_fields_present = linear.axis_lengths.is_some()
1226 && linear.determinant.is_some()
1227 && linear.orientation.is_some();
1228 if linear.classification == LinearTransformClassification::NonFinite {
1229 if linear.axis_lengths.is_some()
1230 || linear.determinant.is_some()
1231 || linear.orientation.is_some()
1232 || linear.rotation_xyzw.is_some()
1233 || linear.uniform_scale.is_some()
1234 {
1235 return Err(invalid(
1236 path.into(),
1237 "a non_finite classification cannot carry numeric linear-transform facts",
1238 ));
1239 }
1240 return Ok(());
1241 }
1242 if !numeric_fields_present {
1243 return Err(invalid(
1244 path.into(),
1245 "a finite classification requires axis_lengths, determinant, and orientation",
1246 ));
1247 }
1248 let rotation_present = linear.rotation_xyzw.is_some();
1249 if revision == MeasurementRevision::V18 {
1250 let rotation_required =
1251 linear.classification == LinearTransformClassification::UnitOrthonormal;
1252 if rotation_present != rotation_required {
1253 return Err(invalid(
1254 path.into(),
1255 "rotation_xyzw must be present exactly for unit_orthonormal linear transforms",
1256 ));
1257 }
1258 if let Some(rotation) = linear.rotation_xyzw {
1259 for (component, value) in rotation.into_iter().enumerate() {
1260 if !value.is_finite() {
1261 return Err(MeasurementContractError::NonFiniteValue {
1262 path: format!("{path}.rotation_xyzw[{component}]"),
1263 });
1264 }
1265 }
1266 }
1267 } else if rotation_present {
1268 return Err(invalid(
1269 path.into(),
1270 "rotation_xyzw is exclusive to measurements-v18",
1271 ));
1272 }
1273 for (axis, value) in linear
1274 .axis_lengths
1275 .expect("presence checked")
1276 .into_iter()
1277 .enumerate()
1278 {
1279 if !value.is_finite() {
1280 return Err(MeasurementContractError::NonFiniteValue {
1281 path: format!("{path}.axis_lengths[{axis}]"),
1282 });
1283 }
1284 if value < 0.0 {
1285 return Err(invalid(
1286 format!("{path}.axis_lengths[{axis}]"),
1287 "axis lengths must be non-negative",
1288 ));
1289 }
1290 }
1291 if !linear.determinant.expect("presence checked").is_finite() {
1292 return Err(MeasurementContractError::NonFiniteValue {
1293 path: format!("{path}.determinant"),
1294 });
1295 }
1296 if let Some(scale) = linear.uniform_scale {
1297 if !scale.is_finite() {
1298 return Err(MeasurementContractError::NonFiniteValue {
1299 path: format!("{path}.uniform_scale"),
1300 });
1301 }
1302 if scale < 0.0 {
1303 return Err(invalid(
1304 format!("{path}.uniform_scale"),
1305 "uniform scale must be non-negative",
1306 ));
1307 }
1308 }
1309 Ok(())
1310}
1311
1312fn validate_skeleton_measurements(
1313 assets: &AssetMeasurements,
1314 revision: MeasurementRevision,
1315 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1316) -> Result<(), MeasurementContractError> {
1317 if assets.skeleton_source_coverage == SourceSkeletonCoverage::Unavailable {
1318 if !assets.skeleton_nodes.is_empty() || !assets.skins.is_empty() {
1319 return Err(invalid(
1320 "skeleton_source_coverage".into(),
1321 "unavailable skeleton source coverage requires empty skeleton_nodes and skins arrays",
1322 ));
1323 }
1324 return Ok(());
1325 }
1326
1327 let finite_matrix = |matrix: &[f32; 16], path: &str| {
1328 for (component, value) in matrix.iter().enumerate() {
1329 if !value.is_finite() {
1330 return Err(MeasurementContractError::NonFiniteValue {
1331 path: format!("{path}[{component}]"),
1332 });
1333 }
1334 }
1335 Ok(())
1336 };
1337 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1338 if node.node_index != offset {
1339 return Err(invalid(
1340 format!("skeleton_nodes[{offset}].node_index"),
1341 "node_index must be contiguous and match source order",
1342 ));
1343 }
1344 match &node.local_rest {
1345 SkeletonNodeLocalRestMeasurements::Trs {
1346 translation_parent_space_m,
1347 rotation_xyzw,
1348 scale,
1349 } => {
1350 for (field, values) in [
1351 (
1352 "translation_parent_space_m",
1353 translation_parent_space_m.as_slice(),
1354 ),
1355 ("rotation_xyzw", rotation_xyzw.as_slice()),
1356 ("scale", scale.as_slice()),
1357 ] {
1358 for (component, value) in values.iter().enumerate() {
1359 if !value.is_finite() {
1360 return Err(MeasurementContractError::NonFiniteValue {
1361 path: format!(
1362 "skeleton_nodes[{offset}].local_rest.{field}[{component}]"
1363 ),
1364 });
1365 }
1366 }
1367 }
1368 }
1369 SkeletonNodeLocalRestMeasurements::Matrix { matrix } => finite_matrix(
1370 matrix,
1371 &format!("skeleton_nodes[{offset}].local_rest.matrix"),
1372 )?,
1373 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => {}
1374 }
1375 let node_path = format!("skeleton_nodes[{offset}]");
1376 validate_linear_transform_fields(
1377 &node.rest_world_linear,
1378 &format!("{node_path}.rest_world_linear"),
1379 revision,
1380 invalid,
1381 )?;
1382 match (
1383 node.rest_world_matrix.as_ref(),
1384 node.rest_world_translation_m.as_ref(),
1385 node.rest_world_matrix_unavailable_reason,
1386 ) {
1387 (Some(matrix), Some(translation), None) => {
1388 finite_matrix(matrix, &format!("{node_path}.rest_world_matrix"))?;
1389 for (component, value) in translation.iter().enumerate() {
1390 if !value.is_finite() {
1391 return Err(MeasurementContractError::NonFiniteValue {
1392 path: format!("{node_path}.rest_world_translation_m[{component}]"),
1393 });
1394 }
1395 }
1396 let expected_translation = [matrix[12], matrix[13], matrix[14]];
1397 if *translation != expected_translation {
1398 return Err(invalid(
1399 format!("{node_path}.rest_world_translation_m"),
1400 "rest_world_translation_m must equal the rest-world matrix translation column",
1401 ));
1402 }
1403 let mut expected_linear = measure_linear_transform(Mat4::from_cols_array(matrix));
1404 if revision != MeasurementRevision::V18 {
1405 expected_linear.rotation_xyzw = None;
1406 }
1407 if node.rest_world_linear != expected_linear {
1408 return Err(invalid(
1409 format!("{node_path}.rest_world_linear"),
1410 "rest_world_linear must be derived from rest_world_matrix",
1411 ));
1412 }
1413 }
1414 (None, None, Some(_)) => {
1415 if node.rest_world_linear.classification != LinearTransformClassification::NonFinite
1416 {
1417 return Err(invalid(
1418 format!("{node_path}.rest_world_linear"),
1419 "an unavailable rest-world matrix requires a non_finite linear classification",
1420 ));
1421 }
1422 }
1423 (Some(_), Some(_), Some(_)) => {
1424 return Err(invalid(
1425 node_path,
1426 "an available rest_world_matrix cannot have an unavailable reason",
1427 ));
1428 }
1429 _ => {
1430 return Err(invalid(
1431 node_path,
1432 "rest-world matrix, translation, and unavailable reason fields are inconsistent",
1433 ));
1434 }
1435 }
1436 }
1437 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1438 if let Some(parent) = node.parent_node_index
1439 && parent >= assets.skeleton_nodes.len()
1440 {
1441 return Err(invalid(
1442 format!("skeleton_nodes[{offset}].parent_node_index"),
1443 "parent_node_index must reference a skeleton node",
1444 ));
1445 }
1446 let mut previous_scene = None;
1447 for (scene_offset, scene_index) in node.scene_root_indices.iter().enumerate() {
1448 if !assets
1449 .scenes
1450 .iter()
1451 .any(|scene| scene.scene_index == *scene_index)
1452 {
1453 return Err(invalid(
1454 format!("skeleton_nodes[{offset}].scene_root_indices[{scene_offset}]"),
1455 "scene_root_indices values must reference declared scenes",
1456 ));
1457 }
1458 if previous_scene.is_some_and(|previous| previous >= *scene_index) {
1459 return Err(invalid(
1460 format!("skeleton_nodes[{offset}].scene_root_indices[{scene_offset}]"),
1461 "scene_root_indices values must be strictly increasing and unique",
1462 ));
1463 }
1464 previous_scene = Some(*scene_index);
1465 }
1466 }
1467 let mut visits = vec![ParentVisit::Unvisited; assets.skeleton_nodes.len()];
1468 for start in 0..assets.skeleton_nodes.len() {
1469 if visits.get(start) != Some(&ParentVisit::Unvisited) {
1470 continue;
1471 }
1472 let mut path = Vec::new();
1473 let mut current = start;
1474 loop {
1475 match visits.get(current).copied().ok_or_else(|| {
1476 invalid(
1477 format!("skeleton_nodes[{current}].parent_node_index"),
1478 "parent_node_index must reference a skeleton node",
1479 )
1480 })? {
1481 ParentVisit::Done => break,
1482 ParentVisit::Visiting => {
1483 return Err(invalid(
1484 format!("skeleton_nodes[{current}].parent_node_index"),
1485 "source node parent graph must be acyclic",
1486 ));
1487 }
1488 ParentVisit::Unvisited => {
1489 *visits.get_mut(current).ok_or_else(|| {
1490 invalid(
1491 format!("skeleton_nodes[{current}].parent_node_index"),
1492 "parent_node_index must reference a skeleton node",
1493 )
1494 })? = ParentVisit::Visiting;
1495 path.push(current);
1496 match assets
1497 .skeleton_nodes
1498 .get(current)
1499 .ok_or_else(|| {
1500 invalid(
1501 format!("skeleton_nodes[{current}].parent_node_index"),
1502 "parent_node_index must reference a skeleton node",
1503 )
1504 })?
1505 .parent_node_index
1506 {
1507 Some(parent) => current = parent,
1508 None => break,
1509 }
1510 }
1511 }
1512 }
1513 for node_index in path {
1514 *visits.get_mut(node_index).ok_or_else(|| {
1515 invalid(
1516 format!("skeleton_nodes[{node_index}].parent_node_index"),
1517 "parent_node_index must reference a skeleton node",
1518 )
1519 })? = ParentVisit::Done;
1520 }
1521 }
1522
1523 for (offset, node) in assets.skeleton_nodes.iter().enumerate() {
1524 let local_rest_available = !matches!(
1525 node.local_rest,
1526 SkeletonNodeLocalRestMeasurements::Unavailable { .. }
1527 );
1528 let path = format!("skeleton_nodes[{offset}]");
1529 if !local_rest_available {
1530 if node.rest_world_matrix.is_some()
1531 || node.rest_world_matrix_unavailable_reason
1532 != Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteLocalRest)
1533 {
1534 return Err(invalid(
1535 path,
1536 "an unavailable local_rest requires a non_finite_local_rest rest-world result",
1537 ));
1538 }
1539 continue;
1540 }
1541
1542 let expected_unavailable_reason = if let Some(parent_index) = node.parent_node_index {
1543 let parent = assets.skeleton_nodes.get(parent_index).ok_or_else(|| {
1544 invalid(
1545 format!("skeleton_nodes[{offset}].parent_node_index"),
1546 "parent_node_index must reference a skeleton node",
1547 )
1548 })?;
1549 if parent.rest_world_matrix.is_none() {
1550 Some(SkeletonRestWorldMatrixUnavailableReason::ParentRestWorldUnavailable)
1551 } else {
1552 Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteWorldMatrix)
1553 }
1554 } else {
1555 None
1556 };
1557 match (
1558 node.rest_world_matrix.is_some(),
1559 expected_unavailable_reason,
1560 ) {
1561 (true, None | Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteWorldMatrix)) => {
1562 }
1563 (false, Some(expected))
1564 if node.rest_world_matrix_unavailable_reason == Some(expected) => {}
1565 _ => {
1566 return Err(invalid(
1567 path,
1568 "rest-world availability must agree with local rest and parent rest-world evidence",
1569 ));
1570 }
1571 }
1572 }
1573
1574 for (offset, skin) in assets.skins.iter().enumerate() {
1575 if skin.skin_index != offset {
1576 return Err(invalid(
1577 format!("skins[{offset}].skin_index"),
1578 "skin_index must be contiguous and match source order",
1579 ));
1580 }
1581 if let Some(root) = skin.skeleton_root_node_index
1582 && root >= assets.skeleton_nodes.len()
1583 {
1584 return Err(invalid(
1585 format!("skins[{offset}].skeleton_root_node_index"),
1586 "skeleton_root_node_index must reference a skeleton node",
1587 ));
1588 }
1589 for (joint_offset, joint) in skin.joints.iter().enumerate() {
1590 if joint.joint_index != joint_offset {
1591 return Err(invalid(
1592 format!("skins[{offset}].joints[{joint_offset}].joint_index"),
1593 "joint_index must be contiguous and match declared skin order",
1594 ));
1595 }
1596 if joint.node_index >= assets.skeleton_nodes.len() {
1597 return Err(invalid(
1598 format!("skins[{offset}].joints[{joint_offset}].node_index"),
1599 "joint node_index must reference a skeleton node",
1600 ));
1601 }
1602 }
1603 match skin.inverse_bind_accessor.status {
1604 SourceInverseBindAccessorStatus::Absent => {
1605 if skin.inverse_bind_accessor.declared_count.is_some()
1606 || !skin.inverse_bind_accessor.matrices.is_empty()
1607 {
1608 return Err(invalid(
1609 format!("skins[{offset}].inverse_bind_accessor"),
1610 "an absent inverse-bind declaration has no declared count or matrices",
1611 ));
1612 }
1613 }
1614 SourceInverseBindAccessorStatus::EmptyAccessor => {
1615 if skin.inverse_bind_accessor.declared_count != Some(0)
1616 || !skin.inverse_bind_accessor.matrices.is_empty()
1617 {
1618 return Err(invalid(
1619 format!("skins[{offset}].inverse_bind_accessor"),
1620 "an empty inverse-bind declaration has declared_count 0 and no matrices",
1621 ));
1622 }
1623 }
1624 SourceInverseBindAccessorStatus::Available => {
1625 if skin.inverse_bind_accessor.declared_count
1626 != Some(skin.inverse_bind_accessor.matrices.len())
1627 || skin.inverse_bind_accessor.matrices.len() < skin.joints.len()
1628 {
1629 return Err(invalid(
1630 format!("skins[{offset}].inverse_bind_accessor"),
1631 "an available inverse-bind declaration must retain its declared finite matrices and cover every joint",
1632 ));
1633 }
1634 }
1635 SourceInverseBindAccessorStatus::CountMismatch => {
1636 if skin.inverse_bind_accessor.declared_count
1637 != Some(skin.inverse_bind_accessor.matrices.len())
1638 || skin.inverse_bind_accessor.matrices.len() >= skin.joints.len()
1639 {
1640 return Err(invalid(
1641 format!("skins[{offset}].inverse_bind_accessor"),
1642 "a count-mismatched inverse-bind declaration retains fewer matrices than joints",
1643 ));
1644 }
1645 }
1646 SourceInverseBindAccessorStatus::Unreadable => {
1647 if skin.inverse_bind_accessor.declared_count.is_none()
1648 || !skin.inverse_bind_accessor.matrices.is_empty()
1649 {
1650 return Err(invalid(
1651 format!("skins[{offset}].inverse_bind_accessor"),
1652 "an unreadable inverse-bind declaration retains its count but cannot serialize matrices",
1653 ));
1654 }
1655 }
1656 }
1657 for (matrix_offset, matrix) in skin.inverse_bind_accessor.matrices.iter().enumerate() {
1658 finite_matrix(
1659 matrix,
1660 &format!("skins[{offset}].inverse_bind_accessor.matrices[{matrix_offset}]"),
1661 )?;
1662 }
1663 for (joint_offset, joint) in skin.joints.iter().enumerate() {
1664 let expected_source = skin.inverse_bind_accessor.matrices.get(joint_offset);
1665 let joint_bind_path =
1666 format!("skins[{offset}].joints[{joint_offset}].joint_bind_to_mesh");
1667 validate_derived_matrix(
1668 &joint.joint_bind_to_mesh,
1669 &joint_bind_path,
1670 revision,
1671 &finite_matrix,
1672 invalid,
1673 )?;
1674 validate_derived_reason_compatibility(
1675 &joint.joint_bind_to_mesh,
1676 skin.inverse_bind_accessor.status,
1677 skin.inverse_bind_accessor.matrices.len(),
1678 joint_offset,
1679 &joint_bind_path,
1680 DerivedMatrixDomain::JointBindToMesh,
1681 invalid,
1682 )?;
1683 validate_derived_source(
1684 &joint.joint_bind_to_mesh,
1685 expected_source,
1686 None,
1687 &joint_bind_path,
1688 DerivedMatrixDomain::JointBindToMesh,
1689 invalid,
1690 )?;
1691
1692 let mesh_bind_path = format!("skins[{offset}].joints[{joint_offset}].mesh_bind_world");
1693 validate_derived_matrix(
1694 &joint.mesh_bind_world,
1695 &mesh_bind_path,
1696 revision,
1697 &finite_matrix,
1698 invalid,
1699 )?;
1700 validate_derived_reason_compatibility(
1701 &joint.mesh_bind_world,
1702 skin.inverse_bind_accessor.status,
1703 skin.inverse_bind_accessor.matrices.len(),
1704 joint_offset,
1705 &mesh_bind_path,
1706 DerivedMatrixDomain::MeshBindWorld,
1707 invalid,
1708 )?;
1709 let joint_rest_world_available = assets
1710 .skeleton_nodes
1711 .get(joint.node_index)
1712 .ok_or_else(|| {
1713 invalid(
1714 format!("skins[{offset}].joints[{joint_offset}].node_index"),
1715 "joint node_index must reference a skeleton node",
1716 )
1717 })?
1718 .rest_world_matrix
1719 .is_some();
1720 let joint_rest_world = assets.skeleton_nodes[joint.node_index]
1721 .rest_world_matrix
1722 .as_ref();
1723 validate_mesh_bind_world_reason_compatibility(
1724 &joint.mesh_bind_world,
1725 joint_rest_world_available,
1726 &mesh_bind_path,
1727 invalid,
1728 )?;
1729 validate_derived_source(
1730 &joint.mesh_bind_world,
1731 expected_source,
1732 joint_rest_world,
1733 &mesh_bind_path,
1734 DerivedMatrixDomain::MeshBindWorld,
1735 invalid,
1736 )?;
1737 }
1738 if let Some(scale) = skin.joint_bind_linear_summary.consistent_uniform_scale
1739 && !scale.is_finite()
1740 {
1741 return Err(MeasurementContractError::NonFiniteValue {
1742 path: format!("skins[{offset}].joint_bind_linear_summary.consistent_uniform_scale"),
1743 });
1744 }
1745 let expected_summary = summarize_skin_bind_linear(&skin.joints);
1746 if skin.joint_bind_linear_summary != expected_summary {
1747 return Err(invalid(
1748 format!("skins[{offset}].joint_bind_linear_summary"),
1749 "joint-bind linear summary must match the skin joint observations",
1750 ));
1751 }
1752 let mut previous_attachment_node = None;
1753 for (attachment_offset, attachment) in skin.attachments.iter().enumerate() {
1754 if attachment.node_index >= assets.skeleton_nodes.len() {
1755 return Err(invalid(
1756 format!("skins[{offset}].attachments[{attachment_offset}].node_index"),
1757 "attachment node_index must reference a skeleton node",
1758 ));
1759 }
1760 if previous_attachment_node.is_some_and(|previous| previous >= attachment.node_index) {
1761 return Err(invalid(
1762 format!("skins[{offset}].attachments[{attachment_offset}].node_index"),
1763 "attachment node_index values must be strictly increasing and unique",
1764 ));
1765 }
1766 previous_attachment_node = Some(attachment.node_index);
1767 }
1768 }
1769 Ok(())
1770}
1771
1772fn validate_derived_reason_compatibility(
1773 matrix: &SkinDerivedMatrixMeasurements,
1774 status: SourceInverseBindAccessorStatus,
1775 readable_matrix_count: usize,
1776 joint_index: usize,
1777 path: &str,
1778 domain: DerivedMatrixDomain,
1779 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1780) -> Result<(), MeasurementContractError> {
1781 let requires_accessor_reason = match status {
1782 SourceInverseBindAccessorStatus::Absent => {
1783 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorAbsent)
1784 }
1785 SourceInverseBindAccessorStatus::EmptyAccessor => {
1786 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorEmpty)
1787 }
1788 SourceInverseBindAccessorStatus::Unreadable => {
1789 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorUnreadable)
1790 }
1791 SourceInverseBindAccessorStatus::CountMismatch if joint_index >= readable_matrix_count => {
1792 Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorCountMismatch)
1793 }
1794 SourceInverseBindAccessorStatus::Available
1795 | SourceInverseBindAccessorStatus::CountMismatch => None,
1796 };
1797 if let Some(expected) = requires_accessor_reason {
1798 if matrix.matrix.is_some() || matrix.unavailable_reason != Some(expected) {
1799 return Err(invalid(
1800 path.into(),
1801 "derived matrices without a usable inverse bind must carry the matching accessor reason",
1802 ));
1803 }
1804 } else {
1805 match (domain, matrix.unavailable_reason) {
1806 (
1807 _,
1808 Some(
1809 SkinDerivedMatrixUnavailableReason::InverseBindAccessorAbsent
1810 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorEmpty
1811 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorCountMismatch
1812 | SkinDerivedMatrixUnavailableReason::InverseBindAccessorUnreadable,
1813 ),
1814 ) => {
1815 return Err(invalid(
1816 format!("{path}.unavailable_reason"),
1817 "a usable inverse-bind matrix cannot be reported as accessor-unavailable",
1818 ));
1819 }
1820 (
1821 DerivedMatrixDomain::JointBindToMesh,
1822 Some(SkinDerivedMatrixUnavailableReason::JointRestWorldUnavailable),
1823 ) => {
1824 return Err(invalid(
1825 format!("{path}.unavailable_reason"),
1826 "joint_bind_to_mesh cannot use a joint-rest-world unavailable reason",
1827 ));
1828 }
1829 (
1830 DerivedMatrixDomain::MeshBindWorld,
1831 Some(
1832 SkinDerivedMatrixUnavailableReason::InverseBindMatrixNonInvertible
1833 | SkinDerivedMatrixUnavailableReason::InverseBindMatrixNonAffine
1834 | SkinDerivedMatrixUnavailableReason::InverseBindMatrixIllConditioned,
1835 ),
1836 ) => {
1837 return Err(invalid(
1838 format!("{path}.unavailable_reason"),
1839 "mesh_bind_world does not require an invertible inverse-bind matrix",
1840 ));
1841 }
1842 _ => {}
1843 }
1844 }
1845 Ok(())
1846}
1847
1848fn validate_mesh_bind_world_reason_compatibility(
1849 matrix: &SkinDerivedMatrixMeasurements,
1850 joint_rest_world_available: bool,
1851 path: &str,
1852 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1853) -> Result<(), MeasurementContractError> {
1854 match matrix.unavailable_reason {
1855 Some(SkinDerivedMatrixUnavailableReason::JointRestWorldUnavailable)
1856 if joint_rest_world_available =>
1857 {
1858 Err(invalid(
1859 format!("{path}.unavailable_reason"),
1860 "an available joint rest-world matrix cannot be reported as unavailable",
1861 ))
1862 }
1863 Some(SkinDerivedMatrixUnavailableReason::NonFiniteDerivedMatrix)
1864 if !joint_rest_world_available =>
1865 {
1866 Err(invalid(
1867 format!("{path}.unavailable_reason"),
1868 "a non-finite mesh-bind-world result requires an available joint rest-world matrix",
1869 ))
1870 }
1871 _ => Ok(()),
1872 }
1873}
1874
1875#[derive(Clone, Copy, PartialEq, Eq)]
1876enum ParentVisit {
1877 Unvisited,
1878 Visiting,
1879 Done,
1880}
1881
1882#[derive(Clone, Copy)]
1883enum DerivedMatrixDomain {
1884 JointBindToMesh,
1885 MeshBindWorld,
1886}
1887
1888fn validate_derived_source(
1889 measurements: &SkinDerivedMatrixMeasurements,
1890 expected_source: Option<&[f32; 16]>,
1891 joint_rest_world: Option<&[f32; 16]>,
1892 path: &str,
1893 domain: DerivedMatrixDomain,
1894 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1895) -> Result<(), MeasurementContractError> {
1896 if measurements.source_inverse_bind_matrix.as_ref() != expected_source {
1897 return Err(invalid(
1898 format!("{path}.source_inverse_bind_matrix"),
1899 "source_inverse_bind_matrix must equal the retained declaration slot exactly",
1900 ));
1901 }
1902 let Some(source) = expected_source else {
1903 if measurements.inversion_quality.is_some() {
1904 return Err(invalid(
1905 format!("{path}.inversion_quality"),
1906 "inversion quality requires a readable source inverse-bind matrix",
1907 ));
1908 }
1909 return Ok(());
1910 };
1911 let raw = Mat4::from_cols_array(source);
1912 match domain {
1913 DerivedMatrixDomain::JointBindToMesh => {
1914 let assessment = assess_inverse_bind(raw);
1915 if measurements.inversion_quality != assessment.quality {
1916 return Err(invalid(
1917 format!("{path}.inversion_quality"),
1918 "inversion quality must be derived from the source linear 3x3",
1919 ));
1920 }
1921 match assessment.inverse {
1922 Ok(inverse) => {
1923 if measurements.matrix != Some(inverse.to_cols_array())
1924 || measurements.unavailable_reason.is_some()
1925 {
1926 return Err(invalid(
1927 path.into(),
1928 "a trustworthy source inverse-bind matrix requires its exact inverse",
1929 ));
1930 }
1931 }
1932 Err(reason) => {
1933 if measurements.matrix.is_some()
1934 || measurements.unavailable_reason != Some(reason)
1935 {
1936 return Err(invalid(
1937 path.into(),
1938 "an untrustworthy source inverse-bind matrix requires its derived reason",
1939 ));
1940 }
1941 }
1942 }
1943 }
1944 DerivedMatrixDomain::MeshBindWorld => {
1945 if measurements.inversion_quality.is_some() {
1946 return Err(invalid(
1947 format!("{path}.inversion_quality"),
1948 "mesh_bind_world does not invert its source matrix",
1949 ));
1950 }
1951 if let Some(world) = joint_rest_world {
1952 let expected = Mat4::from_cols_array(world) * raw;
1953 if expected.to_cols_array().into_iter().all(f32::is_finite) {
1954 if measurements.matrix != Some(expected.to_cols_array())
1955 || measurements.unavailable_reason.is_some()
1956 {
1957 return Err(invalid(
1958 path.into(),
1959 "mesh_bind_world must equal joint_rest_world times the source inverse bind",
1960 ));
1961 }
1962 } else if measurements.unavailable_reason
1963 != Some(SkinDerivedMatrixUnavailableReason::NonFiniteDerivedMatrix)
1964 {
1965 return Err(invalid(
1966 format!("{path}.unavailable_reason"),
1967 "a non-finite mesh-bind product requires its typed unavailable reason",
1968 ));
1969 }
1970 }
1971 }
1972 }
1973 Ok(())
1974}
1975
1976fn validate_derived_matrix(
1977 matrix: &SkinDerivedMatrixMeasurements,
1978 path: &str,
1979 revision: MeasurementRevision,
1980 finite_matrix: &impl Fn(&[f32; 16], &str) -> Result<(), MeasurementContractError>,
1981 invalid: &impl Fn(String, &str) -> MeasurementContractError,
1982) -> Result<(), MeasurementContractError> {
1983 if let Some(source) = &matrix.source_inverse_bind_matrix {
1984 finite_matrix(source, &format!("{path}.source_inverse_bind_matrix"))?;
1985 }
1986 if let Some(quality) = matrix.inversion_quality {
1987 let value = quality.reciprocal_condition_number_inf;
1988 if !value.is_finite() || !(0.0..=1.0).contains(&value) {
1989 return Err(invalid(
1990 format!("{path}.inversion_quality.reciprocal_condition_number_inf"),
1991 "reciprocal condition number must be finite and between zero and one",
1992 ));
1993 }
1994 }
1995 match (
1996 &matrix.matrix,
1997 matrix.linear.as_ref(),
1998 matrix.unavailable_reason,
1999 ) {
2000 (Some(matrix), Some(linear), None) => {
2001 finite_matrix(matrix, &format!("{path}.matrix"))?;
2002 validate_linear_transform_fields(linear, &format!("{path}.linear"), revision, invalid)?;
2003 let mut expected_linear = measure_linear_transform(Mat4::from_cols_array(matrix));
2004 if revision != MeasurementRevision::V18 {
2005 expected_linear.rotation_xyzw = None;
2006 }
2007 if *linear != expected_linear {
2008 return Err(invalid(
2009 format!("{path}.linear"),
2010 "linear facts must be derived from the available matrix",
2011 ));
2012 }
2013 }
2014 (None, None, Some(_)) => {}
2015 (Some(_), Some(_), Some(_)) => {
2016 return Err(invalid(
2017 path.into(),
2018 "an available derived matrix cannot have an unavailable reason",
2019 ));
2020 }
2021 _ => {
2022 return Err(invalid(
2023 path.into(),
2024 "derived matrix, linear facts, and unavailable reason fields are inconsistent",
2025 ));
2026 }
2027 }
2028 Ok(())
2029}
2030
2031fn validate_material_resources(
2032 assets: &AssetMeasurements,
2033 revision: MeasurementRevision,
2034 invalid: &impl Fn(String, &str) -> MeasurementContractError,
2035) -> Result<(), MeasurementContractError> {
2036 let absent = assets.material_definitions.is_empty()
2037 && assets.textures.is_empty()
2038 && assets.images.is_empty();
2039 if assets.material_resource_coverage == MaterialResourceCoverage::Unavailable && !absent {
2040 return Err(invalid(
2041 "material_resource_coverage".into(),
2042 "unavailable resource coverage requires empty material, texture, and image arrays",
2043 ));
2044 }
2045
2046 for (offset, material) in assets.material_definitions.iter().enumerate() {
2047 if material.material_index != offset {
2048 return Err(invalid(
2049 format!("material_definitions[{offset}].material_index"),
2050 "material_index must be contiguous and match source order",
2051 ));
2052 }
2053 let mut previous_slot = None;
2054 for (binding_offset, binding) in material.texture_bindings.iter().enumerate() {
2055 if binding.texture_index >= assets.textures.len() {
2056 return Err(invalid(
2057 format!(
2058 "material_definitions[{offset}].texture_bindings[{binding_offset}].texture_index"
2059 ),
2060 "texture_index must reference a source texture",
2061 ));
2062 }
2063 if previous_slot.is_some_and(|previous| previous >= binding.slot) {
2064 return Err(invalid(
2065 format!(
2066 "material_definitions[{offset}].texture_bindings[{binding_offset}].slot"
2067 ),
2068 "texture bindings must be strictly ordered by slot and unique",
2069 ));
2070 }
2071 previous_slot = Some(binding.slot);
2072 }
2073 }
2074 for (offset, texture) in assets.textures.iter().enumerate() {
2075 if texture.texture_index != offset {
2076 return Err(invalid(
2077 format!("textures[{offset}].texture_index"),
2078 "texture_index must be contiguous and match source order",
2079 ));
2080 }
2081 if texture.image_index >= assets.images.len() {
2082 return Err(invalid(
2083 format!("textures[{offset}].image_index"),
2084 "image_index must reference a source image",
2085 ));
2086 }
2087 }
2088 for (offset, image) in assets.images.iter().enumerate() {
2089 validate_image_measurement(image, offset, revision, invalid)?;
2090 }
2091 Ok(())
2092}
2093
2094fn validate_image_measurement(
2095 image: &ImageMeasurements,
2096 offset: usize,
2097 revision: MeasurementRevision,
2098 invalid: &impl Fn(String, &str) -> MeasurementContractError,
2099) -> Result<(), MeasurementContractError> {
2100 if image.image_index != offset {
2101 return Err(invalid(
2102 format!("images[{offset}].image_index"),
2103 "image_index must be contiguous and match source order",
2104 ));
2105 }
2106 let available = [
2107 image.width.is_some(),
2108 image.height.is_some(),
2109 image.channel_count.is_some(),
2110 image.decoded_color_type.is_some(),
2111 ];
2112 match (
2113 available.into_iter().all(|value| value),
2114 image.unavailable_reason,
2115 ) {
2116 (true, None) => {
2117 let (Some(width), Some(height), Some(channel_count), Some(decoded_color_type)) = (
2118 image.width,
2119 image.height,
2120 image.channel_count,
2121 image.decoded_color_type,
2122 ) else {
2123 return Err(invalid(
2124 format!("images[{offset}]"),
2125 "available image metadata must include width, height, channel_count, and decoded_color_type",
2126 ));
2127 };
2128 if width == 0 || height == 0 {
2129 return Err(invalid(
2130 format!("images[{offset}]"),
2131 "available image dimensions must be greater than zero",
2132 ));
2133 }
2134 if channel_count != color_type_channel_count(decoded_color_type) {
2135 return Err(invalid(
2136 format!("images[{offset}].channel_count"),
2137 "channel_count must match decoded_color_type",
2138 ));
2139 }
2140 if image.detected_container.is_none() {
2141 return Err(invalid(
2142 format!("images[{offset}].detected_container"),
2143 "available image metadata requires a detected_container",
2144 ));
2145 }
2146 }
2147 (false, Some(_)) if available.into_iter().all(|value| !value) => {}
2148 (true, Some(_)) => {
2149 return Err(invalid(
2150 format!("images[{offset}]"),
2151 "available image metadata cannot have an unavailable_reason",
2152 ));
2153 }
2154 (false, None) if available.into_iter().all(|value| !value) => {
2155 return Err(invalid(
2156 format!("images[{offset}]"),
2157 "missing image metadata requires an unavailable_reason",
2158 ));
2159 }
2160 (false, _) => {
2161 return Err(invalid(
2162 format!("images[{offset}]"),
2163 "available image metadata must include width, height, channel_count, and decoded_color_type",
2164 ));
2165 }
2166 }
2167 match image.unavailable_reason {
2168 Some(crate::model::ImageUnavailableReason::DecodeFailed)
2169 if image.detected_container.is_none() =>
2170 {
2171 return Err(invalid(
2172 format!("images[{offset}].detected_container"),
2173 "decode_failed requires a detected_container",
2174 ));
2175 }
2176 Some(
2177 crate::model::ImageUnavailableReason::SourceUnavailable
2178 | crate::model::ImageUnavailableReason::InvalidDataUri
2179 | crate::model::ImageUnavailableReason::UnsupportedContainer,
2180 ) if image.detected_container.is_some() => {
2181 return Err(invalid(
2182 format!("images[{offset}].detected_container"),
2183 "this unavailable_reason cannot have a detected_container",
2184 ));
2185 }
2186 _ => {}
2187 }
2188 match (revision, image.unavailable_reason, &image.leading_magic_hex) {
2189 (MeasurementRevision::V15, _, Some(_)) => {
2190 return Err(invalid(
2191 format!("images[{offset}].leading_magic_hex"),
2192 "measurements-v15 cannot carry leading-magic evidence",
2193 ));
2194 }
2195 (
2196 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
2197 Some(crate::model::ImageUnavailableReason::UnsupportedContainer),
2198 Some(magic),
2199 ) => {
2200 if magic.is_empty()
2201 || magic.len() > 32
2202 || !magic.len().is_multiple_of(2)
2203 || !magic
2204 .bytes()
2205 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
2206 {
2207 return Err(invalid(
2208 format!("images[{offset}].leading_magic_hex"),
2209 "leading_magic_hex must be nonempty lowercase even-length hex for at most 16 bytes",
2210 ));
2211 }
2212 }
2213 (
2214 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
2215 Some(crate::model::ImageUnavailableReason::UnsupportedContainer),
2216 None,
2217 )
2218 | (MeasurementRevision::V15, _, None) => {}
2219 (
2220 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
2221 _,
2222 Some(_),
2223 ) => {
2224 return Err(invalid(
2225 format!("images[{offset}].leading_magic_hex"),
2226 "leading_magic_hex is permitted only for unsupported_container",
2227 ));
2228 }
2229 (
2230 MeasurementRevision::V16 | MeasurementRevision::V17 | MeasurementRevision::V18,
2231 _,
2232 None,
2233 ) => {}
2234 }
2235 Ok(())
2236}
2237
2238fn color_type_channel_count(color_type: DecodedImageColorType) -> u8 {
2239 match color_type {
2240 DecodedImageColorType::L8 | DecodedImageColorType::L16 => 1,
2241 DecodedImageColorType::La8 | DecodedImageColorType::La16 => 2,
2242 DecodedImageColorType::Rgb8 | DecodedImageColorType::Rgb16 => 3,
2243 DecodedImageColorType::Rgba8 | DecodedImageColorType::Rgba16 => 4,
2244 }
2245}
2246
2247#[derive(Debug)]
2255pub struct MeasurementReportInput {
2256 schema_version: Option<u32>,
2257 schema: Option<String>,
2258 _tool: Option<Box<RawValue>>,
2259 command: Option<String>,
2260 summary: Option<MeasurementReportSummaryInput>,
2261 files: Option<Vec<Box<RawValue>>>,
2262 _inputs: Option<Box<RawValue>>,
2263 _deltas: Option<Box<RawValue>>,
2264 extra: BTreeMap<String, Box<RawValue>>,
2265}
2266
2267impl<'de> Deserialize<'de> for MeasurementReportInput {
2268 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2269 where
2270 D: Deserializer<'de>,
2271 {
2272 struct MeasurementReportInputVisitor;
2273
2274 impl<'de> Visitor<'de> for MeasurementReportInputVisitor {
2275 type Value = MeasurementReportInput;
2276
2277 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2278 formatter.write_str("an output report object")
2279 }
2280
2281 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2282 where
2283 A: MapAccess<'de>,
2284 {
2285 let mut schema_version = None;
2286 let mut schema = None;
2287 let mut tool = None;
2288 let mut command = None;
2289 let mut summary = None;
2290 let mut files = None;
2291 let mut inputs = None;
2292 let mut deltas = None;
2293 let mut extra = BTreeMap::new();
2294 while let Some(field) = map.next_key::<String>()? {
2295 match field.as_str() {
2296 "schema_version" => {
2297 if schema_version.is_some() {
2298 return Err(serde::de::Error::duplicate_field("schema_version"));
2299 }
2300 schema_version = Some(map.next_value()?);
2301 }
2302 "schema" => {
2303 if schema.is_some() {
2304 return Err(serde::de::Error::duplicate_field("schema"));
2305 }
2306 schema = Some(map.next_value()?);
2307 }
2308 "tool" => {
2309 if tool.is_some() {
2310 return Err(serde::de::Error::duplicate_field("tool"));
2311 }
2312 tool = Some(map.next_value()?);
2313 }
2314 "command" => {
2315 if command.is_some() {
2316 return Err(serde::de::Error::duplicate_field("command"));
2317 }
2318 command = Some(map.next_value()?);
2319 }
2320 "summary" => {
2321 if summary.is_some() {
2322 return Err(serde::de::Error::duplicate_field("summary"));
2323 }
2324 summary = Some(map.next_value()?);
2325 }
2326 "files" => {
2327 if files.is_some() {
2328 return Err(serde::de::Error::duplicate_field("files"));
2329 }
2330 files = Some(map.next_value()?);
2331 }
2332 "inputs" => {
2333 if inputs.is_some() {
2334 return Err(serde::de::Error::duplicate_field("inputs"));
2335 }
2336 inputs = Some(map.next_value()?);
2337 }
2338 "deltas" => {
2339 if deltas.is_some() {
2340 return Err(serde::de::Error::duplicate_field("deltas"));
2341 }
2342 deltas = Some(map.next_value()?);
2343 }
2344 _ => {
2345 extra.insert(field, map.next_value()?);
2346 }
2347 }
2348 }
2349 Ok(MeasurementReportInput {
2350 schema_version: schema_version.unwrap_or_default(),
2351 schema: schema.unwrap_or_default(),
2352 _tool: tool,
2353 command: command.unwrap_or_default(),
2354 summary: summary.unwrap_or_default(),
2355 files: files.unwrap_or_default(),
2356 _inputs: inputs.unwrap_or_default(),
2357 _deltas: deltas.unwrap_or_default(),
2358 extra,
2359 })
2360 }
2361 }
2362
2363 deserializer.deserialize_map(MeasurementReportInputVisitor)
2364 }
2365}
2366
2367#[derive(Debug, Deserialize)]
2368#[serde(deny_unknown_fields)]
2369struct MeasurementFileWireInput {
2370 path: Option<String>,
2371 input: Option<InputIdentityInput>,
2372 rig: Box<RawValue>,
2373 measurements: Option<Box<RawValue>>,
2374 #[serde(default, deserialize_with = "deserialize_required_nullable")]
2375 prediction_provenance: RequiredNullable<Box<RawValue>>,
2376 checks: Option<Vec<Box<RawValue>>>,
2377}
2378
2379#[derive(Debug)]
2380struct MeasurementFileInput {
2381 path: Option<String>,
2382 input: Option<InputIdentityInput>,
2383 rig_v17: Option<RigInfo>,
2387 measurements: Option<Box<RawValue>>,
2388 prediction_provenance: RequiredNullable<PredictionProvenanceV2>,
2389 checks: Option<Vec<PredictionCheckInput>>,
2390 legacy_prediction_provenance: RequiredNullable<PredictionProvenanceV1>,
2391 legacy_checks: Option<Vec<LegacyPredictionCheckInput>>,
2392 prediction_provenance_v3: RequiredNullable<PredictionProvenanceV3>,
2393 checks_v3: Option<Vec<PredictionCheckInputV3>>,
2394 prediction_provenance_v4: RequiredNullable<PredictionProvenanceV4>,
2395 checks_v4: Option<Vec<PredictionCheckInputV4>>,
2396 prediction_provenance_v5: RequiredNullable<PredictionProvenanceV5>,
2397 checks_v5: Option<Vec<PredictionCheckInputV5>>,
2398 prediction_provenance_v6: RequiredNullable<PredictionProvenanceV6>,
2399 checks_v6: Option<Vec<PredictionCheckInputV6>>,
2400}
2401
2402#[derive(Debug, Deserialize)]
2403#[serde(deny_unknown_fields)]
2404struct LegacyPredictionCheckWireV11 {
2405 check_id: String,
2406 selection: SelectionState,
2407 configuration: ConfigurationState,
2408 applicability: Applicability,
2409 evaluation: EvaluationState,
2410 findings: Vec<PredictionFindingInput>,
2411 #[serde(default)]
2412 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2413 #[serde(default)]
2414 gaps: Vec<PredictionGapInput>,
2415 prediction: Option<Box<RawValue>>,
2416}
2417
2418#[derive(Debug)]
2422struct LegacyPredictionCheckInput {
2423 check_id: String,
2424 selection: SelectionState,
2425 configuration: ConfigurationState,
2426 applicability: Applicability,
2427 evaluation: EvaluationState,
2428 findings: Vec<PredictionFindingInput>,
2429 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2430 gaps: Vec<PredictionGapInput>,
2431 prediction: Option<EnginePredictionV1>,
2432}
2433
2434#[derive(Debug, Default)]
2435enum RequiredNullable<T> {
2436 #[default]
2437 Missing,
2438 Present(Option<T>),
2439}
2440
2441impl<T> RequiredNullable<T> {
2442 fn as_present(&self) -> Option<&T> {
2443 match self {
2444 Self::Missing | Self::Present(None) => None,
2445 Self::Present(Some(value)) => Some(value),
2446 }
2447 }
2448}
2449
2450fn deserialize_required_nullable<'de, D, T>(
2451 deserializer: D,
2452) -> Result<RequiredNullable<T>, D::Error>
2453where
2454 D: Deserializer<'de>,
2455 T: Deserialize<'de>,
2456{
2457 Option::<T>::deserialize(deserializer).map(RequiredNullable::Present)
2458}
2459
2460#[derive(Debug, Deserialize)]
2461#[serde(deny_unknown_fields)]
2462struct MeasurementReportSummaryInput {
2463 #[serde(rename = "files")]
2464 _files: Option<Box<RawValue>>,
2465 #[serde(rename = "findings")]
2466 _findings: Option<Box<RawValue>>,
2467 #[serde(rename = "checks")]
2468 _checks: Option<Box<RawValue>>,
2469 #[serde(rename = "deltas")]
2470 _deltas: Option<Box<RawValue>>,
2471 prediction_facets: Option<PredictionFacetSummaryInput>,
2472}
2473
2474#[derive(Debug, Deserialize)]
2475#[serde(deny_unknown_fields)]
2476struct PredictionFacetSummaryInput {
2477 available: usize,
2478 required_prediction_unavailable: usize,
2479}
2480
2481#[derive(Debug, Deserialize)]
2482#[serde(deny_unknown_fields)]
2483struct PredictionCheckWireInput {
2484 check_id: String,
2485 selection: SelectionState,
2486 configuration: ConfigurationState,
2487 applicability: Applicability,
2488 evaluation: EvaluationState,
2489 findings: Vec<PredictionFindingInput>,
2490 #[serde(default)]
2491 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2492 #[serde(default)]
2493 gaps: Vec<PredictionGapInput>,
2494 prediction: Option<Box<RawValue>>,
2495}
2496
2497#[derive(Debug)]
2498struct PredictionCheckInput {
2499 check_id: String,
2500 selection: SelectionState,
2501 configuration: ConfigurationState,
2502 applicability: Applicability,
2503 evaluation: EvaluationState,
2504 findings: Vec<PredictionFindingInput>,
2505 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2506 gaps: Vec<PredictionGapInput>,
2507 prediction: Option<EnginePredictionV2>,
2508}
2509
2510#[derive(Debug)]
2511struct PredictionCheckInputV3 {
2512 check_id: String,
2513 selection: SelectionState,
2514 configuration: ConfigurationState,
2515 applicability: Applicability,
2516 evaluation: EvaluationState,
2517 findings: Vec<PredictionFindingInput>,
2518 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2519 gaps: Vec<PredictionGapInput>,
2520 prediction: Option<EnginePredictionV3>,
2521}
2522
2523#[derive(Debug)]
2524struct PredictionCheckInputV4 {
2525 check_id: String,
2526 selection: SelectionState,
2527 configuration: ConfigurationState,
2528 applicability: Applicability,
2529 evaluation: EvaluationState,
2530 findings: Vec<PredictionFindingInput>,
2531 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2532 gaps: Vec<PredictionGapInput>,
2533 prediction: Option<EnginePredictionV4>,
2534}
2535
2536#[derive(Debug)]
2537struct PredictionCheckInputV5 {
2538 check_id: String,
2539 selection: SelectionState,
2540 configuration: ConfigurationState,
2541 applicability: Applicability,
2542 evaluation: EvaluationState,
2543 findings: Vec<PredictionFindingInput>,
2544 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2545 gaps: Vec<PredictionGapInput>,
2546 prediction: Option<EnginePredictionV5>,
2547}
2548
2549#[derive(Debug)]
2550struct PredictionCheckInputV6 {
2551 check_id: String,
2552 selection: SelectionState,
2553 configuration: ConfigurationState,
2554 applicability: Applicability,
2555 evaluation: EvaluationState,
2556 findings: Vec<PredictionFindingInput>,
2557 evaluated_scopes: Vec<crate::evaluation::EvaluationScope>,
2558 gaps: Vec<PredictionGapInput>,
2559 prediction: Option<EnginePredictionV6>,
2560}
2561
2562#[derive(Debug, Serialize, Deserialize)]
2563#[serde(deny_unknown_fields)]
2564struct PredictionFindingInput {
2565 check_id: String,
2566 #[serde(rename = "severity")]
2567 _severity: PredictionSeverityInput,
2568 #[serde(rename = "clip", skip_serializing_if = "Option::is_none")]
2569 _clip: Option<String>,
2570 #[serde(rename = "bone", skip_serializing_if = "Option::is_none")]
2571 _bone: Option<String>,
2572 #[serde(rename = "node", skip_serializing_if = "Option::is_none")]
2573 _node: Option<String>,
2574 #[serde(skip_serializing_if = "Option::is_none")]
2575 prediction_scope: Option<crate::evaluation::EvaluationScope>,
2576 #[serde(rename = "time_s", skip_serializing_if = "Option::is_none")]
2577 _time_s: Option<f32>,
2578 #[serde(rename = "measured", skip_serializing_if = "Option::is_none")]
2579 _measured: Option<Box<RawValue>>,
2580 #[serde(rename = "expected", skip_serializing_if = "Option::is_none")]
2581 _expected: Option<Box<RawValue>>,
2582 #[serde(rename = "members", skip_serializing_if = "Option::is_none")]
2583 _members: Option<Box<RawValue>>,
2584 #[serde(rename = "message")]
2585 _message: String,
2586}
2587
2588#[derive(Debug, Deserialize)]
2589#[serde(deny_unknown_fields)]
2590struct PredictionGapInput {
2591 code: String,
2592 #[serde(rename = "message")]
2593 _message: String,
2594 scope: Option<crate::evaluation::EvaluationScope>,
2595}
2596
2597#[derive(Debug, Serialize, Deserialize)]
2598#[serde(rename_all = "snake_case")]
2599enum PredictionSeverityInput {
2600 Error,
2601 Warning,
2602 Note,
2603}
2604
2605#[derive(Debug, Deserialize)]
2606#[serde(deny_unknown_fields)]
2607struct InputIdentityInput {
2608 sha256: Option<String>,
2609 bytes: Option<u64>,
2610}
2611
2612struct MeasurementF32NarrowingDeserializer<D>(D);
2622
2623macro_rules! delegate_measurement_deserializer {
2624 ($method:ident $(, $argument:ident: $argument_type:ty)*) => {
2625 fn $method<V>(
2626 self,
2627 $($argument: $argument_type,)*
2628 visitor: V,
2629 ) -> Result<V::Value, Self::Error>
2630 where
2631 V: Visitor<'de>,
2632 {
2633 self.0.$method(
2634 $($argument,)*
2635 MeasurementF32NarrowingVisitor(visitor),
2636 )
2637 }
2638 };
2639}
2640
2641impl<'de, D> Deserializer<'de> for MeasurementF32NarrowingDeserializer<D>
2642where
2643 D: Deserializer<'de>,
2644{
2645 type Error = D::Error;
2646
2647 delegate_measurement_deserializer!(deserialize_any);
2648 delegate_measurement_deserializer!(deserialize_bool);
2649 delegate_measurement_deserializer!(deserialize_i8);
2650 delegate_measurement_deserializer!(deserialize_i16);
2651 delegate_measurement_deserializer!(deserialize_i32);
2652 delegate_measurement_deserializer!(deserialize_i64);
2653 delegate_measurement_deserializer!(deserialize_i128);
2654 delegate_measurement_deserializer!(deserialize_u8);
2655 delegate_measurement_deserializer!(deserialize_u16);
2656 delegate_measurement_deserializer!(deserialize_u32);
2657 delegate_measurement_deserializer!(deserialize_u64);
2658 delegate_measurement_deserializer!(deserialize_u128);
2659
2660 fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
2661 where
2662 V: Visitor<'de>,
2663 {
2664 self.0
2665 .deserialize_f64(MeasurementF32NarrowingNumberVisitor(visitor))
2666 }
2667
2668 delegate_measurement_deserializer!(deserialize_f64);
2669 delegate_measurement_deserializer!(deserialize_char);
2670 delegate_measurement_deserializer!(deserialize_str);
2671 delegate_measurement_deserializer!(deserialize_string);
2672 delegate_measurement_deserializer!(deserialize_bytes);
2673 delegate_measurement_deserializer!(deserialize_byte_buf);
2674 delegate_measurement_deserializer!(deserialize_option);
2675 delegate_measurement_deserializer!(deserialize_unit);
2676 delegate_measurement_deserializer!(deserialize_unit_struct, name: &'static str);
2677 delegate_measurement_deserializer!(deserialize_newtype_struct, name: &'static str);
2678 delegate_measurement_deserializer!(deserialize_seq);
2679 delegate_measurement_deserializer!(deserialize_tuple, len: usize);
2680 delegate_measurement_deserializer!(
2681 deserialize_tuple_struct,
2682 name: &'static str,
2683 len: usize
2684 );
2685 delegate_measurement_deserializer!(deserialize_map);
2686 delegate_measurement_deserializer!(
2687 deserialize_struct,
2688 name: &'static str,
2689 fields: &'static [&'static str]
2690 );
2691 delegate_measurement_deserializer!(
2692 deserialize_enum,
2693 name: &'static str,
2694 variants: &'static [&'static str]
2695 );
2696 delegate_measurement_deserializer!(deserialize_identifier);
2697 delegate_measurement_deserializer!(deserialize_ignored_any);
2698
2699 fn is_human_readable(&self) -> bool {
2700 self.0.is_human_readable()
2701 }
2702}
2703
2704struct MeasurementF32NarrowingNumberVisitor<V>(V);
2705
2706impl<'de, V> Visitor<'de> for MeasurementF32NarrowingNumberVisitor<V>
2707where
2708 V: Visitor<'de>,
2709{
2710 type Value = V::Value;
2711
2712 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2713 self.0.expecting(formatter)
2714 }
2715
2716 fn visit_f32<E>(self, value: f32) -> Result<Self::Value, E>
2717 where
2718 E: serde::de::Error,
2719 {
2720 self.0.visit_f32(value)
2721 }
2722
2723 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
2724 where
2725 E: serde::de::Error,
2726 {
2727 self.0.visit_f32(value as f32)
2728 }
2729
2730 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
2731 where
2732 E: serde::de::Error,
2733 {
2734 self.0.visit_f32(value as f32)
2735 }
2736
2737 fn visit_i128<E>(self, value: i128) -> Result<Self::Value, E>
2738 where
2739 E: serde::de::Error,
2740 {
2741 self.0.visit_f32(value as f32)
2742 }
2743
2744 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
2745 where
2746 E: serde::de::Error,
2747 {
2748 self.0.visit_f32(value as f32)
2749 }
2750
2751 fn visit_u128<E>(self, value: u128) -> Result<Self::Value, E>
2752 where
2753 E: serde::de::Error,
2754 {
2755 self.0.visit_f32(value as f32)
2756 }
2757}
2758
2759struct MeasurementF32NarrowingVisitor<V>(V);
2760
2761macro_rules! delegate_measurement_visitor {
2762 ($method:ident, $value_type:ty) => {
2763 fn $method<E>(self, value: $value_type) -> Result<Self::Value, E>
2764 where
2765 E: serde::de::Error,
2766 {
2767 self.0.$method(value)
2768 }
2769 };
2770}
2771
2772impl<'de, V> Visitor<'de> for MeasurementF32NarrowingVisitor<V>
2773where
2774 V: Visitor<'de>,
2775{
2776 type Value = V::Value;
2777
2778 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2779 self.0.expecting(formatter)
2780 }
2781
2782 delegate_measurement_visitor!(visit_bool, bool);
2783 delegate_measurement_visitor!(visit_i8, i8);
2784 delegate_measurement_visitor!(visit_i16, i16);
2785 delegate_measurement_visitor!(visit_i32, i32);
2786 delegate_measurement_visitor!(visit_i64, i64);
2787 delegate_measurement_visitor!(visit_i128, i128);
2788 delegate_measurement_visitor!(visit_u8, u8);
2789 delegate_measurement_visitor!(visit_u16, u16);
2790 delegate_measurement_visitor!(visit_u32, u32);
2791 delegate_measurement_visitor!(visit_u64, u64);
2792 delegate_measurement_visitor!(visit_u128, u128);
2793 delegate_measurement_visitor!(visit_f32, f32);
2794 delegate_measurement_visitor!(visit_f64, f64);
2795 delegate_measurement_visitor!(visit_char, char);
2796
2797 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
2798 where
2799 E: serde::de::Error,
2800 {
2801 self.0.visit_str(value)
2802 }
2803
2804 fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
2805 where
2806 E: serde::de::Error,
2807 {
2808 self.0.visit_borrowed_str(value)
2809 }
2810
2811 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
2812 where
2813 E: serde::de::Error,
2814 {
2815 self.0.visit_string(value)
2816 }
2817
2818 fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
2819 where
2820 E: serde::de::Error,
2821 {
2822 self.0.visit_bytes(value)
2823 }
2824
2825 fn visit_borrowed_bytes<E>(self, value: &'de [u8]) -> Result<Self::Value, E>
2826 where
2827 E: serde::de::Error,
2828 {
2829 self.0.visit_borrowed_bytes(value)
2830 }
2831
2832 fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
2833 where
2834 E: serde::de::Error,
2835 {
2836 self.0.visit_byte_buf(value)
2837 }
2838
2839 fn visit_none<E>(self) -> Result<Self::Value, E>
2840 where
2841 E: serde::de::Error,
2842 {
2843 self.0.visit_none()
2844 }
2845
2846 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2847 where
2848 D: Deserializer<'de>,
2849 {
2850 self.0
2851 .visit_some(MeasurementF32NarrowingDeserializer(deserializer))
2852 }
2853
2854 fn visit_unit<E>(self) -> Result<Self::Value, E>
2855 where
2856 E: serde::de::Error,
2857 {
2858 self.0.visit_unit()
2859 }
2860
2861 fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2862 where
2863 D: Deserializer<'de>,
2864 {
2865 self.0
2866 .visit_newtype_struct(MeasurementF32NarrowingDeserializer(deserializer))
2867 }
2868
2869 fn visit_seq<A>(self, sequence: A) -> Result<Self::Value, A::Error>
2870 where
2871 A: SeqAccess<'de>,
2872 {
2873 self.0.visit_seq(MeasurementF32NarrowingSeqAccess(sequence))
2874 }
2875
2876 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
2877 where
2878 A: MapAccess<'de>,
2879 {
2880 self.0.visit_map(MeasurementF32NarrowingMapAccess(map))
2881 }
2882
2883 fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
2884 where
2885 A: EnumAccess<'de>,
2886 {
2887 self.0.visit_enum(MeasurementF32NarrowingEnumAccess(data))
2888 }
2889}
2890
2891struct MeasurementF32NarrowingSeed<S>(S);
2892
2893impl<'de, S> DeserializeSeed<'de> for MeasurementF32NarrowingSeed<S>
2894where
2895 S: DeserializeSeed<'de>,
2896{
2897 type Value = S::Value;
2898
2899 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
2900 where
2901 D: Deserializer<'de>,
2902 {
2903 self.0
2904 .deserialize(MeasurementF32NarrowingDeserializer(deserializer))
2905 }
2906}
2907
2908struct MeasurementF32NarrowingSeqAccess<A>(A);
2909
2910impl<'de, A> SeqAccess<'de> for MeasurementF32NarrowingSeqAccess<A>
2911where
2912 A: SeqAccess<'de>,
2913{
2914 type Error = A::Error;
2915
2916 fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
2917 where
2918 T: DeserializeSeed<'de>,
2919 {
2920 self.0.next_element_seed(MeasurementF32NarrowingSeed(seed))
2921 }
2922
2923 fn size_hint(&self) -> Option<usize> {
2924 self.0.size_hint()
2925 }
2926}
2927
2928struct MeasurementF32NarrowingMapAccess<A>(A);
2929
2930impl<'de, A> MapAccess<'de> for MeasurementF32NarrowingMapAccess<A>
2931where
2932 A: MapAccess<'de>,
2933{
2934 type Error = A::Error;
2935
2936 fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
2937 where
2938 K: DeserializeSeed<'de>,
2939 {
2940 self.0.next_key_seed(MeasurementF32NarrowingSeed(seed))
2941 }
2942
2943 fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
2944 where
2945 V: DeserializeSeed<'de>,
2946 {
2947 self.0.next_value_seed(MeasurementF32NarrowingSeed(seed))
2948 }
2949
2950 fn size_hint(&self) -> Option<usize> {
2951 self.0.size_hint()
2952 }
2953}
2954
2955struct MeasurementF32NarrowingEnumAccess<A>(A);
2956
2957impl<'de, A> EnumAccess<'de> for MeasurementF32NarrowingEnumAccess<A>
2958where
2959 A: EnumAccess<'de>,
2960{
2961 type Error = A::Error;
2962 type Variant = MeasurementF32NarrowingVariantAccess<A::Variant>;
2963
2964 fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error>
2965 where
2966 V: DeserializeSeed<'de>,
2967 {
2968 let (value, variant) = self.0.variant_seed(MeasurementF32NarrowingSeed(seed))?;
2969 Ok((value, MeasurementF32NarrowingVariantAccess(variant)))
2970 }
2971}
2972
2973struct MeasurementF32NarrowingVariantAccess<A>(A);
2974
2975impl<'de, A> VariantAccess<'de> for MeasurementF32NarrowingVariantAccess<A>
2976where
2977 A: VariantAccess<'de>,
2978{
2979 type Error = A::Error;
2980
2981 fn unit_variant(self) -> Result<(), Self::Error> {
2982 self.0.unit_variant()
2983 }
2984
2985 fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value, Self::Error>
2986 where
2987 T: DeserializeSeed<'de>,
2988 {
2989 self.0
2990 .newtype_variant_seed(MeasurementF32NarrowingSeed(seed))
2991 }
2992
2993 fn tuple_variant<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
2994 where
2995 V: Visitor<'de>,
2996 {
2997 self.0
2998 .tuple_variant(len, MeasurementF32NarrowingVisitor(visitor))
2999 }
3000
3001 fn struct_variant<V>(
3002 self,
3003 fields: &'static [&'static str],
3004 visitor: V,
3005 ) -> Result<V::Value, Self::Error>
3006 where
3007 V: Visitor<'de>,
3008 {
3009 self.0
3010 .struct_variant(fields, MeasurementF32NarrowingVisitor(visitor))
3011 }
3012}
3013
3014#[derive(Debug, Deserialize)]
3015#[serde(untagged)]
3016enum SkeletonNodeMeasurementInput {
3017 Current(Box<crate::measure::SkeletonNodeMeasurements>),
3018 Earlier {
3019 #[serde(rename = "node_index")]
3020 _node_index: usize,
3021 },
3022}
3023
3024#[derive(Debug, Deserialize)]
3025#[serde(untagged)]
3026enum SkinMeasurementInput {
3027 Current(Box<crate::measure::SkinMeasurements>),
3028 Earlier {
3029 #[serde(rename = "skin_index")]
3030 _skin_index: usize,
3031 },
3032}
3033
3034#[derive(Debug, Deserialize)]
3035struct MeasurementPayloadInput {
3036 schema_version: Option<u32>,
3037 schema: Option<String>,
3038 clips: Option<BTreeMap<String, ClipMeasurements>>,
3039 material_resource_coverage: Option<MaterialResourceCoverage>,
3040 material_definitions: Option<Vec<MaterialDefinitionMeasurements>>,
3041 textures: Option<Vec<TextureMeasurements>>,
3042 images: Option<Vec<ImageMeasurements>>,
3043 skeleton_source_coverage: Option<SourceSkeletonCoverage>,
3044 skeleton_nodes: Option<Vec<SkeletonNodeMeasurementInput>>,
3045 skins: Option<Vec<SkinMeasurementInput>>,
3046 mesh_definitions: Option<Vec<crate::measure::MeshDefinitionMeasurements>>,
3047 node_instances: Option<Vec<crate::measure::NodeInstanceMeasurements>>,
3048 scenes: Option<Vec<crate::measure::SceneMeasurements>>,
3049 default_scene_index: Option<usize>,
3050}
3051
3052#[derive(Debug, Deserialize)]
3057#[serde(deny_unknown_fields)]
3058struct MeasurementPayloadV16Input {
3059 schema_version: Option<u32>,
3060 schema: Option<String>,
3061 clips: Option<BTreeMap<String, ClipMeasurements>>,
3062 material_resource_coverage: Option<MaterialResourceCoverage>,
3063 material_definitions: Option<Vec<MaterialDefinitionMeasurements>>,
3064 textures: Option<Vec<TextureMeasurements>>,
3065 images: Option<Vec<ImageMeasurementsV16Input>>,
3066 skeleton_source_coverage: Option<SourceSkeletonCoverage>,
3067 skeleton_nodes: Option<Vec<SkeletonNodeMeasurementInput>>,
3068 skins: Option<Vec<SkinMeasurementInput>>,
3069 mesh_definitions: Option<Vec<MeshDefinitionMeasurementsV16Input>>,
3070 node_instances: Option<Vec<NodeInstanceMeasurementsV16Input>>,
3071 scenes: Option<Vec<SceneMeasurementsV16Input>>,
3072 default_scene_index: Option<usize>,
3073}
3074
3075#[derive(Debug, Deserialize)]
3076#[serde(deny_unknown_fields)]
3077struct AabbV16Input {
3078 min: [f32; 3],
3079 max: [f32; 3],
3080}
3081
3082impl From<AabbV16Input> for Aabb {
3083 fn from(value: AabbV16Input) -> Self {
3084 Self {
3085 min: value.min,
3086 max: value.max,
3087 }
3088 }
3089}
3090
3091#[derive(Debug, Deserialize)]
3092#[serde(deny_unknown_fields)]
3093struct PrimitiveMeasurementsV16Input {
3094 primitive_index: usize,
3095 #[serde(deserialize_with = "deserialize_required_optional_usize")]
3096 material_index: Option<usize>,
3097 vertex_count: u64,
3098 finite_vertex_count: u64,
3099 geometry_aabb: Option<AabbV16Input>,
3100 geometry_centroid: Option<[f32; 3]>,
3101}
3102
3103fn deserialize_required_optional_usize<'de, D>(deserializer: D) -> Result<Option<usize>, D::Error>
3104where
3105 D: Deserializer<'de>,
3106{
3107 Option::<usize>::deserialize(deserializer)
3108}
3109
3110impl From<PrimitiveMeasurementsV16Input> for PrimitiveMeasurements {
3111 fn from(value: PrimitiveMeasurementsV16Input) -> Self {
3112 Self {
3113 primitive_index: value.primitive_index,
3114 material_index: value.material_index,
3115 vertex_count: value.vertex_count,
3116 finite_vertex_count: value.finite_vertex_count,
3117 geometry_aabb: value.geometry_aabb.map(Into::into),
3118 geometry_centroid: value.geometry_centroid,
3119 }
3120 }
3121}
3122
3123#[derive(Debug, Deserialize)]
3124#[serde(deny_unknown_fields)]
3125struct MeshDefinitionMeasurementsV16Input {
3126 mesh_index: usize,
3127 name: String,
3128 primitives: Option<Vec<PrimitiveMeasurementsV16Input>>,
3129 vertex_count: u64,
3130 geometry_aabb: Option<AabbV16Input>,
3131 geometry_centroid: Option<[f32; 3]>,
3132 max_joints_per_vertex: u32,
3133 weight_sum_min: Option<f64>,
3134 weight_sum_max: Option<f64>,
3135 additional_influence_sets: Vec<AdditionalInfluenceSetMeasurements>,
3136}
3137
3138impl From<MeshDefinitionMeasurementsV16Input> for MeshDefinitionMeasurements {
3139 fn from(value: MeshDefinitionMeasurementsV16Input) -> Self {
3140 Self {
3141 mesh_index: value.mesh_index,
3142 name: value.name,
3143 primitives: value
3144 .primitives
3145 .map(|primitives| primitives.into_iter().map(Into::into).collect()),
3146 vertex_count: value.vertex_count,
3147 geometry_aabb: value.geometry_aabb.map(Into::into),
3148 geometry_centroid: value.geometry_centroid,
3149 max_joints_per_vertex: value.max_joints_per_vertex,
3150 weight_sum_min: value.weight_sum_min,
3151 weight_sum_max: value.weight_sum_max,
3152 additional_influence_sets: value.additional_influence_sets,
3153 }
3154 }
3155}
3156
3157#[derive(Debug, Deserialize)]
3158#[serde(deny_unknown_fields)]
3159struct ImageMeasurementsV16Input {
3160 image_index: usize,
3161 name: Option<String>,
3162 source_kind: crate::model::ImageSourceKind,
3163 declared_mime_type: Option<String>,
3164 detected_container: Option<crate::model::ImageContainerFormat>,
3165 leading_magic_hex: Option<String>,
3166 width: Option<u32>,
3167 height: Option<u32>,
3168 channel_count: Option<u8>,
3169 decoded_color_type: Option<DecodedImageColorType>,
3170 unavailable_reason: Option<crate::model::ImageUnavailableReason>,
3171}
3172
3173impl From<ImageMeasurementsV16Input> for ImageMeasurements {
3174 fn from(value: ImageMeasurementsV16Input) -> Self {
3175 Self {
3176 image_index: value.image_index,
3177 name: value.name,
3178 source_kind: value.source_kind,
3179 declared_mime_type: value.declared_mime_type,
3180 detected_container: value.detected_container,
3181 leading_magic_hex: value.leading_magic_hex,
3182 width: value.width,
3183 height: value.height,
3184 channel_count: value.channel_count,
3185 decoded_color_type: value.decoded_color_type,
3186 unavailable_reason: value.unavailable_reason,
3187 }
3188 }
3189}
3190
3191#[derive(Debug, Deserialize)]
3192#[serde(deny_unknown_fields)]
3193struct NodeInstanceMeasurementsV16Input {
3194 node_index: usize,
3195 node_name: String,
3196 mesh_index: usize,
3197 static_node_world_aabb: Option<AabbV16Input>,
3198 static_node_world_aabb_unavailable_reason: Option<StaticNodeAabbUnavailableReason>,
3199}
3200
3201impl From<NodeInstanceMeasurementsV16Input> for NodeInstanceMeasurements {
3202 fn from(value: NodeInstanceMeasurementsV16Input) -> Self {
3203 Self {
3204 node_index: value.node_index,
3205 node_name: value.node_name,
3206 mesh_index: value.mesh_index,
3207 static_node_world_aabb: value.static_node_world_aabb.map(Into::into),
3208 static_node_world_aabb_unavailable_reason: value
3209 .static_node_world_aabb_unavailable_reason,
3210 }
3211 }
3212}
3213
3214#[derive(Debug, Deserialize)]
3215#[serde(deny_unknown_fields)]
3216struct SceneMeasurementsV16Input {
3217 scene_index: usize,
3218 name: Option<String>,
3219 instance_count: usize,
3220 static_scene_world_aabb: Option<AabbV16Input>,
3221 excluded_instance_count: usize,
3222}
3223
3224impl From<SceneMeasurementsV16Input> for SceneMeasurements {
3225 fn from(value: SceneMeasurementsV16Input) -> Self {
3226 Self {
3227 scene_index: value.scene_index,
3228 name: value.name,
3229 instance_count: value.instance_count,
3230 static_scene_world_aabb: value.static_scene_world_aabb.map(Into::into),
3231 excluded_instance_count: value.excluded_instance_count,
3232 }
3233 }
3234}
3235
3236impl From<MeasurementPayloadV16Input> for MeasurementPayloadInput {
3237 fn from(value: MeasurementPayloadV16Input) -> Self {
3238 Self {
3239 schema_version: value.schema_version,
3240 schema: value.schema,
3241 clips: value.clips,
3242 material_resource_coverage: value.material_resource_coverage,
3243 material_definitions: value.material_definitions,
3244 textures: value.textures,
3245 images: value
3246 .images
3247 .map(|images| images.into_iter().map(Into::into).collect()),
3248 skeleton_source_coverage: value.skeleton_source_coverage,
3249 skeleton_nodes: value.skeleton_nodes,
3250 skins: value.skins,
3251 mesh_definitions: value
3252 .mesh_definitions
3253 .map(|meshes| meshes.into_iter().map(Into::into).collect()),
3254 node_instances: value
3255 .node_instances
3256 .map(|instances| instances.into_iter().map(Into::into).collect()),
3257 scenes: value
3258 .scenes
3259 .map(|scenes| scenes.into_iter().map(Into::into).collect()),
3260 default_scene_index: value.default_scene_index,
3261 }
3262 }
3263}
3264
3265fn decode_measurement_payload(
3266 raw: &RawValue,
3267 strict_v16: bool,
3268) -> Result<MeasurementPayloadInput, serde_json::Error> {
3269 let mut deserializer = serde_json::Deserializer::from_str(raw.get());
3270 let payload = if strict_v16 {
3271 MeasurementPayloadV16Input::deserialize(MeasurementF32NarrowingDeserializer(
3272 &mut deserializer,
3273 ))?
3274 .into()
3275 } else {
3276 MeasurementPayloadInput::deserialize(MeasurementF32NarrowingDeserializer(
3277 &mut deserializer,
3278 ))?
3279 };
3280 deserializer.end()?;
3281 Ok(payload)
3282}
3283
3284#[derive(Debug, Clone)]
3290pub struct MeasurementReportFile {
3291 path: String,
3292 input: InputIdentity,
3293 measurements: MeasurementContract,
3294}
3295
3296impl MeasurementReportFile {
3297 pub fn path(&self) -> &str {
3299 &self.path
3300 }
3301
3302 pub fn input(&self) -> &InputIdentity {
3304 &self.input
3305 }
3306
3307 pub fn measurements(&self) -> &MeasurementContract {
3309 &self.measurements
3310 }
3311
3312 pub fn into_measurements(self) -> MeasurementContract {
3314 self.measurements
3315 }
3316}
3317
3318#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3320#[non_exhaustive]
3321pub enum MeasurementReportError {
3322 #[error("report envelope has no `schema_version`")]
3324 MissingOutputVersion,
3325 #[error("has schema_version {found}; this build reads schema_version {OUTPUT_SCHEMA_VERSION}")]
3327 UnsupportedOutputVersion {
3328 found: u32,
3330 },
3331 #[error("report envelope does not identify output contract {OUTPUT_SCHEMA_ID}")]
3333 WrongOutputIdentity,
3334 #[error("report envelope has no `command`")]
3336 MissingCommand,
3337 #[error("report command {command:?} does not carry measurement file records")]
3339 UnsupportedCommand {
3340 command: String,
3342 },
3343 #[error("report envelope has unknown field `{field}`")]
3345 UnknownOutputField {
3346 field: String,
3348 },
3349 #[error("report envelope has no `tool` object")]
3351 MissingTool,
3352 #[error("report envelope has no `files` array")]
3354 MissingFiles,
3355 #[error("report contains {found} files, exceeding the output-v11 limit of {limit}")]
3357 TooManyFiles {
3358 found: usize,
3360 limit: usize,
3362 },
3363 #[error("lint report summary has no `prediction_facets` object")]
3365 MissingPredictionFacetSummary,
3366 #[error("measure report summary must not carry `prediction_facets`")]
3368 UnexpectedPredictionFacetSummary,
3369 #[error("lint report prediction-facet summary does not match its check records")]
3371 PredictionFacetSummaryMismatch,
3372 #[error("files[{file_index}] {source}")]
3374 File {
3375 file_index: usize,
3377 #[source]
3379 source: MeasurementFileError,
3380 },
3381}
3382
3383#[derive(Debug, thiserror::Error)]
3385#[non_exhaustive]
3386pub enum MeasurementReportReadError {
3387 #[error("cannot read report: {source}")]
3389 Io {
3390 #[source]
3392 source: std::io::Error,
3393 },
3394 #[error("report exceeds the output-v11 limit of {limit} bytes")]
3396 ReportTooLarge {
3397 limit: u64,
3399 },
3400 #[error("invalid report JSON: {source}")]
3402 InvalidJson {
3403 #[source]
3405 source: serde_json::Error,
3406 },
3407}
3408
3409#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3411#[non_exhaustive]
3412pub enum MeasurementFileError {
3413 #[error("has invalid output-v11 file shape: {reason}")]
3416 InvalidFileShape {
3417 reason: String,
3419 },
3420 #[error("has no `path`")]
3422 MissingPath,
3423 #[error("has no `input`")]
3425 MissingInput,
3426 #[error("input has no `sha256`")]
3428 MissingSha256,
3429 #[error("input `sha256` must be 64 lowercase hexadecimal characters")]
3431 InvalidSha256,
3432 #[error("input has no `bytes`")]
3434 MissingBytes,
3435 #[error("has no measurements")]
3437 MissingMeasurements,
3438 #[error("has no required `prediction_provenance` field")]
3440 MissingPredictionProvenance,
3441 #[error("measure file must not carry `prediction_provenance`")]
3443 UnexpectedPredictionProvenance,
3444 #[error("lint file has no `checks` array")]
3446 MissingChecks,
3447 #[error("measure file must not carry `checks`")]
3449 UnexpectedChecks,
3450 #[error("contains {found} checks, exceeding the output-v11 limit of {limit}")]
3452 TooManyChecks {
3453 found: usize,
3455 limit: usize,
3457 },
3458 #[error("prediction provenance primary input does not match file input")]
3460 PredictionPrimaryInputMismatch,
3461 #[error("has invalid prediction provenance: {source}")]
3463 InvalidPredictionProvenance {
3464 #[source]
3466 source: PredictionContractError,
3467 },
3468 #[error("has invalid prediction provenance shape: {reason}")]
3470 InvalidPredictionProvenanceShape {
3471 reason: String,
3473 },
3474 #[error("checks[{check_index}] has prediction without non-null file provenance")]
3476 PredictionWithoutProvenance {
3477 check_index: usize,
3479 },
3480 #[error("checks[{check_index}] has invalid prediction evidence: {source}")]
3482 InvalidPrediction {
3483 check_index: usize,
3485 #[source]
3487 source: PredictionContractError,
3488 },
3489 #[error("checks[{check_index}] has invalid prediction shape: {reason}")]
3491 InvalidPredictionShape {
3492 check_index: usize,
3494 reason: String,
3496 },
3497 #[error("checks[{check_index}] has invalid prediction lifecycle: {reason}")]
3499 InvalidPredictionLifecycle {
3500 check_index: usize,
3502 reason: &'static str,
3504 },
3505 #[error("contains {found} prediction facets, exceeding the V1 limit of {limit}")]
3507 TooManyPredictionFacets {
3508 found: usize,
3510 limit: usize,
3512 },
3513 #[error("facet-budget summary requires exactly {limit} aggregate facets, found {found}")]
3516 FacetBudgetSummaryWithoutExhaustedFileBudget {
3517 found: usize,
3519 limit: usize,
3521 },
3522 #[error("contains {found} prediction basis rows, exceeding the V1 limit of {limit}")]
3524 TooManyPredictionBasisReferences {
3525 found: usize,
3527 limit: usize,
3529 },
3530 #[error("retains {found} prediction text bytes, exceeding the V1 limit of {limit}")]
3532 TooMuchPredictionText {
3533 found: usize,
3535 limit: usize,
3537 },
3538 #[error("prediction bound accounting overflowed")]
3540 PredictionAccountingOverflow,
3541 #[error("has no versioned measurement contract")]
3543 MissingMeasurementVersion,
3544 #[error(
3546 "has measurement schema_version {found}; this reader expects measurement schema_version {expected}"
3547 )]
3548 UnsupportedMeasurementVersion {
3549 found: u32,
3551 expected: u32,
3553 },
3554 #[error("does not identify measurement contract {expected}")]
3556 WrongMeasurementIdentity {
3557 expected: &'static str,
3559 },
3560 #[error("measurement contract has no `clips` map")]
3562 MissingClips,
3563 #[error("measurement contract has no `material_resource_coverage`")]
3565 MissingMaterialResourceCoverage,
3566 #[error("measurement contract has no `material_definitions` array")]
3568 MissingMaterialDefinitions,
3569 #[error("measurement contract has no `textures` array")]
3571 MissingTextures,
3572 #[error("measurement contract has no `images` array")]
3574 MissingImages,
3575 #[error("measurement contract has no `skeleton_source_coverage`")]
3577 MissingSkeletonSourceCoverage,
3578 #[error("measurement contract has no `skeleton_nodes` array")]
3580 MissingSkeletonNodes,
3581 #[error("measurement contract has no `skins` array")]
3583 MissingSkins,
3584 #[error("measurement contract has no `mesh_definitions` array")]
3586 MissingMeshDefinitions,
3587 #[error("measurement contract has no `node_instances` array")]
3589 MissingNodeInstances,
3590 #[error("measurement contract has no `scenes` array")]
3592 MissingScenes,
3593 #[error("has invalid measurements shape: {reason}")]
3596 InvalidMeasurementsShape {
3597 reason: String,
3599 },
3600 #[error("has invalid measurements: {source}")]
3602 InvalidMeasurements {
3603 #[source]
3605 source: MeasurementContractError,
3606 },
3607}
3608
3609impl MeasurementReportError {
3610 pub fn file_index(&self) -> Option<usize> {
3614 match self {
3615 Self::File { file_index, .. } => Some(*file_index),
3616 _ => None,
3617 }
3618 }
3619
3620 fn file(file_index: usize, source: MeasurementFileError) -> Self {
3621 Self::File { file_index, source }
3622 }
3623}
3624
3625fn prediction_file_error(
3626 file_index: usize,
3627 source: MeasurementFileError,
3628) -> MeasurementReportError {
3629 MeasurementReportError::file(file_index, source)
3630}
3631
3632fn decode_prediction_phase_file(
3633 command: &str,
3634 file_index: usize,
3635 raw: &RawValue,
3636 expected_measurement_schema: &'static str,
3637) -> Result<MeasurementFileInput, MeasurementReportError> {
3638 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
3639 prediction_file_error(
3640 file_index,
3641 MeasurementFileError::InvalidFileShape {
3642 reason: source.to_string(),
3643 },
3644 )
3645 })?;
3646
3647 if command == "measure" {
3648 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3649 return Err(prediction_file_error(
3650 file_index,
3651 MeasurementFileError::UnexpectedPredictionProvenance,
3652 ));
3653 }
3654 if wire.checks.is_some() {
3655 return Err(prediction_file_error(
3656 file_index,
3657 MeasurementFileError::UnexpectedChecks,
3658 ));
3659 }
3660 return Ok(MeasurementFileInput {
3661 path: wire.path,
3662 input: wire.input,
3663 rig_v17: None,
3664 measurements: wire.measurements,
3665 prediction_provenance: RequiredNullable::Missing,
3666 checks: None,
3667 legacy_prediction_provenance: RequiredNullable::Missing,
3668 legacy_checks: None,
3669 prediction_provenance_v3: RequiredNullable::Missing,
3670 checks_v3: None,
3671 prediction_provenance_v4: RequiredNullable::Missing,
3672 checks_v4: None,
3673 prediction_provenance_v5: RequiredNullable::Missing,
3674 checks_v5: None,
3675 prediction_provenance_v6: RequiredNullable::Missing,
3676 checks_v6: None,
3677 });
3678 }
3679
3680 if wire
3681 .checks
3682 .as_ref()
3683 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
3684 {
3685 return Err(prediction_file_error(
3686 file_index,
3687 MeasurementFileError::TooManyChecks {
3688 found: wire.checks.as_ref().map_or(0, Vec::len),
3689 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
3690 },
3691 ));
3692 }
3693
3694 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3695 return Err(prediction_file_error(
3696 file_index,
3697 MeasurementFileError::MissingPredictionProvenance,
3698 ));
3699 }
3700
3701 let prediction_provenance = match wire.prediction_provenance {
3702 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3703 RequiredNullable::Present(None) => RequiredNullable::Present(None),
3704 RequiredNullable::Present(Some(raw)) => {
3705 let provenance = decode_prediction_provenance_v2_with_measurement_schema(
3706 raw.get(),
3707 expected_measurement_schema,
3708 )
3709 .map_err(|error| {
3710 let source = match error {
3711 PredictionDecodeError::Shape(source) => {
3712 MeasurementFileError::InvalidPredictionProvenanceShape {
3713 reason: source.to_string(),
3714 }
3715 }
3716 PredictionDecodeError::Semantic(source) => {
3717 MeasurementFileError::InvalidPredictionProvenance { source }
3718 }
3719 PredictionDecodeError::TooManyFileFacets
3720 | PredictionDecodeError::TooManyFileBasisReferences => {
3721 unreachable!("provenance never consumes prediction budgets")
3722 }
3723 };
3724 prediction_file_error(file_index, source)
3725 })?;
3726 RequiredNullable::Present(Some(provenance))
3727 }
3728 };
3729 let mut decoded_facets = 0usize;
3730 let mut decoded_references = 0usize;
3731 let mut has_facet_budget_summary = false;
3732 let mut decoded_text = match &prediction_provenance {
3733 RequiredNullable::Present(Some(provenance)) => {
3734 provenance.retained_text_bytes().map_err(|source| {
3735 prediction_file_error(
3736 file_index,
3737 MeasurementFileError::InvalidPredictionProvenance { source },
3738 )
3739 })?
3740 }
3741 RequiredNullable::Missing | RequiredNullable::Present(None) => 0,
3742 };
3743 let provenance_for_checks = match &prediction_provenance {
3744 RequiredNullable::Present(provenance) => provenance.as_ref(),
3745 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
3746 };
3747 let checks = wire
3748 .checks
3749 .map(|raw_checks| {
3750 let mut checks = Vec::with_capacity(raw_checks.len());
3751 for (check_index, raw) in raw_checks.into_iter().enumerate() {
3752 let wire: PredictionCheckWireInput =
3753 serde_json::from_str(raw.get()).map_err(|source| {
3754 prediction_file_error(
3755 file_index,
3756 MeasurementFileError::InvalidPredictionShape {
3757 check_index,
3758 reason: source.to_string(),
3759 },
3760 )
3761 })?;
3762 if provenance_for_checks.is_none() && wire.prediction.is_some() {
3763 return Err(prediction_file_error(
3764 file_index,
3765 MeasurementFileError::PredictionWithoutProvenance { check_index },
3766 ));
3767 }
3768 if (wire.selection == SelectionState::Unselected
3769 || wire.configuration == ConfigurationState::Disabled
3770 || wire.applicability == Applicability::NotApplicable)
3771 && wire.prediction.is_some()
3772 {
3773 return Err(prediction_file_error(
3774 file_index,
3775 MeasurementFileError::InvalidPredictionLifecycle {
3776 check_index,
3777 reason: "inactive check must have empty output",
3778 },
3779 ));
3780 }
3781 let prediction = wire
3786 .prediction
3787 .map(|raw| {
3788 let facet_limit =
3789 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets);
3790 let reference_limit = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
3791 .saturating_sub(decoded_references);
3792 decode_engine_prediction_v2_with_measurement_schema(
3793 raw.get(),
3794 facet_limit,
3795 reference_limit,
3796 expected_measurement_schema,
3797 )
3798 .map_err(|error| {
3799 let source = match error {
3800 PredictionDecodeError::Shape(source) => {
3801 MeasurementFileError::InvalidPredictionShape {
3802 check_index,
3803 reason: source.to_string(),
3804 }
3805 }
3806 PredictionDecodeError::Semantic(source) => {
3807 MeasurementFileError::InvalidPrediction {
3808 check_index,
3809 source,
3810 }
3811 }
3812 PredictionDecodeError::TooManyFileFacets => {
3813 MeasurementFileError::TooManyPredictionFacets {
3814 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
3815 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3816 }
3817 }
3818 PredictionDecodeError::TooManyFileBasisReferences => {
3819 MeasurementFileError::TooManyPredictionBasisReferences {
3820 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
3821 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
3822 }
3823 }
3824 };
3825 prediction_file_error(file_index, source)
3826 })
3827 })
3828 .transpose()?;
3829 let check = PredictionCheckInput {
3830 check_id: wire.check_id,
3831 selection: wire.selection,
3832 configuration: wire.configuration,
3833 applicability: wire.applicability,
3834 evaluation: wire.evaluation,
3835 findings: wire.findings,
3836 evaluated_scopes: wire.evaluated_scopes,
3837 gaps: wire.gaps,
3838 prediction,
3839 };
3840 check
3841 .validate(
3842 check_index,
3843 provenance_for_checks,
3844 expected_measurement_schema,
3845 )
3846 .map_err(|source| prediction_file_error(file_index, source))?;
3847 if let Some(prediction) = &check.prediction {
3848 has_facet_budget_summary |= prediction.has_facet_budget_summary();
3849 decoded_facets = decoded_facets
3850 .checked_add(prediction.facets().len())
3851 .ok_or_else(|| {
3852 prediction_file_error(
3853 file_index,
3854 MeasurementFileError::PredictionAccountingOverflow,
3855 )
3856 })?;
3857 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
3858 return Err(prediction_file_error(
3859 file_index,
3860 MeasurementFileError::TooManyPredictionFacets {
3861 found: decoded_facets,
3862 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3863 },
3864 ));
3865 }
3866 decoded_references = decoded_references
3867 .checked_add(prediction.basis_reference_count())
3868 .ok_or_else(|| {
3869 prediction_file_error(
3870 file_index,
3871 MeasurementFileError::PredictionAccountingOverflow,
3872 )
3873 })?;
3874 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
3875 return Err(prediction_file_error(
3876 file_index,
3877 MeasurementFileError::TooManyPredictionBasisReferences {
3878 found: decoded_references,
3879 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
3880 },
3881 ));
3882 }
3883 decoded_text = decoded_text
3884 .checked_add(prediction.retained_text_bytes().map_err(|source| {
3885 prediction_file_error(
3886 file_index,
3887 MeasurementFileError::InvalidPrediction {
3888 check_index,
3889 source,
3890 },
3891 )
3892 })?)
3893 .ok_or_else(|| {
3894 prediction_file_error(
3895 file_index,
3896 MeasurementFileError::PredictionAccountingOverflow,
3897 )
3898 })?;
3899 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
3900 return Err(prediction_file_error(
3901 file_index,
3902 MeasurementFileError::TooMuchPredictionText {
3903 found: decoded_text,
3904 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
3905 },
3906 ));
3907 }
3908 }
3909 checks.push(check);
3910 }
3911 if has_facet_budget_summary && decoded_facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
3912 return Err(prediction_file_error(
3913 file_index,
3914 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
3915 found: decoded_facets,
3916 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
3917 },
3918 ));
3919 }
3920 Ok(checks)
3921 })
3922 .transpose()?;
3923 Ok(MeasurementFileInput {
3924 path: wire.path,
3925 input: wire.input,
3926 rig_v17: None,
3927 measurements: wire.measurements,
3928 prediction_provenance,
3929 checks,
3930 legacy_prediction_provenance: RequiredNullable::Missing,
3931 legacy_checks: None,
3932 prediction_provenance_v3: RequiredNullable::Missing,
3933 checks_v3: None,
3934 prediction_provenance_v4: RequiredNullable::Missing,
3935 checks_v4: None,
3936 prediction_provenance_v5: RequiredNullable::Missing,
3937 checks_v5: None,
3938 prediction_provenance_v6: RequiredNullable::Missing,
3939 checks_v6: None,
3940 })
3941}
3942
3943fn decode_prediction_phase_file_v14(
3944 command: &str,
3945 file_index: usize,
3946 raw: &RawValue,
3947) -> Result<MeasurementFileInput, MeasurementReportError> {
3948 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
3949 prediction_file_error(
3950 file_index,
3951 MeasurementFileError::InvalidFileShape {
3952 reason: source.to_string(),
3953 },
3954 )
3955 })?;
3956 if command == "measure" {
3957 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
3958 return Err(prediction_file_error(
3959 file_index,
3960 MeasurementFileError::UnexpectedPredictionProvenance,
3961 ));
3962 }
3963 if wire.checks.is_some() {
3964 return Err(prediction_file_error(
3965 file_index,
3966 MeasurementFileError::UnexpectedChecks,
3967 ));
3968 }
3969 return Ok(MeasurementFileInput {
3970 path: wire.path,
3971 input: wire.input,
3972 rig_v17: None,
3973 measurements: wire.measurements,
3974 prediction_provenance: RequiredNullable::Missing,
3975 checks: None,
3976 legacy_prediction_provenance: RequiredNullable::Missing,
3977 legacy_checks: None,
3978 prediction_provenance_v3: RequiredNullable::Missing,
3979 checks_v3: None,
3980 prediction_provenance_v4: RequiredNullable::Missing,
3981 checks_v4: None,
3982 prediction_provenance_v5: RequiredNullable::Missing,
3983 checks_v5: None,
3984 prediction_provenance_v6: RequiredNullable::Missing,
3985 checks_v6: None,
3986 });
3987 }
3988 if wire
3989 .checks
3990 .as_ref()
3991 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
3992 {
3993 return Err(prediction_file_error(
3994 file_index,
3995 MeasurementFileError::TooManyChecks {
3996 found: wire.checks.as_ref().map_or(0, Vec::len),
3997 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
3998 },
3999 ));
4000 }
4001 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
4002 return Err(prediction_file_error(
4003 file_index,
4004 MeasurementFileError::MissingPredictionProvenance,
4005 ));
4006 }
4007 let prediction_provenance_v3 = match wire.prediction_provenance {
4008 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
4009 RequiredNullable::Present(None) => RequiredNullable::Present(None),
4010 RequiredNullable::Present(Some(raw)) => {
4011 let provenance = decode_prediction_provenance_v3(raw.get()).map_err(|error| {
4012 let source = match error {
4013 PredictionDecodeError::Shape(source) => {
4014 MeasurementFileError::InvalidPredictionProvenanceShape {
4015 reason: source.to_string(),
4016 }
4017 }
4018 PredictionDecodeError::Semantic(source) => {
4019 MeasurementFileError::InvalidPredictionProvenance { source }
4020 }
4021 PredictionDecodeError::TooManyFileFacets
4022 | PredictionDecodeError::TooManyFileBasisReferences => {
4023 unreachable!("provenance never consumes prediction budgets")
4024 }
4025 };
4026 prediction_file_error(file_index, source)
4027 })?;
4028 RequiredNullable::Present(Some(provenance))
4029 }
4030 };
4031 let provenance_for_checks = match &prediction_provenance_v3 {
4032 RequiredNullable::Present(provenance) => provenance.as_ref(),
4033 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
4034 };
4035 let mut decoded_facets = 0usize;
4036 let mut decoded_references = 0usize;
4037 let mut has_facet_budget_summary = false;
4038 let mut decoded_text = provenance_for_checks
4039 .map(PredictionProvenanceV3::retained_text_bytes)
4040 .transpose()
4041 .map_err(|source| {
4042 prediction_file_error(
4043 file_index,
4044 MeasurementFileError::InvalidPredictionProvenance { source },
4045 )
4046 })?
4047 .unwrap_or(0);
4048 let checks_v3 = wire
4049 .checks
4050 .map(|raw_checks| {
4051 let mut checks = Vec::with_capacity(raw_checks.len());
4052 for (check_index, raw) in raw_checks.into_iter().enumerate() {
4053 let wire: PredictionCheckWireInput =
4054 serde_json::from_str(raw.get()).map_err(|source| {
4055 prediction_file_error(
4056 file_index,
4057 MeasurementFileError::InvalidPredictionShape {
4058 check_index,
4059 reason: source.to_string(),
4060 },
4061 )
4062 })?;
4063 if provenance_for_checks.is_none() && wire.prediction.is_some() {
4064 return Err(prediction_file_error(
4065 file_index,
4066 MeasurementFileError::PredictionWithoutProvenance { check_index },
4067 ));
4068 }
4069 if (wire.selection == SelectionState::Unselected
4070 || wire.configuration == ConfigurationState::Disabled
4071 || wire.applicability == Applicability::NotApplicable)
4072 && wire.prediction.is_some()
4073 {
4074 return Err(prediction_file_error(
4075 file_index,
4076 MeasurementFileError::InvalidPredictionLifecycle {
4077 check_index,
4078 reason: "inactive check must have empty output",
4079 },
4080 ));
4081 }
4082 let prediction = wire
4083 .prediction
4084 .map(|raw| {
4085 decode_engine_prediction_v3(
4086 raw.get(),
4087 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
4088 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
4089 .saturating_sub(decoded_references),
4090 )
4091 .map_err(|error| {
4092 let source = match error {
4093 PredictionDecodeError::Shape(source) => {
4094 MeasurementFileError::InvalidPredictionShape {
4095 check_index,
4096 reason: source.to_string(),
4097 }
4098 }
4099 PredictionDecodeError::Semantic(source) => {
4100 MeasurementFileError::InvalidPrediction {
4101 check_index,
4102 source,
4103 }
4104 }
4105 PredictionDecodeError::TooManyFileFacets => {
4106 MeasurementFileError::TooManyPredictionFacets {
4107 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
4108 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4109 }
4110 }
4111 PredictionDecodeError::TooManyFileBasisReferences => {
4112 MeasurementFileError::TooManyPredictionBasisReferences {
4113 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
4114 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4115 }
4116 }
4117 };
4118 prediction_file_error(file_index, source)
4119 })
4120 })
4121 .transpose()?;
4122 let check = PredictionCheckInputV3 {
4123 check_id: wire.check_id,
4124 selection: wire.selection,
4125 configuration: wire.configuration,
4126 applicability: wire.applicability,
4127 evaluation: wire.evaluation,
4128 findings: wire.findings,
4129 evaluated_scopes: wire.evaluated_scopes,
4130 gaps: wire.gaps,
4131 prediction,
4132 };
4133 check
4134 .validate(check_index, provenance_for_checks)
4135 .map_err(|source| prediction_file_error(file_index, source))?;
4136 if let Some(prediction) = &check.prediction {
4137 has_facet_budget_summary |= prediction.has_facet_budget_summary();
4138 decoded_facets = decoded_facets
4139 .checked_add(prediction.facets().len())
4140 .ok_or_else(|| {
4141 prediction_file_error(
4142 file_index,
4143 MeasurementFileError::PredictionAccountingOverflow,
4144 )
4145 })?;
4146 decoded_references = decoded_references
4147 .checked_add(prediction.basis_reference_count())
4148 .ok_or_else(|| {
4149 prediction_file_error(
4150 file_index,
4151 MeasurementFileError::PredictionAccountingOverflow,
4152 )
4153 })?;
4154 decoded_text = decoded_text
4155 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4156 prediction_file_error(
4157 file_index,
4158 MeasurementFileError::InvalidPrediction {
4159 check_index,
4160 source,
4161 },
4162 )
4163 })?)
4164 .ok_or_else(|| {
4165 prediction_file_error(
4166 file_index,
4167 MeasurementFileError::PredictionAccountingOverflow,
4168 )
4169 })?;
4170 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4171 return Err(prediction_file_error(
4172 file_index,
4173 MeasurementFileError::TooMuchPredictionText {
4174 found: decoded_text,
4175 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4176 },
4177 ));
4178 }
4179 }
4180 checks.push(check);
4181 }
4182 if has_facet_budget_summary && decoded_facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
4183 return Err(prediction_file_error(
4184 file_index,
4185 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
4186 found: decoded_facets,
4187 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4188 },
4189 ));
4190 }
4191 Ok(checks)
4192 })
4193 .transpose()?;
4194 Ok(MeasurementFileInput {
4195 path: wire.path,
4196 input: wire.input,
4197 rig_v17: None,
4198 measurements: wire.measurements,
4199 prediction_provenance: RequiredNullable::Missing,
4200 checks: None,
4201 legacy_prediction_provenance: RequiredNullable::Missing,
4202 legacy_checks: None,
4203 prediction_provenance_v3,
4204 checks_v3,
4205 prediction_provenance_v4: RequiredNullable::Missing,
4206 checks_v4: None,
4207 prediction_provenance_v5: RequiredNullable::Missing,
4208 checks_v5: None,
4209 prediction_provenance_v6: RequiredNullable::Missing,
4210 checks_v6: None,
4211 })
4212}
4213
4214fn decode_prediction_phase_file_v15(
4215 command: &str,
4216 file_index: usize,
4217 raw: &RawValue,
4218) -> Result<MeasurementFileInput, MeasurementReportError> {
4219 #[derive(Deserialize)]
4220 struct SchemaProbe {
4221 schema: String,
4222 }
4223
4224 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4225 prediction_file_error(
4226 file_index,
4227 MeasurementFileError::InvalidFileShape {
4228 reason: source.to_string(),
4229 },
4230 )
4231 })?;
4232 if command == "measure"
4233 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4234 {
4235 return decode_prediction_phase_file_v14(command, file_index, raw);
4236 }
4237 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4238 return decode_prediction_phase_file_v14(command, file_index, raw);
4239 };
4240 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4241 prediction_file_error(
4242 file_index,
4243 MeasurementFileError::InvalidPredictionProvenanceShape {
4244 reason: source.to_string(),
4245 },
4246 )
4247 })?;
4248 if schema.schema == crate::prediction::PREDICTION_PROVENANCE_V3_ID {
4249 return decode_prediction_phase_file_v14(command, file_index, raw);
4250 }
4251 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V4_ID {
4252 return Err(prediction_file_error(
4253 file_index,
4254 MeasurementFileError::InvalidPredictionProvenance {
4255 source: PredictionContractError::InvalidSchema {
4256 field: "prediction provenance.schema",
4257 expected: crate::prediction::PREDICTION_PROVENANCE_V4_ID,
4258 found: schema.schema,
4259 },
4260 },
4261 ));
4262 }
4263
4264 let MeasurementFileWireInput {
4265 path,
4266 input,
4267 measurements,
4268 prediction_provenance,
4269 checks,
4270 ..
4271 } = probe;
4272 if checks
4273 .as_ref()
4274 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4275 {
4276 return Err(prediction_file_error(
4277 file_index,
4278 MeasurementFileError::TooManyChecks {
4279 found: checks.as_ref().map_or(0, Vec::len),
4280 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4281 },
4282 ));
4283 }
4284 let RequiredNullable::Present(Some(provenance_raw)) = prediction_provenance else {
4285 unreachable!("V4 provenance was probed above")
4286 };
4287 let provenance = decode_prediction_provenance_v4(provenance_raw.get()).map_err(|error| {
4288 let source = match error {
4289 PredictionDecodeError::Shape(source) => {
4290 MeasurementFileError::InvalidPredictionProvenanceShape {
4291 reason: source.to_string(),
4292 }
4293 }
4294 PredictionDecodeError::Semantic(source) => {
4295 MeasurementFileError::InvalidPredictionProvenance { source }
4296 }
4297 PredictionDecodeError::TooManyFileFacets
4298 | PredictionDecodeError::TooManyFileBasisReferences => unreachable!(),
4299 };
4300 prediction_file_error(file_index, source)
4301 })?;
4302 let mut decoded_facets = 0usize;
4303 let mut decoded_references = 0usize;
4304 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4305 prediction_file_error(
4306 file_index,
4307 MeasurementFileError::InvalidPredictionProvenance { source },
4308 )
4309 })?;
4310 let checks_v4 = checks
4311 .map(|raw_checks| {
4312 let mut decoded = Vec::with_capacity(raw_checks.len());
4313 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4314 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4315 .map_err(|source| {
4316 prediction_file_error(
4317 file_index,
4318 MeasurementFileError::InvalidPredictionShape {
4319 check_index,
4320 reason: source.to_string(),
4321 },
4322 )
4323 })?;
4324 if (wire.selection == SelectionState::Unselected
4325 || wire.configuration == ConfigurationState::Disabled
4326 || wire.applicability == Applicability::NotApplicable)
4327 && wire.prediction.is_some()
4328 {
4329 return Err(prediction_file_error(
4330 file_index,
4331 MeasurementFileError::InvalidPredictionLifecycle {
4332 check_index,
4333 reason: "inactive check must have empty output",
4334 },
4335 ));
4336 }
4337 let prediction = wire
4338 .prediction
4339 .map(|prediction_raw| {
4340 decode_engine_prediction_v4(
4341 prediction_raw.get(),
4342 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
4343 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
4344 .saturating_sub(decoded_references),
4345 )
4346 .map_err(|error| {
4347 let source = match error {
4348 PredictionDecodeError::Shape(source) => {
4349 MeasurementFileError::InvalidPredictionShape {
4350 check_index,
4351 reason: source.to_string(),
4352 }
4353 }
4354 PredictionDecodeError::Semantic(source) => {
4355 MeasurementFileError::InvalidPrediction {
4356 check_index,
4357 source,
4358 }
4359 }
4360 PredictionDecodeError::TooManyFileFacets => {
4361 MeasurementFileError::TooManyPredictionFacets {
4362 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
4363 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4364 }
4365 }
4366 PredictionDecodeError::TooManyFileBasisReferences => {
4367 MeasurementFileError::TooManyPredictionBasisReferences {
4368 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
4369 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4370 }
4371 }
4372 };
4373 prediction_file_error(file_index, source)
4374 })
4375 })
4376 .transpose()?;
4377 let check = PredictionCheckInputV4 {
4378 check_id: wire.check_id,
4379 selection: wire.selection,
4380 configuration: wire.configuration,
4381 applicability: wire.applicability,
4382 evaluation: wire.evaluation,
4383 findings: wire.findings,
4384 evaluated_scopes: wire.evaluated_scopes,
4385 gaps: wire.gaps,
4386 prediction,
4387 };
4388 check
4389 .validate(check_index, Some(&provenance))
4390 .map_err(|source| prediction_file_error(file_index, source))?;
4391 if let Some(prediction) = &check.prediction {
4392 decoded_facets = decoded_facets
4393 .checked_add(prediction.facets().len())
4394 .ok_or_else(|| {
4395 prediction_file_error(
4396 file_index,
4397 MeasurementFileError::PredictionAccountingOverflow,
4398 )
4399 })?;
4400 decoded_references = decoded_references
4401 .checked_add(prediction.basis_reference_count())
4402 .ok_or_else(|| {
4403 prediction_file_error(
4404 file_index,
4405 MeasurementFileError::PredictionAccountingOverflow,
4406 )
4407 })?;
4408 decoded_text = decoded_text
4409 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4410 prediction_file_error(
4411 file_index,
4412 MeasurementFileError::InvalidPrediction {
4413 check_index,
4414 source,
4415 },
4416 )
4417 })?)
4418 .ok_or_else(|| {
4419 prediction_file_error(
4420 file_index,
4421 MeasurementFileError::PredictionAccountingOverflow,
4422 )
4423 })?;
4424 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4425 return Err(prediction_file_error(
4426 file_index,
4427 MeasurementFileError::TooMuchPredictionText {
4428 found: decoded_text,
4429 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4430 },
4431 ));
4432 }
4433 }
4434 decoded.push(check);
4435 }
4436 Ok(decoded)
4437 })
4438 .transpose()?;
4439 Ok(MeasurementFileInput {
4440 path,
4441 input,
4442 rig_v17: None,
4443 measurements,
4444 prediction_provenance: RequiredNullable::Missing,
4445 checks: None,
4446 legacy_prediction_provenance: RequiredNullable::Missing,
4447 legacy_checks: None,
4448 prediction_provenance_v3: RequiredNullable::Missing,
4449 checks_v3: None,
4450 prediction_provenance_v4: RequiredNullable::Present(Some(provenance)),
4451 checks_v4,
4452 prediction_provenance_v5: RequiredNullable::Missing,
4453 checks_v5: None,
4454 prediction_provenance_v6: RequiredNullable::Missing,
4455 checks_v6: None,
4456 })
4457}
4458
4459fn decode_prediction_phase_file_v16(
4460 command: &str,
4461 file_index: usize,
4462 raw: &RawValue,
4463) -> Result<MeasurementFileInput, MeasurementReportError> {
4464 #[derive(Deserialize)]
4465 struct SchemaProbe {
4466 schema: String,
4467 }
4468
4469 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4470 prediction_file_error(
4471 file_index,
4472 MeasurementFileError::InvalidFileShape {
4473 reason: source.to_string(),
4474 },
4475 )
4476 })?;
4477 if command == "measure"
4478 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4479 {
4480 return decode_prediction_phase_file_v15(command, file_index, raw);
4481 }
4482 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4483 return decode_prediction_phase_file_v15(command, file_index, raw);
4484 };
4485 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4486 prediction_file_error(
4487 file_index,
4488 MeasurementFileError::InvalidPredictionProvenanceShape {
4489 reason: source.to_string(),
4490 },
4491 )
4492 })?;
4493 if schema.schema == crate::prediction::PREDICTION_PROVENANCE_V3_ID {
4494 return decode_prediction_phase_file_v15(command, file_index, raw);
4495 }
4496 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V5_ID {
4497 return Err(prediction_file_error(
4498 file_index,
4499 MeasurementFileError::InvalidPredictionProvenance {
4500 source: PredictionContractError::InvalidSchema {
4501 field: "prediction provenance.schema",
4502 expected: crate::prediction::PREDICTION_PROVENANCE_V5_ID,
4503 found: schema.schema,
4504 },
4505 },
4506 ));
4507 }
4508 let MeasurementFileWireInput {
4509 path,
4510 input,
4511 measurements,
4512 prediction_provenance,
4513 checks,
4514 ..
4515 } = probe;
4516 if checks
4517 .as_ref()
4518 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4519 {
4520 return Err(prediction_file_error(
4521 file_index,
4522 MeasurementFileError::TooManyChecks {
4523 found: checks.as_ref().map_or(0, Vec::len),
4524 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4525 },
4526 ));
4527 }
4528 let RequiredNullable::Present(Some(raw_provenance)) = prediction_provenance else {
4529 unreachable!()
4530 };
4531 let provenance: PredictionProvenanceV5 =
4532 serde_json::from_str(raw_provenance.get()).map_err(|source| {
4533 prediction_file_error(
4534 file_index,
4535 MeasurementFileError::InvalidPredictionProvenanceShape {
4536 reason: source.to_string(),
4537 },
4538 )
4539 })?;
4540 provenance.validate().map_err(|source| {
4541 prediction_file_error(
4542 file_index,
4543 MeasurementFileError::InvalidPredictionProvenance { source },
4544 )
4545 })?;
4546 let mut decoded_facets = 0usize;
4547 let mut decoded_references = 0usize;
4548 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4549 prediction_file_error(
4550 file_index,
4551 MeasurementFileError::InvalidPredictionProvenance { source },
4552 )
4553 })?;
4554 let checks_v5 = checks
4555 .map(|raw_checks| {
4556 let mut decoded = Vec::with_capacity(raw_checks.len());
4557 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4558 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4559 .map_err(|source| {
4560 prediction_file_error(
4561 file_index,
4562 MeasurementFileError::InvalidPredictionShape {
4563 check_index,
4564 reason: source.to_string(),
4565 },
4566 )
4567 })?;
4568 if (wire.selection == SelectionState::Unselected
4569 || wire.configuration == ConfigurationState::Disabled
4570 || wire.applicability == Applicability::NotApplicable)
4571 && wire.prediction.is_some()
4572 {
4573 return Err(prediction_file_error(
4574 file_index,
4575 MeasurementFileError::InvalidPredictionLifecycle {
4576 check_index,
4577 reason: "inactive check must have empty output",
4578 },
4579 ));
4580 }
4581 let prediction =
4582 wire.prediction
4583 .map(|raw_prediction| {
4584 serde_json::from_str::<EnginePredictionV5>(raw_prediction.get())
4585 .map_err(|source| {
4586 prediction_file_error(
4587 file_index,
4588 MeasurementFileError::InvalidPredictionShape {
4589 check_index,
4590 reason: source.to_string(),
4591 },
4592 )
4593 })
4594 })
4595 .transpose()?;
4596 let check = PredictionCheckInputV5 {
4597 check_id: wire.check_id,
4598 selection: wire.selection,
4599 configuration: wire.configuration,
4600 applicability: wire.applicability,
4601 evaluation: wire.evaluation,
4602 findings: wire.findings,
4603 evaluated_scopes: wire.evaluated_scopes,
4604 gaps: wire.gaps,
4605 prediction,
4606 };
4607 check
4608 .validate(check_index, Some(&provenance))
4609 .map_err(|source| prediction_file_error(file_index, source))?;
4610 if let Some(prediction) = &check.prediction {
4611 decoded_facets = decoded_facets
4612 .checked_add(prediction.facets().len())
4613 .ok_or_else(|| {
4614 prediction_file_error(
4615 file_index,
4616 MeasurementFileError::PredictionAccountingOverflow,
4617 )
4618 })?;
4619 decoded_references = decoded_references
4620 .checked_add(prediction.basis_reference_count())
4621 .ok_or_else(|| {
4622 prediction_file_error(
4623 file_index,
4624 MeasurementFileError::PredictionAccountingOverflow,
4625 )
4626 })?;
4627 decoded_text = decoded_text
4628 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4629 prediction_file_error(
4630 file_index,
4631 MeasurementFileError::InvalidPrediction {
4632 check_index,
4633 source,
4634 },
4635 )
4636 })?)
4637 .ok_or_else(|| {
4638 prediction_file_error(
4639 file_index,
4640 MeasurementFileError::PredictionAccountingOverflow,
4641 )
4642 })?;
4643 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
4644 return Err(prediction_file_error(
4645 file_index,
4646 MeasurementFileError::TooManyPredictionFacets {
4647 found: decoded_facets,
4648 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4649 },
4650 ));
4651 }
4652 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
4653 return Err(prediction_file_error(
4654 file_index,
4655 MeasurementFileError::TooManyPredictionBasisReferences {
4656 found: decoded_references,
4657 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4658 },
4659 ));
4660 }
4661 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4662 return Err(prediction_file_error(
4663 file_index,
4664 MeasurementFileError::TooMuchPredictionText {
4665 found: decoded_text,
4666 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4667 },
4668 ));
4669 }
4670 }
4671 decoded.push(check);
4672 }
4673 Ok(decoded)
4674 })
4675 .transpose()?;
4676 Ok(MeasurementFileInput {
4677 path,
4678 input,
4679 rig_v17: None,
4680 measurements,
4681 prediction_provenance: RequiredNullable::Missing,
4682 checks: None,
4683 legacy_prediction_provenance: RequiredNullable::Missing,
4684 legacy_checks: None,
4685 prediction_provenance_v3: RequiredNullable::Missing,
4686 checks_v3: None,
4687 prediction_provenance_v4: RequiredNullable::Missing,
4688 checks_v4: None,
4689 prediction_provenance_v5: RequiredNullable::Present(Some(provenance)),
4690 checks_v5,
4691 prediction_provenance_v6: RequiredNullable::Missing,
4692 checks_v6: None,
4693 })
4694}
4695
4696fn decode_prediction_phase_file_v17(
4697 command: &str,
4698 file_index: usize,
4699 raw: &RawValue,
4700) -> Result<MeasurementFileInput, MeasurementReportError> {
4701 #[derive(Deserialize)]
4702 struct SchemaProbe {
4703 schema: String,
4704 }
4705 let probe: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4706 prediction_file_error(
4707 file_index,
4708 MeasurementFileError::InvalidFileShape {
4709 reason: source.to_string(),
4710 },
4711 )
4712 })?;
4713 if command == "measure"
4714 || matches!(probe.prediction_provenance, RequiredNullable::Present(None))
4715 {
4716 return decode_prediction_phase_file_v16(command, file_index, raw);
4717 }
4718 let RequiredNullable::Present(Some(provenance_raw)) = &probe.prediction_provenance else {
4719 return decode_prediction_phase_file_v16(command, file_index, raw);
4720 };
4721 let schema = serde_json::from_str::<SchemaProbe>(provenance_raw.get()).map_err(|source| {
4722 prediction_file_error(
4723 file_index,
4724 MeasurementFileError::InvalidPredictionProvenanceShape {
4725 reason: source.to_string(),
4726 },
4727 )
4728 })?;
4729 if matches!(
4730 schema.schema.as_str(),
4731 crate::prediction::PREDICTION_PROVENANCE_V3_ID
4732 | crate::prediction::PREDICTION_PROVENANCE_V5_ID
4733 ) {
4734 return decode_prediction_phase_file_v16(command, file_index, raw);
4735 }
4736 if schema.schema != crate::prediction::PREDICTION_PROVENANCE_V6_ID {
4737 return Err(prediction_file_error(
4738 file_index,
4739 MeasurementFileError::InvalidPredictionProvenance {
4740 source: PredictionContractError::InvalidSchema {
4741 field: "prediction provenance.schema",
4742 expected: crate::prediction::PREDICTION_PROVENANCE_V6_ID,
4743 found: schema.schema,
4744 },
4745 },
4746 ));
4747 }
4748 let MeasurementFileWireInput {
4749 path,
4750 input,
4751 rig,
4752 measurements,
4753 prediction_provenance,
4754 checks,
4755 } = probe;
4756 let rig_v17 = serde_json::from_str::<RigInfo>(rig.get()).map_err(|source| {
4757 prediction_file_error(
4758 file_index,
4759 MeasurementFileError::InvalidFileShape {
4760 reason: format!("invalid output-v17 rig evidence: {source}"),
4761 },
4762 )
4763 })?;
4764 if checks
4765 .as_ref()
4766 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4767 {
4768 return Err(prediction_file_error(
4769 file_index,
4770 MeasurementFileError::TooManyChecks {
4771 found: checks.as_ref().map_or(0, Vec::len),
4772 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
4773 },
4774 ));
4775 }
4776 let RequiredNullable::Present(Some(raw_provenance)) = prediction_provenance else {
4777 unreachable!()
4778 };
4779 let provenance: PredictionProvenanceV6 =
4780 serde_json::from_str(raw_provenance.get()).map_err(|source| {
4781 prediction_file_error(
4782 file_index,
4783 MeasurementFileError::InvalidPredictionProvenanceShape {
4784 reason: source.to_string(),
4785 },
4786 )
4787 })?;
4788 provenance.validate().map_err(|source| {
4789 prediction_file_error(
4790 file_index,
4791 MeasurementFileError::InvalidPredictionProvenance { source },
4792 )
4793 })?;
4794 let mut decoded_facets = 0usize;
4795 let mut decoded_references = 0usize;
4796 let mut decoded_text = provenance.retained_text_bytes().map_err(|source| {
4797 prediction_file_error(
4798 file_index,
4799 MeasurementFileError::InvalidPredictionProvenance { source },
4800 )
4801 })?;
4802 let checks_v6 = checks
4803 .map(|raw_checks| {
4804 let mut decoded = Vec::with_capacity(raw_checks.len());
4805 for (check_index, raw_check) in raw_checks.into_iter().enumerate() {
4806 let wire: PredictionCheckWireInput = serde_json::from_str(raw_check.get())
4807 .map_err(|source| {
4808 prediction_file_error(
4809 file_index,
4810 MeasurementFileError::InvalidPredictionShape {
4811 check_index,
4812 reason: source.to_string(),
4813 },
4814 )
4815 })?;
4816 if (wire.selection == SelectionState::Unselected
4817 || wire.configuration == ConfigurationState::Disabled
4818 || wire.applicability == Applicability::NotApplicable)
4819 && wire.prediction.is_some()
4820 {
4821 return Err(prediction_file_error(
4822 file_index,
4823 MeasurementFileError::InvalidPredictionLifecycle {
4824 check_index,
4825 reason: "inactive check must have empty output",
4826 },
4827 ));
4828 }
4829 let prediction =
4830 wire.prediction
4831 .map(|raw_prediction| {
4832 serde_json::from_str::<EnginePredictionV6>(raw_prediction.get())
4833 .map_err(|source| {
4834 prediction_file_error(
4835 file_index,
4836 MeasurementFileError::InvalidPredictionShape {
4837 check_index,
4838 reason: source.to_string(),
4839 },
4840 )
4841 })
4842 })
4843 .transpose()?;
4844 let check = PredictionCheckInputV6 {
4845 check_id: wire.check_id,
4846 selection: wire.selection,
4847 configuration: wire.configuration,
4848 applicability: wire.applicability,
4849 evaluation: wire.evaluation,
4850 findings: wire.findings,
4851 evaluated_scopes: wire.evaluated_scopes,
4852 gaps: wire.gaps,
4853 prediction,
4854 };
4855 check
4856 .validate(check_index, Some(&provenance))
4857 .map_err(|source| prediction_file_error(file_index, source))?;
4858 if let Some(prediction) = &check.prediction {
4859 decoded_facets = decoded_facets
4860 .checked_add(prediction.facets().len())
4861 .ok_or_else(|| {
4862 prediction_file_error(
4863 file_index,
4864 MeasurementFileError::PredictionAccountingOverflow,
4865 )
4866 })?;
4867 decoded_references = decoded_references
4868 .checked_add(prediction.basis_reference_count())
4869 .ok_or_else(|| {
4870 prediction_file_error(
4871 file_index,
4872 MeasurementFileError::PredictionAccountingOverflow,
4873 )
4874 })?;
4875 decoded_text = decoded_text
4876 .checked_add(prediction.retained_text_bytes().map_err(|source| {
4877 prediction_file_error(
4878 file_index,
4879 MeasurementFileError::InvalidPrediction {
4880 check_index,
4881 source,
4882 },
4883 )
4884 })?)
4885 .ok_or_else(|| {
4886 prediction_file_error(
4887 file_index,
4888 MeasurementFileError::PredictionAccountingOverflow,
4889 )
4890 })?;
4891 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
4892 return Err(prediction_file_error(
4893 file_index,
4894 MeasurementFileError::TooManyPredictionFacets {
4895 found: decoded_facets,
4896 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
4897 },
4898 ));
4899 }
4900 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
4901 return Err(prediction_file_error(
4902 file_index,
4903 MeasurementFileError::TooManyPredictionBasisReferences {
4904 found: decoded_references,
4905 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
4906 },
4907 ));
4908 }
4909 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
4910 return Err(prediction_file_error(
4911 file_index,
4912 MeasurementFileError::TooMuchPredictionText {
4913 found: decoded_text,
4914 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
4915 },
4916 ));
4917 }
4918 }
4919 decoded.push(check);
4920 }
4921 Ok(decoded)
4922 })
4923 .transpose()?;
4924 Ok(MeasurementFileInput {
4925 path,
4926 input,
4927 rig_v17: Some(rig_v17),
4928 measurements,
4929 prediction_provenance: RequiredNullable::Missing,
4930 checks: None,
4931 legacy_prediction_provenance: RequiredNullable::Missing,
4932 legacy_checks: None,
4933 prediction_provenance_v3: RequiredNullable::Missing,
4934 checks_v3: None,
4935 prediction_provenance_v4: RequiredNullable::Missing,
4936 checks_v4: None,
4937 prediction_provenance_v5: RequiredNullable::Missing,
4938 checks_v5: None,
4939 prediction_provenance_v6: RequiredNullable::Present(Some(provenance)),
4940 checks_v6,
4941 })
4942}
4943
4944fn decode_legacy_v11_file(
4948 command: &str,
4949 file_index: usize,
4950 raw: &RawValue,
4951) -> Result<MeasurementFileInput, MeasurementReportError> {
4952 let wire: MeasurementFileWireInput = serde_json::from_str(raw.get()).map_err(|source| {
4953 prediction_file_error(
4954 file_index,
4955 MeasurementFileError::InvalidFileShape {
4956 reason: source.to_string(),
4957 },
4958 )
4959 })?;
4960 if command == "measure" {
4961 if !matches!(wire.prediction_provenance, RequiredNullable::Missing) {
4962 return Err(prediction_file_error(
4963 file_index,
4964 MeasurementFileError::UnexpectedPredictionProvenance,
4965 ));
4966 }
4967 if wire.checks.is_some() {
4968 return Err(prediction_file_error(
4969 file_index,
4970 MeasurementFileError::UnexpectedChecks,
4971 ));
4972 }
4973 return Ok(MeasurementFileInput {
4974 path: wire.path,
4975 input: wire.input,
4976 rig_v17: None,
4977 measurements: wire.measurements,
4978 prediction_provenance: RequiredNullable::Missing,
4979 checks: None,
4980 legacy_prediction_provenance: RequiredNullable::Missing,
4981 legacy_checks: None,
4982 prediction_provenance_v3: RequiredNullable::Missing,
4983 checks_v3: None,
4984 prediction_provenance_v4: RequiredNullable::Missing,
4985 checks_v4: None,
4986 prediction_provenance_v5: RequiredNullable::Missing,
4987 checks_v5: None,
4988 prediction_provenance_v6: RequiredNullable::Missing,
4989 checks_v6: None,
4990 });
4991 }
4992
4993 if wire
4994 .checks
4995 .as_ref()
4996 .is_some_and(|checks| checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE)
4997 {
4998 return Err(prediction_file_error(
4999 file_index,
5000 MeasurementFileError::TooManyChecks {
5001 found: wire.checks.as_ref().map_or(0, Vec::len),
5002 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
5003 },
5004 ));
5005 }
5006 if matches!(wire.prediction_provenance, RequiredNullable::Missing) {
5007 return Err(prediction_file_error(
5008 file_index,
5009 MeasurementFileError::MissingPredictionProvenance,
5010 ));
5011 }
5012 let legacy_prediction_provenance = match wire.prediction_provenance {
5013 RequiredNullable::Missing => unreachable!("missing provenance was rejected above"),
5014 RequiredNullable::Present(None) => RequiredNullable::Present(None),
5015 RequiredNullable::Present(Some(raw)) => RequiredNullable::Present(Some(
5016 decode_prediction_provenance_v1_with_measurement_schema(
5017 raw.get(),
5018 MEASUREMENTS_V15_SCHEMA_ID,
5019 )
5020 .map_err(|error| {
5021 prediction_file_error(
5022 file_index,
5023 match error {
5024 PredictionDecodeError::Shape(source) => {
5025 MeasurementFileError::InvalidPredictionProvenanceShape {
5026 reason: source.to_string(),
5027 }
5028 }
5029 PredictionDecodeError::Semantic(source) => {
5030 MeasurementFileError::InvalidPredictionProvenance { source }
5031 }
5032 PredictionDecodeError::TooManyFileFacets
5033 | PredictionDecodeError::TooManyFileBasisReferences => {
5034 unreachable!("provenance decoding cannot consume prediction budgets")
5035 }
5036 },
5037 )
5038 })?,
5039 )),
5040 };
5041 let mut decoded_facets = 0usize;
5042 let mut decoded_references = 0usize;
5043 let mut decoded_text = legacy_prediction_provenance
5044 .as_present()
5045 .map(PredictionProvenanceV1::retained_text_bytes)
5046 .transpose()
5047 .map_err(|source| {
5048 prediction_file_error(
5049 file_index,
5050 MeasurementFileError::InvalidPredictionProvenance { source },
5051 )
5052 })?
5053 .unwrap_or(0);
5054 let provenance_for_checks = legacy_prediction_provenance.as_present();
5055 let legacy_checks = wire
5056 .checks
5057 .map(|raw_checks| {
5058 let mut checks = Vec::with_capacity(raw_checks.len());
5059 for (check_index, raw) in raw_checks.into_iter().enumerate() {
5060 let wire: LegacyPredictionCheckWireV11 =
5061 serde_json::from_str(raw.get()).map_err(|source| {
5062 prediction_file_error(
5063 file_index,
5064 MeasurementFileError::InvalidPredictionShape {
5065 check_index,
5066 reason: source.to_string(),
5067 },
5068 )
5069 })?;
5070 if provenance_for_checks.is_none() && wire.prediction.is_some() {
5074 return Err(prediction_file_error(
5075 file_index,
5076 MeasurementFileError::PredictionWithoutProvenance { check_index },
5077 ));
5078 }
5079 if (wire.selection == SelectionState::Unselected
5080 || wire.configuration == ConfigurationState::Disabled
5081 || wire.applicability == Applicability::NotApplicable)
5082 && wire.prediction.is_some()
5083 {
5084 return Err(prediction_file_error(
5085 file_index,
5086 MeasurementFileError::InvalidPredictionLifecycle {
5087 check_index,
5088 reason: "inactive check must have empty output",
5089 },
5090 ));
5091 }
5092 let prediction = wire
5093 .prediction
5094 .map(|raw| {
5095 decode_engine_prediction_v1_with_measurement_schema(
5096 raw.get(),
5097 PREDICTION_V1_MAX_FACETS_PER_FILE.saturating_sub(decoded_facets),
5098 PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
5099 .saturating_sub(decoded_references),
5100 MEASUREMENTS_V15_SCHEMA_ID,
5101 )
5102 .map_err(|error| {
5103 prediction_file_error(
5104 file_index,
5105 match error {
5106 PredictionDecodeError::Shape(source) => {
5107 MeasurementFileError::InvalidPredictionShape {
5108 check_index,
5109 reason: source.to_string(),
5110 }
5111 }
5112 PredictionDecodeError::Semantic(source) => {
5113 MeasurementFileError::InvalidPrediction {
5114 check_index,
5115 source,
5116 }
5117 }
5118 PredictionDecodeError::TooManyFileFacets => {
5119 MeasurementFileError::TooManyPredictionFacets {
5120 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
5121 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5122 }
5123 }
5124 PredictionDecodeError::TooManyFileBasisReferences => {
5125 MeasurementFileError::TooManyPredictionBasisReferences {
5126 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
5127 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5128 }
5129 }
5130 },
5131 )
5132 })
5133 })
5134 .transpose()?;
5135 let check = LegacyPredictionCheckInput {
5136 check_id: wire.check_id,
5137 selection: wire.selection,
5138 configuration: wire.configuration,
5139 applicability: wire.applicability,
5140 evaluation: wire.evaluation,
5141 findings: wire.findings,
5142 evaluated_scopes: wire.evaluated_scopes,
5143 gaps: wire.gaps,
5144 prediction,
5145 };
5146 check
5147 .validate(
5148 check_index,
5149 provenance_for_checks,
5150 MEASUREMENTS_V15_SCHEMA_ID,
5151 )
5152 .map_err(|source| prediction_file_error(file_index, source))?;
5153 if let Some(prediction) = &check.prediction {
5154 decoded_facets = decoded_facets
5155 .checked_add(prediction.facets().len())
5156 .ok_or_else(|| {
5157 prediction_file_error(
5158 file_index,
5159 MeasurementFileError::PredictionAccountingOverflow,
5160 )
5161 })?;
5162 if decoded_facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5163 return Err(prediction_file_error(
5164 file_index,
5165 MeasurementFileError::TooManyPredictionFacets {
5166 found: decoded_facets,
5167 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5168 },
5169 ));
5170 }
5171 decoded_references = decoded_references
5172 .checked_add(prediction.basis_reference_count())
5173 .ok_or_else(|| {
5174 prediction_file_error(
5175 file_index,
5176 MeasurementFileError::PredictionAccountingOverflow,
5177 )
5178 })?;
5179 if decoded_references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5180 return Err(prediction_file_error(
5181 file_index,
5182 MeasurementFileError::TooManyPredictionBasisReferences {
5183 found: decoded_references,
5184 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5185 },
5186 ));
5187 }
5188 decoded_text = decoded_text
5189 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5190 prediction_file_error(
5191 file_index,
5192 MeasurementFileError::InvalidPrediction {
5193 check_index,
5194 source,
5195 },
5196 )
5197 })?)
5198 .ok_or_else(|| {
5199 prediction_file_error(
5200 file_index,
5201 MeasurementFileError::PredictionAccountingOverflow,
5202 )
5203 })?;
5204 if decoded_text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5205 return Err(prediction_file_error(
5206 file_index,
5207 MeasurementFileError::TooMuchPredictionText {
5208 found: decoded_text,
5209 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5210 },
5211 ));
5212 }
5213 }
5214 checks.push(check);
5215 }
5216 Ok(checks)
5217 })
5218 .transpose()?;
5219 Ok(MeasurementFileInput {
5220 path: wire.path,
5221 input: wire.input,
5222 rig_v17: None,
5223 measurements: wire.measurements,
5224 prediction_provenance: RequiredNullable::Missing,
5225 checks: None,
5226 legacy_prediction_provenance,
5227 legacy_checks,
5228 prediction_provenance_v3: RequiredNullable::Missing,
5229 checks_v3: None,
5230 prediction_provenance_v4: RequiredNullable::Missing,
5231 checks_v4: None,
5232 prediction_provenance_v5: RequiredNullable::Missing,
5233 checks_v5: None,
5234 prediction_provenance_v6: RequiredNullable::Missing,
5235 checks_v6: None,
5236 })
5237}
5238
5239fn validate_legacy_v11_prediction_phase_file(
5240 command: &str,
5241 file_index: usize,
5242 file: &MeasurementFileInput,
5243) -> Result<(usize, usize), MeasurementReportError> {
5244 match command {
5245 "measure" => return Ok((0, 0)),
5246 "lint" => {}
5247 _ => unreachable!("command was validated before prediction phase"),
5248 }
5249 let provenance = match &file.legacy_prediction_provenance {
5250 RequiredNullable::Missing => {
5251 return Err(prediction_file_error(
5252 file_index,
5253 MeasurementFileError::MissingPredictionProvenance,
5254 ));
5255 }
5256 RequiredNullable::Present(provenance) => provenance.as_ref(),
5257 };
5258 let checks = file
5259 .legacy_checks
5260 .as_ref()
5261 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5262 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
5263 return Err(prediction_file_error(
5264 file_index,
5265 MeasurementFileError::TooManyChecks {
5266 found: checks.len(),
5267 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
5268 },
5269 ));
5270 }
5271 if let Some(provenance) = provenance {
5272 provenance
5273 .validate_with_measurement_schema(MEASUREMENTS_V15_SCHEMA_ID)
5274 .map_err(|source| {
5275 prediction_file_error(
5276 file_index,
5277 MeasurementFileError::InvalidPredictionProvenance { source },
5278 )
5279 })?;
5280 let input = file
5281 .input
5282 .as_ref()
5283 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5284 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5285 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5286 {
5287 return Err(prediction_file_error(
5288 file_index,
5289 MeasurementFileError::PredictionPrimaryInputMismatch,
5290 ));
5291 }
5292 }
5293 let mut facets = 0usize;
5294 let mut references = 0usize;
5295 let mut text = provenance
5296 .map(PredictionProvenanceV1::retained_text_bytes)
5297 .transpose()
5298 .map_err(|source| {
5299 prediction_file_error(
5300 file_index,
5301 MeasurementFileError::InvalidPredictionProvenance { source },
5302 )
5303 })?
5304 .unwrap_or(0);
5305 let mut available = 0usize;
5306 let mut unavailable = 0usize;
5307 for (check_index, check) in checks.iter().enumerate() {
5308 if let Some(prediction) = &check.prediction {
5309 facets = facets
5310 .checked_add(prediction.facets().len())
5311 .ok_or_else(|| {
5312 prediction_file_error(
5313 file_index,
5314 MeasurementFileError::PredictionAccountingOverflow,
5315 )
5316 })?;
5317 references = references
5318 .checked_add(prediction.basis_reference_count())
5319 .ok_or_else(|| {
5320 prediction_file_error(
5321 file_index,
5322 MeasurementFileError::PredictionAccountingOverflow,
5323 )
5324 })?;
5325 text = text
5326 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5327 prediction_file_error(
5328 file_index,
5329 MeasurementFileError::InvalidPrediction {
5330 check_index,
5331 source,
5332 },
5333 )
5334 })?)
5335 .ok_or_else(|| {
5336 prediction_file_error(
5337 file_index,
5338 MeasurementFileError::PredictionAccountingOverflow,
5339 )
5340 })?;
5341 for facet in prediction.facets() {
5342 match facet.state() {
5343 EnginePredictionFacetStateV1::Available => {
5344 available = available
5345 .checked_add(1)
5346 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
5347 }
5348 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
5349 unavailable = unavailable
5350 .checked_add(1)
5351 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
5352 }
5353 }
5354 }
5355 }
5356 }
5357 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5358 return Err(prediction_file_error(
5359 file_index,
5360 MeasurementFileError::TooManyPredictionFacets {
5361 found: facets,
5362 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5363 },
5364 ));
5365 }
5366 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5367 return Err(prediction_file_error(
5368 file_index,
5369 MeasurementFileError::TooManyPredictionBasisReferences {
5370 found: references,
5371 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5372 },
5373 ));
5374 }
5375 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5376 return Err(prediction_file_error(
5377 file_index,
5378 MeasurementFileError::TooMuchPredictionText {
5379 found: text,
5380 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5381 },
5382 ));
5383 }
5384 Ok((available, unavailable))
5385}
5386
5387impl LegacyPredictionCheckInput {
5388 fn validate(
5389 &self,
5390 check_index: usize,
5391 provenance: Option<&PredictionProvenanceV1>,
5392 expected_measurement_schema: &'static str,
5393 ) -> Result<(), MeasurementFileError> {
5394 let gap_refs = self
5395 .gaps
5396 .iter()
5397 .map(|gap| CheckEvaluationGapRef {
5398 code: &gap.code,
5399 scope: gap.scope.as_ref(),
5400 })
5401 .collect::<Vec<_>>();
5402 let finding_check_ids = self
5403 .findings
5404 .iter()
5405 .map(|finding| finding.check_id.as_str())
5406 .collect::<Vec<_>>();
5407 let prediction_scopes = self
5408 .prediction
5409 .as_ref()
5410 .into_iter()
5411 .flat_map(EnginePredictionV1::facets)
5412 .map(|facet| facet.scope())
5413 .collect::<Vec<_>>();
5414 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
5415 check_id: &self.check_id,
5416 selection: self.selection,
5417 configuration: self.configuration,
5418 applicability: self.applicability,
5419 finding_check_ids: &finding_check_ids,
5420 evaluated_scopes: &self.evaluated_scopes,
5421 gaps: &gap_refs,
5422 prediction_scopes: &prediction_scopes,
5423 has_prediction: self.prediction.is_some(),
5424 prediction_has_required_unavailable: self
5425 .prediction
5426 .as_ref()
5427 .is_some_and(EnginePredictionV1::has_required_unavailable),
5428 })
5429 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
5430 check_index,
5431 reason: error.reason(),
5432 })?;
5433 if self.evaluation != derived {
5434 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5435 check_index,
5436 reason: "evaluation does not match completed and missing prediction work",
5437 });
5438 }
5439 let Some(prediction) = &self.prediction else {
5440 if self
5441 .findings
5442 .iter()
5443 .any(|finding| finding.prediction_scope.is_some())
5444 {
5445 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5446 check_index,
5447 reason: "finding has prediction_scope without prediction",
5448 });
5449 }
5450 return Ok(());
5451 };
5452 let provenance =
5453 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
5454 prediction
5455 .validate_against_provenance_with_measurement_schema(
5456 provenance,
5457 expected_measurement_schema,
5458 )
5459 .map_err(|source| MeasurementFileError::InvalidPrediction {
5460 check_index,
5461 source,
5462 })?;
5463 for facet in prediction.facets() {
5464 let evaluated = self
5465 .evaluated_scopes
5466 .iter()
5467 .filter(|scope| *scope == facet.scope())
5468 .count();
5469 let duplicated_gap = self
5470 .gaps
5471 .iter()
5472 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
5473 match facet.state() {
5474 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
5475 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5476 check_index,
5477 reason: "available facet scope must occur exactly once in evaluated_scopes",
5478 });
5479 }
5480 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
5481 if evaluated != 0 || duplicated_gap =>
5482 {
5483 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5484 check_index,
5485 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
5486 });
5487 }
5488 _ => {}
5489 }
5490 }
5491 for finding in &self.findings {
5492 let Some(scope) = &finding.prediction_scope else {
5493 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5494 check_index,
5495 reason: "prediction-backed finding must carry prediction_scope",
5496 });
5497 };
5498 if prediction
5499 .facets()
5500 .iter()
5501 .filter(|facet| {
5502 facet.scope() == scope
5503 && facet.state() == EnginePredictionFacetStateV1::Available
5504 })
5505 .count()
5506 != 1
5507 {
5508 return Err(MeasurementFileError::InvalidPredictionLifecycle {
5509 check_index,
5510 reason: "finding prediction_scope must name one available facet",
5511 });
5512 }
5513 }
5514 Ok(())
5515 }
5516}
5517
5518fn validate_prediction_phase_file(
5519 command: &str,
5520 file_index: usize,
5521 file: &MeasurementFileInput,
5522 expected_measurement_schema: &'static str,
5523) -> Result<(usize, usize), MeasurementReportError> {
5524 let mut available = 0usize;
5525 let mut unavailable = 0usize;
5526 match command {
5527 "measure" => {
5528 if !matches!(file.prediction_provenance, RequiredNullable::Missing) {
5529 return Err(prediction_file_error(
5530 file_index,
5531 MeasurementFileError::UnexpectedPredictionProvenance,
5532 ));
5533 }
5534 if file.checks.is_some() {
5535 return Err(prediction_file_error(
5536 file_index,
5537 MeasurementFileError::UnexpectedChecks,
5538 ));
5539 }
5540 }
5541 "lint" => {
5542 let provenance = match &file.prediction_provenance {
5543 RequiredNullable::Missing => {
5544 return Err(prediction_file_error(
5545 file_index,
5546 MeasurementFileError::MissingPredictionProvenance,
5547 ));
5548 }
5549 RequiredNullable::Present(provenance) => provenance.as_ref(),
5550 };
5551 let checks = file.checks.as_ref().ok_or_else(|| {
5552 prediction_file_error(file_index, MeasurementFileError::MissingChecks)
5553 })?;
5554 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
5555 return Err(prediction_file_error(
5556 file_index,
5557 MeasurementFileError::TooManyChecks {
5558 found: checks.len(),
5559 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
5560 },
5561 ));
5562 }
5563 if let Some(provenance) = provenance {
5564 provenance
5565 .validate_with_measurement_schema(expected_measurement_schema)
5566 .map_err(|source| {
5567 prediction_file_error(
5568 file_index,
5569 MeasurementFileError::InvalidPredictionProvenance { source },
5570 )
5571 })?;
5572 let input = file.input.as_ref().ok_or_else(|| {
5573 prediction_file_error(file_index, MeasurementFileError::MissingInput)
5574 })?;
5575 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5576 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5577 {
5578 return Err(prediction_file_error(
5579 file_index,
5580 MeasurementFileError::PredictionPrimaryInputMismatch,
5581 ));
5582 }
5583 }
5584
5585 let mut facets = 0usize;
5586 let mut references = 0usize;
5587 let mut text = provenance
5588 .map(PredictionProvenanceV2::retained_text_bytes)
5589 .transpose()
5590 .map_err(|source| {
5591 prediction_file_error(
5592 file_index,
5593 MeasurementFileError::InvalidPredictionProvenance { source },
5594 )
5595 })?
5596 .unwrap_or(0);
5597 for (check_index, check) in checks.iter().enumerate() {
5598 if let Some(prediction) = &check.prediction {
5599 facets = facets
5600 .checked_add(prediction.facets().len())
5601 .ok_or_else(|| {
5602 prediction_file_error(
5603 file_index,
5604 MeasurementFileError::PredictionAccountingOverflow,
5605 )
5606 })?;
5607 references = references
5608 .checked_add(prediction.basis_reference_count())
5609 .ok_or_else(|| {
5610 prediction_file_error(
5611 file_index,
5612 MeasurementFileError::PredictionAccountingOverflow,
5613 )
5614 })?;
5615 text = text
5616 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5617 prediction_file_error(
5618 file_index,
5619 MeasurementFileError::InvalidPrediction {
5620 check_index,
5621 source,
5622 },
5623 )
5624 })?)
5625 .ok_or_else(|| {
5626 prediction_file_error(
5627 file_index,
5628 MeasurementFileError::PredictionAccountingOverflow,
5629 )
5630 })?;
5631 for facet in prediction.facets() {
5632 match facet.state() {
5633 EnginePredictionFacetStateV1::Available => {
5634 available = available.checked_add(1).ok_or(
5635 MeasurementReportError::PredictionFacetSummaryMismatch,
5636 )?;
5637 }
5638 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
5639 unavailable = unavailable.checked_add(1).ok_or(
5640 MeasurementReportError::PredictionFacetSummaryMismatch,
5641 )?;
5642 }
5643 }
5644 }
5645 }
5646 }
5647 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5648 return Err(prediction_file_error(
5649 file_index,
5650 MeasurementFileError::TooManyPredictionFacets {
5651 found: facets,
5652 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5653 },
5654 ));
5655 }
5656 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5657 return Err(prediction_file_error(
5658 file_index,
5659 MeasurementFileError::TooManyPredictionBasisReferences {
5660 found: references,
5661 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5662 },
5663 ));
5664 }
5665 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5666 return Err(prediction_file_error(
5667 file_index,
5668 MeasurementFileError::TooMuchPredictionText {
5669 found: text,
5670 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5671 },
5672 ));
5673 }
5674 }
5675 _ => unreachable!("command was validated before prediction phase"),
5676 }
5677 Ok((available, unavailable))
5678}
5679
5680fn validate_prediction_phase_file_v14(
5681 command: &str,
5682 file_index: usize,
5683 file: &MeasurementFileInput,
5684) -> Result<(usize, usize), MeasurementReportError> {
5685 if command == "measure" {
5686 if !matches!(file.prediction_provenance_v3, RequiredNullable::Missing) {
5687 return Err(prediction_file_error(
5688 file_index,
5689 MeasurementFileError::UnexpectedPredictionProvenance,
5690 ));
5691 }
5692 if file.checks_v3.is_some() {
5693 return Err(prediction_file_error(
5694 file_index,
5695 MeasurementFileError::UnexpectedChecks,
5696 ));
5697 }
5698 return Ok((0, 0));
5699 }
5700 let provenance = match &file.prediction_provenance_v3 {
5701 RequiredNullable::Missing => {
5702 return Err(prediction_file_error(
5703 file_index,
5704 MeasurementFileError::MissingPredictionProvenance,
5705 ));
5706 }
5707 RequiredNullable::Present(provenance) => provenance.as_ref(),
5708 };
5709 let checks = file
5710 .checks_v3
5711 .as_ref()
5712 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5713 if let Some(provenance) = provenance {
5714 provenance.validate().map_err(|source| {
5715 prediction_file_error(
5716 file_index,
5717 MeasurementFileError::InvalidPredictionProvenance { source },
5718 )
5719 })?;
5720 let input = file
5721 .input
5722 .as_ref()
5723 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5724 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5725 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5726 {
5727 return Err(prediction_file_error(
5728 file_index,
5729 MeasurementFileError::PredictionPrimaryInputMismatch,
5730 ));
5731 }
5732 }
5733 let mut available = 0usize;
5734 let mut unavailable = 0usize;
5735 let mut facets = 0usize;
5736 let mut references = 0usize;
5737 let mut text = provenance
5738 .map(PredictionProvenanceV3::retained_text_bytes)
5739 .transpose()
5740 .map_err(|source| {
5741 prediction_file_error(
5742 file_index,
5743 MeasurementFileError::InvalidPredictionProvenance { source },
5744 )
5745 })?
5746 .unwrap_or(0);
5747 for (check_index, check) in checks.iter().enumerate() {
5748 check
5749 .validate(check_index, provenance)
5750 .map_err(|source| prediction_file_error(file_index, source))?;
5751 if let Some(prediction) = &check.prediction {
5752 facets = facets
5753 .checked_add(prediction.facets().len())
5754 .ok_or_else(|| {
5755 prediction_file_error(
5756 file_index,
5757 MeasurementFileError::PredictionAccountingOverflow,
5758 )
5759 })?;
5760 references = references
5761 .checked_add(prediction.basis_reference_count())
5762 .ok_or_else(|| {
5763 prediction_file_error(
5764 file_index,
5765 MeasurementFileError::PredictionAccountingOverflow,
5766 )
5767 })?;
5768 text = text
5769 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5770 prediction_file_error(
5771 file_index,
5772 MeasurementFileError::InvalidPrediction {
5773 check_index,
5774 source,
5775 },
5776 )
5777 })?)
5778 .ok_or_else(|| {
5779 prediction_file_error(
5780 file_index,
5781 MeasurementFileError::PredictionAccountingOverflow,
5782 )
5783 })?;
5784 for facet in prediction.facets() {
5785 match facet.state() {
5786 EnginePredictionFacetStateV1::Available => available += 1,
5787 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
5788 }
5789 }
5790 }
5791 }
5792 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5793 return Err(prediction_file_error(
5794 file_index,
5795 MeasurementFileError::TooManyPredictionFacets {
5796 found: facets,
5797 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5798 },
5799 ));
5800 }
5801 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5802 return Err(prediction_file_error(
5803 file_index,
5804 MeasurementFileError::TooManyPredictionBasisReferences {
5805 found: references,
5806 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5807 },
5808 ));
5809 }
5810 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5811 return Err(prediction_file_error(
5812 file_index,
5813 MeasurementFileError::TooMuchPredictionText {
5814 found: text,
5815 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5816 },
5817 ));
5818 }
5819 Ok((available, unavailable))
5820}
5821
5822fn validate_prediction_phase_file_v15(
5823 command: &str,
5824 file_index: usize,
5825 file: &MeasurementFileInput,
5826) -> Result<(usize, usize), MeasurementReportError> {
5827 if matches!(file.prediction_provenance_v4, RequiredNullable::Missing) {
5828 return validate_prediction_phase_file_v14(command, file_index, file);
5829 }
5830 if command == "measure" {
5831 return Err(prediction_file_error(
5832 file_index,
5833 MeasurementFileError::UnexpectedPredictionProvenance,
5834 ));
5835 }
5836 let provenance = match &file.prediction_provenance_v4 {
5837 RequiredNullable::Present(provenance) => provenance.as_ref(),
5838 RequiredNullable::Missing => unreachable!(),
5839 };
5840 let checks = file
5841 .checks_v4
5842 .as_ref()
5843 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5844 if let Some(provenance) = provenance {
5845 provenance.validate().map_err(|source| {
5846 prediction_file_error(
5847 file_index,
5848 MeasurementFileError::InvalidPredictionProvenance { source },
5849 )
5850 })?;
5851 let input = file
5852 .input
5853 .as_ref()
5854 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5855 if input.sha256.as_deref() != Some(provenance.raw_source().primary_input().sha256())
5856 || input.bytes != Some(provenance.raw_source().primary_input().bytes())
5857 {
5858 return Err(prediction_file_error(
5859 file_index,
5860 MeasurementFileError::PredictionPrimaryInputMismatch,
5861 ));
5862 }
5863 }
5864 let mut available = 0usize;
5865 let mut unavailable = 0usize;
5866 let mut has_facet_budget_summary = false;
5867 let mut facets = 0usize;
5868 let mut references = 0usize;
5869 let mut text = provenance
5870 .map(PredictionProvenanceV4::retained_text_bytes)
5871 .transpose()
5872 .map_err(|source| {
5873 prediction_file_error(
5874 file_index,
5875 MeasurementFileError::InvalidPredictionProvenance { source },
5876 )
5877 })?
5878 .unwrap_or(0);
5879 for (check_index, check) in checks.iter().enumerate() {
5880 check
5881 .validate(check_index, provenance)
5882 .map_err(|source| prediction_file_error(file_index, source))?;
5883 if let Some(prediction) = &check.prediction {
5884 has_facet_budget_summary |= prediction.has_facet_budget_summary();
5885 facets = facets
5886 .checked_add(prediction.facets().len())
5887 .ok_or_else(|| {
5888 prediction_file_error(
5889 file_index,
5890 MeasurementFileError::PredictionAccountingOverflow,
5891 )
5892 })?;
5893 references = references
5894 .checked_add(prediction.basis_reference_count())
5895 .ok_or_else(|| {
5896 prediction_file_error(
5897 file_index,
5898 MeasurementFileError::PredictionAccountingOverflow,
5899 )
5900 })?;
5901 text = text
5902 .checked_add(prediction.retained_text_bytes().map_err(|source| {
5903 prediction_file_error(
5904 file_index,
5905 MeasurementFileError::InvalidPrediction {
5906 check_index,
5907 source,
5908 },
5909 )
5910 })?)
5911 .ok_or_else(|| {
5912 prediction_file_error(
5913 file_index,
5914 MeasurementFileError::PredictionAccountingOverflow,
5915 )
5916 })?;
5917 for facet in prediction.facets() {
5918 match facet.state() {
5919 EnginePredictionFacetStateV1::Available => available += 1,
5920 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
5921 }
5922 }
5923 }
5924 }
5925 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
5926 return Err(prediction_file_error(
5927 file_index,
5928 MeasurementFileError::TooManyPredictionFacets {
5929 found: facets,
5930 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5931 },
5932 ));
5933 }
5934 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
5935 return Err(prediction_file_error(
5936 file_index,
5937 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
5938 found: facets,
5939 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
5940 },
5941 ));
5942 }
5943 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
5944 return Err(prediction_file_error(
5945 file_index,
5946 MeasurementFileError::TooManyPredictionBasisReferences {
5947 found: references,
5948 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
5949 },
5950 ));
5951 }
5952 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
5953 return Err(prediction_file_error(
5954 file_index,
5955 MeasurementFileError::TooMuchPredictionText {
5956 found: text,
5957 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
5958 },
5959 ));
5960 }
5961 Ok((available, unavailable))
5962}
5963
5964fn validate_prediction_phase_file_v16(
5965 command: &str,
5966 file_index: usize,
5967 file: &MeasurementFileInput,
5968) -> Result<(usize, usize), MeasurementReportError> {
5969 if matches!(file.prediction_provenance_v5, RequiredNullable::Missing) {
5970 return validate_prediction_phase_file_v15(command, file_index, file);
5971 }
5972 if command == "measure" {
5973 return Err(prediction_file_error(
5974 file_index,
5975 MeasurementFileError::UnexpectedPredictionProvenance,
5976 ));
5977 }
5978 let provenance = match &file.prediction_provenance_v5 {
5979 RequiredNullable::Present(provenance) => provenance.as_ref(),
5980 RequiredNullable::Missing => unreachable!(),
5981 };
5982 let checks = file
5983 .checks_v5
5984 .as_ref()
5985 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
5986 if let Some(provenance) = provenance {
5987 provenance.validate().map_err(|source| {
5988 prediction_file_error(
5989 file_index,
5990 MeasurementFileError::InvalidPredictionProvenance { source },
5991 )
5992 })?;
5993 let input = file
5994 .input
5995 .as_ref()
5996 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
5997 if input.sha256.as_deref()
5998 != Some(provenance.raw_animation_channels().primary_input().sha256())
5999 || input.bytes != Some(provenance.raw_animation_channels().primary_input().bytes())
6000 {
6001 return Err(prediction_file_error(
6002 file_index,
6003 MeasurementFileError::PredictionPrimaryInputMismatch,
6004 ));
6005 }
6006 }
6007 let mut available = 0usize;
6008 let mut unavailable = 0usize;
6009 let mut facets = 0usize;
6010 let mut references = 0usize;
6011 let mut has_facet_budget_summary = false;
6012 let mut text = provenance
6013 .map(PredictionProvenanceV5::retained_text_bytes)
6014 .transpose()
6015 .map_err(|source| {
6016 prediction_file_error(
6017 file_index,
6018 MeasurementFileError::InvalidPredictionProvenance { source },
6019 )
6020 })?
6021 .unwrap_or(0);
6022 for (check_index, check) in checks.iter().enumerate() {
6023 check
6024 .validate(check_index, provenance)
6025 .map_err(|source| prediction_file_error(file_index, source))?;
6026 validate_current_engine_track_support_prediction_v5(
6027 &check.check_id,
6028 check.selection,
6029 check.configuration,
6030 check.applicability,
6031 check.prediction.as_ref(),
6032 provenance,
6033 check.findings.is_empty(),
6034 )
6035 .map_err(|source| {
6036 prediction_file_error(
6037 file_index,
6038 MeasurementFileError::InvalidPrediction {
6039 check_index,
6040 source,
6041 },
6042 )
6043 })?;
6044 if let Some(prediction) = &check.prediction {
6045 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
6046 facets = facets
6047 .checked_add(prediction.facets().len())
6048 .ok_or_else(|| {
6049 prediction_file_error(
6050 file_index,
6051 MeasurementFileError::PredictionAccountingOverflow,
6052 )
6053 })?;
6054 references = references
6055 .checked_add(prediction.basis_reference_count())
6056 .ok_or_else(|| {
6057 prediction_file_error(
6058 file_index,
6059 MeasurementFileError::PredictionAccountingOverflow,
6060 )
6061 })?;
6062 text = text
6063 .checked_add(prediction.retained_text_bytes().map_err(|source| {
6064 prediction_file_error(
6065 file_index,
6066 MeasurementFileError::InvalidPrediction {
6067 check_index,
6068 source,
6069 },
6070 )
6071 })?)
6072 .ok_or_else(|| {
6073 prediction_file_error(
6074 file_index,
6075 MeasurementFileError::PredictionAccountingOverflow,
6076 )
6077 })?;
6078 for facet in prediction.facets() {
6079 match facet.state() {
6080 EnginePredictionFacetStateV1::Available => available += 1,
6081 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
6082 }
6083 }
6084 }
6085 }
6086 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
6087 return Err(prediction_file_error(
6088 file_index,
6089 MeasurementFileError::TooManyPredictionFacets {
6090 found: facets,
6091 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6092 },
6093 ));
6094 }
6095 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
6096 return Err(prediction_file_error(
6097 file_index,
6098 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
6099 found: facets,
6100 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6101 },
6102 ));
6103 }
6104 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
6105 return Err(prediction_file_error(
6106 file_index,
6107 MeasurementFileError::TooManyPredictionBasisReferences {
6108 found: references,
6109 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
6110 },
6111 ));
6112 }
6113 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
6114 return Err(prediction_file_error(
6115 file_index,
6116 MeasurementFileError::TooMuchPredictionText {
6117 found: text,
6118 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
6119 },
6120 ));
6121 }
6122 Ok((available, unavailable))
6123}
6124
6125fn validate_prediction_phase_file_v17(
6126 command: &str,
6127 file_index: usize,
6128 file: &MeasurementFileInput,
6129) -> Result<(usize, usize), MeasurementReportError> {
6130 if matches!(file.prediction_provenance_v6, RequiredNullable::Missing) {
6131 return validate_prediction_phase_file_v16(command, file_index, file);
6132 }
6133 if command == "measure" {
6134 return Err(prediction_file_error(
6135 file_index,
6136 MeasurementFileError::UnexpectedPredictionProvenance,
6137 ));
6138 }
6139 let provenance = match &file.prediction_provenance_v6 {
6140 RequiredNullable::Present(provenance) => provenance.as_ref(),
6141 RequiredNullable::Missing => unreachable!(),
6142 };
6143 let checks = file
6144 .checks_v6
6145 .as_ref()
6146 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingChecks))?;
6147 if let Some(provenance) = provenance {
6148 provenance.validate().map_err(|source| {
6149 prediction_file_error(
6150 file_index,
6151 MeasurementFileError::InvalidPredictionProvenance { source },
6152 )
6153 })?;
6154 let input = file
6155 .input
6156 .as_ref()
6157 .ok_or_else(|| prediction_file_error(file_index, MeasurementFileError::MissingInput))?;
6158 let primary = provenance.raw_transform_paths().primary_input();
6159 if input.sha256.as_deref() != Some(primary.sha256()) || input.bytes != Some(primary.bytes())
6160 {
6161 return Err(prediction_file_error(
6162 file_index,
6163 MeasurementFileError::PredictionPrimaryInputMismatch,
6164 ));
6165 }
6166 }
6167 let mut available = 0usize;
6168 let mut unavailable = 0usize;
6169 let mut facets = 0usize;
6170 let mut references = 0usize;
6171 let mut has_facet_budget_summary = false;
6172 let mut text = provenance
6173 .map(PredictionProvenanceV6::retained_text_bytes)
6174 .transpose()
6175 .map_err(|source| {
6176 prediction_file_error(
6177 file_index,
6178 MeasurementFileError::InvalidPredictionProvenance { source },
6179 )
6180 })?
6181 .unwrap_or(0);
6182 for (check_index, check) in checks.iter().enumerate() {
6183 check
6184 .validate(check_index, provenance)
6185 .map_err(|source| prediction_file_error(file_index, source))?;
6186 if let Some(prediction) = &check.prediction {
6187 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
6188 facets = facets
6189 .checked_add(prediction.facets().len())
6190 .ok_or_else(|| {
6191 prediction_file_error(
6192 file_index,
6193 MeasurementFileError::PredictionAccountingOverflow,
6194 )
6195 })?;
6196 references = references
6197 .checked_add(prediction.basis_reference_count())
6198 .ok_or_else(|| {
6199 prediction_file_error(
6200 file_index,
6201 MeasurementFileError::PredictionAccountingOverflow,
6202 )
6203 })?;
6204 text = text
6205 .checked_add(prediction.retained_text_bytes().map_err(|source| {
6206 prediction_file_error(
6207 file_index,
6208 MeasurementFileError::InvalidPrediction {
6209 check_index,
6210 source,
6211 },
6212 )
6213 })?)
6214 .ok_or_else(|| {
6215 prediction_file_error(
6216 file_index,
6217 MeasurementFileError::PredictionAccountingOverflow,
6218 )
6219 })?;
6220 for facet in prediction.facets() {
6221 match facet.state() {
6222 EnginePredictionFacetStateV1::Available => available += 1,
6223 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => unavailable += 1,
6224 }
6225 }
6226 }
6227 }
6228 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
6229 return Err(prediction_file_error(
6230 file_index,
6231 MeasurementFileError::TooManyPredictionFacets {
6232 found: facets,
6233 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6234 },
6235 ));
6236 }
6237 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
6238 return Err(prediction_file_error(
6239 file_index,
6240 MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
6241 found: facets,
6242 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
6243 },
6244 ));
6245 }
6246 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
6247 return Err(prediction_file_error(
6248 file_index,
6249 MeasurementFileError::TooManyPredictionBasisReferences {
6250 found: references,
6251 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
6252 },
6253 ));
6254 }
6255 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
6256 return Err(prediction_file_error(
6257 file_index,
6258 MeasurementFileError::TooMuchPredictionText {
6259 found: text,
6260 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
6261 },
6262 ));
6263 }
6264 Ok((available, unavailable))
6265}
6266
6267fn validate_prediction_summary(
6268 command: &str,
6269 summary: Option<&MeasurementReportSummaryInput>,
6270 available: usize,
6271 unavailable: usize,
6272) -> Result<(), MeasurementReportError> {
6273 let summary = summary.and_then(|summary| summary.prediction_facets.as_ref());
6274 match (command, summary) {
6275 ("measure", Some(_)) => Err(MeasurementReportError::UnexpectedPredictionFacetSummary),
6276 ("measure", None) => Ok(()),
6277 ("lint", None) => Err(MeasurementReportError::MissingPredictionFacetSummary),
6278 ("lint", Some(summary))
6279 if summary.available != available
6280 || summary.required_prediction_unavailable != unavailable =>
6281 {
6282 Err(MeasurementReportError::PredictionFacetSummaryMismatch)
6283 }
6284 ("lint", Some(_)) => Ok(()),
6285 _ => unreachable!("command was validated before prediction summary"),
6286 }
6287}
6288
6289fn validate_prediction_summary_presence(
6290 command: &str,
6291 summary: Option<&MeasurementReportSummaryInput>,
6292) -> Result<(), MeasurementReportError> {
6293 match (
6294 command,
6295 summary.and_then(|summary| summary.prediction_facets.as_ref()),
6296 ) {
6297 ("measure", Some(_)) => Err(MeasurementReportError::UnexpectedPredictionFacetSummary),
6298 ("lint", None) => Err(MeasurementReportError::MissingPredictionFacetSummary),
6299 ("measure", None) | ("lint", Some(_)) => Ok(()),
6300 _ => unreachable!("command was validated before prediction summary"),
6301 }
6302}
6303
6304impl PredictionCheckInput {
6305 fn validate(
6306 &self,
6307 check_index: usize,
6308 provenance: Option<&PredictionProvenanceV2>,
6309 expected_measurement_schema: &'static str,
6310 ) -> Result<(), MeasurementFileError> {
6311 let gap_refs = self
6312 .gaps
6313 .iter()
6314 .map(|gap| CheckEvaluationGapRef {
6315 code: &gap.code,
6316 scope: gap.scope.as_ref(),
6317 })
6318 .collect::<Vec<_>>();
6319 let finding_check_ids = self
6320 .findings
6321 .iter()
6322 .map(|finding| finding.check_id.as_str())
6323 .collect::<Vec<_>>();
6324 let prediction_scopes = self
6325 .prediction
6326 .as_ref()
6327 .into_iter()
6328 .flat_map(EnginePredictionV2::facets)
6329 .map(|facet| facet.scope())
6330 .collect::<Vec<_>>();
6331 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6332 check_id: &self.check_id,
6333 selection: self.selection,
6334 configuration: self.configuration,
6335 applicability: self.applicability,
6336 finding_check_ids: &finding_check_ids,
6337 evaluated_scopes: &self.evaluated_scopes,
6338 gaps: &gap_refs,
6339 prediction_scopes: &prediction_scopes,
6340 has_prediction: self.prediction.is_some(),
6341 prediction_has_required_unavailable: self
6342 .prediction
6343 .as_ref()
6344 .is_some_and(EnginePredictionV2::has_required_unavailable),
6345 })
6346 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6347 check_index,
6348 reason: error.reason(),
6349 })?;
6350 if self.evaluation != derived {
6351 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6352 check_index,
6353 reason: "evaluation does not match completed and missing prediction work",
6354 });
6355 }
6356
6357 let Some(prediction) = &self.prediction else {
6358 if self
6359 .findings
6360 .iter()
6361 .any(|finding| finding.prediction_scope.is_some())
6362 {
6363 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6364 check_index,
6365 reason: "finding has prediction_scope without prediction",
6366 });
6367 }
6368 return Ok(());
6369 };
6370 let provenance =
6371 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6372 prediction
6373 .validate_against_provenance_with_measurement_schema(
6374 provenance,
6375 expected_measurement_schema,
6376 )
6377 .map_err(|source| MeasurementFileError::InvalidPrediction {
6378 check_index,
6379 source,
6380 })?;
6381 prediction
6382 .validate_facet_budget_summary_for_check(&self.check_id)
6383 .map_err(|source| MeasurementFileError::InvalidPrediction {
6384 check_index,
6385 source,
6386 })?;
6387 validate_current_engine_addressability_prediction_v2(
6388 &self.check_id,
6389 prediction,
6390 provenance,
6391 )
6392 .map_err(|source| MeasurementFileError::InvalidPrediction {
6393 check_index,
6394 source,
6395 })?;
6396 for facet in prediction.facets() {
6397 let evaluated = self
6398 .evaluated_scopes
6399 .iter()
6400 .filter(|scope| *scope == facet.scope())
6401 .count();
6402 let duplicated_gap = self
6403 .gaps
6404 .iter()
6405 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6406 match facet.state() {
6407 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6408 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6409 check_index,
6410 reason: "available facet scope must occur exactly once in evaluated_scopes",
6411 });
6412 }
6413 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6414 if evaluated != 0 || duplicated_gap =>
6415 {
6416 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6417 check_index,
6418 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6419 });
6420 }
6421 _ => {}
6422 }
6423 }
6424 for finding in &self.findings {
6425 let Some(scope) = &finding.prediction_scope else {
6426 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6427 check_index,
6428 reason: "prediction-backed finding must carry prediction_scope",
6429 });
6430 };
6431 if prediction
6432 .facets()
6433 .iter()
6434 .filter(|facet| {
6435 facet.scope() == scope
6436 && facet.state() == EnginePredictionFacetStateV1::Available
6437 })
6438 .count()
6439 != 1
6440 {
6441 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6442 check_index,
6443 reason: "finding prediction_scope must name one available facet",
6444 });
6445 }
6446 }
6447 Ok(())
6448 }
6449}
6450
6451impl PredictionCheckInputV3 {
6452 fn validate(
6453 &self,
6454 check_index: usize,
6455 provenance: Option<&PredictionProvenanceV3>,
6456 ) -> Result<(), MeasurementFileError> {
6457 let gap_refs = self
6458 .gaps
6459 .iter()
6460 .map(|gap| CheckEvaluationGapRef {
6461 code: &gap.code,
6462 scope: gap.scope.as_ref(),
6463 })
6464 .collect::<Vec<_>>();
6465 let finding_check_ids = self
6466 .findings
6467 .iter()
6468 .map(|finding| finding.check_id.as_str())
6469 .collect::<Vec<_>>();
6470 let prediction_scopes = self
6471 .prediction
6472 .as_ref()
6473 .into_iter()
6474 .flat_map(EnginePredictionV3::facets)
6475 .map(|facet| facet.scope())
6476 .collect::<Vec<_>>();
6477 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6478 check_id: &self.check_id,
6479 selection: self.selection,
6480 configuration: self.configuration,
6481 applicability: self.applicability,
6482 finding_check_ids: &finding_check_ids,
6483 evaluated_scopes: &self.evaluated_scopes,
6484 gaps: &gap_refs,
6485 prediction_scopes: &prediction_scopes,
6486 has_prediction: self.prediction.is_some(),
6487 prediction_has_required_unavailable: self
6488 .prediction
6489 .as_ref()
6490 .is_some_and(EnginePredictionV3::has_required_unavailable),
6491 })
6492 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6493 check_index,
6494 reason: error.reason(),
6495 })?;
6496 if self.evaluation != derived {
6497 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6498 check_index,
6499 reason: "evaluation does not match completed and missing prediction work",
6500 });
6501 }
6502 validate_current_engine_clip_boundary_applicability_v3(
6503 &self.check_id,
6504 self.applicability,
6505 provenance,
6506 )
6507 .map_err(|source| MeasurementFileError::InvalidPrediction {
6508 check_index,
6509 source,
6510 })?;
6511 let Some(prediction) = &self.prediction else {
6512 if self
6513 .findings
6514 .iter()
6515 .any(|finding| finding.prediction_scope.is_some())
6516 {
6517 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6518 check_index,
6519 reason: "finding has prediction_scope without prediction",
6520 });
6521 }
6522 if self.check_id == "engine-clip-boundary"
6523 && self.selection == SelectionState::Selected
6524 && self.configuration == ConfigurationState::Enabled
6525 && self.applicability == Applicability::Applicable
6526 {
6527 return Err(MeasurementFileError::InvalidPrediction {
6528 check_index,
6529 source: PredictionContractError::EngineClipBoundaryFacetMismatch,
6530 });
6531 }
6532 return Ok(());
6533 };
6534 let provenance =
6535 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6536 prediction
6537 .validate_against_provenance(provenance)
6538 .map_err(|source| MeasurementFileError::InvalidPrediction {
6539 check_index,
6540 source,
6541 })?;
6542 prediction
6543 .validate_facet_budget_summary_for_check(&self.check_id)
6544 .map_err(|source| MeasurementFileError::InvalidPrediction {
6545 check_index,
6546 source,
6547 })?;
6548 validate_current_engine_addressability_prediction_v3(
6549 &self.check_id,
6550 prediction,
6551 provenance,
6552 )
6553 .map_err(|source| MeasurementFileError::InvalidPrediction {
6554 check_index,
6555 source,
6556 })?;
6557 for facet in prediction.facets() {
6558 let evaluated = self
6559 .evaluated_scopes
6560 .iter()
6561 .filter(|scope| *scope == facet.scope())
6562 .count();
6563 let duplicated_gap = self
6564 .gaps
6565 .iter()
6566 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6567 match facet.state() {
6568 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6569 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6570 check_index,
6571 reason: "available facet scope must occur exactly once in evaluated_scopes",
6572 });
6573 }
6574 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6575 if evaluated != 0 || duplicated_gap =>
6576 {
6577 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6578 check_index,
6579 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6580 });
6581 }
6582 _ => {}
6583 }
6584 }
6585 for finding in &self.findings {
6586 let Some(scope) = &finding.prediction_scope else {
6587 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6588 check_index,
6589 reason: "prediction-backed finding must carry prediction_scope",
6590 });
6591 };
6592 if prediction
6593 .facets()
6594 .iter()
6595 .filter(|facet| {
6596 facet.scope() == scope
6597 && facet.state() == EnginePredictionFacetStateV1::Available
6598 })
6599 .count()
6600 != 1
6601 {
6602 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6603 check_index,
6604 reason: "finding prediction_scope must name one available facet",
6605 });
6606 }
6607 }
6608 let finding_scopes = self
6609 .findings
6610 .iter()
6611 .filter_map(|finding| finding.prediction_scope.as_ref())
6612 .collect::<Vec<_>>();
6613 validate_current_engine_clip_boundary_prediction_v3(
6614 &self.check_id,
6615 prediction,
6616 provenance,
6617 &self.evaluated_scopes,
6618 &finding_scopes,
6619 )
6620 .map_err(|source| MeasurementFileError::InvalidPrediction {
6621 check_index,
6622 source,
6623 })?;
6624 Ok(())
6625 }
6626}
6627
6628impl PredictionCheckInputV4 {
6629 fn validate(
6630 &self,
6631 check_index: usize,
6632 provenance: Option<&PredictionProvenanceV4>,
6633 ) -> Result<(), MeasurementFileError> {
6634 let gap_refs = self
6635 .gaps
6636 .iter()
6637 .map(|gap| CheckEvaluationGapRef {
6638 code: &gap.code,
6639 scope: gap.scope.as_ref(),
6640 })
6641 .collect::<Vec<_>>();
6642 let finding_check_ids = self
6643 .findings
6644 .iter()
6645 .map(|finding| finding.check_id.as_str())
6646 .collect::<Vec<_>>();
6647 let prediction_scopes = self
6648 .prediction
6649 .as_ref()
6650 .into_iter()
6651 .flat_map(EnginePredictionV4::facets)
6652 .map(|facet| facet.scope())
6653 .collect::<Vec<_>>();
6654 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6655 check_id: &self.check_id,
6656 selection: self.selection,
6657 configuration: self.configuration,
6658 applicability: self.applicability,
6659 finding_check_ids: &finding_check_ids,
6660 evaluated_scopes: &self.evaluated_scopes,
6661 gaps: &gap_refs,
6662 prediction_scopes: &prediction_scopes,
6663 has_prediction: self.prediction.is_some(),
6664 prediction_has_required_unavailable: self
6665 .prediction
6666 .as_ref()
6667 .is_some_and(EnginePredictionV4::has_required_unavailable),
6668 })
6669 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6670 check_index,
6671 reason: error.reason(),
6672 })?;
6673 if self.evaluation != derived {
6674 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6675 check_index,
6676 reason: "evaluation does not match completed and missing prediction work",
6677 });
6678 }
6679 let Some(prediction) = &self.prediction else {
6680 if self
6681 .findings
6682 .iter()
6683 .any(|finding| finding.prediction_scope.is_some())
6684 {
6685 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6686 check_index,
6687 reason: "finding has prediction_scope without prediction",
6688 });
6689 }
6690 return Ok(());
6691 };
6692 let provenance =
6693 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6694 prediction
6695 .validate_against_provenance(provenance)
6696 .map_err(|source| MeasurementFileError::InvalidPrediction {
6697 check_index,
6698 source,
6699 })?;
6700 prediction
6701 .validate_facet_budget_summary_for_check(&self.check_id)
6702 .map_err(|source| MeasurementFileError::InvalidPrediction {
6703 check_index,
6704 source,
6705 })?;
6706 for facet in prediction.facets() {
6707 let evaluated = self
6708 .evaluated_scopes
6709 .iter()
6710 .filter(|scope| *scope == facet.scope())
6711 .count();
6712 let duplicated_gap = self
6713 .gaps
6714 .iter()
6715 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6716 match facet.state() {
6717 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6718 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6719 check_index,
6720 reason: "available facet scope must occur exactly once in evaluated_scopes",
6721 });
6722 }
6723 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6724 if evaluated != 0 || duplicated_gap =>
6725 {
6726 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6727 check_index,
6728 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6729 });
6730 }
6731 _ => {}
6732 }
6733 }
6734 for finding in &self.findings {
6735 let Some(scope) = &finding.prediction_scope else {
6736 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6737 check_index,
6738 reason: "prediction-backed finding must carry prediction_scope",
6739 });
6740 };
6741 if prediction
6742 .facets()
6743 .iter()
6744 .filter(|facet| {
6745 facet.scope() == scope
6746 && facet.state() == EnginePredictionFacetStateV1::Available
6747 })
6748 .count()
6749 != 1
6750 {
6751 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6752 check_index,
6753 reason: "finding prediction_scope must name one available facet",
6754 });
6755 }
6756 }
6757 Ok(())
6758 }
6759}
6760
6761impl PredictionCheckInputV5 {
6762 fn validate(
6763 &self,
6764 check_index: usize,
6765 provenance: Option<&PredictionProvenanceV5>,
6766 ) -> Result<(), MeasurementFileError> {
6767 let gap_refs = self
6768 .gaps
6769 .iter()
6770 .map(|gap| CheckEvaluationGapRef {
6771 code: &gap.code,
6772 scope: gap.scope.as_ref(),
6773 })
6774 .collect::<Vec<_>>();
6775 let finding_check_ids = self
6776 .findings
6777 .iter()
6778 .map(|finding| finding.check_id.as_str())
6779 .collect::<Vec<_>>();
6780 let prediction_scopes = self
6781 .prediction
6782 .as_ref()
6783 .into_iter()
6784 .flat_map(EnginePredictionV5::facets)
6785 .map(|facet| facet.scope())
6786 .collect::<Vec<_>>();
6787 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6788 check_id: &self.check_id,
6789 selection: self.selection,
6790 configuration: self.configuration,
6791 applicability: self.applicability,
6792 finding_check_ids: &finding_check_ids,
6793 evaluated_scopes: &self.evaluated_scopes,
6794 gaps: &gap_refs,
6795 prediction_scopes: &prediction_scopes,
6796 has_prediction: self.prediction.is_some(),
6797 prediction_has_required_unavailable: self
6798 .prediction
6799 .as_ref()
6800 .is_some_and(EnginePredictionV5::has_required_unavailable),
6801 })
6802 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6803 check_index,
6804 reason: error.reason(),
6805 })?;
6806 if self.evaluation != derived {
6807 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6808 check_index,
6809 reason: "evaluation does not match completed and missing prediction work",
6810 });
6811 }
6812 let Some(prediction) = &self.prediction else {
6813 if self
6814 .findings
6815 .iter()
6816 .any(|finding| finding.prediction_scope.is_some())
6817 {
6818 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6819 check_index,
6820 reason: "finding has prediction_scope without prediction",
6821 });
6822 }
6823 return Ok(());
6824 };
6825 let provenance =
6826 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6827 prediction
6828 .validate_against_provenance(provenance)
6829 .map_err(|source| MeasurementFileError::InvalidPrediction {
6830 check_index,
6831 source,
6832 })?;
6833 prediction
6834 .base_prediction()
6835 .validate_facet_budget_summary_for_check(&self.check_id)
6836 .map_err(|source| MeasurementFileError::InvalidPrediction {
6837 check_index,
6838 source,
6839 })?;
6840 for facet in prediction.facets() {
6841 let evaluated = self
6842 .evaluated_scopes
6843 .iter()
6844 .filter(|scope| *scope == facet.scope())
6845 .count();
6846 let duplicated_gap = self
6847 .gaps
6848 .iter()
6849 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6850 match facet.state() {
6851 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6852 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6853 check_index,
6854 reason: "available facet scope must occur exactly once in evaluated_scopes",
6855 });
6856 }
6857 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6858 if evaluated != 0 || duplicated_gap =>
6859 {
6860 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6861 check_index,
6862 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6863 });
6864 }
6865 _ => {}
6866 }
6867 }
6868 for finding in &self.findings {
6869 let Some(scope) = &finding.prediction_scope else {
6870 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6871 check_index,
6872 reason: "prediction-backed finding must carry prediction_scope",
6873 });
6874 };
6875 if prediction
6876 .facets()
6877 .iter()
6878 .filter(|facet| {
6879 facet.scope() == scope
6880 && facet.state() == EnginePredictionFacetStateV1::Available
6881 })
6882 .count()
6883 != 1
6884 {
6885 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6886 check_index,
6887 reason: "finding prediction_scope must name one available facet",
6888 });
6889 }
6890 }
6891 Ok(())
6892 }
6893}
6894
6895impl PredictionCheckInputV6 {
6896 fn validate(
6897 &self,
6898 check_index: usize,
6899 provenance: Option<&PredictionProvenanceV6>,
6900 ) -> Result<(), MeasurementFileError> {
6901 let gap_refs = self
6902 .gaps
6903 .iter()
6904 .map(|gap| CheckEvaluationGapRef {
6905 code: &gap.code,
6906 scope: gap.scope.as_ref(),
6907 })
6908 .collect::<Vec<_>>();
6909 let finding_check_ids = self
6910 .findings
6911 .iter()
6912 .map(|finding| finding.check_id.as_str())
6913 .collect::<Vec<_>>();
6914 let prediction_scopes = self
6915 .prediction
6916 .as_ref()
6917 .into_iter()
6918 .flat_map(EnginePredictionV6::facets)
6919 .map(|facet| facet.scope())
6920 .collect::<Vec<_>>();
6921 let derived = validate_and_derive_check_evaluation(CheckEvaluationValidationInput {
6922 check_id: &self.check_id,
6923 selection: self.selection,
6924 configuration: self.configuration,
6925 applicability: self.applicability,
6926 finding_check_ids: &finding_check_ids,
6927 evaluated_scopes: &self.evaluated_scopes,
6928 gaps: &gap_refs,
6929 prediction_scopes: &prediction_scopes,
6930 has_prediction: self.prediction.is_some(),
6931 prediction_has_required_unavailable: self
6932 .prediction
6933 .as_ref()
6934 .is_some_and(EnginePredictionV6::has_required_unavailable),
6935 })
6936 .map_err(|error| MeasurementFileError::InvalidPredictionLifecycle {
6937 check_index,
6938 reason: error.reason(),
6939 })?;
6940 if self.evaluation != derived {
6941 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6942 check_index,
6943 reason: "evaluation does not match completed and missing prediction work",
6944 });
6945 }
6946 let Some(prediction) = &self.prediction else {
6947 if self
6948 .findings
6949 .iter()
6950 .any(|finding| finding.prediction_scope.is_some())
6951 {
6952 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6953 check_index,
6954 reason: "finding has prediction_scope without prediction",
6955 });
6956 }
6957 return Ok(());
6958 };
6959 let provenance =
6960 provenance.ok_or(MeasurementFileError::PredictionWithoutProvenance { check_index })?;
6961 prediction
6962 .validate_against_provenance(provenance)
6963 .map_err(|source| MeasurementFileError::InvalidPrediction {
6964 check_index,
6965 source,
6966 })?;
6967 prediction
6968 .base_prediction()
6969 .validate_facet_budget_summary_for_check(&self.check_id)
6970 .map_err(|source| MeasurementFileError::InvalidPrediction {
6971 check_index,
6972 source,
6973 })?;
6974 for facet in prediction.facets() {
6975 let evaluated = self
6976 .evaluated_scopes
6977 .iter()
6978 .filter(|scope| *scope == facet.scope())
6979 .count();
6980 let duplicated_gap = self
6981 .gaps
6982 .iter()
6983 .any(|gap| gap.scope.as_ref() == Some(facet.scope()));
6984 match facet.state() {
6985 EnginePredictionFacetStateV1::Available if evaluated != 1 => {
6986 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6987 check_index,
6988 reason: "available facet scope must occur exactly once in evaluated_scopes",
6989 });
6990 }
6991 EnginePredictionFacetStateV1::RequiredPredictionUnavailable
6992 if evaluated != 0 || duplicated_gap =>
6993 {
6994 return Err(MeasurementFileError::InvalidPredictionLifecycle {
6995 check_index,
6996 reason: "required-unavailable facet scope must be absent from evaluated_scopes and gaps",
6997 });
6998 }
6999 _ => {}
7000 }
7001 }
7002 for finding in &self.findings {
7003 let Some(scope) = &finding.prediction_scope else {
7004 return Err(MeasurementFileError::InvalidPredictionLifecycle {
7005 check_index,
7006 reason: "prediction-backed finding must carry prediction_scope",
7007 });
7008 };
7009 if prediction
7010 .facets()
7011 .iter()
7012 .filter(|facet| {
7013 facet.scope() == scope
7014 && facet.state() == EnginePredictionFacetStateV1::Available
7015 })
7016 .count()
7017 != 1
7018 {
7019 return Err(MeasurementFileError::InvalidPredictionLifecycle {
7020 check_index,
7021 reason: "finding prediction_scope must name one available facet",
7022 });
7023 }
7024 }
7025 Ok(())
7026 }
7027}
7028
7029fn validate_current_engine_addressability_prediction_v2(
7033 check_id: &str,
7034 prediction: &EnginePredictionV2,
7035 provenance: &PredictionProvenanceV2,
7036) -> Result<(), PredictionContractError> {
7037 if check_id != "engine-addressability" {
7038 return Ok(());
7039 }
7040 let raw_partial =
7041 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
7042 let settings_partial = matches!(
7043 provenance.settings().clip_coverage().state(),
7044 ResolvedEngineSettingsCoverageStateV2::Partial
7045 );
7046 let inventories = prediction
7047 .facets()
7048 .iter()
7049 .filter(|facet| {
7050 facet.scope().code.as_str() == "animation_asset_label_inventory"
7051 && facet.scope().subject.is_none()
7052 })
7053 .collect::<Vec<_>>();
7054 if !raw_partial && !settings_partial {
7055 if prediction.facets().iter().any(|facet| {
7056 facet
7057 .reasons()
7058 .contains(&PredictionUnavailableReasonV2::ResolvedSettingsOverflow)
7059 }) {
7060 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7061 }
7062 let available = prediction
7063 .facets()
7064 .iter()
7065 .filter(|facet| facet.state() == EnginePredictionFacetStateV1::Available)
7066 .collect::<Vec<_>>();
7067 let expected_rows = provenance.settings().clips().len();
7068 if (!prediction.has_facet_budget_summary() && available.len() != expected_rows)
7069 || (prediction.has_facet_budget_summary() && available.len() >= expected_rows)
7070 {
7071 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7072 }
7073 let mut seen = vec![false; available.len()];
7074 for facet in available {
7075 let Some(ordinal) = facet
7076 .scope()
7077 .subject
7078 .as_deref()
7079 .and_then(|subject| subject.strip_prefix("Animation"))
7080 .and_then(|ordinal| ordinal.parse::<usize>().ok())
7081 else {
7082 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7083 };
7084 if facet.scope().code.as_str() != "animation_asset_label"
7085 || ordinal >= seen.len()
7086 || std::mem::replace(&mut seen[ordinal], true)
7087 || !facet.basis().references().iter().any(|reference| {
7088 matches!(
7089 reference,
7090 PredictionBasisReferenceV1::RawSource { reference }
7091 if reference.domain() == RawSourceDomainV1::Clip
7092 && matches!(
7093 reference.key(),
7094 RawSourceKeyV1::Clip { source_clip_index }
7095 if *source_clip_index == ordinal as u64
7096 )
7097 && reference.field().as_str() == "source_name.state"
7098 )
7099 })
7100 {
7101 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7102 }
7103 }
7104 if seen.iter().any(|seen| !seen) {
7105 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7106 }
7107 return Ok(());
7108 }
7109
7110 let mut expected = Vec::new();
7111 if raw_partial {
7112 expected.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
7113 }
7114 if settings_partial {
7115 expected.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
7116 }
7117 if inventories.len() != 1 && !(inventories.is_empty() && prediction.has_facet_budget_summary())
7118 {
7119 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7120 }
7121 if let Some(inventory) = inventories.first()
7122 && (inventory.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7123 || inventory.reasons() != expected)
7124 {
7125 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7126 }
7127 if inventories.is_empty() {
7128 return Ok(());
7132 }
7133 Ok(())
7134}
7135
7136fn validate_current_engine_addressability_prediction_v3(
7139 check_id: &str,
7140 prediction: &EnginePredictionV3,
7141 provenance: &PredictionProvenanceV3,
7142) -> Result<(), PredictionContractError> {
7143 if check_id != "engine-addressability" {
7144 return Ok(());
7145 }
7146 let raw_partial =
7147 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
7148 let settings_partial = matches!(
7149 provenance.settings().clip_coverage().state(),
7150 ResolvedEngineSettingsCoverageStateV2::Partial
7151 );
7152 let inventories = prediction
7153 .facets()
7154 .iter()
7155 .filter(|facet| {
7156 facet.scope().code.as_str() == "animation_asset_label_inventory"
7157 && facet.scope().subject.is_none()
7158 })
7159 .collect::<Vec<_>>();
7160 if !raw_partial && !settings_partial {
7161 if prediction.facets().iter().any(|facet| {
7162 facet
7163 .reasons()
7164 .contains(&PredictionUnavailableReasonV2::ResolvedSettingsOverflow)
7165 }) {
7166 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7167 }
7168 let available = prediction
7169 .facets()
7170 .iter()
7171 .filter(|facet| facet.state() == EnginePredictionFacetStateV1::Available)
7172 .collect::<Vec<_>>();
7173 let expected_rows = provenance.settings().clips().len();
7174 if (!prediction.has_facet_budget_summary() && available.len() != expected_rows)
7175 || (prediction.has_facet_budget_summary() && available.len() >= expected_rows)
7176 {
7177 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7178 }
7179 let mut seen = vec![false; available.len()];
7180 for facet in available {
7181 let Some(ordinal) = facet
7182 .scope()
7183 .subject
7184 .as_deref()
7185 .and_then(|subject| subject.strip_prefix("Animation"))
7186 .and_then(|ordinal| ordinal.parse::<usize>().ok())
7187 else {
7188 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7189 };
7190 if facet.scope().code.as_str() != "animation_asset_label"
7191 || ordinal >= seen.len()
7192 || std::mem::replace(&mut seen[ordinal], true)
7193 || !facet.basis().references().iter().any(|reference| {
7194 matches!(
7195 reference,
7196 PredictionBasisReferenceV2::V1(PredictionBasisReferenceV1::RawSource { reference })
7197 if reference.domain() == RawSourceDomainV1::Clip
7198 && matches!(
7199 reference.key(),
7200 RawSourceKeyV1::Clip { source_clip_index }
7201 if *source_clip_index == ordinal as u64
7202 )
7203 && reference.field().as_str() == "source_name.state"
7204 )
7205 })
7206 {
7207 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7208 }
7209 }
7210 if seen.iter().any(|seen| !seen) {
7211 return Err(PredictionContractError::EngineAddressabilityFacetPrefixMismatch);
7212 }
7213 return Ok(());
7214 }
7215
7216 let mut expected = Vec::new();
7217 if raw_partial {
7218 expected.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
7219 }
7220 if settings_partial {
7221 expected.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
7222 }
7223 if inventories.len() != 1 && !(inventories.is_empty() && prediction.has_facet_budget_summary())
7224 {
7225 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7226 }
7227 if let Some(inventory) = inventories.first()
7228 && (inventory.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7229 || inventory.reasons() != expected)
7230 {
7231 return Err(PredictionContractError::EngineAddressabilityInventoryReasonsMismatch);
7232 }
7233 Ok(())
7234}
7235
7236const ENGINE_TRACK_SUPPORT_CHECK_ID: &str = "engine-track-support";
7237const ENGINE_TRACK_SUPPORT_ANIMATION_SCOPE: &str = "engine-track-support:animation";
7238const ENGINE_TRACK_SUPPORT_CHANNEL_SCOPE: &str = "engine-track-support:animation-channel";
7239const ENGINE_TRACK_SUPPORT_INVENTORY_SCOPE: &str = "engine-track-support:inventory";
7240const ENGINE_TRACK_SUPPORT_BUDGET_SCOPE: &str = "engine-track-support:facet-budget";
7241
7242fn validate_current_engine_track_support_prediction_v5(
7243 check_id: &str,
7244 selection_state: SelectionState,
7245 configuration: ConfigurationState,
7246 applicability: Applicability,
7247 prediction: Option<&EnginePredictionV5>,
7248 provenance: Option<&PredictionProvenanceV5>,
7249 findings_empty: bool,
7250) -> Result<(), PredictionContractError> {
7251 if check_id != ENGINE_TRACK_SUPPORT_CHECK_ID {
7252 if prediction.is_some_and(|prediction| {
7253 prediction.facets().iter().any(|facet| {
7254 matches!(
7255 facet.result(),
7256 Some(EngineMachineResultV1::SourceImportDisposition(_))
7257 )
7258 })
7259 }) {
7260 return Err(PredictionContractError::InvalidMachineResult(
7261 "source-import disposition is confined to engine-track-support",
7262 ));
7263 }
7264 return Ok(());
7265 }
7266 let exact_profile = provenance.is_some_and(|provenance| {
7267 let base = provenance.base();
7268 let selected = base.profile().selection();
7269 selected.family() == "bevy"
7270 && selected.profile_revision() == 3
7271 && selected.engine_version() == "0.19.0"
7272 && selected.importer() == "gltf-asset-loader"
7273 && base.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:3"
7274 && base.profile().facts_identity().sha256()
7275 == "d532b00621bf06a2db2dedf896c19aae2c07b3b1873a1b05beade2252d7a89c5"
7276 && base.profile().facts_identity().bytes() == 4_849
7277 && matches!(
7278 base.source_format(),
7279 SourceFormatV1::GltfJson | SourceFormatV1::Glb
7280 )
7281 && !provenance.raw_animation_channels().is_complete_empty()
7282 });
7283 let expected_applicability = if exact_profile {
7284 Applicability::Applicable
7285 } else {
7286 Applicability::NotApplicable
7287 };
7288 if applicability != expected_applicability {
7289 return Err(PredictionContractError::InvalidMachineResult(
7290 "engine-track-support applicability mismatch",
7291 ));
7292 }
7293 if !exact_profile
7294 || selection_state != SelectionState::Selected
7295 || configuration != ConfigurationState::Enabled
7296 {
7297 return if prediction.is_none() {
7298 Ok(())
7299 } else {
7300 Err(PredictionContractError::InvalidMachineResult(
7301 "inactive engine-track-support carried prediction",
7302 ))
7303 };
7304 }
7305 if !findings_empty {
7306 return Err(PredictionContractError::InvalidMachineResult(
7307 "engine-track-support must not emit findings",
7308 ));
7309 }
7310 let provenance = provenance.ok_or(PredictionContractError::ProvenanceIdentityMismatch)?;
7311 let prediction = prediction.ok_or(PredictionContractError::InvalidMachineResult(
7312 "applicable engine-track-support check requires prediction",
7313 ))?;
7314 let inventory = provenance.raw_animation_channels();
7315 let mut expected = Vec::new();
7316 let has_summary = prediction.base_prediction().has_facet_budget_summary();
7317 let expected_demand = if !inventory.source_coverage_complete() {
7318 1
7319 } else {
7320 inventory.rows().len()
7321 };
7322 if inventory.source_coverage_complete() && inventory.candidate_overflow() && !has_summary {
7323 return Err(PredictionContractError::InvalidMachineResult(
7324 "saturated engine-track-support demand requires facet-budget summary",
7325 ));
7326 }
7327 let candidate_capacity = if has_summary {
7328 prediction.facets().len().saturating_sub(1)
7329 } else {
7330 expected_demand
7331 };
7332 if !inventory.is_complete_empty() && candidate_capacity != 0 {
7333 if !inventory.source_coverage_complete() {
7334 expected.push(track_unavailable(
7335 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(
7336 ENGINE_TRACK_SUPPORT_INVENTORY_SCOPE,
7337 )),
7338 track_inventory_basis(inventory),
7339 PredictionUnavailableReasonV2::RawSourceIncomplete,
7340 ));
7341 } else {
7342 for row in inventory.rows().iter().take(candidate_capacity) {
7343 let animation = row.source_animation_index();
7344 if let Some(channel) = row.source_channel_index() {
7345 expected.push(track_subject_facet(
7346 track_scope(
7347 ENGINE_TRACK_SUPPORT_CHANNEL_SCOPE,
7348 format!("source_animation:{animation}:source_channel:{channel}"),
7349 ),
7350 track_row_basis(inventory, animation, Some(channel)),
7351 SourceImportSubjectKindV1::AnimationChannel,
7352 track_gate(provenance),
7353 ));
7354 } else {
7355 expected.push(track_subject_facet(
7356 track_scope(
7357 ENGINE_TRACK_SUPPORT_ANIMATION_SCOPE,
7358 format!("source_animation:{animation}"),
7359 ),
7360 track_row_basis(inventory, animation, None),
7361 SourceImportSubjectKindV1::Animation,
7362 track_gate(provenance),
7363 ));
7364 }
7365 }
7366 }
7367 }
7368 if has_summary {
7369 expected.push(track_unavailable(
7370 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(
7371 ENGINE_TRACK_SUPPORT_BUDGET_SCOPE,
7372 )),
7373 track_static_basis(),
7374 PredictionUnavailableReasonV2::FacetBudgetExceeded,
7375 ));
7376 }
7377 let expected = EnginePredictionV4::new(provenance.base().identity().clone(), expected)?
7378 .facets()
7379 .to_vec();
7380 if prediction.facets() != expected {
7381 return Err(PredictionContractError::InvalidMachineResult(
7382 "engine-track-support facets do not reconstruct from V5 provenance",
7383 ));
7384 }
7385 Ok(())
7386}
7387
7388#[allow(clippy::too_many_arguments)]
7395fn validate_current_engine_root_motion_prediction_v6<F: RootMotionFindingEvidence>(
7396 check_id: &str,
7397 selection: SelectionState,
7398 configuration: ConfigurationState,
7399 applicability: Applicability,
7400 prediction: Option<&EnginePredictionV6>,
7401 provenance: Option<&PredictionProvenanceV6>,
7402 findings: &[F],
7403 rig: &RigInfo,
7404 measurements: &MeasurementContract,
7405) -> Result<(), PredictionContractError> {
7406 const CHECK_ID: &str = "engine-root-motion";
7407 if check_id != CHECK_ID
7408 && prediction.is_some_and(|prediction| {
7409 prediction.facets().iter().any(|facet| {
7410 matches!(
7411 facet.result(),
7412 Some(EngineMachineResultV1::RootMotionRouting(_))
7413 )
7414 })
7415 })
7416 {
7417 return Err(PredictionContractError::InvalidMachineResult(
7418 "root-motion routing is confined to engine-root-motion",
7419 ));
7420 }
7421 if check_id != CHECK_ID {
7422 return Ok(());
7423 }
7424 let active =
7425 selection == SelectionState::Selected && configuration == ConfigurationState::Enabled;
7426 let exact = provenance.is_some_and(root_motion_is_exact_unity_v2);
7427 let has_work = provenance.is_some_and(root_motion_has_work);
7428 let expected_applicability = if exact && has_work {
7429 Applicability::Applicable
7430 } else {
7431 Applicability::NotApplicable
7432 };
7433 if applicability != expected_applicability {
7434 return Err(PredictionContractError::InvalidMachineResult(
7435 "engine-root-motion applicability does not reconstruct from V6 provenance",
7436 ));
7437 }
7438 if !active || expected_applicability == Applicability::NotApplicable {
7439 if prediction.is_some() || !findings.is_empty() {
7440 return Err(PredictionContractError::InvalidMachineResult(
7441 "inactive or inapplicable engine-root-motion must carry no prediction or findings",
7442 ));
7443 }
7444 return Ok(());
7445 }
7446 let provenance = provenance.ok_or(PredictionContractError::InvalidMachineResult(
7447 "applicable engine-root-motion has no V6 provenance",
7448 ))?;
7449 let prediction = prediction.ok_or(PredictionContractError::InvalidMachineResult(
7450 "applicable engine-root-motion has no V6 prediction",
7451 ))?;
7452 validate_root_motion_facets_v6(prediction, provenance, findings, rig, measurements)
7453}
7454
7455fn root_motion_has_work(provenance: &PredictionProvenanceV6) -> bool {
7460 let intent = provenance.root_motion_project_intent();
7461 if intent.clip_coverage() != crate::EngineRootMotionProjectIntentCoverageV1::Complete
7462 || provenance.base().base().settings().clip_coverage().state()
7463 != ResolvedEngineSettingsCoverageStateV2::Complete
7464 {
7465 return true;
7466 }
7467 match intent.declared_axis_candidates() {
7468 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => count != 0,
7469 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => true,
7470 }
7471}
7472
7473fn root_motion_is_exact_unity_v2(provenance: &PredictionProvenanceV6) -> bool {
7474 let profile = provenance.base().base().profile();
7475 let selection = profile.selection();
7476 selection.family() == "unity-generic"
7477 && selection.profile_revision() == 2
7478 && selection.engine_version() == "6000.3"
7479 && selection.importer() == "fbx-model-importer"
7480 && profile.fact_bundle_urn() == "urn:animsmith:engine-profile:unity-generic:2"
7481 && profile.facts_identity().sha256()
7482 == "740e1c324a7a5b13efa2d9980fe255a6245d858adec55fb3387614a3ff45274c"
7483 && profile.facts_identity().bytes() == 2_776
7484 && provenance.base().base().source_format() == SourceFormatV1::Fbx
7485 && profile.setting_descriptors().len() == 7
7486 && profile.primary_sources().len() == 3
7487 && [
7488 "unity-fbx-animation-clip-6000.3",
7489 "unity-fbx-model-importer-6000.3",
7490 "unity-fbx-motion-node-6000.3",
7491 ]
7492 .into_iter()
7493 .all(|id| profile.source(id).is_some())
7494}
7495
7496fn validate_root_motion_facets_v6<F: RootMotionFindingEvidence>(
7497 prediction: &EnginePredictionV6,
7498 provenance: &PredictionProvenanceV6,
7499 findings: &[F],
7500 rig: &RigInfo,
7501 measurements: &MeasurementContract,
7502) -> Result<(), PredictionContractError> {
7503 const INVENTORY_SCOPE: &str = "engine-root-motion:inventory";
7504 const AXIS_SCOPE: &str = "engine-root-motion:clip-axis";
7505 const BUDGET_SCOPE: &str = "engine-root-motion:facet-budget";
7506 let intent = provenance.root_motion_project_intent();
7507 let mut atomic = Vec::new();
7508 if provenance
7509 .base()
7510 .base()
7511 .raw_source()
7512 .clips_coverage()
7513 .state()
7514 != RawSourceSetCoverageStateV1::Complete
7515 || !matches!(
7516 provenance.raw_transform_paths().coverage(),
7517 crate::RawTransformPathCoverageV1::Complete
7518 )
7519 {
7520 atomic.push(PredictionUnavailableReasonV2::RawSourceIncomplete);
7521 }
7522 if intent.clip_coverage() != crate::EngineRootMotionProjectIntentCoverageV1::Complete {
7523 atomic.push(PredictionUnavailableReasonV2::ProjectIntentUnavailable);
7524 }
7525 if intent.declared_axis_candidates().overflowed()
7526 || intent.unmapped_declared_axis_candidates().overflowed()
7527 {
7528 atomic.push(PredictionUnavailableReasonV2::custom(
7529 "animsmith:root_motion_intent_work_budget_exceeded",
7530 )?);
7531 }
7532 if !matches!(
7533 intent.unmapped_declared_axis_candidates(),
7534 crate::EngineRootMotionProjectIntentCountV1::Exact { count: 0 }
7535 ) {
7536 atomic.push(PredictionUnavailableReasonV2::ProjectIntentUnavailable);
7537 }
7538 if provenance.base().base().settings().clip_coverage().state()
7539 != ResolvedEngineSettingsCoverageStateV2::Complete
7540 {
7541 atomic.push(PredictionUnavailableReasonV2::ResolvedSettingsOverflow);
7542 }
7543 atomic.sort_by(|left, right| left.as_str().cmp(right.as_str()));
7544 atomic.dedup();
7545 if !atomic.is_empty() {
7546 let facets = prediction.facets();
7547 let valid_inventory = facets.len() == 1
7548 && facets[0].scope().code.as_str() == INVENTORY_SCOPE
7549 && facets[0].scope().subject.is_none()
7550 && facets[0].state() == EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7551 && facets[0].reasons() == atomic
7552 && facets[0].basis() == &root_motion_inventory_basis(provenance)?;
7553 if !valid_inventory || !findings.is_empty() {
7554 return Err(PredictionContractError::InvalidMachineResult(
7555 "engine-root-motion atomic unavailable summary is not canonical",
7556 ));
7557 }
7558 return Ok(());
7559 }
7560
7561 let configured_path = provenance
7562 .base()
7563 .base()
7564 .settings()
7565 .document_setting(EngineSettingIdV2::RootMotionSource)
7566 .and_then(|row| match row.value() {
7567 EngineSettingValueV2::SourceTransformPath(path) => {
7568 crate::RawTransformPathV1::parse(path).ok()
7569 }
7570 _ => None,
7571 });
7572 let path_resolution = configured_path
7573 .as_ref()
7574 .map(|path| provenance.raw_transform_paths().resolve(path));
7575 let root_name = rig.resolved_roles.get("root").map(String::as_str);
7576 let mut name_counts = BTreeMap::new();
7577 for clip in intent.clips() {
7578 if let Some(name) = clip.normalized_clip_name() {
7579 *name_counts.entry(name).or_insert(0usize) += 1;
7580 }
7581 }
7582 let mut expected = Vec::new();
7583 for clip in intent.clips() {
7584 let Some(name) = clip.normalized_clip_name() else {
7585 continue;
7586 };
7587 for (axis, owner) in [
7588 (
7589 crate::RootMotionAxisV1::HorizontalXz,
7590 clip.movement_owner_xz(),
7591 ),
7592 (crate::RootMotionAxisV1::VerticalY, clip.movement_owner_y()),
7593 (crate::RootMotionAxisV1::Yaw, clip.movement_owner_yaw()),
7594 ]
7595 .into_iter()
7596 .filter_map(|(axis, owner)| owner.map(|owner| (axis, owner)))
7597 {
7598 let axis_name = match axis {
7599 crate::RootMotionAxisV1::HorizontalXz => "horizontal_xz",
7600 crate::RootMotionAxisV1::VerticalY => "vertical_y",
7601 crate::RootMotionAxisV1::Yaw => "yaw",
7602 };
7603 let scope = EvaluationScope::new(crate::EvaluationScopeCode::custom(AXIS_SCOPE))
7604 .subject(format!(
7605 "source_clip:{:020}:axis:{axis_name}",
7606 clip.source_clip_index()
7607 ));
7608 let setting_id = match axis {
7609 crate::RootMotionAxisV1::HorizontalXz => EngineSettingIdV2::RootPositionXz,
7610 crate::RootMotionAxisV1::VerticalY => EngineSettingIdV2::RootPositionY,
7611 crate::RootMotionAxisV1::Yaw => EngineSettingIdV2::RootRotation,
7612 };
7613 let setting = provenance
7614 .base()
7615 .base()
7616 .settings()
7617 .clip_row(
7618 clip.normalized_clip_index().ok_or(
7619 PredictionContractError::InvalidMachineResult(
7620 "mapped root-motion intent is missing normalized clip index",
7621 ),
7622 )?,
7623 name,
7624 )
7625 .and_then(|row| row.setting(setting_id));
7626 let measurement = measurements.clips().get(name);
7627 let reason = if name_counts.get(name).copied().unwrap_or(0) > 1 {
7628 Some(PredictionUnavailableReasonV2::MeasurementUnavailable)
7629 } else {
7630 root_motion_unavailable_reason(
7631 path_resolution.as_ref(),
7632 intent.resolved_root_bone_index(),
7633 root_name,
7634 measurement,
7635 setting.map(|row| row.value()),
7636 axis,
7637 )?
7638 };
7639 let basis = root_motion_candidate_basis(
7640 provenance,
7641 clip,
7642 name,
7643 name_counts.get(name).copied().unwrap_or(0),
7644 axis,
7645 owner,
7646 configured_path.as_ref(),
7647 path_resolution.as_ref(),
7648 setting.map(|row| row.value_origin()),
7649 measurement,
7650 )?;
7651 expected.push((
7652 scope.clone(),
7653 name.to_owned(),
7654 axis,
7655 owner,
7656 setting.map(|row| row.value()).cloned(),
7657 reason,
7658 basis,
7659 ));
7660 }
7661 }
7662 let facets = prediction.facets();
7663 let has_budget = facets
7664 .last()
7665 .is_some_and(|facet| facet.scope().code.as_str() == BUDGET_SCOPE);
7666 let candidates = if has_budget {
7667 &facets[..facets.len() - 1]
7668 } else {
7669 facets
7670 };
7671 if candidates.len() > expected.len() || (candidates.len() < expected.len() && !has_budget) {
7672 return Err(PredictionContractError::InvalidMachineResult(
7673 "engine-root-motion facet allocation is not a canonical prefix",
7674 ));
7675 }
7676 if has_budget {
7677 let facet = facets.last().unwrap();
7678 if facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7679 || facet.scope().subject.is_some()
7680 || facet.reasons() != [PredictionUnavailableReasonV2::FacetBudgetExceeded]
7681 || facet.basis() != &root_motion_static_basis()?
7682 {
7683 return Err(PredictionContractError::InvalidMachineResult(
7684 "engine-root-motion facet-budget summary is invalid",
7685 ));
7686 }
7687 }
7688 let mut retained_conflicts = Vec::new();
7689 for (facet, (scope, clip_name, axis, owner, setting, reason, basis)) in
7690 candidates.iter().zip(expected.iter())
7691 {
7692 if facet.scope() != scope || facet.basis() != basis {
7693 return Err(PredictionContractError::InvalidMachineResult(
7694 "engine-root-motion scope or basis is not canonical",
7695 ));
7696 }
7697 match reason {
7698 Some(reason)
7699 if facet.state() == EnginePredictionFacetStateV1::RequiredPredictionUnavailable
7700 && facet.reasons() == std::slice::from_ref(reason) => {}
7701 None => {
7702 let disposition = root_motion_disposition(setting.as_ref().unwrap()).unwrap();
7703 let compatible = matches!(
7704 (owner, disposition),
7705 (
7706 crate::RootMotionProjectOwnerV1::Gameplay,
7707 crate::RootMotionImporterDispositionV1::BakedIntoPose
7708 ) | (
7709 crate::RootMotionProjectOwnerV1::Animation,
7710 crate::RootMotionImporterDispositionV1::StoredAsRootMotion
7711 )
7712 );
7713 let expected_result = crate::RootMotionRoutingResultV1 {
7714 axis: *axis,
7715 project_owner: *owner,
7716 importer_disposition: disposition,
7717 compatibility: if compatible {
7718 crate::RootMotionCompatibilityV1::Compatible
7719 } else {
7720 crate::RootMotionCompatibilityV1::Conflict
7721 },
7722 };
7723 if !matches!(facet.result(), Some(EngineMachineResultV1::RootMotionRouting(result)) if result == &expected_result)
7724 {
7725 return Err(PredictionContractError::InvalidMachineResult(
7726 "engine-root-motion result is not canonical",
7727 ));
7728 }
7729 if !compatible {
7730 let axis_label = match axis {
7731 crate::RootMotionAxisV1::HorizontalXz => "horizontal XZ",
7732 crate::RootMotionAxisV1::VerticalY => "vertical Y",
7733 crate::RootMotionAxisV1::Yaw => "yaw",
7734 };
7735 let owner_label = match owner {
7736 crate::RootMotionProjectOwnerV1::Gameplay => "gameplay",
7737 crate::RootMotionProjectOwnerV1::Animation => "animation",
7738 };
7739 let disposition_label = match disposition {
7740 crate::RootMotionImporterDispositionV1::BakedIntoPose => {
7741 "baked into the pose"
7742 }
7743 crate::RootMotionImporterDispositionV1::StoredAsRootMotion => {
7744 "stored as root motion"
7745 }
7746 };
7747 retained_conflicts.push(serde_json::json!({
7748 "check_id": "engine-root-motion",
7749 "severity": "error",
7750 "clip": clip_name,
7751 "prediction_scope": scope,
7752 "message": format!("clip {:?} assigns {} movement to {}, but Unity imports that axis as {}", clip_name, axis_label, owner_label, disposition_label),
7753 }));
7754 }
7755 }
7756 _ => {
7757 return Err(PredictionContractError::InvalidMachineResult(
7758 "engine-root-motion unavailable facet is not canonical",
7759 ));
7760 }
7761 }
7762 }
7763 let actual_findings = findings
7764 .iter()
7765 .map(RootMotionFindingEvidence::root_motion_wire_value)
7766 .collect::<Vec<_>>();
7767 if actual_findings != retained_conflicts {
7768 return Err(PredictionContractError::InvalidMachineResult(
7769 "engine-root-motion conflict findings are not canonical",
7770 ));
7771 }
7772 Ok(())
7773}
7774
7775fn root_motion_lift(reference: PredictionBasisReferenceV1) -> PredictionBasisReferenceV4 {
7776 PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(reference))
7777}
7778
7779fn root_motion_static_basis() -> Result<EnginePredictionBasisV4, PredictionContractError> {
7780 let mut references = vec![root_motion_lift(PredictionBasisReferenceV1::profile_fact(
7781 "root_motion_addressability",
7782 )?)];
7783 for source in [
7784 "unity-fbx-model-importer-6000.3",
7785 "unity-fbx-animation-clip-6000.3",
7786 "unity-fbx-motion-node-6000.3",
7787 ] {
7788 references.push(root_motion_lift(
7789 PredictionBasisReferenceV1::primary_source(source)?,
7790 ));
7791 }
7792 for setting in [
7793 EngineSettingIdV2::AnimationType,
7794 EngineSettingIdV2::AvatarSetup,
7795 EngineSettingIdV2::ImportAnimation,
7796 EngineSettingIdV2::RootMotionSource,
7797 ] {
7798 references.push(root_motion_lift(
7799 PredictionBasisReferenceV1::resolved_setting(
7800 ResolvedSettingLocationV1::Document,
7801 setting.as_str(),
7802 )?,
7803 ));
7804 }
7805 EnginePredictionBasisV4::new(references)
7806}
7807
7808fn root_motion_inventory_basis(
7809 provenance: &PredictionProvenanceV6,
7810) -> Result<EnginePredictionBasisV4, PredictionContractError> {
7811 let mut references = root_motion_static_basis()?.references().to_vec();
7812 let path_coverage = match provenance.raw_transform_paths().coverage() {
7813 crate::RawTransformPathCoverageV1::Complete => "complete",
7814 crate::RawTransformPathCoverageV1::Partial(_) => "partial",
7815 crate::RawTransformPathCoverageV1::Unavailable(_) => "unavailable",
7816 };
7817 let clip_coverage = match provenance.root_motion_project_intent().clip_coverage() {
7818 crate::EngineRootMotionProjectIntentCoverageV1::Complete => "complete",
7819 crate::EngineRootMotionProjectIntentCoverageV1::PartialProjectionBudgetExceeded => {
7820 "partial_projection_budget_exceeded"
7821 }
7822 };
7823 let count = match provenance
7824 .root_motion_project_intent()
7825 .declared_axis_candidates()
7826 {
7827 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => {
7828 PredictionScalarV1::UnsignedInteger { value: count }
7829 }
7830 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => {
7831 PredictionScalarV1::token("n_plus_one")?
7832 }
7833 };
7834 let unmapped_count = match provenance
7835 .root_motion_project_intent()
7836 .unmapped_declared_axis_candidates()
7837 {
7838 crate::EngineRootMotionProjectIntentCountV1::Exact { count } => {
7839 PredictionScalarV1::UnsignedInteger { value: count }
7840 }
7841 crate::EngineRootMotionProjectIntentCountV1::NPlusOne => {
7842 PredictionScalarV1::token("n_plus_one")?
7843 }
7844 };
7845 let raw_clip_coverage = match provenance
7846 .base()
7847 .base()
7848 .raw_source()
7849 .clips_coverage()
7850 .state()
7851 {
7852 RawSourceSetCoverageStateV1::Complete => "complete",
7853 RawSourceSetCoverageStateV1::Partial => "partial",
7854 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
7855 };
7856 let settings_coverage = match provenance.base().base().settings().clip_coverage().state() {
7857 ResolvedEngineSettingsCoverageStateV2::Complete => "complete",
7858 ResolvedEngineSettingsCoverageStateV2::Partial => "partial",
7859 };
7860 for (field, value) in [
7861 (
7862 "raw_source.clips.coverage",
7863 PredictionScalarV1::token(raw_clip_coverage)?,
7864 ),
7865 (
7866 "raw_transform_path_inventory.coverage",
7867 PredictionScalarV1::token(path_coverage)?,
7868 ),
7869 (
7870 "root_motion_project_intent.clip_coverage",
7871 PredictionScalarV1::token(clip_coverage)?,
7872 ),
7873 ("root_motion_project_intent.declared_axis_candidates", count),
7874 (
7875 "root_motion_project_intent.unmapped_declared_axis_candidates",
7876 unmapped_count,
7877 ),
7878 (
7879 "resolved_settings.clips.coverage",
7880 PredictionScalarV1::token(settings_coverage)?,
7881 ),
7882 ] {
7883 references.push(root_motion_lift(PredictionBasisReferenceV1::project_field(
7884 field, value,
7885 )?));
7886 }
7887 EnginePredictionBasisV4::new(references)
7888}
7889
7890fn root_motion_project_reference(
7891 field: &'static str,
7892 value: PredictionScalarV1,
7893) -> Result<PredictionBasisReferenceV4, PredictionContractError> {
7894 Ok(root_motion_lift(PredictionBasisReferenceV1::project_field(
7895 field, value,
7896 )?))
7897}
7898
7899fn root_motion_token(value: &'static str) -> Result<PredictionScalarV1, PredictionContractError> {
7900 PredictionScalarV1::token(value)
7901}
7902
7903#[allow(clippy::too_many_arguments)]
7904fn root_motion_candidate_basis(
7905 provenance: &PredictionProvenanceV6,
7906 clip: &crate::EngineRootMotionClipIntentV1,
7907 name: &str,
7908 duplicate_count: usize,
7909 axis: crate::RootMotionAxisV1,
7910 owner: crate::RootMotionProjectOwnerV1,
7911 configured_path: Option<&crate::RawTransformPathV1>,
7912 path_resolution: Option<&crate::RawTransformPathResolutionV1>,
7913 setting_origin: Option<crate::EngineSettingValueOriginV3>,
7914 measurement: Option<&ClipMeasurements>,
7915) -> Result<EnginePredictionBasisV4, PredictionContractError> {
7916 let mut refs = root_motion_static_basis()?.references().to_vec();
7917 let settings = provenance.base().base().settings();
7918 for (id, field) in [
7919 (
7920 EngineSettingIdV2::AnimationType,
7921 "resolved_setting.document.animation_type.value_origin",
7922 ),
7923 (
7924 EngineSettingIdV2::AvatarSetup,
7925 "resolved_setting.document.avatar_setup.value_origin",
7926 ),
7927 (
7928 EngineSettingIdV2::ImportAnimation,
7929 "resolved_setting.document.import_animation.value_origin",
7930 ),
7931 (
7932 EngineSettingIdV2::RootMotionSource,
7933 "resolved_setting.document.root_motion_source.value_origin",
7934 ),
7935 ] {
7936 let value = settings
7937 .document_setting(id)
7938 .map_or(PredictionScalarV1::Null, |row| {
7939 root_motion_token(match row.value_origin() {
7940 crate::EngineSettingValueOriginV3::ExplicitConfig => "explicit_config",
7941 crate::EngineSettingValueOriginV3::ProfileDefault => "profile_default",
7942 })
7943 .expect("static origin token")
7944 });
7945 refs.push(root_motion_project_reference(field, value)?);
7946 }
7947 let setting_id = match axis {
7948 crate::RootMotionAxisV1::HorizontalXz => EngineSettingIdV2::RootPositionXz,
7949 crate::RootMotionAxisV1::VerticalY => EngineSettingIdV2::RootPositionY,
7950 crate::RootMotionAxisV1::Yaw => EngineSettingIdV2::RootRotation,
7951 };
7952 if let Some(index) = clip.normalized_clip_index() {
7953 refs.push(root_motion_lift(
7954 PredictionBasisReferenceV1::resolved_setting(
7955 ResolvedSettingLocationV1::Clip {
7956 clip_ordinal: index,
7957 clip_name: name.to_owned(),
7958 },
7959 setting_id.as_str(),
7960 )?,
7961 ));
7962 }
7963 refs.push(root_motion_project_reference(
7964 "root_motion_project_intent.source_clip_index",
7965 PredictionScalarV1::UnsignedInteger {
7966 value: clip.source_clip_index(),
7967 },
7968 )?);
7969 let mapping_state = match clip.normalized_clip_mapping_state() {
7970 crate::EngineRootMotionClipMappingStateV1::Observed => "observed",
7971 crate::EngineRootMotionClipMappingStateV1::ProvenAbsent => "proven_absent",
7972 crate::EngineRootMotionClipMappingStateV1::Unavailable => "unavailable",
7973 };
7974 for field in ["normalized_clip_index.state", "normalized_clip_index.value"] {
7975 if field.ends_with(".value") && clip.normalized_clip_index().is_none() {
7976 continue;
7977 }
7978 let value = if field.ends_with(".state") {
7979 root_motion_token(mapping_state)?
7980 } else {
7981 PredictionScalarV1::UnsignedInteger {
7982 value: clip.normalized_clip_index().unwrap(),
7983 }
7984 };
7985 let raw: RawSourceBasisReferenceV1 = serde_json::from_value(serde_json::json!({
7986 "domain": "clip", "key": {"kind": "clip", "source_clip_index": clip.source_clip_index()}, "field": field, "value": value
7987 })).map_err(|_| PredictionContractError::InvalidMachineResult("failed to reconstruct root-motion raw clip reference"))?;
7988 refs.push(root_motion_lift(PredictionBasisReferenceV1::raw_source(
7989 raw,
7990 )));
7991 }
7992 let axis_name = match axis {
7993 crate::RootMotionAxisV1::HorizontalXz => "horizontal_xz",
7994 crate::RootMotionAxisV1::VerticalY => "vertical_y",
7995 crate::RootMotionAxisV1::Yaw => "yaw",
7996 };
7997 let owner_name = match owner {
7998 crate::RootMotionProjectOwnerV1::Gameplay => "gameplay",
7999 crate::RootMotionProjectOwnerV1::Animation => "animation",
8000 };
8001 let origin = setting_origin.map_or(PredictionScalarV1::Null, |origin| {
8002 root_motion_token(match origin {
8003 crate::EngineSettingValueOriginV3::ExplicitConfig => "explicit_config",
8004 crate::EngineSettingValueOriginV3::ProfileDefault => "profile_default",
8005 })
8006 .expect("origin token")
8007 });
8008 let path_coverage = match provenance.raw_transform_paths().coverage() {
8009 crate::RawTransformPathCoverageV1::Complete => "complete",
8010 crate::RawTransformPathCoverageV1::Partial(_) => "partial",
8011 crate::RawTransformPathCoverageV1::Unavailable(_) => "unavailable",
8012 };
8013 let raw_coverage = match provenance
8014 .base()
8015 .base()
8016 .raw_source()
8017 .clips_coverage()
8018 .state()
8019 {
8020 RawSourceSetCoverageStateV1::Complete => "complete",
8021 RawSourceSetCoverageStateV1::Partial => "partial",
8022 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
8023 };
8024 let resolution = match path_resolution {
8025 Some(crate::RawTransformPathResolutionV1::Exact(_)) => "exact",
8026 Some(crate::RawTransformPathResolutionV1::NoMatch) => "no_match",
8027 Some(crate::RawTransformPathResolutionV1::Ambiguous { .. }) => "ambiguous",
8028 Some(crate::RawTransformPathResolutionV1::CoverageIncomplete { .. }) => {
8029 "coverage_incomplete"
8030 }
8031 None => "invalid_configured_path",
8032 };
8033 for (field, value) in [
8034 (
8035 "root_motion_project_intent.clip_mapping_state",
8036 root_motion_token(mapping_state)?,
8037 ),
8038 (
8039 "root_motion_project_intent.normalized_clip_index",
8040 clip.normalized_clip_index()
8041 .map_or(PredictionScalarV1::Null, |value| {
8042 PredictionScalarV1::UnsignedInteger { value }
8043 }),
8044 ),
8045 (
8046 "root_motion_project_intent.normalized_clip_name",
8047 PredictionScalarV1::text(name)?,
8048 ),
8049 (
8050 "measurement.clip_name_match_count",
8051 PredictionScalarV1::UnsignedInteger {
8052 value: duplicate_count as u64,
8053 },
8054 ),
8055 (
8056 "measurement.clip_identity_state",
8057 root_motion_token(if duplicate_count > 1 {
8058 "duplicate"
8059 } else {
8060 "unique"
8061 })?,
8062 ),
8063 (
8064 "root_motion_project_intent.axis",
8065 root_motion_token(axis_name)?,
8066 ),
8067 (
8068 "root_motion_project_intent.owner",
8069 root_motion_token(owner_name)?,
8070 ),
8071 ("resolved_setting.clip.value_origin", origin),
8072 (
8073 "raw_source.clips.coverage",
8074 root_motion_token(raw_coverage)?,
8075 ),
8076 (
8077 "raw_transform_path_inventory.coverage",
8078 root_motion_token(path_coverage)?,
8079 ),
8080 (
8081 "resolved_role.root.bone_index",
8082 provenance
8083 .root_motion_project_intent()
8084 .resolved_root_bone_index()
8085 .map_or(PredictionScalarV1::Null, |value| {
8086 PredictionScalarV1::UnsignedInteger { value }
8087 }),
8088 ),
8089 (
8090 "root_motion_source.configured_path",
8091 configured_path.map_or(PredictionScalarV1::Null, |path| {
8092 PredictionScalarV1::text(path.as_str()).expect("path text")
8093 }),
8094 ),
8095 (
8096 "root_motion_source.resolution_state",
8097 root_motion_token(resolution)?,
8098 ),
8099 ] {
8100 refs.push(root_motion_project_reference(field, value)?);
8101 }
8102 if let Some(crate::RawTransformPathResolutionV1::Ambiguous { matches }) = path_resolution {
8103 refs.push(root_motion_project_reference(
8104 "root_motion_source.match_count",
8105 PredictionScalarV1::UnsignedInteger {
8106 value: matches.len() as u64,
8107 },
8108 )?);
8109 }
8110 if let Some(crate::RawTransformPathResolutionV1::Exact(path_match)) = path_resolution {
8111 let digest = crate::sha256_hex(
8112 &serde_json::to_vec(path_match.parent_chain()).expect("parent chain serializes"),
8113 );
8114 for (field, value) in [
8115 (
8116 "root_motion_source.source_node_index",
8117 PredictionScalarV1::UnsignedInteger {
8118 value: path_match.source_node_index(),
8119 },
8120 ),
8121 (
8122 "root_motion_source.projected_bone_index",
8123 path_match
8124 .projected_bone_index()
8125 .map_or(PredictionScalarV1::Null, |value| {
8126 PredictionScalarV1::UnsignedInteger { value }
8127 }),
8128 ),
8129 (
8130 "root_motion_source.path",
8131 PredictionScalarV1::text(path_match.path().as_str())?,
8132 ),
8133 ("root_motion_source.node_kind", root_motion_token("source")?),
8134 (
8135 "root_motion_source.parent_chain_count",
8136 PredictionScalarV1::UnsignedInteger {
8137 value: path_match.parent_chain().len() as u64,
8138 },
8139 ),
8140 (
8141 "root_motion_source.parent_chain_sha256",
8142 PredictionScalarV1::text(digest)?,
8143 ),
8144 ] {
8145 refs.push(root_motion_project_reference(field, value)?);
8146 }
8147 }
8148 if duplicate_count == 1
8149 && let Some(measurement) = measurement
8150 {
8151 let escaped = name.replace('~', "~0").replace('/', "~1");
8152 let prefix = format!("/measurements/clips/{escaped}/root_trajectory");
8153 let availability = |value| {
8154 root_motion_token(match value {
8155 MeasurementAvailability::Measured => "measured",
8156 MeasurementAvailability::NotApplicable => "not_applicable",
8157 MeasurementAvailability::Unavailable => "unavailable",
8158 })
8159 };
8160 let mut add_measurement = |pointer: String, value| -> Result<(), PredictionContractError> {
8161 refs.push(root_motion_lift(
8162 PredictionBasisReferenceV1::measurement_v16(
8163 crate::MeasurementPointerV1::new(pointer)?,
8164 value,
8165 ),
8166 ));
8167 Ok(())
8168 };
8169 add_measurement(
8170 format!("{prefix}_availability"),
8171 availability(measurement.root_trajectory_availability)?,
8172 )?;
8173 if let Some(trajectory) = measurement.root_trajectory.as_ref() {
8174 add_measurement(
8175 format!("{prefix}/bone_index"),
8176 PredictionScalarV1::UnsignedInteger {
8177 value: u64::from(trajectory.bone_index),
8178 },
8179 )?;
8180 add_measurement(
8181 format!("{prefix}/source_role"),
8182 root_motion_token(trajectory.source_role.as_str())?,
8183 )?;
8184 let (suffix, present_field, availability_value, present) = match axis {
8185 crate::RootMotionAxisV1::HorizontalXz | crate::RootMotionAxisV1::VerticalY => (
8186 "translation_availability",
8187 "measurement.root_translation_present",
8188 trajectory.translation_availability,
8189 trajectory.translation.is_some(),
8190 ),
8191 crate::RootMotionAxisV1::Yaw => (
8192 "yaw_availability",
8193 "measurement.root_yaw_present",
8194 trajectory.yaw_availability,
8195 trajectory.yaw.is_some(),
8196 ),
8197 };
8198 add_measurement(
8199 format!("{prefix}/{suffix}"),
8200 availability(availability_value)?,
8201 )?;
8202 refs.push(root_motion_project_reference(
8203 present_field,
8204 PredictionScalarV1::Boolean { value: present },
8205 )?);
8206 }
8207 }
8208 EnginePredictionBasisV4::new(refs)
8209}
8210
8211trait RootMotionFindingEvidence {
8212 fn root_motion_wire_value(&self) -> serde_json::Value;
8213}
8214
8215impl RootMotionFindingEvidence for crate::Finding {
8216 fn root_motion_wire_value(&self) -> serde_json::Value {
8217 serde_json::to_value(self).expect("Finding serializes")
8218 }
8219}
8220
8221impl RootMotionFindingEvidence for PredictionFindingInput {
8222 fn root_motion_wire_value(&self) -> serde_json::Value {
8223 serde_json::to_value(self).expect("finding input serializes")
8224 }
8225}
8226
8227fn root_motion_disposition(
8228 setting: &EngineSettingValueV2,
8229) -> Option<crate::RootMotionImporterDispositionV1> {
8230 match setting {
8231 EngineSettingValueV2::BakeOrExtract(crate::EngineBakeOrExtractV1::Bake) => {
8232 Some(crate::RootMotionImporterDispositionV1::BakedIntoPose)
8233 }
8234 EngineSettingValueV2::BakeOrExtract(crate::EngineBakeOrExtractV1::Extract) => {
8235 Some(crate::RootMotionImporterDispositionV1::StoredAsRootMotion)
8236 }
8237 _ => None,
8238 }
8239}
8240
8241fn root_motion_unavailable_reason(
8242 path_resolution: Option<&crate::RawTransformPathResolutionV1>,
8243 resolved_root_bone_index: Option<u64>,
8244 root_name: Option<&str>,
8245 measurement: Option<&ClipMeasurements>,
8246 setting: Option<&EngineSettingValueV2>,
8247 axis: crate::RootMotionAxisV1,
8248) -> Result<Option<PredictionUnavailableReasonV2>, PredictionContractError> {
8249 let path_match = match path_resolution {
8250 Some(crate::RawTransformPathResolutionV1::Exact(path_match)) => path_match,
8251 Some(crate::RawTransformPathResolutionV1::NoMatch) | None => {
8252 return Ok(Some(PredictionUnavailableReasonV2::SourceSelectorNoMatch));
8253 }
8254 Some(crate::RawTransformPathResolutionV1::Ambiguous { .. }) => {
8255 return Ok(Some(PredictionUnavailableReasonV2::SourceSelectorAmbiguous));
8256 }
8257 Some(crate::RawTransformPathResolutionV1::CoverageIncomplete { .. }) => {
8258 return Ok(Some(PredictionUnavailableReasonV2::RawSourceIncomplete));
8259 }
8260 };
8261 if resolved_root_bone_index.is_none()
8262 || path_match.projected_bone_index() != resolved_root_bone_index
8263 {
8264 return Ok(Some(PredictionUnavailableReasonV2::custom(
8265 "animsmith:root_motion_source_not_explicit_root",
8266 )?));
8267 }
8268 let Some(trajectory) = measurement
8269 .filter(|measurement| {
8270 measurement.root_trajectory_availability == MeasurementAvailability::Measured
8271 })
8272 .and_then(|measurement| measurement.root_trajectory.as_ref())
8273 else {
8274 return Ok(Some(PredictionUnavailableReasonV2::MeasurementUnavailable));
8275 };
8276 if trajectory.source_role != crate::measure::RootTrajectorySourceRole::Root
8277 || root_name != Some(trajectory.bone_name.as_str())
8278 || resolved_root_bone_index != Some(u64::from(trajectory.bone_index))
8279 {
8280 return Ok(Some(PredictionUnavailableReasonV2::custom(
8281 "animsmith:root_motion_source_not_explicit_root",
8282 )?));
8283 }
8284 let measured = match axis {
8285 crate::RootMotionAxisV1::HorizontalXz | crate::RootMotionAxisV1::VerticalY => {
8286 trajectory.translation_availability == MeasurementAvailability::Measured
8287 && trajectory.translation.is_some()
8288 }
8289 crate::RootMotionAxisV1::Yaw => {
8290 trajectory.yaw_availability == MeasurementAvailability::Measured
8291 && trajectory.yaw.is_some()
8292 }
8293 };
8294 if !measured {
8295 return Ok(Some(PredictionUnavailableReasonV2::MeasurementUnavailable));
8296 }
8297 if setting.and_then(root_motion_disposition).is_none() {
8298 return Ok(Some(
8299 PredictionUnavailableReasonV2::ResolvedSettingsOverflow,
8300 ));
8301 }
8302 Ok(None)
8303}
8304
8305fn track_scope(code: &'static str, subject: String) -> EvaluationScope {
8306 EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(code)).subject(subject)
8307}
8308
8309fn track_subject_facet(
8310 scope: EvaluationScope,
8311 basis: EnginePredictionBasisV4,
8312 subject_kind: SourceImportSubjectKindV1,
8313 gate: Option<EngineSettingIdV2>,
8314) -> crate::EnginePredictionFacetV4 {
8315 match gate {
8316 None => track_unavailable(
8317 scope,
8318 basis,
8319 PredictionUnavailableReasonV2::RuntimeAnimationSurvivalUnavailable,
8320 ),
8321 Some(controlling_gate) => crate::EnginePredictionFacetV4::available(
8322 scope,
8323 basis,
8324 EngineMachineResultV1::SourceImportDisposition(SourceImportDispositionResultV1 {
8325 subject_kind,
8326 disposition: SourceImportDispositionV1::Dropped,
8327 controlling_gate: Some(controlling_gate),
8328 }),
8329 )
8330 .expect("reconstructed track result is valid"),
8331 }
8332}
8333
8334fn track_unavailable(
8335 scope: EvaluationScope,
8336 basis: EnginePredictionBasisV4,
8337 reason: PredictionUnavailableReasonV2,
8338) -> crate::EnginePredictionFacetV4 {
8339 crate::EnginePredictionFacetV4::required_unavailable(scope, basis, vec![reason])
8340 .expect("reconstructed track unavailable facet is valid")
8341}
8342
8343fn track_gate(provenance: &PredictionProvenanceV5) -> Option<EngineSettingIdV2> {
8344 let settings = provenance.base().settings();
8345 if matches!(
8346 settings
8347 .document_setting(EngineSettingIdV2::BevyAnimationFeature)
8348 .map(|row| row.value()),
8349 Some(EngineSettingValueV2::Boolean(false))
8350 ) {
8351 Some(EngineSettingIdV2::BevyAnimationFeature)
8352 } else if matches!(
8353 settings
8354 .document_setting(EngineSettingIdV2::LoadAnimations)
8355 .map(|row| row.value()),
8356 Some(EngineSettingValueV2::Boolean(false))
8357 ) {
8358 Some(EngineSettingIdV2::LoadAnimations)
8359 } else {
8360 None
8361 }
8362}
8363
8364fn track_static_basis() -> EnginePredictionBasisV4 {
8365 let v1 = |reference| PredictionBasisReferenceV4::v2(PredictionBasisReferenceV2::v1(reference));
8366 EnginePredictionBasisV4::new(vec![
8367 v1(PredictionBasisReferenceV1::profile_fact("source_import_disposition").unwrap()),
8368 v1(PredictionBasisReferenceV1::primary_source("bevy-gltf-loader-0.19.0-c6f634ca").unwrap()),
8369 v1(
8370 PredictionBasisReferenceV1::primary_source("bevy-feature-manifest-0.19.0-c6f634ca")
8371 .unwrap(),
8372 ),
8373 v1(PredictionBasisReferenceV1::resolved_setting(
8374 ResolvedSettingLocationV1::Document,
8375 EngineSettingIdV2::BevyAnimationFeature.as_str(),
8376 )
8377 .unwrap()),
8378 v1(PredictionBasisReferenceV1::resolved_setting(
8379 ResolvedSettingLocationV1::Document,
8380 EngineSettingIdV2::LoadAnimations.as_str(),
8381 )
8382 .unwrap()),
8383 ])
8384 .unwrap()
8385}
8386
8387fn track_inventory_basis(inventory: &RawAnimationChannelInventoryV1) -> EnginePredictionBasisV4 {
8388 let mut references = track_static_basis().references().to_vec();
8389 references.push(PredictionBasisReferenceV4::v2(
8390 PredictionBasisReferenceV2::v1(
8391 PredictionBasisReferenceV1::project_field(
8392 "raw_animation_channel_inventory.animation_coverage",
8393 PredictionScalarV1::text(match inventory.animation_coverage().state() {
8394 RawSourceSetCoverageStateV1::Complete => "complete",
8395 RawSourceSetCoverageStateV1::Partial => "partial",
8396 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
8397 })
8398 .unwrap(),
8399 )
8400 .unwrap(),
8401 ),
8402 ));
8403 references.push(PredictionBasisReferenceV4::v2(
8404 PredictionBasisReferenceV2::v1(
8405 PredictionBasisReferenceV1::project_field(
8406 "raw_animation_channel_inventory.source_coverage_complete",
8407 PredictionScalarV1::Boolean {
8408 value: inventory.source_coverage_complete(),
8409 },
8410 )
8411 .unwrap(),
8412 ),
8413 ));
8414 if let Some(row) = inventory.rows().iter().find(|row| {
8415 row.channel_coverage()
8416 .is_some_and(|coverage| coverage.state() != RawSourceSetCoverageStateV1::Complete)
8417 }) {
8418 let coverage = row.channel_coverage().unwrap();
8419 let state = match coverage.state() {
8420 RawSourceSetCoverageStateV1::Complete => "complete",
8421 RawSourceSetCoverageStateV1::Partial => "partial",
8422 RawSourceSetCoverageStateV1::Unavailable => "unavailable",
8423 };
8424 references.push(PredictionBasisReferenceV4::v2(
8425 PredictionBasisReferenceV2::v1(
8426 PredictionBasisReferenceV1::project_field(
8427 "raw_animation_channel_inventory.incomplete_channel_animation_row",
8428 PredictionScalarV1::UnsignedInteger {
8429 value: row.source_animation_index(),
8430 },
8431 )
8432 .unwrap(),
8433 ),
8434 ));
8435 references.push(PredictionBasisReferenceV4::v2(
8436 PredictionBasisReferenceV2::v1(
8437 PredictionBasisReferenceV1::project_field(
8438 "raw_animation_channel_inventory.incomplete_channel_coverage",
8439 PredictionScalarV1::text(state).unwrap(),
8440 )
8441 .unwrap(),
8442 ),
8443 ));
8444 if let Some(reason) = coverage.reason() {
8445 let reason = serde_json::to_value(reason).unwrap();
8446 references.push(PredictionBasisReferenceV4::v2(
8447 PredictionBasisReferenceV2::v1(
8448 PredictionBasisReferenceV1::project_field(
8449 "raw_animation_channel_inventory.incomplete_channel_reason",
8450 PredictionScalarV1::text(reason.as_str().unwrap()).unwrap(),
8451 )
8452 .unwrap(),
8453 ),
8454 ));
8455 }
8456 }
8457 EnginePredictionBasisV4::new(references).unwrap()
8458}
8459
8460fn track_row_basis(
8461 inventory: &RawAnimationChannelInventoryV1,
8462 animation: u64,
8463 channel: Option<u64>,
8464) -> EnginePredictionBasisV4 {
8465 let mut references = track_inventory_basis(inventory).references().to_vec();
8466 references.push(PredictionBasisReferenceV4::v2(
8467 PredictionBasisReferenceV2::v1(
8468 PredictionBasisReferenceV1::project_field(
8469 "raw_animation_channel_inventory.animation_row",
8470 PredictionScalarV1::UnsignedInteger { value: animation },
8471 )
8472 .unwrap(),
8473 ),
8474 ));
8475 if let Some(channel) = channel {
8476 references.push(PredictionBasisReferenceV4::v2(
8477 PredictionBasisReferenceV2::v1(
8478 PredictionBasisReferenceV1::project_field(
8479 "raw_animation_channel_inventory.channel_row",
8480 PredictionScalarV1::UnsignedInteger { value: channel },
8481 )
8482 .unwrap(),
8483 ),
8484 ));
8485 }
8486 EnginePredictionBasisV4::new(references).unwrap()
8487}
8488
8489const ENGINE_UNIT_SCALE_CHECK_ID: &str = "engine-unit-scale";
8490const ENGINE_UNIT_SCALE_FILE_SCOPE: &str = "engine-unit-scale:file-unit";
8491const ENGINE_UNIT_SCALE_SCENE_SCOPE: &str = "engine-unit-scale:loader-scene-root";
8492const ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE: &str = "engine-unit-scale:scene-inventory";
8493const ENGINE_UNIT_SCALE_MESH_SCOPE: &str = "engine-unit-scale:loader-mesh-primitive";
8494const ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE: &str = "engine-unit-scale:mesh-inventory";
8495const ENGINE_UNIT_SCALE_SELECTED_SCOPE: &str = "engine-unit-scale:selected-source-node";
8496const ENGINE_UNIT_SCALE_BUDGET_SCOPE: &str = "engine-unit-scale:facet-budget";
8497const ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON: &str =
8498 "animsmith:selected_node_scene_reachability_unavailable";
8499const ENGINE_UNIT_SCALE_SELECTED_MAX_REACHABILITY_NODES: usize = 128;
8500
8501#[derive(Clone, PartialEq, Eq)]
8502struct ExpectedUnitScaleFacet {
8503 scope: EvaluationScope,
8504 result: Option<EngineMachineResultV1>,
8505 reasons: Vec<PredictionUnavailableReasonV2>,
8506}
8507
8508fn unit_scale_scope(code: &'static str, subject: Option<String>) -> EvaluationScope {
8509 let scope = EvaluationScope::new(crate::evaluation::EvaluationScopeCode::custom(code));
8510 subject.map_or(scope.clone(), |subject| scope.subject(subject))
8511}
8512
8513fn unit_scale_unavailable_reasons(
8514 reason: PredictionUnavailableReasonV2,
8515 dependency_complete: bool,
8516) -> Vec<PredictionUnavailableReasonV2> {
8517 let mut reasons = vec![reason];
8518 if !dependency_complete {
8519 reasons.push(PredictionUnavailableReasonV2::DependencyClosureIncomplete);
8520 }
8521 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
8522 reasons
8523}
8524
8525#[derive(Clone, Copy, PartialEq, Eq)]
8526enum UnitScaleSelectedReachability {
8527 Reachable(u64, u64),
8528 Unreachable,
8529 Unavailable,
8530 WorkBudgetExceeded,
8531}
8532
8533enum UnitScaleSelectedReachabilityPlan {
8534 Complete {
8535 node_index: u64,
8536 scene_witnesses: BTreeMap<u64, (u64, u64)>,
8537 },
8538 Refused {
8539 node_index: u64,
8540 },
8541}
8542
8543enum UnitScaleSelectedReachabilityPlans {
8544 Complete {
8545 plans: BTreeMap<String, UnitScaleSelectedReachabilityPlan>,
8546 selected_facet_count: usize,
8547 },
8548 Overflow {
8549 plans: BTreeMap<String, UnitScaleSelectedReachabilityPlan>,
8550 },
8551}
8552
8553impl UnitScaleSelectedReachabilityPlans {
8554 fn get(&self, selector: &str) -> Option<&UnitScaleSelectedReachabilityPlan> {
8555 match self {
8556 Self::Complete { plans, .. } | Self::Overflow { plans } => plans.get(selector),
8557 }
8558 }
8559
8560 const fn selected_facet_count(&self) -> usize {
8561 match self {
8562 Self::Complete {
8563 selected_facet_count,
8564 ..
8565 } => *selected_facet_count,
8566 Self::Overflow { .. } => PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE + 1,
8567 }
8568 }
8569}
8570
8571impl UnitScaleSelectedReachabilityPlan {
8572 const fn node_index(&self) -> u64 {
8573 match self {
8574 Self::Complete { node_index, .. } | Self::Refused { node_index } => *node_index,
8575 }
8576 }
8577}
8578
8579fn unit_scale_selected_reachability_budget_exceeded(work: &mut usize) -> bool {
8580 *work = work.saturating_add(1);
8581 *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
8582}
8583
8584fn unit_scale_selected_reachability(
8585 nodes: &BTreeMap<usize, &crate::measure::SkeletonNodeMeasurements>,
8586 start: u64,
8587 roots: &[u64],
8588 work: &mut usize,
8589) -> UnitScaleSelectedReachability {
8590 let Ok(start) = usize::try_from(start) else {
8591 return UnitScaleSelectedReachability::Unavailable;
8592 };
8593 if *work >= PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8594 return UnitScaleSelectedReachability::WorkBudgetExceeded;
8595 }
8596 let roots = roots
8597 .iter()
8598 .copied()
8599 .enumerate()
8600 .map(|(ordinal, node)| (node, ordinal as u64))
8601 .collect::<BTreeMap<_, _>>();
8602 let mut seen = BTreeSet::new();
8603 let mut current = Some(start);
8604 for _ in 0..ENGINE_UNIT_SCALE_SELECTED_MAX_REACHABILITY_NODES {
8605 let Some(index) = current else {
8606 return UnitScaleSelectedReachability::Unreachable;
8607 };
8608 if unit_scale_selected_reachability_budget_exceeded(work) {
8609 return UnitScaleSelectedReachability::WorkBudgetExceeded;
8610 }
8611 if let Some(ordinal) = roots.get(&(index as u64)) {
8612 return UnitScaleSelectedReachability::Reachable(*ordinal, index as u64);
8613 }
8614 if !seen.insert(index) {
8615 return UnitScaleSelectedReachability::Unavailable;
8616 }
8617 current = match nodes.get(&index) {
8618 Some(node) => node.parent_node_index,
8619 None => return UnitScaleSelectedReachability::Unavailable,
8620 };
8621 }
8622 match current {
8623 Some(_) => UnitScaleSelectedReachability::Unavailable,
8624 None => UnitScaleSelectedReachability::Unreachable,
8625 }
8626}
8627
8628fn unit_scale_selected_reachability_plans(
8629 provenance: &PredictionProvenanceV4,
8630 measurements: &MeasurementContract,
8631) -> UnitScaleSelectedReachabilityPlans {
8632 let assets = measurements.assets();
8633 let inventory = provenance
8634 .raw_scene_attachment()
8635 .inventory()
8636 .filter(|inventory| {
8637 assets.skeleton_source_coverage == SourceSkeletonCoverage::Complete
8638 && inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
8639 && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
8640 });
8641 let nodes = assets
8642 .skeleton_nodes
8643 .iter()
8644 .map(|node| (node.node_index, node))
8645 .collect::<BTreeMap<_, _>>();
8646 let mut plans = BTreeMap::new();
8647 let mut selected_facet_count = 0usize;
8648 for selector in provenance.rule_inputs().runtime_node_selectors() {
8649 let Some(inventory) = inventory else {
8650 selected_facet_count = selected_facet_count.saturating_add(1);
8651 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8652 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8653 }
8654 continue;
8655 };
8656 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
8657 node.name
8658 .as_deref()
8659 .is_some_and(|name| crate::config::glob_match(selector, name))
8660 });
8661 let Some(node) = matches.next() else {
8662 selected_facet_count = selected_facet_count.saturating_add(1);
8663 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8664 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8665 }
8666 continue;
8667 };
8668 if matches.next().is_some() {
8669 selected_facet_count = selected_facet_count.saturating_add(1);
8670 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8671 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8672 }
8673 continue;
8674 }
8675 let node_index = node.node_index as u64;
8676 let mut work = 0usize;
8677 let remaining_capacity =
8678 PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE.saturating_sub(selected_facet_count);
8679 let mut would_overflow = false;
8680 let mut plan = UnitScaleSelectedReachabilityPlan::Complete {
8681 node_index,
8682 scene_witnesses: BTreeMap::new(),
8683 };
8684 for scene in inventory.scenes().rows() {
8685 match unit_scale_selected_reachability(
8686 &nodes,
8687 node_index,
8688 scene.root_node_indices(),
8689 &mut work,
8690 ) {
8691 UnitScaleSelectedReachability::Reachable(root_ordinal, root_node_index) => {
8692 let UnitScaleSelectedReachabilityPlan::Complete {
8693 scene_witnesses, ..
8694 } = &mut plan
8695 else {
8696 unreachable!("a reachable scene cannot follow a refusal");
8697 };
8698 if scene_witnesses.len() < remaining_capacity {
8699 scene_witnesses
8700 .insert(scene.source_scene_index(), (root_ordinal, root_node_index));
8701 } else {
8702 would_overflow = true;
8703 }
8704 }
8705 UnitScaleSelectedReachability::Unreachable => {}
8706 UnitScaleSelectedReachability::Unavailable
8707 | UnitScaleSelectedReachability::WorkBudgetExceeded => {
8708 plan = UnitScaleSelectedReachabilityPlan::Refused { node_index };
8709 break;
8710 }
8711 }
8712 }
8713 if matches!(&plan, UnitScaleSelectedReachabilityPlan::Complete { .. }) && would_overflow {
8714 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8715 }
8716 let facets = match &plan {
8717 UnitScaleSelectedReachabilityPlan::Complete {
8718 scene_witnesses, ..
8719 } => scene_witnesses.len().max(1),
8720 UnitScaleSelectedReachabilityPlan::Refused { .. } => 1,
8721 };
8722 selected_facet_count = selected_facet_count.saturating_add(facets);
8723 if selected_facet_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE {
8724 return UnitScaleSelectedReachabilityPlans::Overflow { plans };
8725 }
8726 plans.insert(selector.clone(), plan);
8727 }
8728 UnitScaleSelectedReachabilityPlans::Complete {
8729 plans,
8730 selected_facet_count,
8731 }
8732}
8733
8734fn unit_scale_selected_ancestry_reason(
8735 nodes: &BTreeMap<usize, &crate::measure::SkeletonNodeMeasurements>,
8736 start: usize,
8737) -> Option<PredictionUnavailableReasonV2> {
8738 let mut current = start;
8739 let mut seen = BTreeSet::new();
8740 for _ in 0..128 {
8741 if !seen.insert(current) {
8742 return Some(
8743 PredictionUnavailableReasonV2::custom(
8744 "animsmith:selected_node_ancestry_unavailable",
8745 )
8746 .expect("static reason is valid"),
8747 );
8748 }
8749 let Some(node) = nodes.get(¤t) else {
8750 return Some(
8751 PredictionUnavailableReasonV2::custom(
8752 "animsmith:selected_node_ancestry_unavailable",
8753 )
8754 .expect("static reason is valid"),
8755 );
8756 };
8757 match node.local_rest {
8758 SkeletonNodeLocalRestMeasurements::Matrix { .. } => {
8759 return Some(
8760 PredictionUnavailableReasonV2::custom(
8761 "animsmith:matrix_authored_selected_node_or_ancestry",
8762 )
8763 .expect("static reason is valid"),
8764 );
8765 }
8766 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => {
8767 return Some(
8768 PredictionUnavailableReasonV2::custom(
8769 "animsmith:selected_node_ancestry_unavailable",
8770 )
8771 .expect("static reason is valid"),
8772 );
8773 }
8774 SkeletonNodeLocalRestMeasurements::Trs { .. } => {}
8775 }
8776 let parent = node.parent_node_index?;
8777 current = parent;
8778 }
8779 Some(
8780 PredictionUnavailableReasonV2::custom("animsmith:selected_node_ancestry_unavailable")
8781 .expect("static reason is valid"),
8782 )
8783}
8784
8785#[derive(Clone, Copy)]
8786struct CurrentUnitScaleMeshRow {
8787 source_scene_index: u64,
8788 source_root_ordinal: u64,
8789 root_node_index: u64,
8790 source_node_index: u64,
8791 source_mesh_index: u64,
8792 source_primitive_index: u64,
8793}
8794
8795enum CurrentUnitScaleMeshPlan {
8796 Detailed(Vec<CurrentUnitScaleMeshRow>),
8797 CompleteEmpty,
8798 Incomplete,
8799 JoinOverflow,
8800}
8801
8802fn unit_scale_join_budget_exceeded(work: &mut usize) -> bool {
8803 *work = work.saturating_add(1);
8804 *work > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE
8805}
8806
8807fn unit_scale_reachable_root_indexed(
8808 start: u64,
8809 roots: &BTreeMap<u64, u64>,
8810 parents: &BTreeMap<u64, Option<u64>>,
8811 work: &mut usize,
8812) -> Result<Option<(u64, u64)>, ()> {
8813 let mut seen = BTreeSet::new();
8814 let mut current = Some(start);
8815 while let Some(index) = current {
8816 if unit_scale_join_budget_exceeded(work) {
8817 return Err(());
8818 }
8819 if let Some(ordinal) = roots.get(&index) {
8820 return Ok(Some((*ordinal, index)));
8821 }
8822 if !seen.insert(index) {
8823 return Ok(None);
8824 }
8825 current = match parents.get(&index) {
8826 Some(parent) => *parent,
8827 None => return Ok(None),
8828 };
8829 }
8830 Ok(None)
8831}
8832
8833fn current_unit_scale_mesh_plan(
8834 provenance: &PredictionProvenanceV4,
8835 measurements: &MeasurementContract,
8836) -> CurrentUnitScaleMeshPlan {
8837 let Some(inventory) = provenance.raw_scene_attachment().inventory() else {
8838 return CurrentUnitScaleMeshPlan::Incomplete;
8839 };
8840 if inventory.scenes().coverage() != RawSceneAttachmentCoverageV1::Complete
8841 || inventory.node_mesh_attachments().coverage() != RawSceneAttachmentCoverageV1::Complete
8842 || inventory.mesh_primitives().coverage() != RawSceneAttachmentCoverageV1::Complete
8843 || inventory.source_skeleton().coverage() != RawSceneAttachmentCoverageV1::Complete
8844 || measurements.assets().skeleton_source_coverage != SourceSkeletonCoverage::Complete
8845 {
8846 return CurrentUnitScaleMeshPlan::Incomplete;
8847 }
8848 let parents = measurements
8849 .assets()
8850 .skeleton_nodes
8851 .iter()
8852 .map(|node| {
8853 (
8854 node.node_index as u64,
8855 node.parent_node_index.map(|parent| parent as u64),
8856 )
8857 })
8858 .collect::<BTreeMap<_, _>>();
8859 let scene_roots = inventory
8860 .scenes()
8861 .rows()
8862 .iter()
8863 .map(|scene| {
8864 (
8865 scene.source_scene_index(),
8866 scene
8867 .root_node_indices()
8868 .iter()
8869 .copied()
8870 .enumerate()
8871 .map(|(ordinal, node)| (node, ordinal as u64))
8872 .collect::<BTreeMap<_, _>>(),
8873 )
8874 })
8875 .collect::<Vec<_>>();
8876 let primitives_by_mesh = inventory.mesh_primitives().rows().iter().fold(
8877 BTreeMap::<u64, Vec<u64>>::new(),
8878 |mut grouped, primitive| {
8879 grouped
8880 .entry(primitive.source_mesh_index())
8881 .or_default()
8882 .push(primitive.source_primitive_index());
8883 grouped
8884 },
8885 );
8886 let mut work = 0usize;
8887 let mut rows = Vec::new();
8888 for (source_scene_index, roots) in scene_roots {
8889 for attachment in inventory.node_mesh_attachments().rows() {
8890 if unit_scale_join_budget_exceeded(&mut work) {
8891 return CurrentUnitScaleMeshPlan::JoinOverflow;
8892 }
8893 let Ok(reachable) = unit_scale_reachable_root_indexed(
8894 attachment.source_node_index(),
8895 &roots,
8896 &parents,
8897 &mut work,
8898 ) else {
8899 return CurrentUnitScaleMeshPlan::JoinOverflow;
8900 };
8901 let Some((source_root_ordinal, root_node_index)) = reachable else {
8902 continue;
8903 };
8904 for &source_primitive_index in primitives_by_mesh
8905 .get(&attachment.source_mesh_index())
8906 .into_iter()
8907 .flatten()
8908 {
8909 if unit_scale_join_budget_exceeded(&mut work) {
8910 return CurrentUnitScaleMeshPlan::JoinOverflow;
8911 }
8912 rows.push(CurrentUnitScaleMeshRow {
8913 source_scene_index,
8914 source_root_ordinal,
8915 root_node_index,
8916 source_node_index: attachment.source_node_index(),
8917 source_mesh_index: attachment.source_mesh_index(),
8918 source_primitive_index,
8919 });
8920 }
8921 }
8922 }
8923 if rows.is_empty() {
8924 CurrentUnitScaleMeshPlan::CompleteEmpty
8925 } else {
8926 CurrentUnitScaleMeshPlan::Detailed(rows)
8927 }
8928}
8929
8930fn current_unit_scale_selected_facet_count(
8931 reachability_plans: &UnitScaleSelectedReachabilityPlans,
8932) -> usize {
8933 reachability_plans.selected_facet_count()
8934}
8935
8936fn expected_current_engine_unit_scale_facets(
8937 provenance: &PredictionProvenanceV4,
8938 measurements: &MeasurementContract,
8939 mesh_plan: &CurrentUnitScaleMeshPlan,
8940 candidate_capacity: usize,
8941 reachability_plans: &UnitScaleSelectedReachabilityPlans,
8942) -> Option<Vec<ExpectedUnitScaleFacet>> {
8943 let dependency_complete = matches!(
8944 provenance.dependency_closure().coverage(),
8945 DependencyClosureCoverageV1::Complete
8946 );
8947 let available = |scope, result| ExpectedUnitScaleFacet {
8948 scope,
8949 result: Some(result),
8950 reasons: vec![],
8951 };
8952 let unavailable = |scope, reasons| ExpectedUnitScaleFacet {
8953 scope,
8954 result: None,
8955 reasons,
8956 };
8957 let mut expected = Vec::with_capacity(candidate_capacity);
8958 if expected.len() < candidate_capacity {
8959 expected.push(if dependency_complete {
8960 available(
8961 unit_scale_scope(ENGINE_UNIT_SCALE_FILE_SCOPE, None),
8962 EngineMachineResultV1::UnitMapping(
8963 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
8964 ),
8965 )
8966 } else {
8967 unavailable(
8968 unit_scale_scope(ENGINE_UNIT_SCALE_FILE_SCOPE, None),
8969 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
8970 )
8971 });
8972 }
8973
8974 let inventory = provenance.raw_scene_attachment().inventory();
8975 match inventory
8976 .filter(|inventory| inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete)
8977 {
8978 Some(inventory) => {
8979 for scene in inventory.scenes().rows() {
8980 if expected.len() >= candidate_capacity {
8981 break;
8982 }
8983 let scope = unit_scale_scope(
8984 ENGINE_UNIT_SCALE_SCENE_SCOPE,
8985 Some(format!("source_scene:{}", scene.source_scene_index())),
8986 );
8987 expected.push(if dependency_complete {
8988 available(
8989 scope,
8990 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
8991 subject_kind: TransformScaleSubjectKindV1::LoaderSceneEntity,
8992 creation: ImporterSubjectCreationV1::Created,
8993 domain: TransformScaleDomainV1::Local,
8994 classification: Some(LinearTransformClassification::UnitOrthonormal),
8995 }),
8996 )
8997 } else {
8998 unavailable(
8999 scope,
9000 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9001 )
9002 });
9003 }
9004 }
9005 None if expected.len() < candidate_capacity => expected.push(unavailable(
9006 unit_scale_scope(ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE, None),
9007 unit_scale_unavailable_reasons(
9008 PredictionUnavailableReasonV2::RawSourceIncomplete,
9009 dependency_complete,
9010 ),
9011 )),
9012 None => {}
9013 }
9014
9015 let assets = measurements.assets();
9016 let nodes = assets
9017 .skeleton_nodes
9018 .iter()
9019 .map(|node| (node.node_index, node))
9020 .collect::<BTreeMap<_, _>>();
9021 if expected.len() < candidate_capacity {
9022 match mesh_plan {
9023 CurrentUnitScaleMeshPlan::Incomplete => expected.push(unavailable(
9024 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
9025 unit_scale_unavailable_reasons(
9026 PredictionUnavailableReasonV2::RawSourceIncomplete,
9027 dependency_complete,
9028 ),
9029 )),
9030 CurrentUnitScaleMeshPlan::JoinOverflow => expected.push(unavailable(
9031 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
9032 vec![
9033 PredictionUnavailableReasonV2::custom(
9034 "animsmith:mesh_join_work_budget_exceeded",
9035 )
9036 .expect("static reason is valid"),
9037 ],
9038 )),
9039 CurrentUnitScaleMeshPlan::CompleteEmpty => expected.push(if dependency_complete {
9040 available(
9041 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
9042 EngineMachineResultV1::InventoryCoverage(InventoryCoverageResultV1 {
9043 domain: PredictionInventoryDomainV1::LoaderMeshPrimitiveSubjects,
9044 coverage: PredictionInventoryCoverageStateV1::Complete,
9045 retained_rows: 0,
9046 }),
9047 )
9048 } else {
9049 unavailable(
9050 unit_scale_scope(ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE, None),
9051 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9052 )
9053 }),
9054 CurrentUnitScaleMeshPlan::Detailed(rows) => {
9055 let load_meshes = matches!(
9056 provenance
9057 .settings()
9058 .document_setting(EngineSettingIdV2::LoadMeshes)
9059 .map(|setting| setting.value()),
9060 Some(EngineSettingValueV2::Token(value)) if value == "nonempty"
9061 );
9062 for row in rows {
9063 if expected.len() >= candidate_capacity {
9064 break;
9065 }
9066 let scope = unit_scale_scope(
9067 ENGINE_UNIT_SCALE_MESH_SCOPE,
9068 Some(format!(
9069 "source_scene:{}:source_node:{}:source_mesh:{}:source_primitive:{}",
9070 row.source_scene_index,
9071 row.source_node_index,
9072 row.source_mesh_index,
9073 row.source_primitive_index,
9074 )),
9075 );
9076 expected.push(if dependency_complete {
9077 available(
9078 scope,
9079 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
9080 subject_kind:
9081 TransformScaleSubjectKindV1::LoaderMeshPrimitiveEntity,
9082 creation: if load_meshes {
9083 ImporterSubjectCreationV1::Created
9084 } else {
9085 ImporterSubjectCreationV1::SuppressedBySetting
9086 },
9087 domain: TransformScaleDomainV1::Local,
9088 classification: load_meshes
9089 .then_some(LinearTransformClassification::UnitOrthonormal),
9090 }),
9091 )
9092 } else {
9093 unavailable(
9094 scope,
9095 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9096 )
9097 });
9098 }
9099 }
9100 }
9101 }
9102
9103 for selector in provenance.rule_inputs().runtime_node_selectors() {
9104 if expected.len() >= candidate_capacity {
9105 break;
9106 }
9107 if assets.skeleton_source_coverage != SourceSkeletonCoverage::Complete
9108 || !inventory.is_some_and(|inventory| {
9109 inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
9110 && inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
9111 })
9112 {
9113 expected.push(unavailable(
9114 unit_scale_scope(
9115 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9116 Some(format!("selector:{selector}")),
9117 ),
9118 unit_scale_unavailable_reasons(
9119 PredictionUnavailableReasonV2::RawSourceIncomplete,
9120 dependency_complete,
9121 ),
9122 ));
9123 continue;
9124 }
9125 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
9126 node.name
9127 .as_deref()
9128 .is_some_and(|name| crate::config::glob_match(selector, name))
9129 });
9130 let first = matches.next();
9131 let second = matches.next();
9132 if first.is_none() || second.is_some() {
9133 expected.push(unavailable(
9134 unit_scale_scope(
9135 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9136 Some(format!("selector:{selector}")),
9137 ),
9138 unit_scale_unavailable_reasons(
9139 if first.is_none() {
9140 PredictionUnavailableReasonV2::SourceSelectorNoMatch
9141 } else {
9142 PredictionUnavailableReasonV2::SourceSelectorAmbiguous
9143 },
9144 dependency_complete,
9145 ),
9146 ));
9147 continue;
9148 }
9149 let node = first.expect("one selected source node was established");
9150 let inventory = inventory.expect("selected inventory was proven complete");
9151 let mut reachable_scenes = Vec::new();
9152 let plan = reachability_plans.get(selector)?;
9153 let reachability_unavailable =
9154 match plan {
9155 UnitScaleSelectedReachabilityPlan::Refused {
9156 node_index: cached_node_index,
9157 } if *cached_node_index == node.node_index as u64 => true,
9158 UnitScaleSelectedReachabilityPlan::Complete {
9159 node_index: cached_node_index,
9160 scene_witnesses,
9161 } if *cached_node_index == node.node_index as u64 => {
9162 reachable_scenes.extend(
9163 inventory.scenes().rows().iter().filter(|scene| {
9164 scene_witnesses.contains_key(&scene.source_scene_index())
9165 }),
9166 );
9167 false
9168 }
9169 UnitScaleSelectedReachabilityPlan::Refused { .. }
9170 | UnitScaleSelectedReachabilityPlan::Complete { .. } => return None,
9171 };
9172 if reachability_unavailable {
9173 expected.push(unavailable(
9174 unit_scale_scope(
9175 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9176 Some(format!("selector:{selector}")),
9177 ),
9178 unit_scale_unavailable_reasons(
9179 PredictionUnavailableReasonV2::custom(
9180 ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON,
9181 )
9182 .expect("static reason is valid"),
9183 dependency_complete,
9184 ),
9185 ));
9186 continue;
9187 }
9188 let reason = unit_scale_selected_ancestry_reason(&nodes, node.node_index).or_else(|| {
9189 (node.rest_world_linear.classification == LinearTransformClassification::NonFinite
9190 || node.rest_world_matrix.is_none())
9191 .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
9192 });
9193 let mut reachable_found = false;
9194 for scene in reachable_scenes {
9195 reachable_found = true;
9196 if expected.len() >= candidate_capacity {
9197 break;
9198 }
9199 let scope = unit_scale_scope(
9200 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9201 Some(format!(
9202 "selector:{selector}:source_scene:{}:source_node:{}",
9203 scene.source_scene_index(),
9204 node.node_index,
9205 )),
9206 );
9207 expected.push(if let Some(reason) = reason.clone() {
9208 unavailable(
9209 scope,
9210 unit_scale_unavailable_reasons(reason, dependency_complete),
9211 )
9212 } else if dependency_complete {
9213 available(
9214 scope,
9215 EngineMachineResultV1::TransformScale(TransformScaleResultV1 {
9216 subject_kind: TransformScaleSubjectKindV1::SelectedSourceNode,
9217 creation: ImporterSubjectCreationV1::Created,
9218 domain: TransformScaleDomainV1::LoaderRootToSubject,
9219 classification: Some(node.rest_world_linear.classification),
9220 }),
9221 )
9222 } else {
9223 unavailable(
9224 scope,
9225 vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9226 )
9227 });
9228 }
9229 if !reachable_found && expected.len() < candidate_capacity {
9230 expected.push(unavailable(
9231 unit_scale_scope(
9232 ENGINE_UNIT_SCALE_SELECTED_SCOPE,
9233 Some(format!("selector:{selector}")),
9234 ),
9235 unit_scale_unavailable_reasons(
9236 PredictionUnavailableReasonV2::custom("animsmith:selected_node_unreachable")
9237 .expect("static reason is valid"),
9238 dependency_complete,
9239 ),
9240 ));
9241 }
9242 }
9243 Some(expected)
9244}
9245
9246fn unit_scale_exact_raw_row_references(
9247 basis: &EnginePredictionBasisV4,
9248 expected: &[RawSceneAttachmentBasisReferenceV1],
9249) -> bool {
9250 let actual = basis
9251 .references()
9252 .iter()
9253 .filter_map(|reference| match reference {
9254 PredictionBasisReferenceV4::RawSceneAttachment(reference)
9255 if !matches!(
9256 reference,
9257 RawSceneAttachmentBasisReferenceV1::Coverage { .. }
9258 ) =>
9259 {
9260 Some(reference)
9261 }
9262 _ => None,
9263 })
9264 .collect::<Vec<_>>();
9265 actual.len() == expected.len() && actual.iter().all(|reference| expected.contains(reference))
9266}
9267
9268fn unit_scale_mesh_scope_keys(subject: &str) -> Option<(u64, u64, u64, u64)> {
9269 let values = subject
9270 .strip_prefix("source_scene:")?
9271 .split(':')
9272 .collect::<Vec<_>>();
9273 if values.len() != 7
9274 || values[1] != "source_node"
9275 || values[3] != "source_mesh"
9276 || values[5] != "source_primitive"
9277 {
9278 return None;
9279 }
9280 Some((
9281 values[0].parse().ok()?,
9282 values[2].parse().ok()?,
9283 values[4].parse().ok()?,
9284 values[6].parse().ok()?,
9285 ))
9286}
9287
9288fn unit_scale_expected_mesh_raw_rows(
9289 subject: &str,
9290 mesh_plan: &CurrentUnitScaleMeshPlan,
9291) -> Option<Vec<RawSceneAttachmentBasisReferenceV1>> {
9292 let (source_scene_index, source_node_index, source_mesh_index, source_primitive_index) =
9293 unit_scale_mesh_scope_keys(subject)?;
9294 let CurrentUnitScaleMeshPlan::Detailed(rows) = mesh_plan else {
9295 return None;
9296 };
9297 let row = rows.iter().find(|row| {
9298 row.source_scene_index == source_scene_index
9299 && row.source_node_index == source_node_index
9300 && row.source_mesh_index == source_mesh_index
9301 && row.source_primitive_index == source_primitive_index
9302 })?;
9303 Some(vec![
9304 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
9305 RawSceneAttachmentBasisReferenceV1::SceneRoot {
9306 source_scene_index,
9307 source_root_ordinal: row.source_root_ordinal,
9308 source_node_index: row.root_node_index,
9309 },
9310 RawSceneAttachmentBasisReferenceV1::NodeMeshAttachmentRow {
9311 source_node_index,
9312 source_mesh_index,
9313 },
9314 RawSceneAttachmentBasisReferenceV1::MeshPrimitiveRow {
9315 source_mesh_index,
9316 source_primitive_index,
9317 },
9318 ])
9319}
9320
9321fn unit_scale_selected_scope_keys<'a>(
9322 subject: &str,
9323 selectors: &'a [String],
9324) -> Option<(&'a str, Option<(u64, u64)>)> {
9325 for selector in selectors {
9326 let prefix = format!("selector:{selector}");
9327 if subject == prefix {
9328 return Some((selector, None));
9329 }
9330 let Some(values) = subject
9331 .strip_prefix(&prefix)
9332 .and_then(|suffix| suffix.strip_prefix(":source_scene:"))
9333 else {
9334 continue;
9335 };
9336 let values = values.split(':').collect::<Vec<_>>();
9337 if values.len() == 3 && values[1] == "source_node" {
9338 return Some((
9339 selector,
9340 Some((values[0].parse().ok()?, values[2].parse().ok()?)),
9341 ));
9342 }
9343 }
9344 None
9345}
9346
9347fn unit_scale_classification_name(value: LinearTransformClassification) -> &'static str {
9348 match value {
9349 LinearTransformClassification::UnitOrthonormal => "unit_orthonormal",
9350 LinearTransformClassification::UniformScaled => "uniform_scaled",
9351 LinearTransformClassification::NonUniform => "non_uniform",
9352 LinearTransformClassification::Sheared => "sheared",
9353 LinearTransformClassification::Reflected => "reflected",
9354 LinearTransformClassification::Singular => "singular",
9355 LinearTransformClassification::NonFinite => "non_finite",
9356 }
9357}
9358
9359fn unit_scale_raw_source_node_reference(
9360 source_index: u64,
9361 field: &str,
9362 value: PredictionScalarV1,
9363) -> Option<RawSourceBasisReferenceV1> {
9364 RawSourceBasisReferenceV1::from_wire(
9365 RawSourceDomainV1::SourceNode,
9366 RawSourceKeyV1::SourceSkeleton {
9367 row_kind: SourceSkeletonRowKindV1::SourceNode,
9368 source_index,
9369 },
9370 RawSourceFieldIdV1::new(field).ok()?,
9371 value,
9372 )
9373 .ok()
9374}
9375
9376fn unit_scale_exact_raw_source_references(
9377 basis: &EnginePredictionBasisV4,
9378 expected: &[RawSourceBasisReferenceV1],
9379) -> bool {
9380 let actual = basis
9381 .references()
9382 .iter()
9383 .filter_map(|reference| match reference {
9384 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9385 PredictionBasisReferenceV1::RawSource { reference },
9386 )) => Some(reference),
9387 _ => None,
9388 })
9389 .collect::<Vec<_>>();
9390 actual.len() == expected.len() && actual.iter().all(|reference| expected.contains(reference))
9391}
9392
9393fn unit_scale_selected_authored_kind(
9394 basis: &EnginePredictionBasisV4,
9395 source_index: u64,
9396) -> Option<&str> {
9397 let mut values = basis.references().iter().filter_map(|reference| {
9398 let PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9399 PredictionBasisReferenceV1::RawSource { reference },
9400 )) = reference
9401 else {
9402 return None;
9403 };
9404 if reference.domain() != RawSourceDomainV1::SourceNode
9405 || reference.key()
9406 != &(RawSourceKeyV1::SourceSkeleton {
9407 row_kind: SourceSkeletonRowKindV1::SourceNode,
9408 source_index,
9409 })
9410 || reference.field().as_str() != "local_rest.kind"
9411 {
9412 return None;
9413 }
9414 match reference.value() {
9415 PredictionScalarV1::Token { value } if matches!(value.as_str(), "trs" | "matrix") => {
9416 Some(value.as_str())
9417 }
9418 _ => None,
9419 }
9420 });
9421 let value = values.next()?;
9422 values.next().is_none().then_some(value)
9423}
9424
9425fn unit_scale_selected_expected_reasons(
9426 primary: Option<PredictionUnavailableReasonV2>,
9427 dependency_complete: bool,
9428) -> Vec<PredictionUnavailableReasonV2> {
9429 match primary {
9430 Some(reason) => unit_scale_unavailable_reasons(reason, dependency_complete),
9431 None if dependency_complete => Vec::new(),
9432 None => vec![PredictionUnavailableReasonV2::DependencyClosureIncomplete],
9433 }
9434}
9435
9436struct CurrentUnitScaleSelectedEvidence {
9437 raw_source_references: Vec<RawSourceBasisReferenceV1>,
9438 reasons: Vec<PredictionUnavailableReasonV2>,
9439}
9440
9441fn unit_scale_expected_selected_raw_source_references(
9442 selector: &str,
9443 keys: Option<(u64, u64)>,
9444 basis: &EnginePredictionBasisV4,
9445 provenance: &PredictionProvenanceV4,
9446 measurements: &MeasurementContract,
9447 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9448) -> Option<CurrentUnitScaleSelectedEvidence> {
9449 let assets = measurements.assets();
9450 let dependency_complete = matches!(
9451 provenance.dependency_closure().coverage(),
9452 DependencyClosureCoverageV1::Complete
9453 );
9454 let inventory = provenance.raw_scene_attachment().inventory();
9455 let inventory_complete = inventory.is_some_and(|inventory| {
9456 inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete
9457 && inventory.source_skeleton().coverage() == RawSceneAttachmentCoverageV1::Complete
9458 });
9459 if assets.skeleton_source_coverage != SourceSkeletonCoverage::Complete || !inventory_complete {
9460 return Some(CurrentUnitScaleSelectedEvidence {
9461 raw_source_references: Vec::new(),
9462 reasons: unit_scale_selected_expected_reasons(
9463 Some(PredictionUnavailableReasonV2::RawSourceIncomplete),
9464 dependency_complete,
9465 ),
9466 });
9467 }
9468 let mut matches = assets.skeleton_nodes.iter().filter(|node| {
9469 node.name
9470 .as_deref()
9471 .is_some_and(|name| crate::config::glob_match(selector, name))
9472 });
9473 let first = matches.next();
9474 let second = matches.next();
9475 let name_reference = |node: &crate::measure::SkeletonNodeMeasurements| {
9476 unit_scale_raw_source_node_reference(
9477 node.node_index as u64,
9478 "name",
9479 node.name.as_ref().map_or(PredictionScalarV1::Null, |name| {
9480 PredictionScalarV1::text(name).expect("retained measurement text is bounded")
9481 }),
9482 )
9483 };
9484 if first.is_none() {
9485 return keys.is_none().then(|| CurrentUnitScaleSelectedEvidence {
9486 raw_source_references: Vec::new(),
9487 reasons: unit_scale_selected_expected_reasons(
9488 Some(PredictionUnavailableReasonV2::SourceSelectorNoMatch),
9489 dependency_complete,
9490 ),
9491 });
9492 }
9493 if let Some(second) = second {
9494 if keys.is_some() {
9495 return None;
9496 }
9497 return Some(CurrentUnitScaleSelectedEvidence {
9498 raw_source_references: vec![name_reference(first?)?, name_reference(second)?],
9499 reasons: unit_scale_selected_expected_reasons(
9500 Some(PredictionUnavailableReasonV2::SourceSelectorAmbiguous),
9501 dependency_complete,
9502 ),
9503 });
9504 }
9505 let node = first?;
9506 if keys.is_some_and(|(_, source_node_index)| source_node_index != node.node_index as u64) {
9507 return None;
9508 }
9509 let mut expected = vec![name_reference(node)?];
9510 let nodes = assets
9511 .skeleton_nodes
9512 .iter()
9513 .map(|node| (node.node_index, node))
9514 .collect::<BTreeMap<_, _>>();
9515 let plan = reachability_plans.get(selector)?;
9516 let reachability_unavailable = match plan {
9517 UnitScaleSelectedReachabilityPlan::Refused {
9518 node_index: cached_node_index,
9519 } if *cached_node_index == node.node_index as u64 => true,
9520 UnitScaleSelectedReachabilityPlan::Complete {
9521 node_index: cached_node_index,
9522 ..
9523 } if *cached_node_index == node.node_index as u64 => false,
9524 UnitScaleSelectedReachabilityPlan::Refused { .. }
9525 | UnitScaleSelectedReachabilityPlan::Complete { .. } => return None,
9526 };
9527 if reachability_unavailable {
9528 if keys.is_some() {
9529 return None;
9530 }
9531 return Some(CurrentUnitScaleSelectedEvidence {
9532 raw_source_references: expected,
9533 reasons: unit_scale_selected_expected_reasons(
9534 Some(
9535 PredictionUnavailableReasonV2::custom(
9536 ENGINE_UNIT_SCALE_SELECTED_REACHABILITY_UNAVAILABLE_REASON,
9537 )
9538 .expect("static reason is valid"),
9539 ),
9540 dependency_complete,
9541 ),
9542 });
9543 }
9544 let mut current = node.node_index;
9545 let mut seen = BTreeSet::new();
9546 let mut ancestry_reason = None;
9547 let mut ancestry_complete = false;
9548 for _ in 0..128 {
9549 if !seen.insert(current) {
9550 ancestry_reason = Some(
9551 PredictionUnavailableReasonV2::custom(
9552 "animsmith:selected_node_ancestry_unavailable",
9553 )
9554 .expect("static reason is valid"),
9555 );
9556 break;
9557 }
9558 let Some(ancestry_node) = nodes.get(¤t).copied() else {
9559 ancestry_reason = Some(
9560 PredictionUnavailableReasonV2::custom(
9561 "animsmith:selected_node_ancestry_unavailable",
9562 )
9563 .expect("static reason is valid"),
9564 );
9565 break;
9566 };
9567 let retained_kind = unit_scale_selected_authored_kind(basis, current as u64)?;
9571 let local_kind = match ancestry_node.local_rest {
9572 SkeletonNodeLocalRestMeasurements::Trs { .. } if retained_kind == "trs" => "trs",
9573 SkeletonNodeLocalRestMeasurements::Matrix { .. } if retained_kind == "matrix" => {
9574 "matrix"
9575 }
9576 SkeletonNodeLocalRestMeasurements::Unavailable { .. } => retained_kind,
9577 _ => return None,
9578 };
9579 expected.push(unit_scale_raw_source_node_reference(
9580 current as u64,
9581 "local_rest.kind",
9582 PredictionScalarV1::token(local_kind).ok()?,
9583 )?);
9584 expected.push(unit_scale_raw_source_node_reference(
9585 current as u64,
9586 "parent_source_node_index",
9587 ancestry_node
9588 .parent_node_index
9589 .map_or(PredictionScalarV1::Null, |parent| {
9590 PredictionScalarV1::UnsignedInteger {
9591 value: parent as u64,
9592 }
9593 }),
9594 )?);
9595 if local_kind == "matrix" {
9596 ancestry_reason = Some(
9597 PredictionUnavailableReasonV2::custom(
9598 "animsmith:matrix_authored_selected_node_or_ancestry",
9599 )
9600 .expect("static reason is valid"),
9601 );
9602 break;
9603 }
9604 let Some(parent) = ancestry_node.parent_node_index else {
9605 ancestry_complete = true;
9606 break;
9607 };
9608 current = parent;
9609 }
9610 if ancestry_reason.is_none() && !ancestry_complete {
9611 ancestry_reason = Some(
9612 PredictionUnavailableReasonV2::custom("animsmith:selected_node_ancestry_unavailable")
9613 .expect("static reason is valid"),
9614 );
9615 }
9616 let primary_reason = if keys.is_none() {
9617 Some(
9618 PredictionUnavailableReasonV2::custom("animsmith:selected_node_unreachable")
9619 .expect("static reason is valid"),
9620 )
9621 } else {
9622 ancestry_reason.or_else(|| {
9623 (node.rest_world_linear.classification == LinearTransformClassification::NonFinite
9624 || node.rest_world_matrix.is_none())
9625 .then_some(PredictionUnavailableReasonV2::MeasurementUnavailable)
9626 })
9627 };
9628 Some(CurrentUnitScaleSelectedEvidence {
9629 raw_source_references: expected,
9630 reasons: unit_scale_selected_expected_reasons(primary_reason, dependency_complete),
9631 })
9632}
9633
9634fn unit_scale_exact_selected_evidence(
9635 selector: &str,
9636 keys: Option<(u64, u64)>,
9637 reasons: &[PredictionUnavailableReasonV2],
9638 basis: &EnginePredictionBasisV4,
9639 provenance: &PredictionProvenanceV4,
9640 measurements: &MeasurementContract,
9641 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9642) -> bool {
9643 let Some(expected) = unit_scale_expected_selected_raw_source_references(
9644 selector,
9645 keys,
9646 basis,
9647 provenance,
9648 measurements,
9649 reachability_plans,
9650 ) else {
9651 return false;
9652 };
9653 if reasons != expected.reasons
9654 || !unit_scale_exact_raw_source_references(basis, &expected.raw_source_references)
9655 {
9656 return false;
9657 }
9658 let measurement_references = basis
9659 .references()
9660 .iter()
9661 .filter_map(|reference| match reference {
9662 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(
9663 PredictionBasisReferenceV1::Measurement {
9664 schema,
9665 pointer,
9666 value,
9667 },
9668 )) => Some((*schema, pointer.as_str(), value)),
9669 _ => None,
9670 })
9671 .collect::<Vec<_>>();
9672 let Some((source_scene_index, source_node_index)) = keys else {
9673 return unit_scale_exact_raw_row_references(basis, &[])
9674 && measurement_references.is_empty();
9675 };
9676 let Some(inventory) = provenance.raw_scene_attachment().inventory() else {
9677 return false;
9678 };
9679 let Some(_scene) = inventory
9680 .scenes()
9681 .rows()
9682 .iter()
9683 .find(|scene| scene.source_scene_index() == source_scene_index)
9684 else {
9685 return false;
9686 };
9687 let witness = reachability_plans
9688 .get(selector)
9689 .filter(|plan| plan.node_index() == source_node_index)
9690 .and_then(|plan| match plan {
9691 UnitScaleSelectedReachabilityPlan::Complete {
9692 scene_witnesses, ..
9693 } => scene_witnesses.get(&source_scene_index).copied(),
9694 UnitScaleSelectedReachabilityPlan::Refused { .. } => None,
9695 });
9696 let Some((source_root_ordinal, source_node_index_at_root)) = witness else {
9697 return false;
9698 };
9699 if !unit_scale_exact_raw_row_references(
9700 basis,
9701 &[
9702 RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index },
9703 RawSceneAttachmentBasisReferenceV1::SceneRoot {
9704 source_scene_index,
9705 source_root_ordinal,
9706 source_node_index: source_node_index_at_root,
9707 },
9708 ],
9709 ) {
9710 return false;
9711 }
9712 let Some((ordinal, node)) = measurements
9713 .assets()
9714 .skeleton_nodes
9715 .iter()
9716 .enumerate()
9717 .find(|(_, node)| node.node_index as u64 == source_node_index)
9718 else {
9719 return measurement_references.is_empty();
9720 };
9721 let pointer =
9722 format!("/measurements/skeleton_nodes/{ordinal}/rest_world_linear/classification");
9723 matches!(
9724 measurement_references.as_slice(),
9725 [(
9726 MEASUREMENTS_V16_SCHEMA_ID,
9727 actual_pointer,
9728 PredictionScalarV1::Token { value },
9729 )] if *actual_pointer == pointer
9730 && value == unit_scale_classification_name(node.rest_world_linear.classification)
9731 )
9732}
9733
9734fn validate_current_engine_unit_scale_basis(
9735 scope: &EvaluationScope,
9736 basis: &EnginePredictionBasisV4,
9737 reasons: &[PredictionUnavailableReasonV2],
9738 provenance: &PredictionProvenanceV4,
9739 measurements: &MeasurementContract,
9740 mesh_plan: &CurrentUnitScaleMeshPlan,
9741 reachability_plans: &UnitScaleSelectedReachabilityPlans,
9742) -> bool {
9743 let v1 = |predicate: &dyn Fn(&PredictionBasisReferenceV1) -> bool| {
9744 basis.references().iter().any(|reference| {
9745 matches!(
9746 reference,
9747 PredictionBasisReferenceV4::V2(PredictionBasisReferenceV2::V1(reference))
9748 if predicate(reference)
9749 )
9750 })
9751 };
9752 let fact = |expected: &str| {
9753 v1(
9754 &|reference| matches!(reference, PredictionBasisReferenceV1::ProfileFact { fact_id } if fact_id == expected),
9755 )
9756 };
9757 let setting = |expected: &str| {
9758 v1(
9759 &|reference| matches!(reference, PredictionBasisReferenceV1::ResolvedSetting { setting_id, .. } if setting_id == expected),
9760 )
9761 };
9762 let source = |expected: &str| {
9763 v1(
9764 &|reference| matches!(reference, PredictionBasisReferenceV1::PrimarySource { source_id } if source_id == expected),
9765 )
9766 };
9767 let raw_coverage = |expected: RawSceneAttachmentBasisDomainV1| {
9768 basis.references().iter().any(|reference| {
9769 matches!(
9770 reference,
9771 PredictionBasisReferenceV4::RawSceneAttachment(
9772 RawSceneAttachmentBasisReferenceV1::Coverage { domain }
9773 ) if *domain == expected
9774 )
9775 })
9776 };
9777 let common_transform = source("bevy-gltf-loader-0.19.0-c6f634ca")
9778 && source("bevy-gltf-coordinate-conversion-0.19.0-c6f634ca")
9779 && fact("resulting_transform_scale")
9780 && setting("extension_handler_environment");
9781 match scope.code.as_str() {
9782 ENGINE_UNIT_SCALE_FILE_SCOPE => {
9783 unit_scale_exact_raw_row_references(basis, &[])
9784 && [
9785 "application_world_unit_policy",
9786 "importer_scale_conversion",
9787 "physical_dimensions_preserved",
9788 "source_to_target_unit_mapping",
9789 "target_linear_unit",
9790 ]
9791 .into_iter()
9792 .all(fact)
9793 && [
9794 "bevy-gltf-loader-0.19.0-c6f634ca",
9795 "bevy-gltf-coordinate-conversion-0.19.0-c6f634ca",
9796 "khronos-gltf-2.0-coordinate-units",
9797 ]
9798 .into_iter()
9799 .all(source)
9800 }
9801 ENGINE_UNIT_SCALE_SCENE_SCOPE | ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE => {
9802 let expected_rows = if scope.code.as_str() == ENGINE_UNIT_SCALE_SCENE_INVENTORY_SCOPE {
9803 Some(Vec::new())
9804 } else {
9805 scope
9806 .subject
9807 .as_deref()
9808 .and_then(|subject| subject.strip_prefix("source_scene:"))
9809 .and_then(|index| index.parse::<u64>().ok())
9810 .filter(|index| {
9811 scope.subject.as_deref() == Some(&format!("source_scene:{index}"))
9812 })
9813 .map(|source_scene_index| {
9814 vec![RawSceneAttachmentBasisReferenceV1::SceneRow { source_scene_index }]
9815 })
9816 };
9817 expected_rows.as_deref().is_some_and(|expected_rows| {
9818 unit_scale_exact_raw_row_references(basis, expected_rows)
9819 }) && common_transform
9820 && setting("rotate_scene_entity")
9821 && (provenance.raw_scene_attachment().inventory().is_none()
9822 || raw_coverage(RawSceneAttachmentBasisDomainV1::Scenes))
9823 }
9824 ENGINE_UNIT_SCALE_MESH_SCOPE | ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE => {
9825 let expected_rows = if scope.code.as_str() == ENGINE_UNIT_SCALE_MESH_INVENTORY_SCOPE {
9826 Some(Vec::new())
9827 } else {
9828 scope
9829 .subject
9830 .as_deref()
9831 .and_then(|subject| unit_scale_expected_mesh_raw_rows(subject, mesh_plan))
9832 };
9833 expected_rows.as_deref().is_some_and(|expected_rows| {
9834 unit_scale_exact_raw_row_references(basis, expected_rows)
9835 }) && common_transform
9836 && source("bevy-render-asset-usages-0.19.0-c6f634ca")
9837 && setting("load_meshes")
9838 && setting("rotate_meshes")
9839 && (provenance.raw_scene_attachment().inventory().is_none()
9840 || [
9841 RawSceneAttachmentBasisDomainV1::SourceSkeleton,
9842 RawSceneAttachmentBasisDomainV1::Scenes,
9843 RawSceneAttachmentBasisDomainV1::NodeMeshAttachments,
9844 RawSceneAttachmentBasisDomainV1::MeshPrimitives,
9845 ]
9846 .into_iter()
9847 .all(raw_coverage))
9848 }
9849 ENGINE_UNIT_SCALE_SELECTED_SCOPE => {
9850 let selected = scope.subject.as_deref().and_then(|subject| {
9851 unit_scale_selected_scope_keys(
9852 subject,
9853 provenance.rule_inputs().runtime_node_selectors(),
9854 )
9855 });
9856 let selector = selected.map(|(selector, _)| selector);
9857 common_transform
9858 && setting("rotate_scene_entity")
9859 && selected.is_some_and(|(selector, keys)| {
9860 unit_scale_exact_selected_evidence(
9861 selector,
9862 keys,
9863 reasons,
9864 basis,
9865 provenance,
9866 measurements,
9867 reachability_plans,
9868 )
9869 })
9870 && selector.is_some_and(|selector| {
9871 v1(&|reference| {
9872 matches!(
9873 reference,
9874 PredictionBasisReferenceV1::ProjectField {
9875 field_id,
9876 value: PredictionScalarV1::Text { value }
9877 } if field_id == "runtime_nodes.selector" && value == selector
9878 )
9879 })
9880 })
9881 && (provenance.raw_scene_attachment().inventory().is_none()
9882 || (raw_coverage(RawSceneAttachmentBasisDomainV1::SourceSkeleton)
9883 && raw_coverage(RawSceneAttachmentBasisDomainV1::Scenes)))
9884 }
9885 ENGINE_UNIT_SCALE_BUDGET_SCOPE => {
9886 unit_scale_exact_raw_row_references(basis, &[])
9887 && source("bevy-gltf-loader-0.19.0-c6f634ca")
9888 }
9889 _ => false,
9890 }
9891}
9892
9893fn validate_current_engine_unit_scale_prediction_v4(
9894 check_id: &str,
9895 selection: SelectionState,
9896 configuration: ConfigurationState,
9897 applicability: Applicability,
9898 prediction: Option<&EnginePredictionV4>,
9899 provenance: Option<&PredictionProvenanceV4>,
9900 measurements: &MeasurementContract,
9901) -> Result<(), PredictionContractError> {
9902 if check_id != ENGINE_UNIT_SCALE_CHECK_ID {
9903 return Ok(());
9904 }
9905 let exact_profile = provenance.is_some_and(|provenance| {
9906 let selection = provenance.profile().selection();
9907 selection.family() == "bevy"
9908 && selection.profile_revision() == 2
9909 && selection.engine_version() == "0.19.0"
9910 && selection.importer() == "gltf-asset-loader"
9911 && provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:2"
9912 && provenance.profile().facts_identity().sha256()
9913 == "bcd663e891b25029ecdf17e942f6fa93f71a8d0598fc8cb02bc7634749c34597"
9914 && provenance.profile().facts_identity().bytes() == 4_783
9915 && matches!(
9916 provenance.source_format(),
9917 SourceFormatV1::GltfJson | SourceFormatV1::Glb
9918 )
9919 });
9920 validate_current_engine_unit_scale_prediction_common(
9921 check_id,
9922 selection,
9923 configuration,
9924 applicability,
9925 prediction,
9926 provenance,
9927 measurements,
9928 exact_profile,
9929 )
9930}
9931
9932fn validate_current_engine_unit_scale_prediction_v5(
9933 check_id: &str,
9934 selection: SelectionState,
9935 configuration: ConfigurationState,
9936 applicability: Applicability,
9937 prediction: Option<&EnginePredictionV5>,
9938 provenance: Option<&PredictionProvenanceV5>,
9939 measurements: &MeasurementContract,
9940) -> Result<(), PredictionContractError> {
9941 let base = provenance.map(PredictionProvenanceV5::base);
9942 let exact_profile = base.is_some_and(|provenance| {
9943 let selection = provenance.profile().selection();
9944 let exact_identity = match selection.profile_revision() {
9945 2 => {
9946 provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:2"
9947 && provenance.profile().facts_identity().sha256()
9948 == "bcd663e891b25029ecdf17e942f6fa93f71a8d0598fc8cb02bc7634749c34597"
9949 && provenance.profile().facts_identity().bytes() == 4_783
9950 }
9951 3 => {
9952 provenance.profile().fact_bundle_urn() == "urn:animsmith:engine-profile:bevy:3"
9953 && provenance.profile().facts_identity().sha256()
9954 == "d532b00621bf06a2db2dedf896c19aae2c07b3b1873a1b05beade2252d7a89c5"
9955 && provenance.profile().facts_identity().bytes() == 4_849
9956 }
9957 _ => false,
9958 };
9959 selection.family() == "bevy"
9960 && selection.engine_version() == "0.19.0"
9961 && selection.importer() == "gltf-asset-loader"
9962 && exact_identity
9963 && matches!(
9964 provenance.source_format(),
9965 SourceFormatV1::GltfJson | SourceFormatV1::Glb
9966 )
9967 });
9968 validate_current_engine_unit_scale_prediction_common(
9969 check_id,
9970 selection,
9971 configuration,
9972 applicability,
9973 prediction.map(EnginePredictionV5::base_prediction),
9974 base,
9975 measurements,
9976 exact_profile,
9977 )
9978}
9979
9980#[allow(clippy::too_many_arguments)]
9981fn validate_current_engine_unit_scale_prediction_common(
9982 check_id: &str,
9983 selection: SelectionState,
9984 configuration: ConfigurationState,
9985 applicability: Applicability,
9986 prediction: Option<&EnginePredictionV4>,
9987 provenance: Option<&PredictionProvenanceV4>,
9988 measurements: &MeasurementContract,
9989 exact_profile: bool,
9990) -> Result<(), PredictionContractError> {
9991 if check_id != ENGINE_UNIT_SCALE_CHECK_ID {
9992 return Ok(());
9993 }
9994 if applicability
9995 != if exact_profile {
9996 Applicability::Applicable
9997 } else {
9998 Applicability::NotApplicable
9999 }
10000 {
10001 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
10002 }
10003 if !exact_profile {
10004 return if prediction.is_none() {
10005 Ok(())
10006 } else {
10007 Err(PredictionContractError::EngineUnitScaleFacetMismatch)
10008 };
10009 }
10010 if selection != SelectionState::Selected || configuration != ConfigurationState::Enabled {
10011 return if prediction.is_none() {
10012 Ok(())
10013 } else {
10014 Err(PredictionContractError::EngineUnitScaleFacetMismatch)
10015 };
10016 }
10017 let provenance = provenance.ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
10018 let prediction = prediction.ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
10019 let mesh_plan = current_unit_scale_mesh_plan(provenance, measurements);
10020 let reachability_plans = unit_scale_selected_reachability_plans(provenance, measurements);
10021 if prediction.facets().iter().any(|facet| {
10022 !validate_current_engine_unit_scale_basis(
10023 facet.scope(),
10024 facet.basis(),
10025 facet.reasons(),
10026 provenance,
10027 measurements,
10028 &mesh_plan,
10029 &reachability_plans,
10030 )
10031 }) {
10032 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
10033 }
10034 let has_summary = prediction.facets().iter().any(|facet| {
10035 facet.scope().code.as_str() == ENGINE_UNIT_SCALE_BUDGET_SCOPE
10036 && facet.reasons() == [PredictionUnavailableReasonV2::FacetBudgetExceeded]
10037 });
10038 let retained = prediction
10039 .facets()
10040 .iter()
10041 .filter(|facet| facet.scope().code.as_str() != ENGINE_UNIT_SCALE_BUDGET_SCOPE)
10042 .collect::<Vec<_>>();
10043 let scene_facets = provenance
10044 .raw_scene_attachment()
10045 .inventory()
10046 .filter(|inventory| inventory.scenes().coverage() == RawSceneAttachmentCoverageV1::Complete)
10047 .map_or(1, |inventory| inventory.scenes().rows().len());
10048 let mesh_facets = match &mesh_plan {
10049 CurrentUnitScaleMeshPlan::Detailed(rows) => rows.len(),
10050 CurrentUnitScaleMeshPlan::CompleteEmpty
10051 | CurrentUnitScaleMeshPlan::Incomplete
10052 | CurrentUnitScaleMeshPlan::JoinOverflow => 1,
10053 };
10054 let selected_facets = current_unit_scale_selected_facet_count(&reachability_plans);
10055 let expected_count = 1usize
10056 .checked_add(scene_facets)
10057 .and_then(|count| count.checked_add(mesh_facets))
10058 .and_then(|count| count.checked_add(selected_facets))
10059 .ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
10060 if (!has_summary && expected_count > PREDICTION_V2_MAX_CANDIDATE_FACETS_PER_RULE)
10061 || (has_summary && retained.len() >= expected_count)
10062 {
10063 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
10064 }
10065 let candidate_capacity = if has_summary {
10066 retained.len()
10067 } else {
10068 expected_count
10069 };
10070 let mut expected_retained = expected_current_engine_unit_scale_facets(
10071 provenance,
10072 measurements,
10073 &mesh_plan,
10074 candidate_capacity,
10075 &reachability_plans,
10076 )
10077 .ok_or(PredictionContractError::EngineUnitScaleFacetMismatch)?;
10078 if expected_retained.len() != candidate_capacity {
10079 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
10080 }
10081 expected_retained.sort_by(|left, right| {
10082 left.scope
10083 .code
10084 .as_str()
10085 .cmp(right.scope.code.as_str())
10086 .then_with(|| left.scope.subject.cmp(&right.scope.subject))
10087 });
10088 if retained.len() != expected_retained.len()
10089 || retained
10090 .iter()
10091 .zip(&expected_retained)
10092 .any(|(actual, expected)| {
10093 actual.scope() != &expected.scope
10094 || actual.result() != expected.result.as_ref()
10095 || (actual.scope().code.as_str() != ENGINE_UNIT_SCALE_SELECTED_SCOPE
10096 && actual.reasons() != expected.reasons)
10097 })
10098 {
10099 return Err(PredictionContractError::EngineUnitScaleFacetMismatch);
10100 }
10101 Ok(())
10102}
10103
10104const ENGINE_CLIP_BOUNDARY_CHECK_ID: &str = "engine-clip-boundary";
10105const ENGINE_CLIP_BOUNDARY_SOURCE_ID: &str = "unreal-animation-sequences-5.8";
10106const ENGINE_CLIP_BOUNDARY_PROFILE_FAMILY: &str = "unreal";
10107const ENGINE_CLIP_BOUNDARY_PROFILE_REVISION: u32 = 1;
10108const ENGINE_CLIP_BOUNDARY_ENGINE_VERSION: &str = "5.8";
10109const ENGINE_CLIP_BOUNDARY_IMPORTER: &str = "fbx-importer";
10110const ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256: &str =
10111 "e44ca461aee46312b8265446f08338b988b96abeab0f8f502f560da5f1cdf759";
10112const ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES: u64 = 2_169;
10113
10114fn current_engine_clip_boundary_profile_matches_v3(provenance: &PredictionProvenanceV3) -> bool {
10115 let selection = provenance.profile().selection();
10116 provenance.source_format() == SourceFormatV1::Fbx
10117 && provenance.raw_source().source_format() == SourceFormatV1::Fbx
10118 && selection.family() == ENGINE_CLIP_BOUNDARY_PROFILE_FAMILY
10119 && selection.profile_revision() == ENGINE_CLIP_BOUNDARY_PROFILE_REVISION
10120 && selection.engine_version() == ENGINE_CLIP_BOUNDARY_ENGINE_VERSION
10121 && selection.importer() == ENGINE_CLIP_BOUNDARY_IMPORTER
10122 && provenance.profile().facts_identity().sha256()
10123 == ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256
10124 && provenance.profile().facts_identity().bytes() == ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES
10125 && matches!(
10126 provenance
10127 .profile()
10128 .fact(EngineFactIdV1::WholeEndFrameRequired)
10129 .map(|fact| fact.state()),
10130 Some(EngineFactStateV1::Known(EngineFactValueV1::Boolean(true)))
10131 )
10132 && provenance
10133 .profile()
10134 .source(ENGINE_CLIP_BOUNDARY_SOURCE_ID)
10135 .is_some()
10136}
10137
10138fn validate_current_engine_clip_boundary_applicability_v3(
10139 check_id: &str,
10140 applicability: Applicability,
10141 provenance: Option<&PredictionProvenanceV3>,
10142) -> Result<(), PredictionContractError> {
10143 if check_id != ENGINE_CLIP_BOUNDARY_CHECK_ID {
10144 return Ok(());
10145 }
10146 let expected = match provenance {
10147 Some(provenance)
10148 if current_engine_clip_boundary_profile_matches_v3(provenance)
10149 && !(provenance.raw_source().clips_coverage().state()
10150 == RawSourceSetCoverageStateV1::Complete
10151 && provenance
10152 .raw_source()
10153 .exact_source_timing()
10154 .is_some_and(|timing| timing.clips().is_empty())) =>
10155 {
10156 Applicability::Applicable
10157 }
10158 _ => Applicability::NotApplicable,
10159 };
10160 if applicability != expected {
10161 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10162 }
10163 Ok(())
10164}
10165
10166fn validate_current_engine_clip_boundary_prediction_v3(
10171 check_id: &str,
10172 prediction: &EnginePredictionV3,
10173 provenance: &PredictionProvenanceV3,
10174 evaluated_scopes: &[EvaluationScope],
10175 finding_scopes: &[&EvaluationScope],
10176) -> Result<(), PredictionContractError> {
10177 if check_id != ENGINE_CLIP_BOUNDARY_CHECK_ID {
10178 return Ok(());
10179 }
10180
10181 if !current_engine_clip_boundary_profile_matches_v3(provenance) {
10182 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10183 }
10184
10185 let expected_rows = provenance.settings().clips().len();
10186 let timing = provenance.raw_source().exact_source_timing();
10187 if timing.is_some_and(|timing| timing.clips().len() != expected_rows) {
10188 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10189 }
10190 let inventory_incomplete =
10191 provenance.raw_source().clips_coverage().state() != RawSourceSetCoverageStateV1::Complete;
10192 let has_budget_summary = prediction.has_facet_budget_summary();
10193 let mut seen_rows = vec![false; expected_rows];
10194 let mut row_facets = 0usize;
10195 let mut inventory_facets = 0usize;
10196 let mut available_scopes = Vec::new();
10197 let mut expected_finding_scopes = Vec::new();
10198
10199 for facet in prediction.facets() {
10200 if facet.reasons() == [PredictionUnavailableReasonV2::FacetBudgetExceeded] {
10201 if facet.basis() != &engine_clip_boundary_inventory_basis(timing)? {
10202 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10203 }
10204 continue;
10205 }
10206 if facet.scope().code.as_str() == "engine_clip_boundary" {
10207 let Some(source_clip_index) = facet
10208 .scope()
10209 .subject
10210 .as_deref()
10211 .and_then(|subject| subject.strip_prefix("source_stack:"))
10212 .and_then(|index| index.parse::<usize>().ok())
10213 else {
10214 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10215 };
10216 if source_clip_index >= expected_rows
10217 || std::mem::replace(&mut seen_rows[source_clip_index], true)
10218 {
10219 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10220 }
10221 row_facets += 1;
10222 let expected_basis = engine_clip_boundary_stack_basis(timing, source_clip_index)?;
10223 if facet.basis() != &expected_basis {
10224 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10225 }
10226
10227 let exact = timing.and_then(|timing| {
10228 let declared = matches!(
10229 timing.declared_time_mode().state(),
10230 ExactSourceTimingObservationStateWireV1::Observed(_)
10231 );
10232 let period = match timing.frame_period().state() {
10233 ExactSourceTimingObservationStateWireV1::Observed(period) => {
10234 Some(period.units_per_frame())
10235 }
10236 _ => None,
10237 };
10238 let end = match timing.clips()[source_clip_index]
10239 .source_time_range()
10240 .state()
10241 {
10242 ExactSourceTimingObservationStateWireV1::Observed(range) => {
10243 Some(range.end_units())
10244 }
10245 _ => None,
10246 };
10247 declared.then_some(())?;
10248 Some((period?, end?))
10249 });
10250 match exact {
10251 Some((period, end)) => {
10252 if facet.state() != EnginePredictionFacetStateV1::Available
10253 || !facet.reasons().is_empty()
10254 {
10255 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10256 }
10257 available_scopes.push(facet.scope());
10258 if end.rem_euclid(period) != 0 {
10259 expected_finding_scopes.push(facet.scope());
10260 }
10261 }
10262 None => {
10263 let expected_reasons =
10264 engine_clip_boundary_unavailable_reasons(timing, source_clip_index)?;
10265 if facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
10266 || facet.reasons() != expected_reasons
10267 {
10268 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10269 }
10270 }
10271 }
10272 } else if facet.scope().code.as_str() == "engine_clip_boundary_inventory"
10273 && facet.scope().subject.is_none()
10274 {
10275 inventory_facets += 1;
10276 if inventory_facets != 1
10277 || !inventory_incomplete
10278 || facet.state() != EnginePredictionFacetStateV1::RequiredPredictionUnavailable
10279 || facet.reasons() != [PredictionUnavailableReasonV2::RawSourceIncomplete]
10280 || facet.basis() != &engine_clip_boundary_inventory_basis(timing)?
10281 {
10282 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10283 }
10284 } else {
10285 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10286 }
10287 }
10288
10289 if seen_rows[..row_facets].iter().any(|seen| !seen)
10290 || seen_rows[row_facets..].iter().any(|seen| *seen)
10291 {
10292 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10293 }
10294 let candidate_facets = row_facets + inventory_facets;
10295 let expected_demand = expected_rows + usize::from(inventory_incomplete);
10296 if has_budget_summary {
10297 if candidate_facets >= expected_demand
10298 || inventory_facets != usize::from(inventory_incomplete && candidate_facets != 0)
10299 {
10300 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10301 }
10302 } else if row_facets != expected_rows || inventory_facets != usize::from(inventory_incomplete) {
10303 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10304 }
10305 if evaluated_scopes.len() != available_scopes.len()
10306 || evaluated_scopes
10307 .iter()
10308 .any(|scope| !available_scopes.contains(&scope))
10309 {
10310 return Err(PredictionContractError::EngineClipBoundaryFacetMismatch);
10311 }
10312 if finding_scopes.len() != expected_finding_scopes.len()
10313 || expected_finding_scopes.iter().any(|expected| {
10314 finding_scopes
10315 .iter()
10316 .filter(|actual| **actual == *expected)
10317 .count()
10318 != 1
10319 })
10320 {
10321 return Err(PredictionContractError::EngineClipBoundaryFindingMismatch);
10322 }
10323 Ok(())
10324}
10325
10326fn engine_clip_boundary_common_basis()
10327-> Result<Vec<PredictionBasisReferenceV2>, PredictionContractError> {
10328 Ok(vec![
10329 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::profile_fact(
10330 "whole_end_frame_required",
10331 )?),
10332 PredictionBasisReferenceV2::v1(PredictionBasisReferenceV1::primary_source(
10333 ENGINE_CLIP_BOUNDARY_SOURCE_ID,
10334 )?),
10335 ])
10336}
10337
10338fn engine_clip_boundary_exact_reference(
10339 binding: &ExactSourceTimingBindingV1,
10340 domain: ExactSourceTimingDomainV1,
10341 key: ExactSourceTimingKeyV1,
10342 field: &'static str,
10343) -> Result<PredictionBasisReferenceV2, PredictionContractError> {
10344 Ok(PredictionBasisReferenceV2::exact_source_timing(
10345 ExactSourceTimingBasisReferenceV1::from_binding(
10346 domain,
10347 key,
10348 RawSourceFieldIdV1::new(field)?,
10349 binding,
10350 )?,
10351 ))
10352}
10353
10354fn engine_clip_boundary_stack_basis(
10355 timing: Option<&ExactSourceTimingBindingV1>,
10356 source_clip_index: usize,
10357) -> Result<EnginePredictionBasisV2, PredictionContractError> {
10358 let mut references = engine_clip_boundary_common_basis()?;
10359 let Some(timing) = timing else {
10360 return EnginePredictionBasisV2::new(references);
10361 };
10362 let stack_key = ExactSourceTimingKeyV1::Clip {
10363 source_clip_index: source_clip_index as u64,
10364 };
10365 for (domain, key, field) in [
10366 (
10367 ExactSourceTimingDomainV1::Document,
10368 ExactSourceTimingKeyV1::Document,
10369 "declared_time_mode.state",
10370 ),
10371 (
10372 ExactSourceTimingDomainV1::Document,
10373 ExactSourceTimingKeyV1::Document,
10374 "frame_period.state",
10375 ),
10376 (
10377 ExactSourceTimingDomainV1::Clip,
10378 stack_key.clone(),
10379 "source_time_range.state",
10380 ),
10381 ] {
10382 references.push(engine_clip_boundary_exact_reference(
10383 timing, domain, key, field,
10384 )?);
10385 }
10386 if matches!(
10387 timing.declared_time_mode().state(),
10388 ExactSourceTimingObservationStateWireV1::Observed(_)
10389 ) {
10390 references.push(engine_clip_boundary_exact_reference(
10391 timing,
10392 ExactSourceTimingDomainV1::Document,
10393 ExactSourceTimingKeyV1::Document,
10394 "declared_time_mode.value.time_mode",
10395 )?);
10396 }
10397 if matches!(
10398 timing.frame_period().state(),
10399 ExactSourceTimingObservationStateWireV1::Observed(_)
10400 ) {
10401 references.push(engine_clip_boundary_exact_reference(
10402 timing,
10403 ExactSourceTimingDomainV1::Document,
10404 ExactSourceTimingKeyV1::Document,
10405 "frame_period.value.units_per_frame",
10406 )?);
10407 }
10408 if matches!(
10409 timing.clips()[source_clip_index]
10410 .source_time_range()
10411 .state(),
10412 ExactSourceTimingObservationStateWireV1::Observed(_)
10413 ) {
10414 references.push(engine_clip_boundary_exact_reference(
10415 timing,
10416 ExactSourceTimingDomainV1::Clip,
10417 stack_key,
10418 "source_time_range.value.end_units",
10419 )?);
10420 }
10421 EnginePredictionBasisV2::new(references)
10422}
10423
10424fn engine_clip_boundary_inventory_basis(
10425 timing: Option<&ExactSourceTimingBindingV1>,
10426) -> Result<EnginePredictionBasisV2, PredictionContractError> {
10427 let mut references = engine_clip_boundary_common_basis()?;
10428 if let Some(timing) = timing {
10429 for field in ["clip_coverage.state", "clip_coverage.reason"] {
10430 references.push(engine_clip_boundary_exact_reference(
10431 timing,
10432 ExactSourceTimingDomainV1::Document,
10433 ExactSourceTimingKeyV1::Document,
10434 field,
10435 )?);
10436 }
10437 }
10438 EnginePredictionBasisV2::new(references)
10439}
10440
10441fn engine_clip_boundary_unavailable_reasons(
10442 timing: Option<&ExactSourceTimingBindingV1>,
10443 source_clip_index: usize,
10444) -> Result<Vec<PredictionUnavailableReasonV2>, PredictionContractError> {
10445 let Some(timing) = timing else {
10446 return Ok(vec![PredictionUnavailableReasonV2::custom(
10447 "animsmith:exact_source_timing_unavailable",
10448 )?]);
10449 };
10450 let mut reasons = Vec::new();
10451 if !matches!(
10452 timing.declared_time_mode().state(),
10453 ExactSourceTimingObservationStateWireV1::Observed(_)
10454 ) {
10455 reasons.push(PredictionUnavailableReasonV2::custom(
10456 "animsmith:source_declared_time_mode_unavailable",
10457 )?);
10458 }
10459 if !matches!(
10460 timing.frame_period().state(),
10461 ExactSourceTimingObservationStateWireV1::Observed(_)
10462 ) {
10463 reasons.push(PredictionUnavailableReasonV2::custom(
10464 "animsmith:source_frame_period_unavailable",
10465 )?);
10466 }
10467 if !matches!(
10468 timing.clips()[source_clip_index]
10469 .source_time_range()
10470 .state(),
10471 ExactSourceTimingObservationStateWireV1::Observed(_)
10472 ) {
10473 reasons.push(PredictionUnavailableReasonV2::custom(
10474 "animsmith:source_clip_time_range_unavailable",
10475 )?);
10476 }
10477 reasons.sort_by(|left, right| left.as_str().cmp(right.as_str()));
10478 Ok(reasons)
10479}
10480
10481impl MeasurementReportInput {
10482 pub fn read_from(reader: impl Read) -> Result<Self, MeasurementReportReadError> {
10494 Self::read_from_with_limit(reader, OUTPUT_V11_MAX_REPORT_BYTES)
10495 }
10496
10497 fn read_from_with_limit(
10498 reader: impl Read,
10499 limit: u64,
10500 ) -> Result<Self, MeasurementReportReadError> {
10501 let mut bounded = reader.take(limit + 1);
10502 let mut bytes = Vec::new();
10503 bounded
10504 .read_to_end(&mut bytes)
10505 .map_err(|source| MeasurementReportReadError::Io { source })?;
10506 if bytes.len() as u64 > limit {
10507 return Err(MeasurementReportReadError::ReportTooLarge { limit });
10508 }
10509 serde_json::from_slice(&bytes)
10510 .map_err(|source| MeasurementReportReadError::InvalidJson { source })
10511 }
10512
10513 pub fn file_count(&self) -> Option<usize> {
10519 self.files.as_ref().map(Vec::len)
10520 }
10521
10522 pub fn into_files(self) -> Result<Vec<MeasurementReportFile>, MeasurementReportError> {
10533 #[derive(Clone, Copy, PartialEq, Eq)]
10534 enum ReaderRevision {
10535 V11,
10536 V12,
10537 V13,
10538 V14,
10539 V15,
10540 V16,
10541 V17,
10542 V18,
10543 V19,
10544 }
10545
10546 let revision = match self.schema_version {
10547 Some(OUTPUT_V11_SCHEMA_VERSION) => ReaderRevision::V11,
10548 Some(OUTPUT_V12_SCHEMA_VERSION) => ReaderRevision::V12,
10549 Some(OUTPUT_V13_SCHEMA_VERSION) => ReaderRevision::V13,
10550 Some(OUTPUT_V14_SCHEMA_VERSION) => ReaderRevision::V14,
10551 Some(OUTPUT_V15_SCHEMA_VERSION) => ReaderRevision::V15,
10552 Some(OUTPUT_V16_SCHEMA_VERSION) => ReaderRevision::V16,
10553 Some(OUTPUT_V17_SCHEMA_VERSION) => ReaderRevision::V17,
10554 Some(OUTPUT_V18_SCHEMA_VERSION) => ReaderRevision::V18,
10555 Some(OUTPUT_SCHEMA_VERSION) => ReaderRevision::V19,
10556 Some(found) => {
10557 return Err(MeasurementReportError::UnsupportedOutputVersion { found });
10558 }
10559 None => return Err(MeasurementReportError::MissingOutputVersion),
10560 };
10561 let expected_schema = match revision {
10562 ReaderRevision::V11 => OUTPUT_V11_SCHEMA_ID,
10563 ReaderRevision::V12 => OUTPUT_V12_SCHEMA_ID,
10564 ReaderRevision::V13 => OUTPUT_V13_SCHEMA_ID,
10565 ReaderRevision::V14 => OUTPUT_V14_SCHEMA_ID,
10566 ReaderRevision::V15 => OUTPUT_V15_SCHEMA_ID,
10567 ReaderRevision::V16 => OUTPUT_V16_SCHEMA_ID,
10568 ReaderRevision::V17 => OUTPUT_V17_SCHEMA_ID,
10569 ReaderRevision::V18 => OUTPUT_V18_SCHEMA_ID,
10570 ReaderRevision::V19 => OUTPUT_SCHEMA_ID,
10571 };
10572 if self.schema.as_deref() != Some(expected_schema) {
10573 return Err(MeasurementReportError::WrongOutputIdentity);
10574 }
10575 let command = match self.command.as_deref() {
10576 Some(command @ ("measure" | "lint")) => command,
10577 Some(command) => {
10578 return Err(MeasurementReportError::UnsupportedCommand {
10579 command: command.to_owned(),
10580 });
10581 }
10582 None => return Err(MeasurementReportError::MissingCommand),
10583 };
10584 if let Some(field) = self.extra.keys().next() {
10585 return Err(MeasurementReportError::UnknownOutputField {
10586 field: field.clone(),
10587 });
10588 }
10589 if self._tool.is_none() {
10590 return Err(MeasurementReportError::MissingTool);
10591 }
10592 validate_prediction_summary_presence(command, self.summary.as_ref())?;
10595 let files = self.files.ok_or(MeasurementReportError::MissingFiles)?;
10596 if files.len() > OUTPUT_V11_MAX_FILES {
10597 return Err(MeasurementReportError::TooManyFiles {
10598 found: files.len(),
10599 limit: OUTPUT_V11_MAX_FILES,
10600 });
10601 }
10602 let mut available = 0usize;
10603 let mut unavailable = 0usize;
10604 let mut decoded_files = Vec::with_capacity(files.len());
10605 for (file_index, raw) in files.into_iter().enumerate() {
10606 let file = if revision == ReaderRevision::V11 {
10607 let file = decode_legacy_v11_file(command, file_index, &raw)?;
10608 let (file_available, file_unavailable) =
10609 validate_legacy_v11_prediction_phase_file(command, file_index, &file)?;
10610 available = available
10611 .checked_add(file_available)
10612 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10613 unavailable = unavailable
10614 .checked_add(file_unavailable)
10615 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10616 file
10617 } else if revision == ReaderRevision::V14 {
10618 let file = decode_prediction_phase_file_v14(command, file_index, &raw)?;
10619 let (file_available, file_unavailable) =
10620 validate_prediction_phase_file_v14(command, file_index, &file)?;
10621 available = available
10622 .checked_add(file_available)
10623 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10624 unavailable = unavailable
10625 .checked_add(file_unavailable)
10626 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10627 file
10628 } else if revision == ReaderRevision::V15 {
10629 let file = decode_prediction_phase_file_v15(command, file_index, &raw)?;
10630 let (file_available, file_unavailable) =
10631 validate_prediction_phase_file_v15(command, file_index, &file)?;
10632 available = available
10633 .checked_add(file_available)
10634 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10635 unavailable = unavailable
10636 .checked_add(file_unavailable)
10637 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10638 file
10639 } else if revision == ReaderRevision::V16 {
10640 let file = decode_prediction_phase_file_v16(command, file_index, &raw)?;
10641 let (file_available, file_unavailable) =
10642 validate_prediction_phase_file_v16(command, file_index, &file)?;
10643 available = available
10644 .checked_add(file_available)
10645 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10646 unavailable = unavailable
10647 .checked_add(file_unavailable)
10648 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10649 file
10650 } else if matches!(
10651 revision,
10652 ReaderRevision::V17 | ReaderRevision::V18 | ReaderRevision::V19
10653 ) {
10654 let file = decode_prediction_phase_file_v17(command, file_index, &raw)?;
10655 let (file_available, file_unavailable) =
10656 validate_prediction_phase_file_v17(command, file_index, &file)?;
10657 available = available
10658 .checked_add(file_available)
10659 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10660 unavailable = unavailable
10661 .checked_add(file_unavailable)
10662 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10663 file
10664 } else {
10665 let expected_measurement_schema = if revision == ReaderRevision::V12 {
10666 MEASUREMENTS_V15_SCHEMA_ID
10667 } else {
10668 MEASUREMENTS_V16_SCHEMA_ID
10669 };
10670 let file = decode_prediction_phase_file(
10671 command,
10672 file_index,
10673 &raw,
10674 expected_measurement_schema,
10675 )?;
10676 let (file_available, file_unavailable) = validate_prediction_phase_file(
10677 command,
10678 file_index,
10679 &file,
10680 expected_measurement_schema,
10681 )?;
10682 available = available
10683 .checked_add(file_available)
10684 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10685 unavailable = unavailable
10686 .checked_add(file_unavailable)
10687 .ok_or(MeasurementReportError::PredictionFacetSummaryMismatch)?;
10688 file
10689 };
10690 decoded_files.push(file);
10691 }
10692 validate_prediction_summary(command, self.summary.as_ref(), available, unavailable)?;
10693 let parsed = decoded_files
10694 .into_iter()
10695 .enumerate()
10696 .map(|(file_index, file)| {
10697 let path = file.path.ok_or_else(|| {
10698 MeasurementReportError::file(file_index, MeasurementFileError::MissingPath)
10699 })?;
10700 let input = file.input.ok_or_else(|| {
10701 MeasurementReportError::file(file_index, MeasurementFileError::MissingInput)
10702 })?;
10703 let sha256 = input.sha256.ok_or_else(|| {
10704 MeasurementReportError::file(file_index, MeasurementFileError::MissingSha256)
10705 })?;
10706 if sha256.len() != 64
10707 || !sha256
10708 .bytes()
10709 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
10710 {
10711 return Err(MeasurementReportError::file(
10712 file_index,
10713 MeasurementFileError::InvalidSha256,
10714 ));
10715 }
10716 let bytes = input.bytes.ok_or_else(|| {
10717 MeasurementReportError::file(file_index, MeasurementFileError::MissingBytes)
10718 })?;
10719 let measurements = file.measurements.ok_or_else(|| {
10720 MeasurementReportError::file(
10721 file_index,
10722 MeasurementFileError::MissingMeasurements,
10723 )
10724 })?;
10725 let measurements = decode_measurement_payload(
10726 &measurements,
10727 matches!(
10728 revision,
10729 ReaderRevision::V13
10730 | ReaderRevision::V14
10731 | ReaderRevision::V15
10732 | ReaderRevision::V16
10733 | ReaderRevision::V17
10734 | ReaderRevision::V18
10735 | ReaderRevision::V19
10736 ),
10737 )
10738 .map_err(|source| {
10739 MeasurementReportError::file(
10740 file_index,
10741 MeasurementFileError::InvalidMeasurementsShape {
10742 reason: source.to_string(),
10743 },
10744 )
10745 })?;
10746 let (expected_measurement_version, expected_measurement_schema) = match revision {
10747 ReaderRevision::V11 | ReaderRevision::V12 => {
10748 (MEASUREMENTS_V15_SCHEMA_VERSION, MEASUREMENTS_V15_SCHEMA_ID)
10749 }
10750 ReaderRevision::V13
10751 | ReaderRevision::V14
10752 | ReaderRevision::V15
10753 | ReaderRevision::V16
10754 | ReaderRevision::V17 => {
10755 (MEASUREMENTS_V16_SCHEMA_VERSION, MEASUREMENTS_V16_SCHEMA_ID)
10756 }
10757 ReaderRevision::V18 => {
10758 (MEASUREMENTS_V17_SCHEMA_VERSION, MEASUREMENTS_V17_SCHEMA_ID)
10759 }
10760 ReaderRevision::V19 => (MEASUREMENTS_SCHEMA_VERSION, MEASUREMENTS_SCHEMA_ID),
10761 };
10762 match measurements.schema_version {
10763 Some(found) if found == expected_measurement_version => {}
10764 Some(found) => {
10765 return Err(MeasurementReportError::file(
10766 file_index,
10767 MeasurementFileError::UnsupportedMeasurementVersion {
10768 found,
10769 expected: expected_measurement_version,
10770 },
10771 ));
10772 }
10773 None => {
10774 return Err(MeasurementReportError::file(
10775 file_index,
10776 MeasurementFileError::MissingMeasurementVersion,
10777 ));
10778 }
10779 }
10780 if measurements.schema.as_deref() != Some(expected_measurement_schema) {
10781 return Err(MeasurementReportError::file(
10782 file_index,
10783 MeasurementFileError::WrongMeasurementIdentity {
10784 expected: expected_measurement_schema,
10785 },
10786 ));
10787 }
10788 let clips = measurements.clips.ok_or_else(|| {
10789 MeasurementReportError::file(file_index, MeasurementFileError::MissingClips)
10790 })?;
10791 let material_resource_coverage =
10792 measurements.material_resource_coverage.ok_or_else(|| {
10793 MeasurementReportError::file(
10794 file_index,
10795 MeasurementFileError::MissingMaterialResourceCoverage,
10796 )
10797 })?;
10798 let material_definitions = measurements.material_definitions.ok_or_else(|| {
10799 MeasurementReportError::file(
10800 file_index,
10801 MeasurementFileError::MissingMaterialDefinitions,
10802 )
10803 })?;
10804 let textures = measurements.textures.ok_or_else(|| {
10805 MeasurementReportError::file(file_index, MeasurementFileError::MissingTextures)
10806 })?;
10807 let images = measurements.images.ok_or_else(|| {
10808 MeasurementReportError::file(file_index, MeasurementFileError::MissingImages)
10809 })?;
10810 let skeleton_source_coverage =
10811 measurements.skeleton_source_coverage.ok_or_else(|| {
10812 MeasurementReportError::file(
10813 file_index,
10814 MeasurementFileError::MissingSkeletonSourceCoverage,
10815 )
10816 })?;
10817 let skeleton_nodes = measurements.skeleton_nodes.ok_or_else(|| {
10818 MeasurementReportError::file(
10819 file_index,
10820 MeasurementFileError::MissingSkeletonNodes,
10821 )
10822 })?;
10823 let skeleton_nodes = skeleton_nodes
10824 .into_iter()
10825 .enumerate()
10826 .map(|(offset, node)| match node {
10827 SkeletonNodeMeasurementInput::Current(node) => Ok(*node),
10828 SkeletonNodeMeasurementInput::Earlier { .. } => {
10829 Err(MeasurementReportError::file(
10830 file_index,
10831 MeasurementFileError::InvalidMeasurements {
10832 source: MeasurementContractError::InvalidStructure {
10833 path: format!("skeleton_nodes[{offset}]"),
10834 reason: "uses a shape from an earlier measurement contract"
10835 .into(),
10836 },
10837 },
10838 ))
10839 }
10840 })
10841 .collect::<Result<Vec<_>, _>>()?;
10842 let skins = measurements.skins.ok_or_else(|| {
10843 MeasurementReportError::file(file_index, MeasurementFileError::MissingSkins)
10844 })?;
10845 let skins = skins
10846 .into_iter()
10847 .enumerate()
10848 .map(|(offset, skin)| match skin {
10849 SkinMeasurementInput::Current(skin) => Ok(*skin),
10850 SkinMeasurementInput::Earlier { .. } => Err(MeasurementReportError::file(
10851 file_index,
10852 MeasurementFileError::InvalidMeasurements {
10853 source: MeasurementContractError::InvalidStructure {
10854 path: format!("skins[{offset}]"),
10855 reason: "uses a shape from an earlier measurement contract"
10856 .into(),
10857 },
10858 },
10859 )),
10860 })
10861 .collect::<Result<Vec<_>, _>>()?;
10862 let mesh_definitions = measurements.mesh_definitions.ok_or_else(|| {
10863 MeasurementReportError::file(
10864 file_index,
10865 MeasurementFileError::MissingMeshDefinitions,
10866 )
10867 })?;
10868 let node_instances = measurements.node_instances.ok_or_else(|| {
10869 MeasurementReportError::file(
10870 file_index,
10871 MeasurementFileError::MissingNodeInstances,
10872 )
10873 })?;
10874 let scenes = measurements.scenes.ok_or_else(|| {
10875 MeasurementReportError::file(file_index, MeasurementFileError::MissingScenes)
10876 })?;
10877 let assets = AssetMeasurements {
10878 material_resource_coverage,
10879 material_definitions,
10880 textures,
10881 images,
10882 skeleton_source_coverage,
10883 skeleton_nodes,
10884 skins,
10885 mesh_definitions,
10886 node_instances,
10887 scenes,
10888 default_scene_index: measurements.default_scene_index,
10889 };
10890 let measurements = match revision {
10891 ReaderRevision::V11 | ReaderRevision::V12 => {
10892 MeasurementContract::historical_v15(clips, assets)
10893 }
10894 ReaderRevision::V13
10895 | ReaderRevision::V14
10896 | ReaderRevision::V15
10897 | ReaderRevision::V16
10898 | ReaderRevision::V17 => MeasurementContract::historical_v16(clips, assets),
10899 ReaderRevision::V18 => MeasurementContract::historical_v17(clips, assets),
10900 ReaderRevision::V19 => MeasurementContract::new(clips, assets),
10901 }
10902 .map_err(|source| {
10903 MeasurementReportError::file(
10904 file_index,
10905 MeasurementFileError::InvalidMeasurements { source },
10906 )
10907 })?;
10908 let prediction_measurements =
10909 if matches!(revision, ReaderRevision::V18 | ReaderRevision::V19) {
10910 measurements.prediction_v16_projection().map_err(|source| {
10911 MeasurementReportError::file(
10912 file_index,
10913 MeasurementFileError::InvalidMeasurements { source },
10914 )
10915 })?
10916 } else {
10917 measurements.clone()
10918 };
10919 if revision == ReaderRevision::V15 {
10920 let provenance = file.prediction_provenance_v4.as_present();
10921 for (check_index, check) in file
10922 .checks_v3
10923 .as_deref()
10924 .unwrap_or_default()
10925 .iter()
10926 .enumerate()
10927 {
10928 validate_current_engine_unit_scale_prediction_v4(
10929 &check.check_id,
10930 check.selection,
10931 check.configuration,
10932 check.applicability,
10933 None,
10934 None,
10935 &prediction_measurements,
10936 )
10937 .map_err(|source| {
10938 MeasurementReportError::file(
10939 file_index,
10940 MeasurementFileError::InvalidPrediction {
10941 check_index,
10942 source,
10943 },
10944 )
10945 })?;
10946 }
10947 for (check_index, check) in file
10948 .checks_v4
10949 .as_deref()
10950 .unwrap_or_default()
10951 .iter()
10952 .enumerate()
10953 {
10954 validate_current_engine_unit_scale_prediction_v4(
10955 &check.check_id,
10956 check.selection,
10957 check.configuration,
10958 check.applicability,
10959 check.prediction.as_ref(),
10960 provenance,
10961 &prediction_measurements,
10962 )
10963 .map_err(|source| {
10964 MeasurementReportError::file(
10965 file_index,
10966 MeasurementFileError::InvalidPrediction {
10967 check_index,
10968 source,
10969 },
10970 )
10971 })?;
10972 }
10973 }
10974 if matches!(
10975 revision,
10976 ReaderRevision::V16
10977 | ReaderRevision::V17
10978 | ReaderRevision::V18
10979 | ReaderRevision::V19
10980 ) && !matches!(file.prediction_provenance_v5, RequiredNullable::Missing)
10981 {
10982 let provenance = file.prediction_provenance_v5.as_present();
10983 for (check_index, check) in file
10984 .checks_v5
10985 .as_deref()
10986 .unwrap_or_default()
10987 .iter()
10988 .enumerate()
10989 {
10990 validate_current_engine_unit_scale_prediction_v5(
10991 &check.check_id,
10992 check.selection,
10993 check.configuration,
10994 check.applicability,
10995 check.prediction.as_ref(),
10996 provenance,
10997 &prediction_measurements,
10998 )
10999 .map_err(|source| {
11000 MeasurementReportError::file(
11001 file_index,
11002 MeasurementFileError::InvalidPrediction {
11003 check_index,
11004 source,
11005 },
11006 )
11007 })?;
11008 }
11009 }
11010 if matches!(
11011 revision,
11012 ReaderRevision::V17 | ReaderRevision::V18 | ReaderRevision::V19
11013 ) && !matches!(file.prediction_provenance_v6, RequiredNullable::Missing)
11014 {
11015 let provenance = file.prediction_provenance_v6.as_present();
11016 let rig = file.rig_v17.as_ref().ok_or_else(|| {
11017 MeasurementReportError::file(
11018 file_index,
11019 MeasurementFileError::InvalidFileShape {
11020 reason: "current role-policy rig evidence was not retained".into(),
11021 },
11022 )
11023 })?;
11024 for (check_index, check) in file
11025 .checks_v6
11026 .as_deref()
11027 .unwrap_or_default()
11028 .iter()
11029 .enumerate()
11030 {
11031 validate_current_engine_root_motion_prediction_v6(
11032 &check.check_id,
11033 check.selection,
11034 check.configuration,
11035 check.applicability,
11036 check.prediction.as_ref(),
11037 provenance,
11038 &check.findings,
11039 rig,
11040 &prediction_measurements,
11041 )
11042 .map_err(|source| {
11043 MeasurementReportError::file(
11044 file_index,
11045 MeasurementFileError::InvalidPrediction {
11046 check_index,
11047 source,
11048 },
11049 )
11050 })?;
11051 }
11052 }
11053 Ok((
11054 MeasurementReportFile {
11055 path,
11056 input: InputIdentity { sha256, bytes },
11057 measurements,
11058 },
11059 (
11060 file.checks.unwrap_or_default(),
11061 file.legacy_checks.unwrap_or_default(),
11062 file.checks_v3.unwrap_or_default(),
11063 file.checks_v4.unwrap_or_default(),
11064 file.checks_v5.unwrap_or_default(),
11065 file.checks_v6.unwrap_or_default(),
11066 ),
11067 ))
11068 })
11069 .collect::<Result<Vec<_>, _>>()?;
11070
11071 for (
11074 file_index,
11075 (file, (checks, legacy_checks, checks_v3, checks_v4, checks_v5, checks_v6)),
11076 ) in parsed.iter().enumerate()
11077 {
11078 let prediction_measurements =
11079 if matches!(revision, ReaderRevision::V18 | ReaderRevision::V19) {
11080 file.measurements
11081 .prediction_v16_projection()
11082 .map_err(|source| {
11083 MeasurementReportError::file(
11084 file_index,
11085 MeasurementFileError::InvalidMeasurements { source },
11086 )
11087 })?
11088 } else {
11089 file.measurements.clone()
11090 };
11091 validate_measurement_references_batch_v4(
11092 &prediction_measurements,
11093 checks_v6
11094 .iter()
11095 .enumerate()
11096 .filter_map(|(check_index, check)| {
11097 check
11098 .prediction
11099 .as_ref()
11100 .map(|prediction| (check_index, prediction.base_prediction()))
11101 }),
11102 )
11103 .map_err(|error| {
11104 MeasurementReportError::file(
11105 file_index,
11106 MeasurementFileError::InvalidPrediction {
11107 check_index: error.prediction_index,
11108 source: error.source,
11109 },
11110 )
11111 })?;
11112 validate_measurement_references_batch_v4(
11113 &prediction_measurements,
11114 checks_v5
11115 .iter()
11116 .enumerate()
11117 .filter_map(|(check_index, check)| {
11118 check
11119 .prediction
11120 .as_ref()
11121 .map(|prediction| (check_index, prediction.base_prediction()))
11122 }),
11123 )
11124 .map_err(|error| {
11125 MeasurementReportError::file(
11126 file_index,
11127 MeasurementFileError::InvalidPrediction {
11128 check_index: error.prediction_index,
11129 source: error.source,
11130 },
11131 )
11132 })?;
11133 validate_measurement_references_batch_v4(
11134 &prediction_measurements,
11135 checks_v4
11136 .iter()
11137 .enumerate()
11138 .filter_map(|(check_index, check)| {
11139 check
11140 .prediction
11141 .as_ref()
11142 .map(|prediction| (check_index, prediction))
11143 }),
11144 )
11145 .map_err(|error| {
11146 MeasurementReportError::file(
11147 file_index,
11148 MeasurementFileError::InvalidPrediction {
11149 check_index: error.prediction_index,
11150 source: error.source,
11151 },
11152 )
11153 })?;
11154 validate_measurement_references_batch_v3(
11155 &prediction_measurements,
11156 checks_v3
11157 .iter()
11158 .enumerate()
11159 .filter_map(|(check_index, check)| {
11160 check
11161 .prediction
11162 .as_ref()
11163 .map(|prediction| (check_index, prediction))
11164 }),
11165 )
11166 .map_err(|error| {
11167 MeasurementReportError::file(
11168 file_index,
11169 MeasurementFileError::InvalidPrediction {
11170 check_index: error.prediction_index,
11171 source: error.source,
11172 },
11173 )
11174 })?;
11175 validate_measurement_references_batch_v2(
11176 &prediction_measurements,
11177 checks
11178 .iter()
11179 .enumerate()
11180 .filter_map(|(check_index, check)| {
11181 check
11182 .prediction
11183 .as_ref()
11184 .map(|prediction| (check_index, prediction))
11185 }),
11186 )
11187 .map_err(|error| {
11188 MeasurementReportError::file(
11189 file_index,
11190 MeasurementFileError::InvalidPrediction {
11191 check_index: error.prediction_index,
11192 source: error.source,
11193 },
11194 )
11195 })?;
11196 validate_measurement_references_batch(
11197 &prediction_measurements,
11198 legacy_checks
11199 .iter()
11200 .enumerate()
11201 .filter_map(|(check_index, check)| {
11202 check
11203 .prediction
11204 .as_ref()
11205 .map(|prediction| (check_index, prediction))
11206 }),
11207 )
11208 .map_err(|error| {
11209 MeasurementReportError::file(
11210 file_index,
11211 MeasurementFileError::InvalidPrediction {
11212 check_index: error.prediction_index,
11213 source: error.source,
11214 },
11215 )
11216 })?;
11217 }
11218 Ok(parsed.into_iter().map(|(file, _)| file).collect())
11219 }
11220}
11221
11222#[cfg(test)]
11223mod measurement_report_input_tests {
11224 use std::collections::BTreeMap;
11225
11226 use super::*;
11227 use crate::engine_contract::{
11228 EngineClipSettingsV1, EngineConversionControlV1, EngineCoordinateBasisV1, EngineFactIdV1,
11229 EngineFactStateV1, EngineFactValueV1, EngineForwardAxisV1, EngineHandednessV1,
11230 EngineLinearUnitV1, EnginePrimarySourceV1, EngineProfileFactV1, EngineProfileSelectionV1,
11231 EngineUpAxisV1, ResolvedEngineProfileV1, ResolvedEngineSettingsCoverageV2,
11232 ResolvedEngineSettingsV1, ResolvedEngineSettingsV2, ResolvedEngineSettingsWorkV2,
11233 };
11234 use crate::evaluation::{CheckOutput, EvaluationScope, EvaluationScopeCode};
11235 use crate::measure::{
11236 AssetMeasurements, ImageMeasurements, MeshDefinitionMeasurements, PrimitiveMeasurements,
11237 };
11238 use crate::prediction::{
11239 EngineMachineResultV1, EnginePredictionBasisV1, EnginePredictionBasisV2,
11240 EnginePredictionBasisV4, EnginePredictionFacetV1, EnginePredictionFacetV2,
11241 EnginePredictionFacetV3, EnginePredictionFacetV4, EnginePredictionV1, EnginePredictionV2,
11242 EnginePredictionV3, EnginePredictionV4, PredictionBasisReferenceV1,
11243 PredictionBasisReferenceV2, PredictionBasisReferenceV4, PredictionProvenanceIdentityV4,
11244 PredictionScalarV1, PredictionUnavailableReasonV1, PredictionUnavailableReasonV2,
11245 RawSourceBindingV1, RawSourceBindingV2, UnitMappingResultV1,
11246 };
11247 use crate::source_facts::SourceFormatV1;
11248 use crate::{
11249 DependencyClosureV1, Document, Finding, ImageSourceKind, ImageUnavailableReason,
11250 MaterialResourceCoverage, ResolvedRoles,
11251 };
11252
11253 fn prediction_test_profile() -> ResolvedEngineProfileV1 {
11254 let all_fact_ids = [
11255 EngineFactIdV1::AcceptedInputs,
11256 EngineFactIdV1::AnimationAddressability,
11257 EngineFactIdV1::AnimationChannelHandling,
11258 EngineFactIdV1::AnimationTargetAddressability,
11259 EngineFactIdV1::AxisConversionControl,
11260 EngineFactIdV1::ConstructHandling,
11261 EngineFactIdV1::ExactAxisConversion,
11262 EngineFactIdV1::ExtensionHandling,
11263 EngineFactIdV1::ResultingHierarchyScale,
11264 EngineFactIdV1::RootMotionAddressability,
11265 EngineFactIdV1::TargetCoordinateBasis,
11266 EngineFactIdV1::TargetLinearUnit,
11267 EngineFactIdV1::UnitConversionControl,
11268 EngineFactIdV1::WholeEndFrameRequired,
11269 ];
11270 let facts = all_fact_ids
11271 .into_iter()
11272 .map(|id| {
11273 let state = if id == EngineFactIdV1::AcceptedInputs {
11274 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
11275 SourceFormatV1::Glb,
11276 ]))
11277 } else {
11278 EngineFactStateV1::Unknown
11279 };
11280 EngineProfileFactV1::new(id, state)
11281 })
11282 .collect();
11283 ResolvedEngineProfileV1::new(
11284 EngineProfileSelectionV1::new("test", 1, "1", "test-importer").unwrap(),
11285 "urn:animsmith:engine-profile:test:1",
11286 facts,
11287 vec![],
11288 vec![
11289 EnginePrimarySourceV1::new(
11290 "test-source",
11291 "1",
11292 "https://example.invalid/test",
11293 "2026-08-20",
11294 vec![EngineFactIdV1::AcceptedInputs],
11295 vec![],
11296 )
11297 .unwrap(),
11298 ],
11299 )
11300 .unwrap()
11301 }
11302
11303 fn prediction_test_provenance_v2() -> PredictionProvenanceV2 {
11304 let raw: RawSourceBindingV1 = serde_json::from_value(serde_json::json!({
11305 "schema": crate::RAW_SOURCE_FACTS_V1_ID,
11306 "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
11307 "source_format": "glb",
11308 "linear_unit": {
11309 "state": "observed", "value": 1.0, "disposition": "preserved",
11310 "provenance": {"kind": "format_defined"}
11311 },
11312 "coordinate_basis": {
11313 "state": "observed",
11314 "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
11315 "disposition": "preserved", "provenance": {"kind": "format_defined"}
11316 },
11317 "frames_per_second": {
11318 "state": "observed", "value": 30.0, "disposition": "preserved",
11319 "provenance": {"kind": "format_defined"}
11320 },
11321 "clips_coverage": {"state": "complete"},
11322 "constructs_coverage": {"state": "complete"},
11323 "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
11324 "source_skeleton_coverage": "unavailable",
11325 "work": {
11326 "inspected_rows": 0, "retained_rows": 0,
11327 "retained_text_bytes": 0, "max_traversal_depth": 0
11328 }
11329 }))
11330 .unwrap();
11331 let closure = DependencyClosureV1::unavailable(raw.primary_input().clone());
11332 let profile = prediction_test_profile();
11333 let settings = ResolvedEngineSettingsV2::new(
11334 &profile,
11335 vec![],
11336 vec![],
11337 ResolvedEngineSettingsCoverageV2::complete(),
11338 ResolvedEngineSettingsWorkV2::new(0, 0, 0),
11339 )
11340 .unwrap();
11341 PredictionProvenanceV2::new(profile, SourceFormatV1::Glb, settings, raw, closure).unwrap()
11342 }
11343
11344 fn prediction_test_provenance() -> PredictionProvenanceV3 {
11345 let prior = prediction_test_provenance_v2();
11346 let raw: RawSourceBindingV2 = serde_json::from_value(serde_json::json!({
11347 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11348 "source_facts": prior.raw_source(),
11349 "exact_source_timing": null
11350 }))
11351 .unwrap();
11352 PredictionProvenanceV3::new(
11353 prior.profile().clone(),
11354 prior.source_format(),
11355 prior.settings().clone(),
11356 raw,
11357 prior.dependency_closure().clone(),
11358 )
11359 .unwrap()
11360 }
11361
11362 fn partial_engine_provenance() -> PredictionProvenanceV3 {
11363 let complete = prediction_test_provenance();
11364 let mut raw_wire = serde_json::to_value(complete.raw_source().source_facts()).unwrap();
11365 raw_wire["clips_coverage"] = serde_json::json!({
11366 "state": "partial",
11367 "reason": "projection_budget_exceeded"
11368 });
11369 let raw: RawSourceBindingV2 = serde_json::from_value(serde_json::json!({
11370 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11371 "source_facts": raw_wire,
11372 "exact_source_timing": null
11373 }))
11374 .unwrap();
11375 let clips = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
11376 .map(|index| EngineClipSettingsV1::new(format!("clip-{index:04}"), Vec::new()).unwrap())
11377 .collect();
11378 let settings = ResolvedEngineSettingsV2::new(
11379 complete.profile(),
11380 Vec::new(),
11381 clips,
11382 ResolvedEngineSettingsCoverageV2::actual_clip_rows_exceeded(),
11383 ResolvedEngineSettingsWorkV2::new(4_097, 4_096, 4_096),
11384 )
11385 .unwrap();
11386 PredictionProvenanceV3::new(
11387 complete.profile().clone(),
11388 complete.source_format(),
11389 settings,
11390 raw,
11391 complete.dependency_closure().clone(),
11392 )
11393 .unwrap()
11394 }
11395
11396 fn prediction_test_measurements() -> MeasurementContract {
11397 MeasurementContract::new(BTreeMap::new(), AssetMeasurements::default()).unwrap()
11398 }
11399
11400 fn prediction_measurements_with_rotation_facts() -> MeasurementContract {
11401 let assets: AssetMeasurements = serde_json::from_value(serde_json::json!({
11402 "material_resource_coverage": "unavailable",
11403 "material_definitions": [], "textures": [], "images": [],
11404 "skeleton_source_coverage": "complete",
11405 "skeleton_nodes": [{
11406 "node_index": 0,
11407 "scene_root_indices": [],
11408 "local_rest": {
11409 "kind": "trs",
11410 "translation_parent_space_m": [0.0, 0.0, 0.0],
11411 "rotation_xyzw": [0.0, 0.0, 0.0, 1.0],
11412 "scale": [1.0, 1.0, 1.0]
11413 },
11414 "rest_world_matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
11415 "rest_world_translation_m": [0.0, 0.0, 0.0],
11416 "rest_world_linear": {
11417 "classification": "unit_orthonormal",
11418 "axis_lengths": [1.0, 1.0, 1.0],
11419 "determinant": 1.0,
11420 "orientation": "positive",
11421 "rotation_xyzw": [0.0, 0.0, 0.0, 1.0],
11422 "uniform_scale": 1.0
11423 }
11424 }],
11425 "skins": [{
11426 "skin_index": 0,
11427 "joints": [{
11428 "joint_index": 0, "node_index": 0,
11429 "joint_bind_to_mesh": {
11430 "source_inverse_bind_matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
11431 "inversion_quality": { "reciprocal_condition_number_inf": 1.0 },
11432 "matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
11433 "linear": { "classification": "unit_orthonormal", "axis_lengths": [1.0, 1.0, 1.0], "determinant": 1.0, "orientation": "positive", "rotation_xyzw": [0.0, 0.0, 0.0, 1.0], "uniform_scale": 1.0 }
11434 },
11435 "mesh_bind_world": {
11436 "source_inverse_bind_matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
11437 "matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
11438 "linear": { "classification": "unit_orthonormal", "axis_lengths": [1.0, 1.0, 1.0], "determinant": 1.0, "orientation": "positive", "rotation_xyzw": [0.0, 0.0, 0.0, 1.0], "uniform_scale": 1.0 }
11439 }
11440 }],
11441 "joint_bind_linear_summary": { "classification": "consistent_uniform", "joint_count": 1, "available_joint_count": 1, "unavailable_joint_count": 0, "consistent_uniform_scale": 1.0 },
11442 "inverse_bind_accessor": {
11443 "status": "available", "declared_count": 1,
11444 "matrices": [[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]]
11445 },
11446 "attachments": [{ "node_index": 0 }]
11447 }],
11448 "mesh_definitions": [], "node_instances": [], "scenes": []
11449 }))
11450 .expect("complete skeleton and skin fixture");
11451 MeasurementContract::new(BTreeMap::new(), assets).expect("current rotation facts")
11452 }
11453
11454 fn prediction_test_measurements_v16() -> MeasurementContract {
11455 MeasurementContract::historical_v16(BTreeMap::new(), AssetMeasurements::default()).unwrap()
11456 }
11457
11458 fn loop_projection_measurements(
11459 availability: &[MeasurementAvailability],
11460 ) -> MeasurementContract {
11461 let bones = availability
11462 .iter()
11463 .enumerate()
11464 .map(|(index, availability)| match availability {
11465 MeasurementAvailability::Measured => serde_json::json!({
11466 "bone_index": index,
11467 "bone_name": format!("bone-{index}"),
11468 "availability": "measured",
11469 "position_delta_m": index as f64,
11470 "rotation_delta_deg": index as f64,
11471 "seam_velocity_delta_mps": index as f64,
11472 "seam_angular_velocity_delta_degps": index as f64,
11473 }),
11474 MeasurementAvailability::Unavailable => serde_json::json!({
11475 "bone_index": index,
11476 "bone_name": format!("bone-{index}"),
11477 "availability": "unavailable",
11478 }),
11479 MeasurementAvailability::NotApplicable => {
11480 panic!("loop-continuity rows are always applicable")
11481 }
11482 })
11483 .collect::<Vec<_>>();
11484 let clip: ClipMeasurements = serde_json::from_value(serde_json::json!({
11485 "duration_s": 1.0,
11486 "frame_count": 3,
11487 "animated_bones": [],
11488 "bone_channels": [],
11489 "bone_rotation_range_deg": {},
11490 "loop_continuity": { "bones": bones },
11491 "loop_continuity_availability": "measured",
11492 "loop_endpoint_mode_availability": "not_applicable",
11493 "frame_grid_availability": "not_applicable",
11494 "loop_seam_ratio_availability": "not_applicable",
11495 "gait_availability": "not_applicable",
11496 "root_trajectory_availability": "not_applicable",
11497 "speed_mps_availability": "not_applicable"
11498 }))
11499 .unwrap();
11500 MeasurementContract::new(
11501 BTreeMap::from([("loop".into(), clip)]),
11502 AssetMeasurements::default(),
11503 )
11504 .unwrap()
11505 }
11506
11507 fn measure_wire(measurements: MeasurementContract) -> serde_json::Value {
11508 let file = MeasureFileReport::new(
11509 "test.glb",
11510 InputIdentity::from_bytes(&[]),
11511 prediction_test_rig(),
11512 measurements,
11513 )
11514 .unwrap();
11515 let envelope =
11516 MeasureEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
11517 .unwrap();
11518 let wire = serde_json::to_value(envelope).unwrap();
11519 serde_json::from_value::<MeasurementReportInput>(wire.clone())
11520 .unwrap()
11521 .into_files()
11522 .expect("current measurement fixture reads back");
11523 wire
11524 }
11525
11526 fn primitive_measurement_contract() -> MeasurementContract {
11527 let mut assets = AssetMeasurements::default();
11528 assets.mesh_definitions.push(MeshDefinitionMeasurements {
11529 mesh_index: 0,
11530 name: "mesh".into(),
11531 primitives: Some(vec![
11532 PrimitiveMeasurements {
11533 primitive_index: 1,
11534 material_index: Some(7),
11535 vertex_count: 2,
11536 finite_vertex_count: 1,
11537 geometry_aabb: Some(Aabb {
11538 min: [-2.0, 1.0, 0.0],
11539 max: [-2.0, 1.0, 0.0],
11540 }),
11541 geometry_centroid: Some([-2.0, 1.0, 0.0]),
11542 },
11543 PrimitiveMeasurements {
11544 primitive_index: 3,
11545 material_index: None,
11546 vertex_count: 2,
11547 finite_vertex_count: 2,
11548 geometry_aabb: Some(Aabb {
11549 min: [4.0, 3.0, 0.0],
11550 max: [6.0, 3.0, 0.0],
11551 }),
11552 geometry_centroid: Some([5.0, 3.0, 0.0]),
11553 },
11554 ]),
11555 vertex_count: 4,
11556 geometry_aabb: Some(Aabb {
11557 min: [-2.0, 1.0, 0.0],
11558 max: [6.0, 3.0, 0.0],
11559 }),
11560 geometry_centroid: Some([8.0 / 3.0, 7.0 / 3.0, 0.0]),
11561 max_joints_per_vertex: 0,
11562 weight_sum_min: None,
11563 weight_sum_max: None,
11564 additional_influence_sets: Vec::new(),
11565 });
11566 MeasurementContract::new(BTreeMap::new(), assets).unwrap()
11567 }
11568
11569 fn prediction_test_rig() -> RigInfo {
11570 RigInfo::from_resolved(&Document::default(), &ResolvedRoles::default()).unwrap()
11571 }
11572
11573 fn basis_v2(basis: EnginePredictionBasisV1) -> EnginePredictionBasisV2 {
11574 EnginePredictionBasisV2::new(
11575 basis
11576 .references()
11577 .iter()
11578 .cloned()
11579 .map(PredictionBasisReferenceV2::v1)
11580 .collect(),
11581 )
11582 .unwrap()
11583 }
11584
11585 fn unavailable_facet(
11586 subject: String,
11587 basis: EnginePredictionBasisV1,
11588 ) -> EnginePredictionFacetV3 {
11589 EnginePredictionFacetV3::required_unavailable(
11590 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction-limit"))
11591 .subject(subject),
11592 basis_v2(basis),
11593 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
11594 )
11595 .unwrap()
11596 }
11597
11598 fn unavailable_check(
11599 check_id: &'static str,
11600 provenance: &PredictionProvenanceV3,
11601 facets: Vec<EnginePredictionFacetV3>,
11602 ) -> CheckEvaluation {
11603 let prediction = EnginePredictionV3::new(provenance.identity().clone(), facets).unwrap();
11604 CheckEvaluation::evaluated(
11605 check_id,
11606 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
11607 .with_engine_prediction_v3(prediction),
11608 )
11609 .unwrap()
11610 }
11611
11612 fn unavailable_check_v2(
11613 check_id: &'static str,
11614 provenance: &PredictionProvenanceV2,
11615 facets: Vec<EnginePredictionFacetV2>,
11616 ) -> CheckEvaluation {
11617 let prediction = EnginePredictionV2::new(provenance.identity().clone(), facets).unwrap();
11618 CheckEvaluation::evaluated(
11619 check_id,
11620 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
11621 .with_engine_prediction_v2(prediction),
11622 )
11623 .unwrap()
11624 }
11625
11626 fn lint_file(
11627 provenance: &PredictionProvenanceV3,
11628 checks: Vec<CheckEvaluation>,
11629 ) -> Result<LintFileReport, OutputContractError> {
11630 LintFileReport::new(
11631 "limit.glb",
11632 provenance.raw_source().primary_input().clone(),
11633 prediction_test_rig(),
11634 Some(provenance.clone()),
11635 checks,
11636 prediction_test_measurements_v16(),
11637 )
11638 }
11639
11640 #[test]
11641 fn output_v15_rejects_mixed_v3_provenance_and_v4_predictions() {
11642 let provenance = prediction_test_provenance();
11643 let identity: PredictionProvenanceIdentityV4 =
11644 serde_json::from_value(serde_json::to_value(provenance.identity()).unwrap()).unwrap();
11645 let basis = EnginePredictionBasisV4::new(vec![PredictionBasisReferenceV4::v2(
11646 PredictionBasisReferenceV2::v1(
11647 PredictionBasisReferenceV1::profile_fact("accepted_inputs").unwrap(),
11648 ),
11649 )])
11650 .unwrap();
11651 let scope = EvaluationScope::new(EvaluationScopeCode::custom("acme:v4-mixed"));
11652 let facet = EnginePredictionFacetV4::available(
11653 scope.clone(),
11654 basis,
11655 EngineMachineResultV1::UnitMapping(
11656 UnitMappingResultV1::gltf_to_engine_world_length_unit(),
11657 ),
11658 )
11659 .unwrap();
11660 let prediction = EnginePredictionV4::new(identity, vec![facet]).unwrap();
11661 let check = CheckEvaluation::evaluated(
11662 "acme-v4-mixed",
11663 CheckOutput::from_coverage(vec![], vec![scope], vec![])
11664 .with_engine_prediction_v4(prediction),
11665 )
11666 .unwrap();
11667 assert!(matches!(
11668 lint_file(&provenance, vec![check]),
11669 Err(OutputContractError::PredictionRevisionMismatch)
11670 ));
11671 }
11672
11673 #[test]
11674 fn output_v15_preserves_prediction_without_provenance_error_precedence() {
11675 let provenance = prediction_test_provenance();
11676 let check = unavailable_check(
11677 "acme-v3-without-provenance",
11678 &provenance,
11679 vec![unavailable_facet(
11680 "row".to_owned(),
11681 EnginePredictionBasisV1::new(Vec::new()).unwrap(),
11682 )],
11683 );
11684 let error = LintFileReport::new(
11685 "without-provenance.glb",
11686 provenance.raw_source().primary_input().clone(),
11687 prediction_test_rig(),
11688 None,
11689 vec![check],
11690 prediction_test_measurements_v16(),
11691 )
11692 .unwrap_err();
11693 assert_eq!(error, OutputContractError::PredictionWithoutProvenance);
11694 }
11695
11696 #[test]
11697 fn output_v15_rejects_active_unit_scale_without_v4_provenance() {
11698 let check = CheckEvaluation::evaluated(
11699 ENGINE_UNIT_SCALE_CHECK_ID,
11700 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new()),
11701 )
11702 .unwrap();
11703 let error = LintFileReport::new_v4(
11704 "unit-scale-without-provenance.glb",
11705 InputIdentity::from_bytes(&[]),
11706 prediction_test_rig(),
11707 None,
11708 vec![check],
11709 prediction_test_measurements_v16(),
11710 )
11711 .unwrap_err();
11712 assert_eq!(
11713 error,
11714 OutputContractError::InvalidPrediction(
11715 PredictionContractError::EngineUnitScaleFacetMismatch
11716 )
11717 );
11718 }
11719
11720 fn validated_lint_wire(
11721 provenance: &PredictionProvenanceV3,
11722 checks: Vec<CheckEvaluation>,
11723 ) -> serde_json::Value {
11724 let file = lint_file(provenance, checks).expect("producer accepts exact N");
11725 let envelope =
11726 LintEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
11727 .unwrap();
11728 let wire = serde_json::to_value(envelope).unwrap();
11729 let read: MeasurementReportInput = serde_json::from_value(wire.clone()).unwrap();
11730 read.into_files().expect("reader accepts exact N");
11731 wire
11732 }
11733
11734 fn lint_read_error(wire: serde_json::Value) -> MeasurementReportError {
11735 let read: MeasurementReportInput = serde_json::from_value(wire).unwrap();
11736 read.into_files().expect_err("reader must reject N+1")
11737 }
11738
11739 fn clip_boundary_profile() -> ResolvedEngineProfileV1 {
11740 let all_fact_ids = [
11741 EngineFactIdV1::AcceptedInputs,
11742 EngineFactIdV1::AnimationAddressability,
11743 EngineFactIdV1::AnimationChannelHandling,
11744 EngineFactIdV1::AnimationTargetAddressability,
11745 EngineFactIdV1::AxisConversionControl,
11746 EngineFactIdV1::ConstructHandling,
11747 EngineFactIdV1::ExactAxisConversion,
11748 EngineFactIdV1::ExtensionHandling,
11749 EngineFactIdV1::ResultingHierarchyScale,
11750 EngineFactIdV1::RootMotionAddressability,
11751 EngineFactIdV1::TargetCoordinateBasis,
11752 EngineFactIdV1::TargetLinearUnit,
11753 EngineFactIdV1::UnitConversionControl,
11754 EngineFactIdV1::WholeEndFrameRequired,
11755 ];
11756 let facts = all_fact_ids
11757 .into_iter()
11758 .map(|id| {
11759 let state = match id {
11760 EngineFactIdV1::AcceptedInputs => {
11761 EngineFactStateV1::Known(EngineFactValueV1::AcceptedFormats(vec![
11762 SourceFormatV1::Fbx,
11763 ]))
11764 }
11765 EngineFactIdV1::TargetCoordinateBasis => EngineFactStateV1::Known(
11766 EngineFactValueV1::CoordinateBasis(EngineCoordinateBasisV1 {
11767 handedness: EngineHandednessV1::Left,
11768 up_axis: EngineUpAxisV1::Z,
11769 forward_axis: EngineForwardAxisV1::PositiveX,
11770 }),
11771 ),
11772 EngineFactIdV1::TargetLinearUnit => EngineFactStateV1::Known(
11773 EngineFactValueV1::LinearUnit(EngineLinearUnitV1::Centimetre),
11774 ),
11775 EngineFactIdV1::UnitConversionControl
11776 | EngineFactIdV1::AxisConversionControl => {
11777 EngineFactStateV1::Known(EngineFactValueV1::ConversionControl(
11778 EngineConversionControlV1::ImporterOption,
11779 ))
11780 }
11781 EngineFactIdV1::WholeEndFrameRequired => {
11782 EngineFactStateV1::Known(EngineFactValueV1::Boolean(true))
11783 }
11784 _ => EngineFactStateV1::Unknown,
11785 };
11786 EngineProfileFactV1::new(id, state)
11787 })
11788 .collect();
11789 ResolvedEngineProfileV1::new(
11790 EngineProfileSelectionV1::new("unreal", 1, "5.8", "fbx-importer").unwrap(),
11791 "urn:animsmith:engine-profile:unreal:1",
11792 facts,
11793 vec![],
11794 vec![
11795 EnginePrimarySourceV1::new(
11796 ENGINE_CLIP_BOUNDARY_SOURCE_ID,
11797 "5.8",
11798 "https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-sequences-in-unreal-engine?application_version=5.8",
11799 "2026-08-20",
11800 vec![EngineFactIdV1::WholeEndFrameRequired],
11801 vec![],
11802 )
11803 .unwrap(),
11804 EnginePrimarySourceV1::new(
11805 "unreal-coordinate-system-5.8",
11806 "5.8",
11807 "https://dev.epicgames.com/documentation/en-us/unreal-engine/coordinate-system-and-spaces-in-unreal-engine?application_version=5.8",
11808 "2026-08-20",
11809 vec![EngineFactIdV1::TargetCoordinateBasis],
11810 vec![],
11811 )
11812 .unwrap(),
11813 EnginePrimarySourceV1::new(
11814 "unreal-fbx-import-options-5.8",
11815 "5.8",
11816 "https://dev.epicgames.com/documentation/en-us/unreal-engine/fbx-import-options-reference-in-unreal-engine?application_version=5.8",
11817 "2026-08-20",
11818 vec![
11819 EngineFactIdV1::AcceptedInputs,
11820 EngineFactIdV1::UnitConversionControl,
11821 EngineFactIdV1::AxisConversionControl,
11822 ],
11823 vec![],
11824 )
11825 .unwrap(),
11826 EnginePrimarySourceV1::new(
11827 "unreal-units-5.8",
11828 "5.8",
11829 "https://dev.epicgames.com/documentation/en-us/unreal-engine/units-of-measurement-in-unreal-engine?application_version=5.8",
11830 "2026-08-20",
11831 vec![EngineFactIdV1::TargetLinearUnit],
11832 vec![],
11833 )
11834 .unwrap(),
11835 ],
11836 )
11837 .unwrap()
11838 }
11839
11840 fn exact_observed(value: serde_json::Value, kind: &'static str) -> serde_json::Value {
11841 serde_json::json!({
11842 "state": {"kind": "observed", "value": value},
11843 "disposition": "preserved",
11844 "provenance": {"kind": kind}
11845 })
11846 }
11847
11848 fn clip_boundary_raw_wire(unavailable_last: bool) -> serde_json::Value {
11849 let last_range = if unavailable_last {
11850 serde_json::json!({
11851 "state": {"kind": "unavailable", "value": "malformed"},
11852 "disposition": "baked",
11853 "provenance": null
11854 })
11855 } else {
11856 exact_observed(
11857 serde_json::json!({
11858 "selection": "primary", "begin_units": 0, "end_units": 9_408_000
11859 }),
11860 "parser_projected",
11861 )
11862 };
11863 serde_json::json!({
11864 "schema": crate::RAW_SOURCE_FACTS_V2_ID,
11865 "source_facts": {
11866 "schema": crate::RAW_SOURCE_FACTS_V1_ID,
11867 "primary_input": {"sha256": "00".repeat(32), "bytes": 0},
11868 "source_format": "fbx",
11869 "linear_unit": {
11870 "state": "observed", "value": 0.01, "disposition": "preserved",
11871 "provenance": {"kind": "format_defined"}
11872 },
11873 "coordinate_basis": {
11874 "state": "observed",
11875 "value": {"right": "positive_x", "up": "positive_y", "forward": "positive_z"},
11876 "disposition": "preserved", "provenance": {"kind": "format_defined"}
11877 },
11878 "frames_per_second": {
11879 "state": "observed", "value": 30.0, "disposition": "preserved",
11880 "provenance": {"kind": "format_defined"}
11881 },
11882 "clips_coverage": {"state": "complete"},
11883 "constructs_coverage": {"state": "complete"},
11884 "resources_coverage": {"state": "unavailable", "reason": "parser_unavailable"},
11885 "source_skeleton_coverage": "unavailable",
11886 "work": {
11887 "inspected_rows": 3, "retained_rows": 3,
11888 "retained_text_bytes": 0, "max_traversal_depth": 0
11889 }
11890 },
11891 "exact_source_timing": {
11892 "schema": crate::EXACT_SOURCE_TIMING_V1_ID,
11893 "time_basis": exact_observed(
11894 serde_json::json!({"units_per_second": 141_120_000}),
11895 "format_defined"
11896 ),
11897 "declared_time_mode": exact_observed(
11898 serde_json::json!("fps30"), "source_declared"
11899 ),
11900 "effective_time_mode": exact_observed(
11901 serde_json::json!("fps30"), "parser_projected"
11902 ),
11903 "declared_custom_frame_rate": exact_observed(
11904 serde_json::json!({"binary64_bits": 30.0_f64.to_bits()}),
11905 "source_declared"
11906 ),
11907 "frame_period": exact_observed(
11908 serde_json::json!({"units_per_frame": 4_704_000}),
11909 "derived_from_source"
11910 ),
11911 "declared_time_protocol": exact_observed(
11912 serde_json::json!("default"), "source_declared"
11913 ),
11914 "effective_time_protocol": exact_observed(
11915 serde_json::json!("default"), "parser_projected"
11916 ),
11917 "clip_coverage": {"state": "complete"},
11918 "clips": [
11919 {
11920 "source_clip_index": 0,
11921 "source_time_range": exact_observed(
11922 serde_json::json!({
11923 "selection": "primary", "begin_units": 0,
11924 "end_units": 4_704_000
11925 }),
11926 "parser_projected"
11927 )
11928 },
11929 {
11930 "source_clip_index": 1,
11931 "source_time_range": exact_observed(
11932 serde_json::json!({
11933 "selection": "primary", "begin_units": 0,
11934 "end_units": 4_704_001
11935 }),
11936 "parser_projected"
11937 )
11938 },
11939 {"source_clip_index": 2, "source_time_range": last_range}
11940 ]
11941 }
11942 })
11943 }
11944
11945 fn clip_boundary_provenance(unavailable_last: bool) -> PredictionProvenanceV3 {
11946 let raw: RawSourceBindingV2 =
11947 serde_json::from_value(clip_boundary_raw_wire(unavailable_last)).unwrap();
11948 let profile = clip_boundary_profile();
11949 let clips = (0..3)
11950 .map(|index| EngineClipSettingsV1::new(format!("stack-{index}"), vec![]).unwrap())
11951 .collect();
11952 let settings = ResolvedEngineSettingsV2::new(
11953 &profile,
11954 vec![],
11955 clips,
11956 ResolvedEngineSettingsCoverageV2::complete(),
11957 ResolvedEngineSettingsWorkV2::new(3, 3, 3),
11958 )
11959 .unwrap();
11960 PredictionProvenanceV3::new(
11961 profile,
11962 SourceFormatV1::Fbx,
11963 settings,
11964 raw.clone(),
11965 DependencyClosureV1::unavailable(raw.primary_input().clone()),
11966 )
11967 .unwrap()
11968 }
11969
11970 fn clip_boundary_provenance_with_settings(
11971 provenance: &PredictionProvenanceV3,
11972 profile: ResolvedEngineProfileV1,
11973 clips: Vec<EngineClipSettingsV1>,
11974 ) -> PredictionProvenanceV3 {
11975 let retained = clips.len();
11976 let settings = ResolvedEngineSettingsV2::new(
11977 &profile,
11978 vec![],
11979 clips,
11980 ResolvedEngineSettingsCoverageV2::complete(),
11981 ResolvedEngineSettingsWorkV2::new(retained, retained, retained),
11982 )
11983 .unwrap();
11984 PredictionProvenanceV3::new(
11985 profile,
11986 provenance.source_format(),
11987 settings,
11988 provenance.raw_source().clone(),
11989 provenance.dependency_closure().clone(),
11990 )
11991 .unwrap()
11992 }
11993
11994 fn altered_clip_boundary_source_profile(
11995 provenance: &PredictionProvenanceV3,
11996 ) -> ResolvedEngineProfileV1 {
11997 let sources = provenance
11998 .profile()
11999 .primary_sources()
12000 .iter()
12001 .map(|source| {
12002 let url = if source.id() == ENGINE_CLIP_BOUNDARY_SOURCE_ID {
12003 format!("{}#altered", source.url())
12004 } else {
12005 source.url().to_owned()
12006 };
12007 EnginePrimarySourceV1::new(
12008 source.id(),
12009 source.target_version(),
12010 url,
12011 source.verified_on(),
12012 source.supported_fact_ids().to_vec(),
12013 source.supported_setting_ids().to_vec(),
12014 )
12015 .unwrap()
12016 })
12017 .collect();
12018 ResolvedEngineProfileV1::new(
12019 provenance.profile().selection().clone(),
12020 provenance.profile().fact_bundle_urn(),
12021 provenance.profile().facts().to_vec(),
12022 provenance.profile().setting_descriptors().to_vec(),
12023 sources,
12024 )
12025 .unwrap()
12026 }
12027
12028 fn clip_boundary_scope(source_clip_index: usize) -> EvaluationScope {
12029 EvaluationScope::new(EvaluationScopeCode::ENGINE_CLIP_BOUNDARY)
12030 .subject(format!("source_stack:{source_clip_index}"))
12031 }
12032
12033 struct InapplicableClipBoundaryCheck;
12034
12035 impl crate::Check for InapplicableClipBoundaryCheck {
12036 fn id(&self) -> &'static str {
12037 ENGINE_CLIP_BOUNDARY_CHECK_ID
12038 }
12039
12040 fn applicability(&self, _ctx: &crate::CheckCtx<'_>) -> Applicability {
12041 Applicability::NotApplicable
12042 }
12043
12044 fn evaluate(&self, _ctx: &crate::CheckCtx<'_>) -> CheckOutput {
12045 panic!("an inapplicable check must not be evaluated")
12046 }
12047 }
12048
12049 fn inapplicable_clip_boundary_check() -> CheckEvaluation {
12050 let document = Document::default();
12051 let grids = crate::MetricGrids::new(&document);
12052 let roles = ResolvedRoles::default();
12053 let config = crate::Config::default();
12054 let context = crate::CheckCtx::new(&grids, &roles, &config);
12055 let checks: Vec<Box<dyn crate::Check>> = vec![Box::new(InapplicableClipBoundaryCheck)];
12056 crate::evaluate_checks(&context, &checks, crate::CheckSelection::All)
12057 .unwrap()
12058 .pop()
12059 .unwrap()
12060 }
12061
12062 fn clip_boundary_check(
12063 provenance: &PredictionProvenanceV3,
12064 unavailable_last: bool,
12065 first_basis: Option<EnginePredictionBasisV2>,
12066 ) -> CheckEvaluation {
12067 let timing = provenance.raw_source().exact_source_timing();
12068 let scopes = (0..3).map(clip_boundary_scope).collect::<Vec<_>>();
12069 let mut facets = Vec::new();
12070 for (index, scope) in scopes.iter().cloned().enumerate() {
12071 let basis = if index == 0 {
12072 first_basis
12073 .clone()
12074 .unwrap_or_else(|| engine_clip_boundary_stack_basis(timing, index).unwrap())
12075 } else {
12076 engine_clip_boundary_stack_basis(timing, index).unwrap()
12077 };
12078 if unavailable_last && index == 2 {
12079 facets.push(
12080 EnginePredictionFacetV3::required_unavailable(
12081 scope,
12082 basis,
12083 engine_clip_boundary_unavailable_reasons(timing, index).unwrap(),
12084 )
12085 .unwrap(),
12086 );
12087 } else {
12088 facets.push(EnginePredictionFacetV3::available(scope, basis).unwrap());
12089 }
12090 }
12091 let prediction = EnginePredictionV3::new(provenance.identity().clone(), facets).unwrap();
12092 let finding = Finding::new(
12093 ENGINE_CLIP_BOUNDARY_CHECK_ID,
12094 Severity::Warning,
12095 "fractional exact source clip end",
12096 )
12097 .prediction_scope(scopes[1].clone());
12098 let evaluated_scopes = if unavailable_last {
12099 scopes[..2].to_vec()
12100 } else {
12101 scopes
12102 };
12103 CheckEvaluation::evaluated(
12104 ENGINE_CLIP_BOUNDARY_CHECK_ID,
12105 CheckOutput::from_coverage(vec![finding], evaluated_scopes, vec![])
12106 .with_engine_prediction_v3(prediction),
12107 )
12108 .unwrap()
12109 }
12110
12111 fn clip_boundary_lint_wire(unavailable_last: bool) -> serde_json::Value {
12112 let provenance = clip_boundary_provenance(unavailable_last);
12113 let check = clip_boundary_check(&provenance, unavailable_last, None);
12114 let file = LintFileReport::new(
12115 "test.fbx",
12116 provenance.raw_source().primary_input().clone(),
12117 prediction_test_rig(),
12118 Some(provenance),
12119 vec![check],
12120 prediction_test_measurements_v16(),
12121 )
12122 .unwrap();
12123 let envelope =
12124 LintEnvelope::new(ToolInfo::animsmith(ToolSource::new(None, None)), vec![file])
12125 .unwrap();
12126 let wire = serde_json::to_value(envelope).unwrap();
12127 serde_json::from_value::<MeasurementReportInput>(wire.clone())
12128 .unwrap()
12129 .into_files()
12130 .unwrap();
12131 wire
12132 }
12133
12134 fn assert_clip_boundary_read_error(wire: serde_json::Value, expected: PredictionContractError) {
12135 assert_eq!(
12136 lint_read_error(wire),
12137 MeasurementReportError::File {
12138 file_index: 0,
12139 source: MeasurementFileError::InvalidPrediction {
12140 check_index: 0,
12141 source: expected,
12142 },
12143 }
12144 );
12145 }
12146
12147 #[test]
12148 fn exact_source_raw_source_v2_observed_values_round_trip_and_reject_hostile_mutations() {
12149 let wire = clip_boundary_raw_wire(false);
12150 let binding: RawSourceBindingV2 = serde_json::from_value(wire.clone()).unwrap();
12151 assert_eq!(serde_json::to_value(binding).unwrap(), wire);
12152
12153 let mut invalid_value = wire.clone();
12154 invalid_value["exact_source_timing"]["frame_period"]["state"]["value"]["units_per_frame"] =
12155 serde_json::json!(0);
12156 assert_eq!(
12157 serde_json::from_value::<RawSourceBindingV2>(invalid_value)
12158 .unwrap_err()
12159 .to_string(),
12160 PredictionContractError::ExactSourceTimingValueMismatch.to_string()
12161 );
12162
12163 let mut invalid_coverage = wire.clone();
12164 invalid_coverage["exact_source_timing"]["clip_coverage"] = serde_json::json!({
12165 "state": "partial", "reason": "projection_budget_exceeded"
12166 });
12167 assert_eq!(
12168 serde_json::from_value::<RawSourceBindingV2>(invalid_coverage)
12169 .unwrap_err()
12170 .to_string(),
12171 PredictionContractError::ExactSourceTimingCoverageMismatch.to_string()
12172 );
12173
12174 let mut invalid_prefix = wire;
12175 invalid_prefix["exact_source_timing"]["clips"][1]["source_clip_index"] =
12176 serde_json::json!(2);
12177 assert_eq!(
12178 serde_json::from_value::<RawSourceBindingV2>(invalid_prefix)
12179 .unwrap_err()
12180 .to_string(),
12181 PredictionContractError::ExactSourceTimingClipPrefixMismatch.to_string()
12182 );
12183 }
12184
12185 #[test]
12186 fn clip_boundary_v3_readback_rejects_scope_basis_reason_and_finding_mutations() {
12187 let wire = clip_boundary_lint_wire(false);
12188
12189 let mut wrong_scope = wire.clone();
12190 wrong_scope["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["subject"] =
12191 serde_json::json!("source_stack:9");
12192 wrong_scope["files"][0]["checks"][0]["evaluated_scopes"][0]["subject"] =
12193 serde_json::json!("source_stack:9");
12194 assert_clip_boundary_read_error(
12195 wrong_scope,
12196 PredictionContractError::EngineClipBoundaryFacetMismatch,
12197 );
12198
12199 let mut wrong_basis = wire.clone();
12200 wrong_basis["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"] =
12201 serde_json::to_value(
12202 EnginePredictionBasisV2::new(engine_clip_boundary_common_basis().unwrap()).unwrap(),
12203 )
12204 .unwrap();
12205 assert_clip_boundary_read_error(
12206 wrong_basis,
12207 PredictionContractError::EngineClipBoundaryFacetMismatch,
12208 );
12209
12210 let mut missing_finding = wire;
12211 missing_finding["files"][0]["checks"][0]["findings"] = serde_json::json!([]);
12212 assert_clip_boundary_read_error(
12213 missing_finding,
12214 PredictionContractError::EngineClipBoundaryFindingMismatch,
12215 );
12216
12217 let mut wrong_reason = clip_boundary_lint_wire(true);
12218 wrong_reason["files"][0]["checks"][0]["prediction"]["facets"][2]["reasons"] =
12219 serde_json::json!(["animsmith:source_frame_period_unavailable"]);
12220 assert_clip_boundary_read_error(
12221 wrong_reason,
12222 PredictionContractError::EngineClipBoundaryFacetMismatch,
12223 );
12224 }
12225
12226 #[test]
12227 fn clip_boundary_v3_rederives_applicability_for_producer_and_readback() {
12228 let provenance = clip_boundary_provenance(false);
12229 assert!(matches!(
12230 lint_file(&provenance, vec![inapplicable_clip_boundary_check()]),
12231 Err(OutputContractError::InvalidPrediction(
12232 PredictionContractError::EngineClipBoundaryFacetMismatch
12233 ))
12234 ));
12235
12236 let mut wire = clip_boundary_lint_wire(false);
12237 let check = wire["files"][0]["checks"][0].as_object_mut().unwrap();
12238 check.insert(
12239 "applicability".to_owned(),
12240 serde_json::json!("not_applicable"),
12241 );
12242 check.insert("evaluation".to_owned(), serde_json::json!("not_evaluated"));
12243 check.insert("findings".to_owned(), serde_json::json!([]));
12244 check.remove("evaluated_scopes");
12245 check.remove("gaps");
12246 check.remove("prediction");
12247 assert_clip_boundary_read_error(
12248 wire,
12249 PredictionContractError::EngineClipBoundaryFacetMismatch,
12250 );
12251 }
12252
12253 #[test]
12254 fn clip_boundary_v3_applicability_uses_raw_exact_stack_inventory() {
12255 let original = clip_boundary_provenance(false);
12256 let provenance = clip_boundary_provenance_with_settings(
12257 &original,
12258 original.profile().clone(),
12259 Vec::new(),
12260 );
12261 assert_eq!(
12262 provenance
12263 .raw_source()
12264 .exact_source_timing()
12265 .unwrap()
12266 .clips()
12267 .len(),
12268 3
12269 );
12270 assert!(provenance.settings().clips().is_empty());
12271 assert!(matches!(
12272 lint_file(&provenance, vec![inapplicable_clip_boundary_check()]),
12273 Err(OutputContractError::InvalidPrediction(
12274 PredictionContractError::EngineClipBoundaryFacetMismatch
12275 ))
12276 ));
12277
12278 let mut wire = clip_boundary_lint_wire(false);
12279 wire["files"][0]["prediction_provenance"] = serde_json::to_value(provenance).unwrap();
12280 let check = wire["files"][0]["checks"][0].as_object_mut().unwrap();
12281 check.insert(
12282 "applicability".to_owned(),
12283 serde_json::json!("not_applicable"),
12284 );
12285 check.insert("evaluation".to_owned(), serde_json::json!("not_evaluated"));
12286 check.insert("findings".to_owned(), serde_json::json!([]));
12287 check.remove("evaluated_scopes");
12288 check.remove("gaps");
12289 check.remove("prediction");
12290 assert_clip_boundary_read_error(
12291 wire,
12292 PredictionContractError::EngineClipBoundaryFacetMismatch,
12293 );
12294 }
12295
12296 #[test]
12297 fn clip_boundary_v3_binds_the_frozen_unreal_profile_identity() {
12298 let original = clip_boundary_provenance(false);
12299 assert_eq!(
12300 original.profile().facts_identity().sha256(),
12301 ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_SHA256
12302 );
12303 assert_eq!(
12304 original.profile().facts_identity().bytes(),
12305 ENGINE_CLIP_BOUNDARY_PROFILE_FACTS_BYTES
12306 );
12307 let altered_profile = altered_clip_boundary_source_profile(&original);
12308 assert_ne!(
12309 altered_profile.facts_identity(),
12310 original.profile().facts_identity()
12311 );
12312 let altered = clip_boundary_provenance_with_settings(
12313 &original,
12314 altered_profile,
12315 original.settings().clips().to_vec(),
12316 );
12317 let altered_check = clip_boundary_check(&altered, false, None);
12318 assert!(matches!(
12319 lint_file(&altered, vec![altered_check.clone()]),
12320 Err(OutputContractError::InvalidPrediction(
12321 PredictionContractError::EngineClipBoundaryFacetMismatch
12322 ))
12323 ));
12324
12325 let mut wire = clip_boundary_lint_wire(false);
12326 wire["files"][0]["prediction_provenance"] = serde_json::to_value(altered).unwrap();
12327 wire["files"][0]["checks"][0] = serde_json::to_value(altered_check).unwrap();
12328 assert_clip_boundary_read_error(
12329 wire,
12330 PredictionContractError::EngineClipBoundaryFacetMismatch,
12331 );
12332 }
12333
12334 #[test]
12335 fn clip_boundary_v3_producer_rejects_incomplete_exact_basis() {
12336 let provenance = clip_boundary_provenance(false);
12337 let incomplete_basis =
12338 EnginePredictionBasisV2::new(engine_clip_boundary_common_basis().unwrap()).unwrap();
12339 let check = clip_boundary_check(&provenance, false, Some(incomplete_basis));
12340 assert!(matches!(
12341 lint_file(&provenance, vec![check]),
12342 Err(OutputContractError::InvalidPrediction(
12343 PredictionContractError::EngineClipBoundaryFacetMismatch
12344 ))
12345 ));
12346 }
12347
12348 fn prediction_with_retained_text(
12349 provenance: &PredictionProvenanceV3,
12350 retained_text: usize,
12351 ) -> EnginePredictionV3 {
12352 const FIELD_ID_BYTES: usize = 16;
12353 const MAX_VALUE_BYTES: usize = crate::PREDICTION_V1_MAX_TEXT_BYTES;
12354 let fixed = "test:prediction-limit".len()
12355 + PredictionUnavailableReasonV2::ProjectIntentUnavailable
12356 .as_str()
12357 .len();
12358 let remaining = retained_text.checked_sub(fixed).unwrap();
12359 let full_row = FIELD_ID_BYTES + MAX_VALUE_BYTES;
12360 let full_rows = remaining / full_row;
12361 let remainder = remaining % full_row;
12362 let (full_rows, tail_lengths) = if remainder == 0 {
12363 (full_rows, Vec::new())
12364 } else if remainder >= FIELD_ID_BYTES {
12365 (full_rows, vec![remainder - FIELD_ID_BYTES])
12366 } else {
12367 (
12368 full_rows - 1,
12369 vec![0, MAX_VALUE_BYTES - FIELD_ID_BYTES + remainder],
12370 )
12371 };
12372 let mut references = Vec::with_capacity(full_rows + tail_lengths.len());
12373 for index in 0..full_rows {
12374 references.push(
12375 PredictionBasisReferenceV1::project_field(
12376 format!("f{index:015}"),
12377 PredictionScalarV1::text("x".repeat(MAX_VALUE_BYTES)).unwrap(),
12378 )
12379 .unwrap(),
12380 );
12381 }
12382 for length in tail_lengths {
12383 let index = references.len();
12384 references.push(
12385 PredictionBasisReferenceV1::project_field(
12386 format!("f{index:015}"),
12387 PredictionScalarV1::text("x".repeat(length)).unwrap(),
12388 )
12389 .unwrap(),
12390 );
12391 }
12392 let basis = EnginePredictionBasisV1::new(references).unwrap();
12393 let facet = EnginePredictionFacetV3::required_unavailable(
12394 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction-limit")),
12395 basis_v2(basis),
12396 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
12397 )
12398 .unwrap();
12399 let prediction =
12400 EnginePredictionV3::new(provenance.identity().clone(), vec![facet]).unwrap();
12401 assert_eq!(prediction.retained_text_bytes().unwrap(), retained_text);
12402 prediction
12403 }
12404
12405 #[test]
12406 fn report_reader_enforces_the_byte_cap_before_json_parsing() {
12407 let bytes = br#"{"schema_version":10,"tool":{}}"#;
12408 let report =
12409 MeasurementReportInput::read_from_with_limit(bytes.as_slice(), bytes.len() as u64)
12410 .expect("exact N must parse");
12411 assert_eq!(report.schema_version, Some(10));
12412
12413 assert!(matches!(
12414 MeasurementReportInput::read_from_with_limit(
12415 bytes.as_slice(),
12416 bytes.len() as u64 - 1,
12417 ),
12418 Err(MeasurementReportReadError::ReportTooLarge { limit })
12419 if limit == bytes.len() as u64 - 1
12420 ));
12421 }
12422
12423 #[test]
12424 fn prediction_facet_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12425 let provenance = prediction_test_provenance();
12426 let empty_basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12427 let facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE)
12428 .map(|index| unavailable_facet(format!("facet-{index:04}"), empty_basis.clone()))
12429 .collect();
12430 let at_limit = unavailable_check("test:facet-limit", &provenance, facets);
12431 let mut wire = validated_lint_wire(&provenance, vec![at_limit.clone()]);
12432 let extra = unavailable_check(
12433 "test:facet-extra",
12434 &provenance,
12435 vec![unavailable_facet("facet-extra".into(), empty_basis)],
12436 );
12437
12438 assert_eq!(
12439 lint_file(&provenance, vec![at_limit, extra.clone()]).unwrap_err(),
12440 OutputContractError::TooManyPredictionFacets {
12441 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
12442 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12443 }
12444 );
12445
12446 wire["files"][0]["checks"]
12447 .as_array_mut()
12448 .unwrap()
12449 .push(serde_json::to_value(extra).unwrap());
12450 assert_eq!(
12451 lint_read_error(wire),
12452 MeasurementReportError::File {
12453 file_index: 0,
12454 source: MeasurementFileError::TooManyPredictionFacets {
12455 found: PREDICTION_V1_MAX_FACETS_PER_FILE + 1,
12456 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12457 },
12458 }
12459 );
12460 }
12461
12462 #[test]
12463 fn v2_budget_summary_is_canonical_and_requires_an_exhausted_file_budget() {
12464 let provenance = prediction_test_provenance();
12465 let basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12466 let mut facets = (0..PREDICTION_V1_MAX_FACETS_PER_FILE - 1)
12467 .map(|index| unavailable_facet(format!("facet-{index:04}"), basis.clone()))
12468 .collect::<Vec<_>>();
12469 facets.push(
12470 EnginePredictionFacetV3::required_unavailable(
12471 EvaluationScope::new(EvaluationScopeCode::custom("test:budget:facet-budget")),
12472 basis_v2(basis),
12473 vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
12474 )
12475 .unwrap(),
12476 );
12477 let check = unavailable_check("test:budget", &provenance, facets);
12478 let wire = validated_lint_wire(&provenance, vec![check]);
12479 let facets = wire["files"][0]["checks"][0]["prediction"]["facets"]
12480 .as_array()
12481 .unwrap();
12482 let summary_index = facets
12483 .iter()
12484 .position(|facet| facet["reasons"] == serde_json::json!(["facet_budget_exceeded"]))
12485 .unwrap();
12486
12487 let mut wrong_scope = wire.clone();
12488 wrong_scope["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["scope"]["code"] =
12489 serde_json::json!("test:wrong:facet-budget");
12490 assert!(matches!(
12491 lint_read_error(wrong_scope),
12492 MeasurementReportError::File {
12493 source: MeasurementFileError::InvalidPrediction {
12494 source: PredictionContractError::InvalidFacetBudgetSummary,
12495 ..
12496 },
12497 ..
12498 }
12499 ));
12500
12501 let mut subject = wire.clone();
12502 subject["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["scope"]["subject"] =
12503 serde_json::json!("forged");
12504 assert!(matches!(
12505 lint_read_error(subject),
12506 MeasurementReportError::File {
12507 source: MeasurementFileError::InvalidPrediction {
12508 source: PredictionContractError::InvalidFacetBudgetSummary,
12509 ..
12510 },
12511 ..
12512 }
12513 ));
12514
12515 let mut available = wire.clone();
12516 available["files"][0]["checks"][0]["prediction"]["facets"][summary_index]["state"] =
12517 serde_json::json!("available");
12518 let available_error = lint_read_error(available);
12519 assert!(
12520 matches!(
12521 available_error,
12522 MeasurementReportError::File {
12523 source: MeasurementFileError::InvalidPrediction {
12524 source: PredictionContractError::AvailableBasisEmpty,
12525 ..
12526 },
12527 ..
12528 }
12529 ),
12530 "unexpected available mutation: {available_error:?}"
12531 );
12532
12533 let mut duplicate = wire.clone();
12534 let duplicate_summary =
12535 duplicate["files"][0]["checks"][0]["prediction"]["facets"][summary_index].clone();
12536 duplicate["files"][0]["checks"][0]["prediction"]["facets"]
12537 [if summary_index == 0 { 1 } else { 0 }] = duplicate_summary;
12538 assert!(matches!(
12539 lint_read_error(duplicate),
12540 MeasurementReportError::File {
12541 source: MeasurementFileError::InvalidPrediction {
12542 source: PredictionContractError::DuplicateFacetScope,
12543 ..
12544 },
12545 ..
12546 }
12547 ));
12548
12549 let mut under_full = wire;
12550 under_full["files"][0]["checks"][0]["prediction"]["facets"]
12551 .as_array_mut()
12552 .unwrap()
12553 .remove(if summary_index == 0 { 1 } else { 0 });
12554 under_full["summary"]["prediction_facets"]["required_prediction_unavailable"] =
12555 serde_json::json!(PREDICTION_V1_MAX_FACETS_PER_FILE - 1);
12556 assert_eq!(
12557 lint_read_error(under_full),
12558 MeasurementReportError::File {
12559 file_index: 0,
12560 source: MeasurementFileError::FacetBudgetSummaryWithoutExhaustedFileBudget {
12561 found: PREDICTION_V1_MAX_FACETS_PER_FILE - 1,
12562 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
12563 },
12564 }
12565 );
12566 }
12567
12568 #[test]
12569 fn partial_engine_inventory_can_be_replaced_by_its_budget_summary() {
12570 let provenance = partial_engine_provenance();
12571 let summary = EnginePredictionFacetV3::required_unavailable(
12572 EvaluationScope::new(EvaluationScopeCode::custom(
12573 "engine-addressability:facet-budget",
12574 )),
12575 basis_v2(EnginePredictionBasisV1::new(Vec::new()).unwrap()),
12576 vec![PredictionUnavailableReasonV2::FacetBudgetExceeded],
12577 )
12578 .unwrap();
12579 let engine = unavailable_check("engine-addressability", &provenance, vec![summary]);
12580 let filler_basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12581 let filler = unavailable_check(
12582 "test:filler",
12583 &provenance,
12584 (0..PREDICTION_V1_MAX_FACETS_PER_FILE - 1)
12585 .map(|index| unavailable_facet(format!("filler-{index:04}"), filler_basis.clone()))
12586 .collect(),
12587 );
12588 let wire = validated_lint_wire(&provenance, vec![engine, filler]);
12591 assert_eq!(
12592 wire["summary"]["prediction_facets"]["required_prediction_unavailable"],
12593 serde_json::json!(PREDICTION_V1_MAX_FACETS_PER_FILE)
12594 );
12595 }
12596
12597 #[test]
12598 fn engine_addressability_inventory_reasons_follow_raw_and_settings_coverage() {
12599 let partial = partial_engine_provenance();
12600 let basis = EnginePredictionBasisV1::new(Vec::new()).unwrap();
12601 let inventory = EnginePredictionFacetV3::required_unavailable(
12602 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
12603 basis_v2(basis.clone()),
12604 vec![
12605 PredictionUnavailableReasonV2::RawSourceIncomplete,
12606 PredictionUnavailableReasonV2::ResolvedSettingsOverflow,
12607 ],
12608 )
12609 .unwrap();
12610 let check = unavailable_check("engine-addressability", &partial, vec![inventory]);
12611 let mut wire = validated_lint_wire(&partial, vec![check]);
12612 wire["files"][0]["checks"][0]["prediction"]["facets"][0]["reasons"] =
12613 serde_json::json!(["raw_source_incomplete"]);
12614 assert!(matches!(
12615 lint_read_error(wire),
12616 MeasurementReportError::File {
12617 source: MeasurementFileError::InvalidPrediction {
12618 source: PredictionContractError::EngineAddressabilityInventoryReasonsMismatch,
12619 ..
12620 },
12621 ..
12622 }
12623 ));
12624
12625 let complete = prediction_test_provenance();
12626 let forged = EnginePredictionFacetV3::required_unavailable(
12627 EvaluationScope::new(EvaluationScopeCode::ANIMATION_ASSET_LABEL_INVENTORY),
12628 basis_v2(basis),
12629 vec![PredictionUnavailableReasonV2::ResolvedSettingsOverflow],
12630 )
12631 .unwrap();
12632 assert!(matches!(
12633 lint_file(
12634 &complete,
12635 vec![unavailable_check(
12636 "engine-addressability",
12637 &complete,
12638 vec![forged]
12639 )],
12640 ),
12641 Err(OutputContractError::InvalidPrediction(
12642 PredictionContractError::EngineAddressabilityInventoryReasonsMismatch
12643 ))
12644 ));
12645 }
12646
12647 #[test]
12648 fn engine_addressability_rejects_a_non_addressability_available_facet_prefix() {
12649 let mut wire = clip_boundary_lint_wire(false);
12650 let check = &mut wire["files"][0]["checks"][0];
12651 check["check_id"] = serde_json::json!("engine-addressability");
12652 for (index, facet) in check["prediction"]["facets"]
12653 .as_array_mut()
12654 .unwrap()
12655 .iter_mut()
12656 .enumerate()
12657 {
12658 facet["scope"]["code"] = serde_json::json!("animation_asset_label");
12659 facet["scope"]["subject"] = serde_json::json!(format!("Animation{index}"));
12660 }
12661 for (index, scope) in check["evaluated_scopes"]
12662 .as_array_mut()
12663 .unwrap()
12664 .iter_mut()
12665 .enumerate()
12666 {
12667 scope["code"] = serde_json::json!("animation_asset_label");
12668 scope["subject"] = serde_json::json!(format!("Animation{index}"));
12669 }
12670 check["findings"][0]["check_id"] = serde_json::json!("engine-addressability");
12671 check["findings"][0]["prediction_scope"]["code"] =
12672 serde_json::json!("animation_asset_label");
12673 check["findings"][0]["prediction_scope"]["subject"] = serde_json::json!("Animation1");
12674
12675 assert!(matches!(
12676 lint_read_error(wire),
12677 MeasurementReportError::File {
12678 source: MeasurementFileError::InvalidPrediction {
12679 source: PredictionContractError::EngineAddressabilityFacetPrefixMismatch,
12680 ..
12681 },
12682 ..
12683 }
12684 ));
12685 }
12686
12687 #[test]
12688 fn prediction_basis_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12689 let provenance = prediction_test_provenance();
12690 let basis = EnginePredictionBasisV1::new(
12691 (0..crate::PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET)
12692 .map(|index| {
12693 PredictionBasisReferenceV1::project_field(
12694 format!("project.field.{index:04}"),
12695 PredictionScalarV1::Null,
12696 )
12697 .unwrap()
12698 })
12699 .collect(),
12700 )
12701 .unwrap();
12702 let facet_count = PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE
12703 / crate::PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FACET;
12704 let facets = (0..facet_count)
12705 .map(|index| unavailable_facet(format!("basis-{index:02}"), basis.clone()))
12706 .collect();
12707 let at_limit = unavailable_check("test:basis-limit", &provenance, facets);
12708 let mut wire = validated_lint_wire(&provenance, vec![at_limit.clone()]);
12709 let extra_basis = EnginePredictionBasisV1::new(vec![
12710 PredictionBasisReferenceV1::project_field("project.extra", PredictionScalarV1::Null)
12711 .unwrap(),
12712 ])
12713 .unwrap();
12714 let extra = unavailable_check(
12715 "test:basis-extra",
12716 &provenance,
12717 vec![unavailable_facet("basis-extra".into(), extra_basis)],
12718 );
12719
12720 assert_eq!(
12721 lint_file(&provenance, vec![at_limit, extra.clone()]).unwrap_err(),
12722 OutputContractError::TooManyPredictionBasisReferences {
12723 found: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE + 1,
12724 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
12725 }
12726 );
12727
12728 wire["files"][0]["checks"]
12729 .as_array_mut()
12730 .unwrap()
12731 .push(serde_json::to_value(extra).unwrap());
12732 *wire["files"][0]["checks"]
12733 .as_array_mut()
12734 .unwrap()
12735 .last_mut()
12736 .unwrap()
12737 .get_mut("prediction")
12738 .unwrap()
12739 .get_mut("facets")
12740 .and_then(serde_json::Value::as_array_mut)
12741 .and_then(|facets| facets.first_mut())
12742 .and_then(|facet| facet.get_mut("basis"))
12743 .and_then(|basis| basis.get_mut("references"))
12744 .and_then(serde_json::Value::as_array_mut)
12745 .and_then(|references| references.first_mut())
12746 .unwrap() = serde_json::Value::Null;
12747 assert!(matches!(
12748 lint_read_error(wire),
12749 MeasurementReportError::File {
12750 file_index: 0,
12751 source: MeasurementFileError::TooManyPredictionBasisReferences { .. },
12752 }
12753 ));
12754 }
12755
12756 #[test]
12757 fn prediction_text_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
12758 let provenance = prediction_test_provenance();
12759 let provenance_text = provenance.retained_text_bytes().unwrap();
12760 let at_limit_prediction = prediction_with_retained_text(
12761 &provenance,
12762 PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE - provenance_text,
12763 );
12764 let at_limit = CheckEvaluation::evaluated(
12765 "test:text-limit",
12766 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
12767 .with_engine_prediction_v3(at_limit_prediction),
12768 )
12769 .unwrap();
12770 let mut wire = validated_lint_wire(&provenance, vec![at_limit]);
12771
12772 let above_limit_prediction = prediction_with_retained_text(
12773 &provenance,
12774 PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1 - provenance_text,
12775 );
12776 let above_limit = CheckEvaluation::evaluated(
12777 "test:text-limit",
12778 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new())
12779 .with_engine_prediction_v3(above_limit_prediction),
12780 )
12781 .unwrap();
12782 let above_limit_wire = serde_json::to_value(&above_limit).unwrap();
12783 assert_eq!(
12784 lint_file(&provenance, vec![above_limit]).unwrap_err(),
12785 OutputContractError::TooMuchPredictionText {
12786 found: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1,
12787 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
12788 }
12789 );
12790
12791 wire["files"][0]["checks"][0] = above_limit_wire;
12792 assert_eq!(
12793 lint_read_error(wire),
12794 MeasurementReportError::File {
12795 file_index: 0,
12796 source: MeasurementFileError::TooMuchPredictionText {
12797 found: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE + 1,
12798 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
12799 },
12800 }
12801 );
12802 }
12803
12804 fn reader_error(wire: serde_json::Value) -> MeasurementReportError {
12805 serde_json::from_value::<MeasurementReportInput>(wire)
12806 .expect("outer v11 shape remains valid")
12807 .into_files()
12808 .expect_err("mutated report must fail")
12809 }
12810
12811 fn empty_check(check_id: &'static str) -> CheckEvaluation {
12812 CheckEvaluation::evaluated(
12813 check_id,
12814 CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new()),
12815 )
12816 .unwrap()
12817 }
12818
12819 #[test]
12820 fn staged_reader_rejects_unknown_root_file_and_check_fields() {
12821 let provenance = prediction_test_provenance();
12822 let wire = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12823
12824 let mut root = wire.clone();
12825 root["unknown_root"] = serde_json::json!(true);
12826 let bytes = serde_json::to_vec(&root).unwrap();
12827 assert_eq!(
12828 MeasurementReportInput::read_from(bytes.as_slice())
12829 .expect("unknown root fields are retained through the staged read")
12830 .into_files()
12831 .unwrap_err(),
12832 MeasurementReportError::UnknownOutputField {
12833 field: "unknown_root".into(),
12834 }
12835 );
12836
12837 let mut missing_tool = wire.clone();
12838 missing_tool.as_object_mut().unwrap().remove("tool");
12839 assert_eq!(
12840 reader_error(missing_tool),
12841 MeasurementReportError::MissingTool
12842 );
12843
12844 let bare = br#"{"walk":true}"#;
12845 assert_eq!(
12846 MeasurementReportInput::read_from(bare.as_slice())
12847 .expect("unknown root fields remain staged until header validation")
12848 .into_files()
12849 .unwrap_err(),
12850 MeasurementReportError::MissingOutputVersion,
12851 );
12852
12853 let unsupported = br#"{"schema_version":9,"walk":true}"#;
12854 assert_eq!(
12855 MeasurementReportInput::read_from(unsupported.as_slice())
12856 .expect("unknown root fields remain staged until header validation")
12857 .into_files()
12858 .unwrap_err(),
12859 MeasurementReportError::UnsupportedOutputVersion { found: 9 },
12860 );
12861
12862 let mut file = wire.clone();
12863 file["files"][0]["unknown_file"] = serde_json::json!(true);
12864 assert!(matches!(
12865 reader_error(file),
12866 MeasurementReportError::File {
12867 file_index: 0,
12868 source: MeasurementFileError::InvalidFileShape { reason },
12869 } if reason.contains("unknown field `unknown_file`")
12870 ));
12871
12872 let mut check = wire;
12873 check["files"][0]["checks"][0]["unknown_check"] = serde_json::json!(true);
12874 assert!(matches!(
12875 reader_error(check),
12876 MeasurementReportError::File {
12877 file_index: 0,
12878 source: MeasurementFileError::InvalidPredictionShape {
12879 check_index: 0,
12880 reason,
12881 },
12882 } if reason.contains("unknown field `unknown_check`")
12883 ));
12884
12885 let provenance = prediction_test_provenance();
12886 let mut summary = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12887 summary["summary"]["prediction_facets"]["unknown_prediction_total"] = serde_json::json!(0);
12888 let bytes = serde_json::to_vec(&summary).unwrap();
12889 assert!(matches!(
12890 MeasurementReportInput::read_from(bytes.as_slice()).unwrap_err(),
12891 MeasurementReportReadError::InvalidJson { source }
12892 if source.to_string().contains("unknown field `unknown_prediction_total`")
12893 ));
12894
12895 let provenance = prediction_test_provenance();
12896 let mut summary = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12897 summary["summary"]["unknown_summary"] = serde_json::json!(0);
12898 let bytes = serde_json::to_vec(&summary).unwrap();
12899 assert!(matches!(
12900 MeasurementReportInput::read_from(bytes.as_slice()).unwrap_err(),
12901 MeasurementReportReadError::InvalidJson { source }
12902 if source.to_string().contains("unknown field `unknown_summary`")
12903 ));
12904 }
12905
12906 #[test]
12907 fn staged_reader_preserves_typed_prediction_semantic_errors() {
12908 let provenance = prediction_test_provenance();
12909 let mut provenance_wire = validated_lint_wire(&provenance, Vec::new());
12910 provenance_wire["files"][0]["prediction_provenance"]["schema"] =
12911 serde_json::json!("urn:changed");
12912 assert!(matches!(
12913 reader_error(provenance_wire),
12914 MeasurementReportError::File {
12915 file_index: 0,
12916 source: MeasurementFileError::InvalidPredictionProvenance { .. },
12917 }
12918 ));
12919
12920 let basis = EnginePredictionBasisV1::new(vec![
12921 PredictionBasisReferenceV1::project_field(
12922 "test:project",
12923 PredictionScalarV1::Boolean { value: true },
12924 )
12925 .unwrap(),
12926 ])
12927 .unwrap();
12928 let facet = EnginePredictionFacetV3::required_unavailable(
12929 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction")),
12930 basis_v2(basis),
12931 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
12932 )
12933 .unwrap();
12934 let prediction_wire = validated_lint_wire(
12935 &provenance,
12936 vec![unavailable_check("test:reader", &provenance, vec![facet])],
12937 );
12938
12939 let mut wrong_emitter = prediction_wire.clone();
12940 wrong_emitter["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["code"] =
12941 serde_json::json!("member_existence");
12942 assert!(matches!(
12943 reader_error(wrong_emitter),
12944 MeasurementReportError::File {
12945 file_index: 0,
12946 source: MeasurementFileError::InvalidPredictionLifecycle {
12947 check_index: 0,
12948 reason: "prediction facet scope code is invalid for its parent check",
12949 },
12950 }
12951 ));
12952
12953 let mut empty_scope = prediction_wire.clone();
12954 empty_scope["files"][0]["checks"][0]["prediction"]["facets"][0]["scope"]["code"] =
12955 serde_json::json!("");
12956 assert!(matches!(
12957 reader_error(empty_scope),
12958 MeasurementReportError::File {
12959 file_index: 0,
12960 source: MeasurementFileError::InvalidPrediction { check_index: 0, .. },
12961 }
12962 ));
12963
12964 let mut prediction_wire = prediction_wire;
12965 prediction_wire["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"]["identity"]
12966 ["bytes"] = serde_json::json!(0);
12967 assert!(matches!(
12968 reader_error(prediction_wire),
12969 MeasurementReportError::File {
12970 file_index: 0,
12971 source: MeasurementFileError::InvalidPrediction {
12972 check_index: 0,
12973 source: PredictionContractError::IdentityMismatch {
12974 contract: "engine prediction basis v2",
12975 },
12976 },
12977 }
12978 ));
12979 }
12980
12981 #[test]
12982 fn staged_reader_uses_the_authoritative_check_lifecycle_without_prediction() {
12983 let provenance = prediction_test_provenance();
12984 let base = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
12985
12986 for (field, state) in [
12987 ("selection", "unselected"),
12988 ("configuration", "disabled"),
12989 ("applicability", "not_applicable"),
12990 ] {
12991 let mut inactive = base.clone();
12992 inactive["files"][0]["checks"][0][field] = serde_json::json!(state);
12993 assert!(matches!(
12994 reader_error(inactive),
12995 MeasurementReportError::File {
12996 file_index: 0,
12997 source: MeasurementFileError::InvalidPredictionLifecycle {
12998 check_index: 0,
12999 reason: "evaluation does not match completed and missing prediction work",
13000 },
13001 }
13002 ));
13003 }
13004
13005 let mut inactive = base.clone();
13006 inactive["files"][0]["checks"][0]["selection"] = serde_json::json!("unselected");
13007 inactive["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
13008 serde_json::from_value::<MeasurementReportInput>(inactive)
13009 .unwrap()
13010 .into_files()
13011 .expect("empty inactive record is valid");
13012
13013 let mut not_evaluated = base.clone();
13014 not_evaluated["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
13015 "code": "test:missing",
13016 "message": "missing",
13017 }]);
13018 not_evaluated["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
13019 serde_json::from_value::<MeasurementReportInput>(not_evaluated.clone())
13020 .unwrap()
13021 .into_files()
13022 .expect("missing-only active record derives not_evaluated");
13023 not_evaluated["files"][0]["checks"][0]["evaluation"] = serde_json::json!("complete");
13024 assert!(matches!(
13025 reader_error(not_evaluated),
13026 MeasurementReportError::File {
13027 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13028 ..
13029 }
13030 ));
13031
13032 let mut partial = base.clone();
13033 partial["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
13034 "code": "test:missing",
13035 "message": "missing",
13036 }]);
13037 partial["files"][0]["checks"][0]["evaluated_scopes"] =
13038 serde_json::json!([{ "code": "test:completed" }]);
13039 partial["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
13040 serde_json::from_value::<MeasurementReportInput>(partial.clone())
13041 .unwrap()
13042 .into_files()
13043 .expect("mixed active record derives partial");
13044 partial["files"][0]["checks"][0]["evaluation"] = serde_json::json!("complete");
13045 assert!(matches!(
13046 reader_error(partial),
13047 MeasurementReportError::File {
13048 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13049 ..
13050 }
13051 ));
13052
13053 let mut wrong_complete = base;
13054 wrong_complete["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
13055 assert!(matches!(
13056 reader_error(wrong_complete),
13057 MeasurementReportError::File {
13058 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13059 ..
13060 }
13061 ));
13062 }
13063
13064 #[test]
13065 fn staged_reader_rejects_invalid_scope_gap_and_finding_shapes() {
13066 let provenance = prediction_test_provenance();
13067 let base = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
13068
13069 let mut empty_scope = base.clone();
13070 empty_scope["files"][0]["checks"][0]["evaluated_scopes"] =
13071 serde_json::json!([{ "code": "" }]);
13072 assert!(matches!(
13073 reader_error(empty_scope),
13074 MeasurementReportError::File {
13075 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13076 ..
13077 }
13078 ));
13079
13080 let mut malformed_gap = base.clone();
13081 malformed_gap["files"][0]["checks"][0]["gaps"] = serde_json::json!([{
13082 "code": "",
13083 "message": "missing",
13084 }]);
13085 malformed_gap["files"][0]["checks"][0]["evaluation"] = serde_json::json!("not_evaluated");
13086 assert!(matches!(
13087 reader_error(malformed_gap),
13088 MeasurementReportError::File {
13089 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13090 ..
13091 }
13092 ));
13093
13094 let mut incomplete_finding = base;
13095 incomplete_finding["files"][0]["checks"][0]["findings"] = serde_json::json!([{
13096 "check_id": "test:reader",
13097 }]);
13098 assert!(matches!(
13099 reader_error(incomplete_finding),
13100 MeasurementReportError::File {
13101 source: MeasurementFileError::InvalidPredictionShape { check_index: 0, .. },
13102 ..
13103 }
13104 ));
13105 }
13106
13107 #[test]
13108 fn staged_reader_stops_at_the_first_files_lifecycle_failure() {
13109 let provenance = prediction_test_provenance();
13110 let mut wire = validated_lint_wire(&provenance, vec![empty_check("test:reader")]);
13111 let mut later_file = wire["files"][0].clone();
13112 later_file["prediction_provenance"]["schema"] = serde_json::json!("urn:changed");
13113 wire["files"].as_array_mut().unwrap().push(later_file);
13114 wire["summary"]["files"] = serde_json::json!(2);
13115 wire["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
13116
13117 assert!(matches!(
13118 reader_error(wire),
13119 MeasurementReportError::File {
13120 file_index: 0,
13121 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13122 }
13123 ));
13124 }
13125
13126 #[test]
13127 fn staged_reader_stops_at_the_first_checks_lifecycle_failure() {
13128 let provenance = prediction_test_provenance();
13129 let basis = EnginePredictionBasisV1::new(vec![
13130 PredictionBasisReferenceV1::project_field(
13131 "test:project",
13132 PredictionScalarV1::Boolean { value: true },
13133 )
13134 .unwrap(),
13135 ])
13136 .unwrap();
13137 let facet = EnginePredictionFacetV3::required_unavailable(
13138 EvaluationScope::new(EvaluationScopeCode::custom("test:prediction")),
13139 basis_v2(basis),
13140 vec![PredictionUnavailableReasonV2::ProjectIntentUnavailable],
13141 )
13142 .unwrap();
13143 let mut wire = validated_lint_wire(
13144 &provenance,
13145 vec![
13146 empty_check("test:first"),
13147 unavailable_check("test:second", &provenance, vec![facet]),
13148 ],
13149 );
13150 wire["files"][0]["checks"][0]["evaluation"] = serde_json::json!("partial");
13151 wire["files"][0]["checks"][1]["unknown"] = serde_json::json!(true);
13152
13153 assert!(matches!(
13154 reader_error(wire),
13155 MeasurementReportError::File {
13156 file_index: 0,
13157 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
13158 }
13159 ));
13160 }
13161
13162 #[test]
13163 fn v11_nested_version_is_rejected_before_current_shape_decode() {
13164 let report: MeasurementReportInput = serde_json::from_value(serde_json::json!({
13165 "schema_version": OUTPUT_SCHEMA_VERSION,
13166 "schema": OUTPUT_SCHEMA_ID,
13167 "tool": {},
13168 "command": "measure",
13169 "files": [{
13170 "path": "measurements-v11.json",
13171 "input": { "sha256": "0".repeat(64), "bytes": 0 },
13172 "rig": {},
13173 "measurements": {
13174 "schema_version": 11,
13175 "schema": "urn:animsmith:schema:measurements:11",
13176 "skeleton_nodes": [{
13177 "node_index": 0,
13178 "scene_root_indices": [],
13179 "local_rest": {
13180 "kind": "trs",
13181 "translation_m": [0.0, 0.0, 0.0],
13182 "rotation_xyzw": [0.0, 0.0, 0.0, 1.0],
13183 "scale": [1.0, 1.0, 1.0]
13184 },
13185 "rest_world_matrix": [
13186 1.0, 0.0, 0.0, 0.0,
13187 0.0, 1.0, 0.0, 0.0,
13188 0.0, 0.0, 1.0, 0.0,
13189 0.0, 0.0, 0.0, 1.0
13190 ]
13191 }],
13192 "skins": [{ "skin_index": 0 }]
13193 }
13194 }]
13195 }))
13196 .expect("unsupported payload shapes remain decodable for version rejection");
13197
13198 assert!(matches!(
13199 report.into_files(),
13200 Err(MeasurementReportError::File {
13201 file_index: 0,
13202 source: MeasurementFileError::UnsupportedMeasurementVersion {
13203 found: 11,
13204 expected: MEASUREMENTS_SCHEMA_VERSION,
13205 },
13206 })
13207 ));
13208 }
13209
13210 #[test]
13211 fn current_v18_primitive_mutations_fail_closed_on_readback() {
13212 let wire = measure_wire(primitive_measurement_contract());
13213 let primitive = &wire["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0];
13214 assert_eq!(primitive["primitive_index"], serde_json::json!(1));
13215 assert_eq!(primitive["material_index"], serde_json::json!(7));
13216 assert_eq!(
13217 wire["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["material_index"],
13218 serde_json::Value::Null
13219 );
13220
13221 let mut missing_material = wire.clone();
13222 missing_material["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]
13223 .as_object_mut()
13224 .unwrap()
13225 .remove("material_index");
13226 assert!(matches!(
13227 reader_error(missing_material),
13228 MeasurementReportError::File {
13229 source: MeasurementFileError::InvalidMeasurementsShape { .. },
13230 ..
13231 }
13232 ));
13233
13234 let mut mutations = Vec::new();
13235 let mut missing = wire.clone();
13236 missing["files"][0]["measurements"]["mesh_definitions"][0]
13237 .as_object_mut()
13238 .unwrap()
13239 .remove("primitives");
13240 mutations.push(missing);
13241
13242 let mut duplicate_index = wire.clone();
13243 duplicate_index["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["primitive_index"] =
13244 serde_json::json!(1);
13245 mutations.push(duplicate_index);
13246
13247 let mut decreasing_index = wire.clone();
13248 decreasing_index["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][1]["primitive_index"] =
13249 serde_json::json!(0);
13250 mutations.push(decreasing_index);
13251
13252 let mut finite_over = wire.clone();
13253 finite_over["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["finite_vertex_count"] =
13254 serde_json::json!(3);
13255 mutations.push(finite_over);
13256
13257 let mut zero_with_facts = wire.clone();
13258 zero_with_facts["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["finite_vertex_count"] =
13259 serde_json::json!(0);
13260 mutations.push(zero_with_facts);
13261
13262 let mut positive_without_centroid = wire.clone();
13263 positive_without_centroid["files"][0]["measurements"]["mesh_definitions"][0]["primitives"]
13264 [0]
13265 .as_object_mut()
13266 .unwrap()
13267 .remove("geometry_centroid");
13268 mutations.push(positive_without_centroid);
13269
13270 let mut centroid_outside_aabb = wire.clone();
13271 centroid_outside_aabb["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
13272 ["geometry_centroid"] = serde_json::json!([-3.0, 1.0, 0.0]);
13273 mutations.push(centroid_outside_aabb);
13274
13275 let mut missing_mesh_geometry = wire.clone();
13276 missing_mesh_geometry["files"][0]["measurements"]["mesh_definitions"][0]
13277 .as_object_mut()
13278 .unwrap()
13279 .remove("geometry_centroid");
13280 mutations.push(missing_mesh_geometry);
13281
13282 let mut wrong_mesh_aabb = wire.clone();
13283 wrong_mesh_aabb["files"][0]["measurements"]["mesh_definitions"][0]["geometry_aabb"]["max"]
13284 [0] = serde_json::json!(7.0);
13285 mutations.push(wrong_mesh_aabb);
13286
13287 let mut wrong_mesh_centroid = wire.clone();
13288 wrong_mesh_centroid["files"][0]["measurements"]["mesh_definitions"][0]["geometry_centroid"] =
13289 serde_json::json!([3.0, 7.0 / 3.0, 0.0]);
13290 mutations.push(wrong_mesh_centroid);
13291
13292 let mut wrong_sum = wire.clone();
13293 wrong_sum["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13294 serde_json::json!(3);
13295 mutations.push(wrong_sum);
13296
13297 let mut finite_sum_overflow = wire.clone();
13298 for primitive in
13299 finite_sum_overflow["files"][0]["measurements"]["mesh_definitions"][0]["primitives"]
13300 .as_array_mut()
13301 .unwrap()
13302 {
13303 primitive["vertex_count"] = serde_json::json!(u64::MAX);
13304 primitive["finite_vertex_count"] = serde_json::json!(u64::MAX);
13305 }
13306 finite_sum_overflow["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13307 serde_json::json!(u64::MAX);
13308 mutations.push(finite_sum_overflow);
13309
13310 for mutation in mutations {
13311 assert!(matches!(
13312 reader_error(mutation),
13313 MeasurementReportError::File {
13314 source: MeasurementFileError::InvalidMeasurements { .. },
13315 ..
13316 }
13317 ));
13318 }
13319
13320 let mut unknown_root = wire.clone();
13321 unknown_root["files"][0]["measurements"]
13322 .as_object_mut()
13323 .unwrap()
13324 .insert("bogus".into(), serde_json::json!(true));
13325 let mut unknown_mesh = wire.clone();
13326 unknown_mesh["files"][0]["measurements"]["mesh_definitions"][0]
13327 .as_object_mut()
13328 .unwrap()
13329 .insert("bogus".into(), serde_json::json!(true));
13330 let mut unknown_primitive = wire.clone();
13331 unknown_primitive["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
13332 .as_object_mut()
13333 .unwrap()
13334 .insert("bogus".into(), serde_json::json!(true));
13335 let mut unknown_aabb = wire;
13336 unknown_aabb["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]
13337 ["geometry_aabb"]
13338 .as_object_mut()
13339 .unwrap()
13340 .insert("bogus".into(), serde_json::json!(true));
13341 for mutation in [unknown_root, unknown_mesh, unknown_primitive, unknown_aabb] {
13342 assert!(matches!(
13343 reader_error(mutation),
13344 MeasurementReportError::File {
13345 source: MeasurementFileError::InvalidMeasurementsShape { reason },
13346 ..
13347 } if reason.contains("unknown field `bogus`")
13348 ));
13349 }
13350 }
13351
13352 #[test]
13353 fn current_v18_material_indices_follow_resource_coverage() {
13354 let unavailable = measure_wire(primitive_measurement_contract());
13355 serde_json::from_value::<MeasurementReportInput>(unavailable.clone())
13356 .unwrap()
13357 .into_files()
13358 .expect("unavailable inventory may retain a source material index");
13359
13360 let mut complete_out_of_range = unavailable.clone();
13361 complete_out_of_range["files"][0]["measurements"]["material_resource_coverage"] =
13362 serde_json::json!("complete");
13363 assert!(matches!(
13364 reader_error(complete_out_of_range),
13365 MeasurementReportError::File {
13366 source: MeasurementFileError::InvalidMeasurements { .. },
13367 ..
13368 }
13369 ));
13370
13371 let mut complete_in_range = unavailable;
13372 complete_in_range["files"][0]["measurements"]["material_resource_coverage"] =
13373 serde_json::json!("complete");
13374 complete_in_range["files"][0]["measurements"]["material_definitions"] = serde_json::json!([{
13375 "material_index": 0,
13376 "name": null,
13377 "texture_bindings": []
13378 }]);
13379 complete_in_range["files"][0]["measurements"]["mesh_definitions"][0]["primitives"][0]["material_index"] =
13380 serde_json::json!(0);
13381 serde_json::from_value::<MeasurementReportInput>(complete_in_range)
13382 .unwrap()
13383 .into_files()
13384 .expect("complete inventory accepts an in-range primitive material index");
13385 }
13386
13387 #[test]
13388 fn current_v18_leading_magic_is_bounded_reason_specific_hex() {
13389 let mut assets = AssetMeasurements {
13390 material_resource_coverage: MaterialResourceCoverage::Complete,
13391 ..AssetMeasurements::default()
13392 };
13393 assets.images.push(ImageMeasurements {
13394 image_index: 0,
13395 name: None,
13396 source_kind: ImageSourceKind::Embedded,
13397 declared_mime_type: None,
13398 detected_container: None,
13399 leading_magic_hex: Some("00ff10".into()),
13400 width: None,
13401 height: None,
13402 channel_count: None,
13403 decoded_color_type: None,
13404 unavailable_reason: Some(ImageUnavailableReason::UnsupportedContainer),
13405 });
13406 let wire = measure_wire(MeasurementContract::new(BTreeMap::new(), assets).unwrap());
13407
13408 let mut unknown_image = wire.clone();
13409 unknown_image["files"][0]["measurements"]["images"][0]
13410 .as_object_mut()
13411 .unwrap()
13412 .insert("bogus".into(), serde_json::json!(true));
13413 assert!(matches!(
13414 reader_error(unknown_image),
13415 MeasurementReportError::File {
13416 source: MeasurementFileError::InvalidMeasurementsShape { reason },
13417 ..
13418 } if reason.contains("unknown field `bogus`")
13419 ));
13420
13421 for magic in ["", "0", "0F", "00ff00112233445566778899aabbccdde"] {
13422 let mut mutation = wire.clone();
13423 mutation["files"][0]["measurements"]["images"][0]["leading_magic_hex"] =
13424 serde_json::json!(magic);
13425 assert!(matches!(
13426 reader_error(mutation),
13427 MeasurementReportError::File {
13428 source: MeasurementFileError::InvalidMeasurements { .. },
13429 ..
13430 }
13431 ));
13432 }
13433
13434 let mut wrong_reason = wire;
13435 wrong_reason["files"][0]["measurements"]["images"][0]["unavailable_reason"] =
13436 serde_json::json!("resource_limit");
13437 assert!(matches!(
13438 reader_error(wrong_reason),
13439 MeasurementReportError::File {
13440 source: MeasurementFileError::InvalidMeasurements { .. },
13441 ..
13442 }
13443 ));
13444 }
13445
13446 #[test]
13447 fn v12_v15_round_trips_without_inventing_primitive_rows() {
13448 let current_provenance = prediction_test_provenance_v2();
13449 let historical_provenance = current_provenance.clone().historical_v15_for_test();
13450 let basis =
13451 EnginePredictionBasisV1::new_v16(vec![PredictionBasisReferenceV1::measurement_v16(
13452 crate::MeasurementPointerV1::new("/measurements/schema_version").unwrap(),
13453 PredictionScalarV1::UnsignedInteger { value: 15 },
13454 )])
13455 .unwrap();
13456 let facet = EnginePredictionFacetV2::required_unavailable(
13457 EvaluationScope::new(EvaluationScopeCode::custom("test:v12")),
13458 basis,
13459 vec![PredictionUnavailableReasonV2::MeasurementUnavailable],
13460 )
13461 .unwrap();
13462 let current_check =
13463 unavailable_check_v2("test:v12", ¤t_provenance, vec![facet.clone()]);
13464 let prediction =
13465 EnginePredictionV2::new(current_provenance.identity().clone(), vec![facet])
13466 .unwrap()
13467 .historical_v15_for_test(historical_provenance.identity().clone());
13468 let mut check_wire = serde_json::to_value(current_check).unwrap();
13469 check_wire["prediction"] = serde_json::to_value(prediction).unwrap();
13470 let mut historical_assets = AssetMeasurements::default();
13471 historical_assets
13472 .mesh_definitions
13473 .push(MeshDefinitionMeasurements {
13474 mesh_index: 0,
13475 name: "legacy-mesh".into(),
13476 primitives: None,
13477 vertex_count: 0,
13478 geometry_aabb: None,
13479 geometry_centroid: None,
13480 max_joints_per_vertex: 0,
13481 weight_sum_min: None,
13482 weight_sum_max: None,
13483 additional_influence_sets: Vec::new(),
13484 });
13485 let historical_measurements =
13486 MeasurementContract::historical_v15(BTreeMap::new(), historical_assets).unwrap();
13487 let wire = serde_json::json!({
13488 "schema_version": OUTPUT_V12_SCHEMA_VERSION,
13489 "schema": OUTPUT_V12_SCHEMA_ID,
13490 "tool": {},
13491 "command": "lint",
13492 "summary": {"prediction_facets": {
13493 "available": 0,
13494 "required_prediction_unavailable": 1
13495 }},
13496 "files": [{
13497 "path": "historical-v12.glb",
13498 "input": {"sha256": "00".repeat(32), "bytes": 0},
13499 "rig": serde_json::to_value(prediction_test_rig()).unwrap(),
13500 "measurements": serde_json::to_value(historical_measurements).unwrap(),
13501 "prediction_provenance": serde_json::to_value(historical_provenance).unwrap(),
13502 "checks": [check_wire]
13503 }]
13504 });
13505
13506 let mut historical_with_unknowns = wire.clone();
13507 historical_with_unknowns["files"][0]["measurements"]
13508 .as_object_mut()
13509 .unwrap()
13510 .insert("future_root_field".into(), serde_json::json!(true));
13511 historical_with_unknowns["files"][0]["measurements"]["mesh_definitions"][0]
13512 .as_object_mut()
13513 .unwrap()
13514 .insert("future_mesh_field".into(), serde_json::json!(true));
13515 serde_json::from_value::<MeasurementReportInput>(historical_with_unknowns)
13516 .unwrap()
13517 .into_files()
13518 .expect("historical measurements-v15 retains permissive unknown-field readback");
13519
13520 let mut smuggled_primitives = wire.clone();
13521 smuggled_primitives["files"][0]["measurements"]["mesh_definitions"][0]["primitives"] =
13522 serde_json::json!([]);
13523 assert!(matches!(
13524 serde_json::from_value::<MeasurementReportInput>(smuggled_primitives)
13525 .unwrap()
13526 .into_files(),
13527 Err(MeasurementReportError::File {
13528 source: MeasurementFileError::InvalidMeasurements { source },
13529 ..
13530 }) if source.to_string().contains("measurements-v15 cannot carry per-primitive evidence")
13531 ));
13532
13533 let mut smuggled_magic = wire.clone();
13534 smuggled_magic["files"][0]["measurements"]["material_resource_coverage"] =
13535 serde_json::json!("complete");
13536 smuggled_magic["files"][0]["measurements"]["images"] = serde_json::json!([{
13537 "image_index": 0,
13538 "name": null,
13539 "source_kind": "embedded",
13540 "declared_mime_type": null,
13541 "detected_container": null,
13542 "leading_magic_hex": "00",
13543 "width": null,
13544 "height": null,
13545 "channel_count": null,
13546 "decoded_color_type": null,
13547 "unavailable_reason": "unsupported_container"
13548 }]);
13549 assert!(matches!(
13550 serde_json::from_value::<MeasurementReportInput>(smuggled_magic)
13551 .unwrap()
13552 .into_files(),
13553 Err(MeasurementReportError::File {
13554 source: MeasurementFileError::InvalidMeasurements { source },
13555 ..
13556 }) if source.to_string().contains("measurements-v15 cannot carry leading-magic evidence")
13557 ));
13558
13559 let files = serde_json::from_value::<MeasurementReportInput>(wire.clone())
13560 .unwrap()
13561 .into_files()
13562 .unwrap();
13563 let readback = serde_json::to_value(files[0].measurements()).unwrap();
13564 assert_eq!(readback["schema_version"], serde_json::json!(15));
13565 assert_eq!(
13566 readback["schema"],
13567 serde_json::json!(MEASUREMENTS_V15_SCHEMA_ID)
13568 );
13569 assert_eq!(readback["mesh_definitions"].as_array().unwrap().len(), 1);
13570 assert!(readback["mesh_definitions"][0].get("primitives").is_none());
13571
13572 let mut historical_max = wire.clone();
13573 historical_max["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13574 serde_json::json!(u32::MAX);
13575 serde_json::from_value::<MeasurementReportInput>(historical_max)
13576 .unwrap()
13577 .into_files()
13578 .expect("measurements-v15 retains its historical inclusive u32 maximum");
13579
13580 let mut historical_overflow = wire.clone();
13581 historical_overflow["files"][0]["measurements"]["mesh_definitions"][0]["vertex_count"] =
13582 serde_json::json!(u64::from(u32::MAX) + 1);
13583 assert!(matches!(
13584 lint_read_error(historical_overflow),
13585 MeasurementReportError::File {
13586 source: MeasurementFileError::InvalidMeasurements { .. },
13587 ..
13588 }
13589 ));
13590
13591 let mut v16_basis = wire;
13592 v16_basis["files"][0]["checks"][0]["prediction"]["facets"][0]["basis"]["references"][0]["schema"] =
13593 serde_json::json!(MEASUREMENTS_SCHEMA_ID);
13594 assert!(matches!(
13595 lint_read_error(v16_basis),
13596 MeasurementReportError::File {
13597 source: MeasurementFileError::InvalidPrediction {
13598 source: PredictionContractError::InvalidSchema {
13599 field: "basis.measurement.schema",
13600 expected: MEASUREMENTS_V15_SCHEMA_ID,
13601 ..
13602 },
13603 ..
13604 },
13605 ..
13606 }
13607 ));
13608 }
13609
13610 #[test]
13611 fn adjacent_output_revisions_reject_each_others_nested_measurements() {
13612 let current = measure_wire(prediction_test_measurements());
13613 let mut v12_with_v18 = current.clone();
13614 v12_with_v18["schema_version"] = serde_json::json!(OUTPUT_V12_SCHEMA_VERSION);
13615 v12_with_v18["schema"] = serde_json::json!(OUTPUT_V12_SCHEMA_ID);
13616 assert!(matches!(
13617 reader_error(v12_with_v18),
13618 MeasurementReportError::File {
13619 source: MeasurementFileError::UnsupportedMeasurementVersion {
13620 found: 18,
13621 expected: MEASUREMENTS_V15_SCHEMA_VERSION,
13622 },
13623 ..
13624 }
13625 ));
13626
13627 let historical =
13628 MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
13629 .unwrap();
13630 let mut v13_with_v15 = current;
13631 v13_with_v15["schema_version"] = serde_json::json!(OUTPUT_V13_SCHEMA_VERSION);
13632 v13_with_v15["schema"] = serde_json::json!(OUTPUT_V13_SCHEMA_ID);
13633 v13_with_v15["files"][0]["measurements"] = serde_json::to_value(historical).unwrap();
13634 assert!(matches!(
13635 reader_error(v13_with_v15),
13636 MeasurementReportError::File {
13637 source: MeasurementFileError::UnsupportedMeasurementVersion {
13638 found: 15,
13639 expected: MEASUREMENTS_V16_SCHEMA_VERSION,
13640 },
13641 ..
13642 }
13643 ));
13644 }
13645
13646 #[test]
13647 fn historical_output_v17_writer_projects_current_measurements_to_v16() {
13648 let report = LintFileReportV17::new(
13649 "historical.glb",
13650 InputIdentity::from_bytes(&[]),
13651 prediction_test_rig(),
13652 None,
13653 Vec::new(),
13654 prediction_test_measurements(),
13655 )
13656 .unwrap();
13657 assert_eq!(
13658 report.measurements().schema_version,
13659 MEASUREMENTS_V16_SCHEMA_VERSION
13660 );
13661 assert_eq!(report.measurements().schema, MEASUREMENTS_V16_SCHEMA_ID);
13662 }
13663
13664 #[test]
13665 fn current_v19_writers_refuse_measurements_v16_at_file_and_envelope_boundaries() {
13666 let mismatch = OutputContractError::CurrentMeasurementContractMismatch {
13667 output_version: OUTPUT_SCHEMA_VERSION,
13668 found_version: MEASUREMENTS_V16_SCHEMA_VERSION,
13669 found_schema: MEASUREMENTS_V16_SCHEMA_ID.into(),
13670 };
13671 assert_eq!(
13672 MeasureFileReport::new(
13673 "historical.glb",
13674 InputIdentity::from_bytes(&[]),
13675 prediction_test_rig(),
13676 prediction_test_measurements_v16(),
13677 )
13678 .unwrap_err(),
13679 mismatch
13680 );
13681 assert_eq!(
13682 LintFileReportV19::new(
13683 "historical.glb",
13684 InputIdentity::from_bytes(&[]),
13685 prediction_test_rig(),
13686 None,
13687 Vec::new(),
13688 prediction_test_measurements_v16(),
13689 )
13690 .unwrap_err(),
13691 mismatch
13692 );
13693
13694 let malformed_measure = MeasureFileReport {
13695 evidence: FileEvidence::new(
13696 "historical.glb",
13697 InputIdentity::from_bytes(&[]),
13698 prediction_test_rig(),
13699 prediction_test_measurements_v16(),
13700 ),
13701 };
13702 assert_eq!(
13703 MeasureEnvelope::new(
13704 ToolInfo::animsmith(ToolSource::new(None, None)),
13705 vec![malformed_measure],
13706 )
13707 .unwrap_err(),
13708 mismatch
13709 );
13710 let malformed_lint = LintFileReportV19 {
13711 evidence: FileEvidence::new(
13712 "historical.glb",
13713 InputIdentity::from_bytes(&[]),
13714 prediction_test_rig(),
13715 prediction_test_measurements_v16(),
13716 ),
13717 prediction_provenance: None,
13718 checks: Vec::new(),
13719 };
13720 assert_eq!(
13721 LintEnvelopeV19::new(
13722 ToolInfo::animsmith(ToolSource::new(None, None)),
13723 vec![malformed_lint],
13724 )
13725 .unwrap_err(),
13726 mismatch
13727 );
13728 }
13729
13730 #[test]
13731 fn v17_to_v16_projection_is_total_and_used_by_prediction_and_historical_writers() {
13732 let fully_measured = loop_projection_measurements(&[
13733 MeasurementAvailability::Measured,
13734 MeasurementAvailability::Measured,
13735 ]);
13736 let projected = fully_measured.prediction_v16_projection().unwrap();
13737 assert_eq!(projected.schema_version, MEASUREMENTS_V16_SCHEMA_VERSION);
13738 let continuity = projected.clips()["loop"]
13739 .loop_continuity
13740 .as_ref()
13741 .expect("fully measured rows survive projection");
13742 assert_eq!(continuity.bones.len(), 2);
13743 let wire = serde_json::to_value(&projected).unwrap();
13744 assert!(
13745 wire["clips"]["loop"]["loop_continuity"]["bones"]
13746 .as_array()
13747 .unwrap()
13748 .iter()
13749 .all(|bone| bone.get("availability").is_none())
13750 );
13751
13752 let rotation_facts = prediction_measurements_with_rotation_facts();
13756 let projected_rotation_facts = rotation_facts.prediction_v16_projection().unwrap();
13757 let projected_wire = serde_json::to_value(&projected_rotation_facts).unwrap();
13758 let projected_linear_paths = [
13759 &projected_wire["skeleton_nodes"][0]["rest_world_linear"],
13760 &projected_wire["skins"][0]["joints"][0]["joint_bind_to_mesh"]["linear"],
13761 &projected_wire["skins"][0]["joints"][0]["mesh_bind_world"]["linear"],
13762 ];
13763 assert!(
13764 projected_linear_paths
13765 .into_iter()
13766 .all(|linear| linear.get("rotation_xyzw").is_none()),
13767 "V16 projection must remove every V18-only rotation fact"
13768 );
13769 let historical_rotation_report = LintFileReport::new(
13770 "rotation-facts.glb",
13771 InputIdentity::from_bytes(&[]),
13772 prediction_test_rig(),
13773 None,
13774 Vec::new(),
13775 rotation_facts,
13776 )
13777 .expect("historical writer projects non-empty rotation facts");
13778 let historical_rotation_wire = serde_json::to_value(&historical_rotation_report).unwrap();
13779 assert_eq!(
13780 historical_rotation_report.measurements().schema_version,
13781 MEASUREMENTS_V16_SCHEMA_VERSION
13782 );
13783 let historical_linear_paths = [
13784 &historical_rotation_wire["measurements"]["skeleton_nodes"][0]["rest_world_linear"],
13785 &historical_rotation_wire["measurements"]["skins"][0]["joints"][0]["joint_bind_to_mesh"]
13786 ["linear"],
13787 &historical_rotation_wire["measurements"]["skins"][0]["joints"][0]["mesh_bind_world"]["linear"],
13788 ];
13789 assert!(
13790 historical_linear_paths
13791 .into_iter()
13792 .all(|linear| linear.get("rotation_xyzw").is_none()),
13793 "historical V15 writer must preserve the V16 measurement shape"
13794 );
13795 let historical_envelope = LintEnvelope::new(
13796 ToolInfo::animsmith(ToolSource::new(None, None)),
13797 vec![historical_rotation_report],
13798 )
13799 .expect("historical envelope accepts projected non-empty rotation facts");
13800 let historical_envelope_wire = serde_json::to_value(historical_envelope).unwrap();
13801 let historical_input: MeasurementReportInput =
13802 serde_json::from_value(historical_envelope_wire).unwrap();
13803 assert_eq!(historical_input.file_count(), Some(1));
13804 historical_input
13805 .into_files()
13806 .expect("historical reader accepts the projected V16 wire shape");
13807
13808 let partial = loop_projection_measurements(&[
13809 MeasurementAvailability::Measured,
13810 MeasurementAvailability::Unavailable,
13811 ]);
13812 let all_unavailable = loop_projection_measurements(&[
13813 MeasurementAvailability::Unavailable,
13814 MeasurementAvailability::Unavailable,
13815 ]);
13816 for measurements in [&partial, &all_unavailable] {
13817 let projected = measurements.prediction_v16_projection().unwrap();
13818 assert_eq!(
13819 projected.clips()["loop"].loop_continuity_availability,
13820 MeasurementAvailability::Unavailable
13821 );
13822 assert!(projected.clips()["loop"].loop_continuity.is_none());
13823 }
13824
13825 let current_v3 = LintFileReportV19::new(
13826 "v3.glb",
13827 InputIdentity::from_bytes(&[]),
13828 prediction_test_rig(),
13829 None,
13830 Vec::new(),
13831 fully_measured,
13832 )
13833 .unwrap();
13834 let current_v5 = LintFileReportV19::new_v5(
13835 "v5.glb",
13836 InputIdentity::from_bytes(&[]),
13837 prediction_test_rig(),
13838 None,
13839 Vec::new(),
13840 partial.clone(),
13841 )
13842 .unwrap();
13843 let current_v6 = LintFileReportV19::new_v6(
13844 "v6.glb",
13845 InputIdentity::from_bytes(&[]),
13846 prediction_test_rig(),
13847 None,
13848 Vec::new(),
13849 all_unavailable.clone(),
13850 )
13851 .unwrap();
13852 assert!(
13853 current_v3.measurements().clips()["loop"]
13854 .loop_continuity
13855 .is_some()
13856 );
13857 assert!(
13858 current_v5.measurements().clips()["loop"]
13859 .loop_continuity
13860 .is_some()
13861 );
13862 assert!(
13863 current_v6.measurements().clips()["loop"]
13864 .loop_continuity
13865 .is_some()
13866 );
13867
13868 let historical_v15 = LintFileReport::new(
13869 "v15.glb",
13870 InputIdentity::from_bytes(&[]),
13871 prediction_test_rig(),
13872 None,
13873 Vec::new(),
13874 partial.clone(),
13875 )
13876 .unwrap();
13877 let historical_v16 = LintFileReportV16::new(
13878 "v16.glb",
13879 InputIdentity::from_bytes(&[]),
13880 prediction_test_rig(),
13881 None,
13882 Vec::new(),
13883 partial.clone(),
13884 )
13885 .unwrap();
13886 let historical_v17 = LintFileReportV17::new(
13887 "v17.glb",
13888 InputIdentity::from_bytes(&[]),
13889 prediction_test_rig(),
13890 None,
13891 Vec::new(),
13892 partial,
13893 )
13894 .unwrap();
13895 for measurements in [
13896 historical_v15.measurements(),
13897 historical_v16.measurements(),
13898 historical_v17.measurements(),
13899 ] {
13900 assert_eq!(measurements.schema_version, MEASUREMENTS_V16_SCHEMA_VERSION);
13901 assert_eq!(
13902 measurements.clips()["loop"].loop_continuity_availability,
13903 MeasurementAvailability::Unavailable
13904 );
13905 assert!(measurements.clips()["loop"].loop_continuity.is_none());
13906 }
13907 }
13908
13909 #[test]
13910 fn legacy_output_v11_reader_dispatch_preserves_measurement_recovery() {
13911 let provenance = prediction_test_provenance_v2();
13912 let legacy = PredictionProvenanceV1::new(
13913 provenance.profile().clone(),
13914 provenance.source_format(),
13915 ResolvedEngineSettingsV1::new(provenance.profile(), Vec::new(), Vec::new()).unwrap(),
13916 provenance.raw_source().clone(),
13917 provenance.dependency_closure().clone(),
13918 )
13919 .unwrap()
13920 .historical_v15_for_test();
13921 let legacy_prediction = EnginePredictionV1::new(
13922 legacy.identity().clone(),
13923 vec![
13924 EnginePredictionFacetV1::required_unavailable(
13925 EvaluationScope::new(EvaluationScopeCode::custom("test:legacy-v11")),
13926 EnginePredictionBasisV1::new(Vec::new()).unwrap(),
13927 vec![PredictionUnavailableReasonV1::ProjectIntentUnavailable],
13928 )
13929 .unwrap(),
13930 ],
13931 )
13932 .unwrap()
13933 .historical_v15_for_test(legacy.identity().clone());
13934 let legacy_measurements =
13935 MeasurementContract::historical_v15(BTreeMap::new(), AssetMeasurements::default())
13936 .unwrap();
13937 let wire = serde_json::json!({
13940 "schema_version": OUTPUT_V11_SCHEMA_VERSION,
13941 "schema": OUTPUT_V11_SCHEMA_ID,
13942 "tool": {},
13943 "command": "lint",
13944 "summary": {
13945 "prediction_facets": {
13946 "available": 0,
13947 "required_prediction_unavailable": 1,
13948 },
13949 },
13950 "files": [{
13951 "path": "legacy-v11.glb",
13952 "input": { "sha256": "00".repeat(32), "bytes": 0 },
13953 "rig": serde_json::to_value(prediction_test_rig()).unwrap(),
13954 "measurements": serde_json::to_value(legacy_measurements).unwrap(),
13955 "prediction_provenance": serde_json::to_value(legacy.clone()).unwrap(),
13956 "checks": [{
13957 "check_id": "test:legacy-v11",
13958 "selection": "selected",
13959 "configuration": "enabled",
13960 "applicability": "applicable",
13961 "evaluation": "not_evaluated",
13962 "findings": [],
13963 "evaluated_scopes": [],
13964 "gaps": [],
13965 "prediction": legacy_prediction,
13966 }],
13967 }],
13968 });
13969 let report: MeasurementReportInput = serde_json::from_value(wire.clone()).unwrap();
13970 assert_eq!(report.file_count(), Some(1));
13971 assert_eq!(report.into_files().unwrap().len(), 1);
13972
13973 let mut bad_provenance = wire.clone();
13974 bad_provenance["files"][0]["prediction_provenance"]["schema"] =
13975 serde_json::json!("urn:forged");
13976 assert!(matches!(
13977 lint_read_error(bad_provenance),
13978 MeasurementReportError::File {
13979 source: MeasurementFileError::InvalidPredictionProvenance { .. },
13980 ..
13981 }
13982 ));
13983
13984 let mut bad_prediction = wire.clone();
13985 bad_prediction["files"][0]["checks"] = serde_json::json!([{
13986 "check_id": "test:legacy-v11",
13987 "selection": "selected",
13988 "configuration": "enabled",
13989 "applicability": "applicable",
13990 "evaluation": "not_evaluated",
13991 "findings": [],
13992 "evaluated_scopes": [],
13993 "gaps": [],
13994 "prediction": { "schema": "urn:forged" }
13995 }]);
13996 assert!(matches!(
13997 lint_read_error(bad_prediction),
13998 MeasurementReportError::File {
13999 source: MeasurementFileError::InvalidPredictionShape { .. },
14000 ..
14001 }
14002 ));
14003
14004 let mut missing_provenance_before_malformed_prediction = wire.clone();
14005 missing_provenance_before_malformed_prediction["files"][0]["prediction_provenance"] =
14006 serde_json::Value::Null;
14007 missing_provenance_before_malformed_prediction["files"][0]["checks"][0]["prediction"] =
14008 serde_json::json!({ "schema": "urn:forged" });
14009 assert!(matches!(
14010 lint_read_error(missing_provenance_before_malformed_prediction),
14011 MeasurementReportError::File {
14012 source: MeasurementFileError::PredictionWithoutProvenance { check_index: 0 },
14013 ..
14014 }
14015 ));
14016
14017 let mut inactive_before_malformed_prediction = wire.clone();
14018 inactive_before_malformed_prediction["files"][0]["checks"][0]["selection"] =
14019 serde_json::json!("unselected");
14020 inactive_before_malformed_prediction["files"][0]["checks"][0]["prediction"] =
14021 serde_json::json!({ "schema": "urn:forged" });
14022 assert!(matches!(
14023 lint_read_error(inactive_before_malformed_prediction),
14024 MeasurementReportError::File {
14025 source: MeasurementFileError::InvalidPredictionLifecycle { check_index: 0, .. },
14026 ..
14027 }
14028 ));
14029
14030 let mut overbudget_before_later_malformed = wire.clone();
14033 let facet =
14034 overbudget_before_later_malformed["files"][0]["checks"][0]["prediction"]["facets"][0]
14035 .clone();
14036 overbudget_before_later_malformed["files"][0]["checks"][0]["prediction"]["facets"] =
14037 serde_json::Value::Array(
14038 std::iter::repeat_n(facet, PREDICTION_V1_MAX_FACETS_PER_FILE + 1).collect(),
14039 );
14040 overbudget_before_later_malformed["files"][0]["checks"]
14041 .as_array_mut()
14042 .unwrap()
14043 .push(serde_json::json!({ "check_id": 7 }));
14044 let precedence_error = lint_read_error(overbudget_before_later_malformed);
14045 assert!(
14046 matches!(
14047 precedence_error,
14048 MeasurementReportError::File {
14049 source: MeasurementFileError::InvalidPrediction {
14050 source: PredictionContractError::TooManyFacets { .. },
14051 ..
14052 },
14053 ..
14054 },
14055 ),
14056 "{precedence_error:?}"
14057 );
14058 }
14059}
14060
14061#[derive(Debug, Clone, Serialize)]
14062struct FileEvidence {
14063 path: String,
14064 input: InputIdentity,
14065 rig: RigInfo,
14066 measurements: MeasurementContract,
14067}
14068
14069impl FileEvidence {
14070 fn new(
14071 path: impl Into<String>,
14072 input: InputIdentity,
14073 rig: RigInfo,
14074 measurements: MeasurementContract,
14075 ) -> Self {
14076 Self {
14077 path: path.into(),
14078 input,
14079 rig,
14080 measurements,
14081 }
14082 }
14083
14084 fn historical_v16(
14085 path: impl Into<String>,
14086 input: InputIdentity,
14087 rig: RigInfo,
14088 measurements: MeasurementContract,
14089 ) -> Result<Self, OutputContractError> {
14090 Ok(Self::new(
14091 path,
14092 input,
14093 rig,
14094 measurements.prediction_v16_projection()?,
14095 ))
14096 }
14097}
14098
14099#[derive(Debug, Clone, Serialize)]
14101pub struct MeasureFileReport {
14102 #[serde(flatten)]
14103 evidence: FileEvidence,
14104}
14105
14106impl MeasureFileReport {
14107 pub fn new(
14114 path: impl Into<String>,
14115 input: InputIdentity,
14116 rig: RigInfo,
14117 measurements: MeasurementContract,
14118 ) -> Result<Self, OutputContractError> {
14119 require_measurements_v18(OUTPUT_SCHEMA_VERSION, &measurements)?;
14120 Ok(Self {
14121 evidence: FileEvidence::new(path, input, rig, measurements),
14122 })
14123 }
14124
14125 pub fn path(&self) -> &str {
14127 &self.evidence.path
14128 }
14129
14130 pub fn input(&self) -> &InputIdentity {
14132 &self.evidence.input
14133 }
14134
14135 pub fn measurements(&self) -> &MeasurementContract {
14137 &self.evidence.measurements
14138 }
14139}
14140
14141#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14143#[non_exhaustive]
14144pub enum OutputContractError {
14145 #[error("output contains {found} files, exceeding the v10 limit of {limit}")]
14147 TooManyFiles {
14148 found: usize,
14150 limit: usize,
14152 },
14153 #[error("lint file contains {found} checks, exceeding the v10 limit of {limit}")]
14155 TooManyChecks {
14156 found: usize,
14158 limit: usize,
14160 },
14161 #[error("engine prediction requires non-null file prediction_provenance")]
14163 PredictionWithoutProvenance,
14164 #[error("output-v15 lint cannot carry historical engine-prediction evidence")]
14166 HistoricalPredictionInV2Output,
14167 #[error(
14169 "output-v15 prediction provenance and check attachments must use one correlated revision"
14170 )]
14171 PredictionRevisionMismatch,
14172 #[error("prediction provenance primary input does not match the lint file input")]
14174 PredictionPrimaryInputMismatch,
14175 #[error("lint file contains {found} prediction facets, exceeding the V1 limit of {limit}")]
14177 TooManyPredictionFacets {
14178 found: usize,
14180 limit: usize,
14182 },
14183 #[error("facet-budget summary requires exactly {limit} aggregate facets, found {found}")]
14186 FacetBudgetSummaryWithoutExhaustedFileBudget {
14187 found: usize,
14189 limit: usize,
14191 },
14192 #[error(
14194 "lint file contains {found} prediction basis references, exceeding the V1 limit of {limit}"
14195 )]
14196 TooManyPredictionBasisReferences {
14197 found: usize,
14199 limit: usize,
14201 },
14202 #[error("lint file retains {found} prediction text bytes, exceeding the V1 limit of {limit}")]
14204 TooMuchPredictionText {
14205 found: usize,
14207 limit: usize,
14209 },
14210 #[error("checked arithmetic overflow while validating output-v11 bounds")]
14212 ArithmeticOverflow,
14213 #[error("invalid prediction evidence: {0}")]
14215 InvalidPrediction(#[from] PredictionContractError),
14216 #[error("invalid measurements-v16 prediction projection: {0}")]
14219 InvalidMeasurementProjection(#[from] MeasurementContractError),
14220 #[error(
14223 "output-v{output_version} requires measurements-v16, found version {found_version} with identity {found_schema}"
14224 )]
14225 HistoricalMeasurementContractMismatch {
14226 output_version: u32,
14228 found_version: u32,
14230 found_schema: String,
14232 },
14233 #[error(
14235 "output-v{output_version} requires measurements-v18, found measurement schema_version {found_version} ({found_schema})"
14236 )]
14237 CurrentMeasurementContractMismatch {
14238 output_version: u32,
14240 found_version: u32,
14242 found_schema: String,
14244 },
14245}
14246
14247fn require_measurements_v18(
14248 output_version: u32,
14249 measurements: &MeasurementContract,
14250) -> Result<(), OutputContractError> {
14251 if measurements.schema_version == MEASUREMENTS_SCHEMA_VERSION
14252 && measurements.schema == MEASUREMENTS_SCHEMA_ID
14253 {
14254 Ok(())
14255 } else {
14256 Err(OutputContractError::CurrentMeasurementContractMismatch {
14257 output_version,
14258 found_version: measurements.schema_version,
14259 found_schema: measurements.schema.to_owned(),
14260 })
14261 }
14262}
14263
14264fn require_measurements_v16(
14265 output_version: u32,
14266 measurements: &MeasurementContract,
14267) -> Result<(), OutputContractError> {
14268 if measurements.schema_version == MEASUREMENTS_V16_SCHEMA_VERSION
14269 && measurements.schema == MEASUREMENTS_V16_SCHEMA_ID
14270 {
14271 Ok(())
14272 } else {
14273 Err(OutputContractError::HistoricalMeasurementContractMismatch {
14274 output_version,
14275 found_version: measurements.schema_version,
14276 found_schema: measurements.schema.to_owned(),
14277 })
14278 }
14279}
14280
14281#[derive(Debug, Clone, Serialize)]
14282struct EnvelopeHeader {
14283 schema_version: u32,
14284 schema: &'static str,
14285 tool: ToolInfo,
14286 command: &'static str,
14287}
14288
14289impl EnvelopeHeader {
14290 fn new(tool: ToolInfo, command: &'static str) -> Self {
14291 Self {
14292 schema_version: OUTPUT_SCHEMA_VERSION,
14293 schema: OUTPUT_SCHEMA_ID,
14294 tool,
14295 command,
14296 }
14297 }
14298}
14299
14300#[derive(Debug, Clone, Serialize)]
14301struct MeasureSummary {
14302 files: usize,
14303}
14304
14305#[derive(Debug, Clone, Default, Serialize)]
14306struct FindingSummary {
14307 error: usize,
14308 warning: usize,
14309 note: usize,
14310}
14311
14312impl FindingSummary {
14313 fn add(&mut self, severity: Severity) {
14314 match severity {
14315 Severity::Error => self.error += 1,
14316 Severity::Warning => self.warning += 1,
14317 Severity::Note => self.note += 1,
14318 }
14319 }
14320}
14321
14322#[derive(Debug, Clone, Default, Serialize)]
14323struct SelectionSummary {
14324 selected: usize,
14325 unselected: usize,
14326}
14327
14328#[derive(Debug, Clone, Default, Serialize)]
14329struct ConfigurationSummary {
14330 enabled: usize,
14331 disabled: usize,
14332}
14333
14334#[derive(Debug, Clone, Default, Serialize)]
14335struct ApplicabilitySummary {
14336 applicable: usize,
14337 not_applicable: usize,
14338}
14339
14340#[derive(Debug, Clone, Default, Serialize)]
14341struct EvaluationStateSummary {
14342 complete: usize,
14343 partial: usize,
14344 not_evaluated: usize,
14345}
14346
14347#[derive(Debug, Clone, Default, Serialize)]
14348struct CheckSummary {
14349 total: usize,
14350 selection: SelectionSummary,
14351 configuration: ConfigurationSummary,
14352 applicability: ApplicabilitySummary,
14353 evaluation: EvaluationStateSummary,
14354 gaps: usize,
14355}
14356
14357#[derive(Debug, Clone, Serialize)]
14358struct LintSummary {
14359 files: usize,
14360 findings: FindingSummary,
14361 checks: CheckSummary,
14362 prediction_facets: PredictionFacetSummary,
14363}
14364
14365#[derive(Debug, Clone, Default, Serialize)]
14366struct PredictionFacetSummary {
14367 available: usize,
14368 required_prediction_unavailable: usize,
14369}
14370
14371#[derive(Debug, Clone, Serialize)]
14373pub struct MeasureEnvelope {
14374 #[serde(flatten)]
14375 header: EnvelopeHeader,
14376 summary: MeasureSummary,
14377 files: Vec<MeasureFileReport>,
14378}
14379
14380impl MeasureEnvelope {
14381 pub fn new(tool: ToolInfo, files: Vec<MeasureFileReport>) -> Result<Self, OutputContractError> {
14383 if files.len() > OUTPUT_V11_MAX_FILES {
14384 return Err(OutputContractError::TooManyFiles {
14385 found: files.len(),
14386 limit: OUTPUT_V11_MAX_FILES,
14387 });
14388 }
14389 for file in &files {
14390 require_measurements_v18(OUTPUT_SCHEMA_VERSION, &file.evidence.measurements)?;
14391 }
14392 Ok(Self {
14393 header: EnvelopeHeader::new(tool, "measure"),
14394 summary: MeasureSummary { files: files.len() },
14395 files,
14396 })
14397 }
14398}
14399
14400#[derive(Debug, Clone, Serialize)]
14401#[serde(untagged)]
14402#[allow(
14403 clippy::large_enum_variant,
14404 reason = "the internal correlated revision enum preserves value ownership for both immutable wire types"
14405)]
14406enum CurrentPredictionProvenance {
14407 V3(PredictionProvenanceV3),
14408 V4(PredictionProvenanceV4),
14409}
14410
14411#[derive(Debug, Clone, Serialize)]
14413pub struct LintFileReport {
14414 #[serde(flatten)]
14415 evidence: FileEvidence,
14416 prediction_provenance: Option<CurrentPredictionProvenance>,
14417 checks: Vec<CheckEvaluation>,
14418}
14419
14420impl LintFileReport {
14421 pub fn new(
14423 path: impl Into<String>,
14424 input: InputIdentity,
14425 rig: RigInfo,
14426 prediction_provenance: Option<PredictionProvenanceV3>,
14427 checks: Vec<CheckEvaluation>,
14428 measurements: MeasurementContract,
14429 ) -> Result<Self, OutputContractError> {
14430 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14431 return Err(OutputContractError::TooManyChecks {
14432 found: checks.len(),
14433 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14434 });
14435 }
14436 let report = Self {
14437 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14438 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenance::V3),
14439 checks,
14440 };
14441 report.validate()?;
14442 Ok(report)
14443 }
14444
14445 pub const fn prediction_provenance(&self) -> Option<&PredictionProvenanceV3> {
14447 match self.prediction_provenance.as_ref() {
14448 Some(CurrentPredictionProvenance::V3(provenance)) => Some(provenance),
14449 Some(CurrentPredictionProvenance::V4(_)) | None => None,
14450 }
14451 }
14452
14453 pub fn new_v4(
14455 path: impl Into<String>,
14456 input: InputIdentity,
14457 rig: RigInfo,
14458 prediction_provenance: Option<PredictionProvenanceV4>,
14459 checks: Vec<CheckEvaluation>,
14460 measurements: MeasurementContract,
14461 ) -> Result<Self, OutputContractError> {
14462 if checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14463 return Err(OutputContractError::TooManyChecks {
14464 found: checks.len(),
14465 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14466 });
14467 }
14468 let report = Self {
14469 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14470 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenance::V4),
14471 checks,
14472 };
14473 report.validate()?;
14474 Ok(report)
14475 }
14476
14477 pub const fn prediction_provenance_v4(&self) -> Option<&PredictionProvenanceV4> {
14479 match self.prediction_provenance.as_ref() {
14480 Some(CurrentPredictionProvenance::V4(provenance)) => Some(provenance),
14481 Some(CurrentPredictionProvenance::V3(_)) | None => None,
14482 }
14483 }
14484
14485 pub fn path(&self) -> &str {
14487 &self.evidence.path
14488 }
14489
14490 pub fn input(&self) -> &InputIdentity {
14492 &self.evidence.input
14493 }
14494
14495 pub fn measurements(&self) -> &MeasurementContract {
14497 &self.evidence.measurements
14498 }
14499
14500 pub fn checks(&self) -> &[CheckEvaluation] {
14502 &self.checks
14503 }
14504
14505 fn validate(&self) -> Result<(), OutputContractError> {
14506 require_measurements_v16(OUTPUT_V15_SCHEMA_VERSION, &self.evidence.measurements)?;
14507 if let Some(provenance) = &self.prediction_provenance {
14508 let primary_input = match provenance {
14509 CurrentPredictionProvenance::V3(provenance) => {
14510 provenance.validate()?;
14511 provenance.raw_source().primary_input()
14512 }
14513 CurrentPredictionProvenance::V4(provenance) => {
14514 provenance.validate()?;
14515 provenance.raw_source().primary_input()
14516 }
14517 };
14518 if primary_input != &self.evidence.input {
14519 return Err(OutputContractError::PredictionPrimaryInputMismatch);
14520 }
14521 }
14522 let mut facets = 0usize;
14523 let mut references = 0usize;
14524 let mut has_facet_budget_summary = false;
14525 let mut text = self
14526 .prediction_provenance
14527 .as_ref()
14528 .map(|provenance| match provenance {
14529 CurrentPredictionProvenance::V3(provenance) => provenance.retained_text_bytes(),
14530 CurrentPredictionProvenance::V4(provenance) => provenance.retained_text_bytes(),
14531 })
14532 .transpose()?
14533 .unwrap_or(0);
14534 for check in &self.checks {
14535 if check.engine_prediction().is_some() || check.engine_prediction_v2().is_some() {
14536 return Err(OutputContractError::HistoricalPredictionInV2Output);
14537 }
14538 let provenance_revision_matches = matches!(
14539 (
14540 &self.prediction_provenance,
14541 check.engine_prediction_v3(),
14542 check.engine_prediction_v4()
14543 ),
14544 (Some(CurrentPredictionProvenance::V3(_)), Some(_), None)
14545 | (Some(CurrentPredictionProvenance::V4(_)), None, Some(_))
14546 | (_, None, None)
14547 );
14548 if !provenance_revision_matches {
14549 if self.prediction_provenance.is_none()
14550 && (check.engine_prediction_v3().is_some()
14551 || check.engine_prediction_v4().is_some())
14552 {
14553 return Err(OutputContractError::PredictionWithoutProvenance);
14554 }
14555 return Err(OutputContractError::PredictionRevisionMismatch);
14556 }
14557 let legacy_v3_provenance = match self.prediction_provenance.as_ref() {
14558 Some(CurrentPredictionProvenance::V3(provenance)) => Some(provenance),
14559 Some(CurrentPredictionProvenance::V4(_)) | None => None,
14560 };
14561 if !matches!(
14562 self.prediction_provenance.as_ref(),
14563 Some(CurrentPredictionProvenance::V4(_))
14564 ) {
14565 validate_current_engine_clip_boundary_applicability_v3(
14566 check.check_id(),
14567 check.applicability(),
14568 legacy_v3_provenance,
14569 )?;
14570 }
14571 if check.check_id() == ENGINE_CLIP_BOUNDARY_CHECK_ID
14572 && check.selection() == SelectionState::Selected
14573 && check.configuration() == ConfigurationState::Enabled
14574 && check.applicability() == Applicability::Applicable
14575 && check.engine_prediction_v3().is_none()
14576 {
14577 return Err(OutputContractError::InvalidPrediction(
14578 PredictionContractError::EngineClipBoundaryFacetMismatch,
14579 ));
14580 }
14581 let current_v4_provenance = match self.prediction_provenance.as_ref() {
14582 Some(CurrentPredictionProvenance::V4(provenance)) => Some(provenance),
14583 Some(CurrentPredictionProvenance::V3(_)) | None => None,
14584 };
14585 validate_current_engine_unit_scale_prediction_v4(
14586 check.check_id(),
14587 check.selection(),
14588 check.configuration(),
14589 check.applicability(),
14590 check.engine_prediction_v4(),
14591 current_v4_provenance,
14592 &self.evidence.measurements,
14593 )?;
14594 if let Some(prediction) = check.engine_prediction_v3() {
14595 let Some(CurrentPredictionProvenance::V3(provenance)) =
14596 self.prediction_provenance.as_ref()
14597 else {
14598 return Err(OutputContractError::PredictionRevisionMismatch);
14599 };
14600 prediction.validate_against_provenance(provenance)?;
14601 prediction.validate_for_check(
14602 check.check_id(),
14603 check.evaluated_scopes(),
14604 check.gaps(),
14605 check.findings(),
14606 )?;
14607 validate_current_engine_addressability_prediction_v3(
14608 check.check_id(),
14609 prediction,
14610 provenance,
14611 )?;
14612 let finding_scopes = check
14613 .findings()
14614 .iter()
14615 .filter_map(|finding| finding.prediction_scope.as_ref())
14616 .collect::<Vec<_>>();
14617 validate_current_engine_clip_boundary_prediction_v3(
14618 check.check_id(),
14619 prediction,
14620 provenance,
14621 check.evaluated_scopes(),
14622 &finding_scopes,
14623 )?;
14624 has_facet_budget_summary |= prediction.has_facet_budget_summary();
14625 facets = facets
14626 .checked_add(prediction.facets().len())
14627 .ok_or(OutputContractError::ArithmeticOverflow)?;
14628 references = references
14629 .checked_add(prediction.basis_reference_count())
14630 .ok_or(OutputContractError::ArithmeticOverflow)?;
14631 text = text
14632 .checked_add(prediction.retained_text_bytes()?)
14633 .ok_or(OutputContractError::ArithmeticOverflow)?;
14634 }
14635 if let Some(prediction) = check.engine_prediction_v4() {
14636 let Some(CurrentPredictionProvenance::V4(provenance)) =
14637 self.prediction_provenance.as_ref()
14638 else {
14639 return Err(OutputContractError::PredictionRevisionMismatch);
14640 };
14641 prediction.validate_against_provenance(provenance)?;
14642 prediction.validate_for_check(
14643 check.check_id(),
14644 check.evaluated_scopes(),
14645 check.gaps(),
14646 check.findings(),
14647 )?;
14648 has_facet_budget_summary |= prediction.has_facet_budget_summary();
14649 facets = facets
14650 .checked_add(prediction.facets().len())
14651 .ok_or(OutputContractError::ArithmeticOverflow)?;
14652 references = references
14653 .checked_add(prediction.basis_reference_count())
14654 .ok_or(OutputContractError::ArithmeticOverflow)?;
14655 text = text
14656 .checked_add(prediction.retained_text_bytes()?)
14657 .ok_or(OutputContractError::ArithmeticOverflow)?;
14658 }
14659 }
14660 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
14661 return Err(OutputContractError::TooManyPredictionFacets {
14662 found: facets,
14663 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14664 });
14665 }
14666 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
14667 return Err(
14668 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
14669 found: facets,
14670 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14671 },
14672 );
14673 }
14674 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
14675 return Err(OutputContractError::TooManyPredictionBasisReferences {
14676 found: references,
14677 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
14678 });
14679 }
14680 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
14681 return Err(OutputContractError::TooMuchPredictionText {
14682 found: text,
14683 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
14684 });
14685 }
14686 validate_measurement_references_batch_v3(
14687 &self.evidence.measurements,
14688 self.checks
14689 .iter()
14690 .enumerate()
14691 .filter_map(|(check_index, check)| {
14692 check
14693 .engine_prediction_v3()
14694 .map(|prediction| (check_index, prediction))
14695 }),
14696 )
14697 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
14698 validate_measurement_references_batch_v4(
14699 &self.evidence.measurements,
14700 self.checks
14701 .iter()
14702 .enumerate()
14703 .filter_map(|(check_index, check)| {
14704 check
14705 .engine_prediction_v4()
14706 .map(|prediction| (check_index, prediction))
14707 }),
14708 )
14709 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
14710 Ok(())
14711 }
14712}
14713
14714#[derive(Debug, Clone, Serialize)]
14715struct EnvelopeHeaderV2 {
14716 schema_version: u32,
14717 schema: &'static str,
14718 tool: ToolInfo,
14719 command: &'static str,
14720}
14721
14722#[derive(Debug, Clone, Serialize)]
14724pub struct LintEnvelope {
14725 #[serde(flatten)]
14726 header: EnvelopeHeaderV2,
14727 summary: LintSummary,
14728 files: Vec<LintFileReport>,
14729}
14730
14731impl LintEnvelope {
14732 pub fn new(tool: ToolInfo, files: Vec<LintFileReport>) -> Result<Self, OutputContractError> {
14734 if files.len() > OUTPUT_V11_MAX_FILES {
14735 return Err(OutputContractError::TooManyFiles {
14736 found: files.len(),
14737 limit: OUTPUT_V11_MAX_FILES,
14738 });
14739 }
14740 let mut findings = FindingSummary::default();
14741 let mut checks = CheckSummary::default();
14742 let mut prediction_facets = PredictionFacetSummary::default();
14743 for file in &files {
14744 file.validate()?;
14745 for check in file.checks() {
14746 checks.total += 1;
14747 for finding in check.findings() {
14748 findings.add(finding.severity);
14749 }
14750 match check.selection() {
14751 SelectionState::Selected => checks.selection.selected += 1,
14752 SelectionState::Unselected => checks.selection.unselected += 1,
14753 }
14754 match check.configuration() {
14755 ConfigurationState::Enabled => checks.configuration.enabled += 1,
14756 ConfigurationState::Disabled => checks.configuration.disabled += 1,
14757 }
14758 match check.applicability() {
14759 Applicability::Applicable => checks.applicability.applicable += 1,
14760 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
14761 }
14762 match check.evaluation() {
14763 EvaluationState::Complete => checks.evaluation.complete += 1,
14764 EvaluationState::Partial => checks.evaluation.partial += 1,
14765 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
14766 }
14767 checks.gaps += check.gaps().len();
14768 if let Some(prediction) = check.engine_prediction_v3() {
14769 for facet in prediction.facets() {
14770 match facet.state() {
14771 EnginePredictionFacetStateV1::Available => {
14772 prediction_facets.available += 1;
14773 }
14774 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14775 prediction_facets.required_prediction_unavailable += 1;
14776 }
14777 }
14778 }
14779 }
14780 if let Some(prediction) = check.engine_prediction_v4() {
14781 for facet in prediction.facets() {
14782 match facet.state() {
14783 EnginePredictionFacetStateV1::Available => {
14784 prediction_facets.available += 1;
14785 }
14786 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
14787 prediction_facets.required_prediction_unavailable += 1;
14788 }
14789 }
14790 }
14791 }
14792 }
14793 }
14794 Ok(Self {
14795 header: EnvelopeHeaderV2 {
14796 schema_version: OUTPUT_V15_SCHEMA_VERSION,
14797 schema: OUTPUT_V15_SCHEMA_ID,
14798 tool,
14799 command: "lint",
14800 },
14801 summary: LintSummary {
14802 files: files.len(),
14803 findings,
14804 checks,
14805 prediction_facets,
14806 },
14807 files,
14808 })
14809 }
14810}
14811
14812#[derive(Debug, Clone, Serialize)]
14813#[serde(untagged)]
14814#[allow(clippy::large_enum_variant)]
14815enum CurrentPredictionProvenanceV16 {
14816 V3(PredictionProvenanceV3),
14817 V5(PredictionProvenanceV5),
14818}
14819
14820#[derive(Debug, Clone, Serialize)]
14822pub struct LintFileReportV16 {
14823 #[serde(flatten)]
14824 evidence: FileEvidence,
14825 prediction_provenance: Option<CurrentPredictionProvenanceV16>,
14826 checks: Vec<CheckEvaluation>,
14827}
14828
14829impl LintFileReportV16 {
14830 pub fn new(
14832 path: impl Into<String>,
14833 input: InputIdentity,
14834 rig: RigInfo,
14835 prediction_provenance: Option<PredictionProvenanceV3>,
14836 checks: Vec<CheckEvaluation>,
14837 measurements: MeasurementContract,
14838 ) -> Result<Self, OutputContractError> {
14839 let report = Self {
14840 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14841 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV16::V3),
14842 checks,
14843 };
14844 report.validate()?;
14845 Ok(report)
14846 }
14847
14848 pub fn new_v5(
14850 path: impl Into<String>,
14851 input: InputIdentity,
14852 rig: RigInfo,
14853 prediction_provenance: Option<PredictionProvenanceV5>,
14854 checks: Vec<CheckEvaluation>,
14855 measurements: MeasurementContract,
14856 ) -> Result<Self, OutputContractError> {
14857 let report = Self {
14858 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
14859 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV16::V5),
14860 checks,
14861 };
14862 report.validate()?;
14863 Ok(report)
14864 }
14865
14866 pub fn path(&self) -> &str {
14868 &self.evidence.path
14869 }
14870 pub fn input(&self) -> &InputIdentity {
14872 &self.evidence.input
14873 }
14874 pub fn measurements(&self) -> &MeasurementContract {
14876 &self.evidence.measurements
14877 }
14878 pub fn checks(&self) -> &[CheckEvaluation] {
14880 &self.checks
14881 }
14882 pub const fn prediction_provenance_v3(&self) -> Option<&PredictionProvenanceV3> {
14884 match self.prediction_provenance.as_ref() {
14885 Some(CurrentPredictionProvenanceV16::V3(provenance)) => Some(provenance),
14886 Some(CurrentPredictionProvenanceV16::V5(_)) | None => None,
14887 }
14888 }
14889 pub const fn prediction_provenance_v5(&self) -> Option<&PredictionProvenanceV5> {
14891 match self.prediction_provenance.as_ref() {
14892 Some(CurrentPredictionProvenanceV16::V5(provenance)) => Some(provenance),
14893 Some(CurrentPredictionProvenanceV16::V3(_)) | None => None,
14894 }
14895 }
14896
14897 fn validate(&self) -> Result<(), OutputContractError> {
14898 require_measurements_v16(OUTPUT_V16_SCHEMA_VERSION, &self.evidence.measurements)?;
14899 if self.checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
14900 return Err(OutputContractError::TooManyChecks {
14901 found: self.checks.len(),
14902 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
14903 });
14904 }
14905 if let Some(CurrentPredictionProvenanceV16::V3(provenance)) = &self.prediction_provenance {
14906 return LintFileReport {
14907 evidence: self.evidence.clone(),
14908 prediction_provenance: Some(CurrentPredictionProvenance::V3(provenance.clone())),
14909 checks: self.checks.clone(),
14910 }
14911 .validate();
14912 }
14913 let provenance = match &self.prediction_provenance {
14914 Some(CurrentPredictionProvenanceV16::V5(provenance)) => {
14915 provenance.validate()?;
14916 if provenance.base().raw_source().primary_input() != &self.evidence.input {
14917 return Err(OutputContractError::PredictionPrimaryInputMismatch);
14918 }
14919 Some(provenance)
14920 }
14921 Some(CurrentPredictionProvenanceV16::V3(_)) => unreachable!(),
14922 None => None,
14923 };
14924 let mut facets = 0usize;
14925 let mut references = 0usize;
14926 let mut has_facet_budget_summary = false;
14927 let mut text = provenance
14928 .map(PredictionProvenanceV5::retained_text_bytes)
14929 .transpose()?
14930 .unwrap_or(0);
14931 for check in &self.checks {
14932 if check.engine_prediction().is_some()
14933 || check.engine_prediction_v2().is_some()
14934 || check.engine_prediction_v3().is_some()
14935 || check.engine_prediction_v4().is_some()
14936 {
14937 return Err(OutputContractError::HistoricalPredictionInV2Output);
14938 }
14939 let prediction = check.engine_prediction_v5();
14940 if provenance.is_none() && prediction.is_some() {
14941 return Err(OutputContractError::PredictionWithoutProvenance);
14942 }
14943 if let Some(prediction) = prediction {
14944 let provenance =
14945 provenance.ok_or(OutputContractError::PredictionWithoutProvenance)?;
14946 prediction.validate_against_provenance(provenance)?;
14947 prediction.validate_for_check(
14948 check.check_id(),
14949 check.evaluated_scopes(),
14950 check.gaps(),
14951 check.findings(),
14952 )?;
14953 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
14954 facets = facets
14955 .checked_add(prediction.facets().len())
14956 .ok_or(OutputContractError::ArithmeticOverflow)?;
14957 references = references
14958 .checked_add(prediction.basis_reference_count())
14959 .ok_or(OutputContractError::ArithmeticOverflow)?;
14960 text = text
14961 .checked_add(prediction.retained_text_bytes()?)
14962 .ok_or(OutputContractError::ArithmeticOverflow)?;
14963 }
14964 validate_current_engine_track_support_prediction_v5(
14965 check.check_id(),
14966 check.selection(),
14967 check.configuration(),
14968 check.applicability(),
14969 prediction,
14970 provenance,
14971 check.findings().is_empty(),
14972 )?;
14973 validate_current_engine_unit_scale_prediction_v5(
14974 check.check_id(),
14975 check.selection(),
14976 check.configuration(),
14977 check.applicability(),
14978 prediction,
14979 provenance,
14980 &self.evidence.measurements,
14981 )?;
14982 }
14983 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
14984 return Err(OutputContractError::TooManyPredictionFacets {
14985 found: facets,
14986 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14987 });
14988 }
14989 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
14990 return Err(
14991 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
14992 found: facets,
14993 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
14994 },
14995 );
14996 }
14997 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
14998 return Err(OutputContractError::TooManyPredictionBasisReferences {
14999 found: references,
15000 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
15001 });
15002 }
15003 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
15004 return Err(OutputContractError::TooMuchPredictionText {
15005 found: text,
15006 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
15007 });
15008 }
15009 validate_measurement_references_batch_v4(
15010 &self.evidence.measurements,
15011 self.checks.iter().enumerate().filter_map(|(index, check)| {
15012 check
15013 .engine_prediction_v5()
15014 .map(|prediction| (index, prediction.base_prediction()))
15015 }),
15016 )
15017 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
15018 Ok(())
15019 }
15020}
15021
15022#[derive(Debug, Clone, Serialize)]
15024pub struct LintEnvelopeV16 {
15025 #[serde(flatten)]
15026 header: EnvelopeHeaderV2,
15027 summary: LintSummary,
15028 files: Vec<LintFileReportV16>,
15029}
15030
15031impl LintEnvelopeV16 {
15032 pub fn new(tool: ToolInfo, files: Vec<LintFileReportV16>) -> Result<Self, OutputContractError> {
15034 if files.len() > OUTPUT_V11_MAX_FILES {
15035 return Err(OutputContractError::TooManyFiles {
15036 found: files.len(),
15037 limit: OUTPUT_V11_MAX_FILES,
15038 });
15039 }
15040 let mut findings = FindingSummary::default();
15041 let mut checks = CheckSummary::default();
15042 let mut prediction_facets = PredictionFacetSummary::default();
15043 for file in &files {
15044 file.validate()?;
15045 for check in file.checks() {
15046 checks.total += 1;
15047 for finding in check.findings() {
15048 findings.add(finding.severity);
15049 }
15050 match check.selection() {
15051 SelectionState::Selected => checks.selection.selected += 1,
15052 SelectionState::Unselected => checks.selection.unselected += 1,
15053 }
15054 match check.configuration() {
15055 ConfigurationState::Enabled => checks.configuration.enabled += 1,
15056 ConfigurationState::Disabled => checks.configuration.disabled += 1,
15057 }
15058 match check.applicability() {
15059 Applicability::Applicable => checks.applicability.applicable += 1,
15060 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
15061 }
15062 match check.evaluation() {
15063 EvaluationState::Complete => checks.evaluation.complete += 1,
15064 EvaluationState::Partial => checks.evaluation.partial += 1,
15065 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
15066 }
15067 checks.gaps += check.gaps().len();
15068 for facet in check
15069 .engine_prediction_v3()
15070 .into_iter()
15071 .flat_map(EnginePredictionV3::facets)
15072 {
15073 match facet.state() {
15074 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
15075 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
15076 prediction_facets.required_prediction_unavailable += 1
15077 }
15078 }
15079 }
15080 for facet in check
15081 .engine_prediction_v5()
15082 .into_iter()
15083 .flat_map(EnginePredictionV5::facets)
15084 {
15085 match facet.state() {
15086 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
15087 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
15088 prediction_facets.required_prediction_unavailable += 1
15089 }
15090 }
15091 }
15092 }
15093 }
15094 Ok(Self {
15095 header: EnvelopeHeaderV2 {
15096 schema_version: OUTPUT_V16_SCHEMA_VERSION,
15097 schema: OUTPUT_V16_SCHEMA_ID,
15098 tool,
15099 command: "lint",
15100 },
15101 summary: LintSummary {
15102 files: files.len(),
15103 findings,
15104 checks,
15105 prediction_facets,
15106 },
15107 files,
15108 })
15109 }
15110}
15111
15112#[derive(Debug, Clone, Serialize)]
15113#[serde(untagged)]
15114#[allow(clippy::large_enum_variant)]
15115enum CurrentPredictionProvenanceV17 {
15116 V3(PredictionProvenanceV3),
15117 V5(PredictionProvenanceV5),
15118 V6(PredictionProvenanceV6),
15119}
15120
15121#[derive(Debug, Clone, Serialize)]
15123pub struct LintFileReportV17 {
15124 #[serde(flatten)]
15125 evidence: FileEvidence,
15126 prediction_provenance: Option<CurrentPredictionProvenanceV17>,
15127 checks: Vec<CheckEvaluation>,
15128}
15129
15130impl LintFileReportV17 {
15131 pub fn new(
15133 path: impl Into<String>,
15134 input: InputIdentity,
15135 rig: RigInfo,
15136 prediction_provenance: Option<PredictionProvenanceV3>,
15137 checks: Vec<CheckEvaluation>,
15138 measurements: MeasurementContract,
15139 ) -> Result<Self, OutputContractError> {
15140 let report = Self {
15141 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
15142 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V3),
15143 checks,
15144 };
15145 report.validate()?;
15146 Ok(report)
15147 }
15148
15149 pub fn new_v5(
15151 path: impl Into<String>,
15152 input: InputIdentity,
15153 rig: RigInfo,
15154 prediction_provenance: Option<PredictionProvenanceV5>,
15155 checks: Vec<CheckEvaluation>,
15156 measurements: MeasurementContract,
15157 ) -> Result<Self, OutputContractError> {
15158 let report = Self {
15159 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
15160 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V5),
15161 checks,
15162 };
15163 report.validate()?;
15164 Ok(report)
15165 }
15166
15167 pub fn new_v6(
15169 path: impl Into<String>,
15170 input: InputIdentity,
15171 rig: RigInfo,
15172 prediction_provenance: Option<PredictionProvenanceV6>,
15173 checks: Vec<CheckEvaluation>,
15174 measurements: MeasurementContract,
15175 ) -> Result<Self, OutputContractError> {
15176 let report = Self {
15177 evidence: FileEvidence::historical_v16(path, input, rig, measurements)?,
15178 prediction_provenance: prediction_provenance.map(CurrentPredictionProvenanceV17::V6),
15179 checks,
15180 };
15181 report.validate()?;
15182 Ok(report)
15183 }
15184
15185 pub fn path(&self) -> &str {
15187 &self.evidence.path
15188 }
15189 pub fn input(&self) -> &InputIdentity {
15191 &self.evidence.input
15192 }
15193 pub fn measurements(&self) -> &MeasurementContract {
15195 &self.evidence.measurements
15196 }
15197 pub fn checks(&self) -> &[CheckEvaluation] {
15199 &self.checks
15200 }
15201 pub const fn prediction_provenance_v3(&self) -> Option<&PredictionProvenanceV3> {
15203 match self.prediction_provenance.as_ref() {
15204 Some(CurrentPredictionProvenanceV17::V3(provenance)) => Some(provenance),
15205 Some(CurrentPredictionProvenanceV17::V5(_) | CurrentPredictionProvenanceV17::V6(_))
15206 | None => None,
15207 }
15208 }
15209 pub const fn prediction_provenance_v5(&self) -> Option<&PredictionProvenanceV5> {
15211 match self.prediction_provenance.as_ref() {
15212 Some(CurrentPredictionProvenanceV17::V5(provenance)) => Some(provenance),
15213 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V6(_))
15214 | None => None,
15215 }
15216 }
15217 pub const fn prediction_provenance_v6(&self) -> Option<&PredictionProvenanceV6> {
15219 match self.prediction_provenance.as_ref() {
15220 Some(CurrentPredictionProvenanceV17::V6(provenance)) => Some(provenance),
15221 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V5(_))
15222 | None => None,
15223 }
15224 }
15225
15226 fn validate(&self) -> Result<(), OutputContractError> {
15227 require_measurements_v16(OUTPUT_V17_SCHEMA_VERSION, &self.evidence.measurements)?;
15228 match &self.prediction_provenance {
15229 Some(CurrentPredictionProvenanceV17::V3(provenance)) => {
15230 return LintFileReportV16::new(
15231 self.evidence.path.clone(),
15232 self.evidence.input.clone(),
15233 self.evidence.rig.clone(),
15234 Some(provenance.clone()),
15235 self.checks.clone(),
15236 self.evidence.measurements.clone(),
15237 )
15238 .map(|_| ());
15239 }
15240 Some(CurrentPredictionProvenanceV17::V5(provenance)) => {
15241 return LintFileReportV16::new_v5(
15242 self.evidence.path.clone(),
15243 self.evidence.input.clone(),
15244 self.evidence.rig.clone(),
15245 Some(provenance.clone()),
15246 self.checks.clone(),
15247 self.evidence.measurements.clone(),
15248 )
15249 .map(|_| ());
15250 }
15251 Some(CurrentPredictionProvenanceV17::V6(_)) => {}
15252 None => {
15253 return LintFileReportV16::new(
15254 self.evidence.path.clone(),
15255 self.evidence.input.clone(),
15256 self.evidence.rig.clone(),
15257 None,
15258 self.checks.clone(),
15259 self.evidence.measurements.clone(),
15260 )
15261 .map(|_| ());
15262 }
15263 }
15264 if self.checks.len() > OUTPUT_V11_MAX_CHECKS_PER_FILE {
15265 return Err(OutputContractError::TooManyChecks {
15266 found: self.checks.len(),
15267 limit: OUTPUT_V11_MAX_CHECKS_PER_FILE,
15268 });
15269 }
15270 let provenance = match &self.prediction_provenance {
15271 Some(CurrentPredictionProvenanceV17::V6(provenance)) => {
15272 provenance.validate()?;
15273 if provenance.base().base().raw_source().primary_input() != &self.evidence.input {
15274 return Err(OutputContractError::PredictionPrimaryInputMismatch);
15275 }
15276 Some(provenance)
15277 }
15278 _ => unreachable!(),
15279 };
15280 let mut facets = 0usize;
15281 let mut references = 0usize;
15282 let mut has_facet_budget_summary = false;
15283 let mut text = provenance
15284 .map(PredictionProvenanceV6::retained_text_bytes)
15285 .transpose()?
15286 .unwrap_or(0);
15287 for check in &self.checks {
15288 if check.engine_prediction().is_some()
15289 || check.engine_prediction_v2().is_some()
15290 || check.engine_prediction_v3().is_some()
15291 || check.engine_prediction_v4().is_some()
15292 || check.engine_prediction_v5().is_some()
15293 {
15294 return Err(OutputContractError::HistoricalPredictionInV2Output);
15295 }
15296 let prediction = check.engine_prediction_v6();
15297 if let Some(prediction) = prediction {
15298 let provenance =
15299 provenance.ok_or(OutputContractError::PredictionWithoutProvenance)?;
15300 prediction.validate_against_provenance(provenance)?;
15301 prediction.validate_for_check(
15302 check.check_id(),
15303 check.evaluated_scopes(),
15304 check.gaps(),
15305 check.findings(),
15306 )?;
15307 has_facet_budget_summary |= prediction.base_prediction().has_facet_budget_summary();
15308 facets = facets
15309 .checked_add(prediction.facets().len())
15310 .ok_or(OutputContractError::ArithmeticOverflow)?;
15311 references = references
15312 .checked_add(prediction.basis_reference_count())
15313 .ok_or(OutputContractError::ArithmeticOverflow)?;
15314 text = text
15315 .checked_add(prediction.retained_text_bytes()?)
15316 .ok_or(OutputContractError::ArithmeticOverflow)?;
15317 }
15318 validate_current_engine_root_motion_prediction_v6(
15319 check.check_id(),
15320 check.selection(),
15321 check.configuration(),
15322 check.applicability(),
15323 prediction,
15324 provenance,
15325 check.findings(),
15326 &self.evidence.rig,
15327 &self.evidence.measurements,
15328 )?;
15329 }
15330 if facets > PREDICTION_V1_MAX_FACETS_PER_FILE {
15331 return Err(OutputContractError::TooManyPredictionFacets {
15332 found: facets,
15333 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
15334 });
15335 }
15336 if has_facet_budget_summary && facets != PREDICTION_V1_MAX_FACETS_PER_FILE {
15337 return Err(
15338 OutputContractError::FacetBudgetSummaryWithoutExhaustedFileBudget {
15339 found: facets,
15340 limit: PREDICTION_V1_MAX_FACETS_PER_FILE,
15341 },
15342 );
15343 }
15344 if references > PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE {
15345 return Err(OutputContractError::TooManyPredictionBasisReferences {
15346 found: references,
15347 limit: PREDICTION_V1_MAX_BASIS_REFERENCES_PER_FILE,
15348 });
15349 }
15350 if text > PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE {
15351 return Err(OutputContractError::TooMuchPredictionText {
15352 found: text,
15353 limit: PREDICTION_V1_MAX_TOTAL_TEXT_BYTES_PER_FILE,
15354 });
15355 }
15356 validate_measurement_references_batch_v4(
15357 &self.evidence.measurements,
15358 self.checks.iter().enumerate().filter_map(|(index, check)| {
15359 check
15360 .engine_prediction_v6()
15361 .map(|prediction| (index, prediction.base_prediction()))
15362 }),
15363 )
15364 .map_err(|error| OutputContractError::InvalidPrediction(error.source))?;
15365 Ok(())
15366 }
15367}
15368
15369#[derive(Debug, Clone, Serialize)]
15373pub struct LintFileReportV19 {
15374 #[serde(flatten)]
15375 evidence: FileEvidence,
15376 prediction_provenance: Option<CurrentPredictionProvenanceV17>,
15377 checks: Vec<CheckEvaluation>,
15378}
15379
15380impl LintFileReportV19 {
15381 pub fn new(
15383 path: impl Into<String>,
15384 input: InputIdentity,
15385 rig: RigInfo,
15386 prediction_provenance: Option<PredictionProvenanceV3>,
15387 checks: Vec<CheckEvaluation>,
15388 measurements: MeasurementContract,
15389 ) -> Result<Self, OutputContractError> {
15390 Self::build(
15391 path,
15392 input,
15393 rig,
15394 prediction_provenance.map(CurrentPredictionProvenanceV17::V3),
15395 checks,
15396 measurements,
15397 )
15398 }
15399
15400 pub fn new_v5(
15402 path: impl Into<String>,
15403 input: InputIdentity,
15404 rig: RigInfo,
15405 prediction_provenance: Option<PredictionProvenanceV5>,
15406 checks: Vec<CheckEvaluation>,
15407 measurements: MeasurementContract,
15408 ) -> Result<Self, OutputContractError> {
15409 Self::build(
15410 path,
15411 input,
15412 rig,
15413 prediction_provenance.map(CurrentPredictionProvenanceV17::V5),
15414 checks,
15415 measurements,
15416 )
15417 }
15418
15419 pub fn new_v6(
15421 path: impl Into<String>,
15422 input: InputIdentity,
15423 rig: RigInfo,
15424 prediction_provenance: Option<PredictionProvenanceV6>,
15425 checks: Vec<CheckEvaluation>,
15426 measurements: MeasurementContract,
15427 ) -> Result<Self, OutputContractError> {
15428 Self::build(
15429 path,
15430 input,
15431 rig,
15432 prediction_provenance.map(CurrentPredictionProvenanceV17::V6),
15433 checks,
15434 measurements,
15435 )
15436 }
15437
15438 fn build(
15439 path: impl Into<String>,
15440 input: InputIdentity,
15441 rig: RigInfo,
15442 prediction_provenance: Option<CurrentPredictionProvenanceV17>,
15443 checks: Vec<CheckEvaluation>,
15444 measurements: MeasurementContract,
15445 ) -> Result<Self, OutputContractError> {
15446 require_measurements_v18(OUTPUT_SCHEMA_VERSION, &measurements)?;
15447 let report = Self {
15448 evidence: FileEvidence::new(path, input, rig, measurements),
15449 prediction_provenance,
15450 checks,
15451 };
15452 report.historical_prediction_view()?;
15453 Ok(report)
15454 }
15455
15456 pub fn path(&self) -> &str {
15458 &self.evidence.path
15459 }
15460
15461 pub fn input(&self) -> &InputIdentity {
15463 &self.evidence.input
15464 }
15465
15466 pub fn measurements(&self) -> &MeasurementContract {
15468 &self.evidence.measurements
15469 }
15470
15471 pub fn checks(&self) -> &[CheckEvaluation] {
15473 &self.checks
15474 }
15475
15476 pub const fn prediction_provenance_v3(&self) -> Option<&PredictionProvenanceV3> {
15478 match self.prediction_provenance.as_ref() {
15479 Some(CurrentPredictionProvenanceV17::V3(provenance)) => Some(provenance),
15480 Some(CurrentPredictionProvenanceV17::V5(_) | CurrentPredictionProvenanceV17::V6(_))
15481 | None => None,
15482 }
15483 }
15484
15485 pub const fn prediction_provenance_v5(&self) -> Option<&PredictionProvenanceV5> {
15487 match self.prediction_provenance.as_ref() {
15488 Some(CurrentPredictionProvenanceV17::V5(provenance)) => Some(provenance),
15489 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V6(_))
15490 | None => None,
15491 }
15492 }
15493
15494 pub const fn prediction_provenance_v6(&self) -> Option<&PredictionProvenanceV6> {
15496 match self.prediction_provenance.as_ref() {
15497 Some(CurrentPredictionProvenanceV17::V6(provenance)) => Some(provenance),
15498 Some(CurrentPredictionProvenanceV17::V3(_) | CurrentPredictionProvenanceV17::V5(_))
15499 | None => None,
15500 }
15501 }
15502
15503 fn historical_prediction_view(&self) -> Result<LintFileReportV17, OutputContractError> {
15504 let measurements = self.evidence.measurements.prediction_v16_projection()?;
15505 let report = LintFileReportV17 {
15506 evidence: FileEvidence::new(
15507 self.evidence.path.clone(),
15508 self.evidence.input.clone(),
15509 self.evidence.rig.clone(),
15510 measurements,
15511 ),
15512 prediction_provenance: self.prediction_provenance.clone(),
15513 checks: self.checks.clone(),
15514 };
15515 report.validate()?;
15516 Ok(report)
15517 }
15518}
15519
15520#[derive(Debug, Clone, Serialize)]
15523pub struct LintEnvelopeV17 {
15524 #[serde(flatten)]
15525 header: EnvelopeHeaderV2,
15526 summary: LintSummary,
15527 files: Vec<LintFileReportV17>,
15528}
15529
15530impl LintEnvelopeV17 {
15531 pub fn new(tool: ToolInfo, files: Vec<LintFileReportV17>) -> Result<Self, OutputContractError> {
15533 if files.len() > OUTPUT_V11_MAX_FILES {
15534 return Err(OutputContractError::TooManyFiles {
15535 found: files.len(),
15536 limit: OUTPUT_V11_MAX_FILES,
15537 });
15538 }
15539 let mut findings = FindingSummary::default();
15540 let mut checks = CheckSummary::default();
15541 let mut prediction_facets = PredictionFacetSummary::default();
15542 for file in &files {
15543 file.validate()?;
15544 for check in file.checks() {
15545 checks.total += 1;
15546 for finding in check.findings() {
15547 findings.add(finding.severity);
15548 }
15549 match check.selection() {
15550 SelectionState::Selected => checks.selection.selected += 1,
15551 SelectionState::Unselected => checks.selection.unselected += 1,
15552 }
15553 match check.configuration() {
15554 ConfigurationState::Enabled => checks.configuration.enabled += 1,
15555 ConfigurationState::Disabled => checks.configuration.disabled += 1,
15556 }
15557 match check.applicability() {
15558 Applicability::Applicable => checks.applicability.applicable += 1,
15559 Applicability::NotApplicable => checks.applicability.not_applicable += 1,
15560 }
15561 match check.evaluation() {
15562 EvaluationState::Complete => checks.evaluation.complete += 1,
15563 EvaluationState::Partial => checks.evaluation.partial += 1,
15564 EvaluationState::NotEvaluated => checks.evaluation.not_evaluated += 1,
15565 }
15566 checks.gaps += check.gaps().len();
15567 let mut add_facet = |state| match state {
15568 EnginePredictionFacetStateV1::Available => prediction_facets.available += 1,
15569 EnginePredictionFacetStateV1::RequiredPredictionUnavailable => {
15570 prediction_facets.required_prediction_unavailable += 1
15571 }
15572 };
15573 for facet in check
15574 .engine_prediction_v3()
15575 .into_iter()
15576 .flat_map(EnginePredictionV3::facets)
15577 {
15578 add_facet(facet.state());
15579 }
15580 for facet in check
15581 .engine_prediction_v5()
15582 .into_iter()
15583 .flat_map(EnginePredictionV5::facets)
15584 {
15585 add_facet(facet.state());
15586 }
15587 for facet in check
15588 .engine_prediction_v6()
15589 .into_iter()
15590 .flat_map(EnginePredictionV6::facets)
15591 {
15592 add_facet(facet.state());
15593 }
15594 }
15595 }
15596 Ok(Self {
15597 header: EnvelopeHeaderV2 {
15598 schema_version: OUTPUT_V17_SCHEMA_VERSION,
15599 schema: OUTPUT_V17_SCHEMA_ID,
15600 tool,
15601 command: "lint",
15602 },
15603 summary: LintSummary {
15604 files: files.len(),
15605 findings,
15606 checks,
15607 prediction_facets,
15608 },
15609 files,
15610 })
15611 }
15612}
15613
15614#[derive(Debug, Clone, Serialize)]
15616pub struct LintEnvelopeV19 {
15617 #[serde(flatten)]
15618 header: EnvelopeHeaderV2,
15619 summary: LintSummary,
15620 files: Vec<LintFileReportV19>,
15621}
15622
15623impl LintEnvelopeV19 {
15624 pub fn new(tool: ToolInfo, files: Vec<LintFileReportV19>) -> Result<Self, OutputContractError> {
15626 if files.len() > OUTPUT_V11_MAX_FILES {
15627 return Err(OutputContractError::TooManyFiles {
15628 found: files.len(),
15629 limit: OUTPUT_V11_MAX_FILES,
15630 });
15631 }
15632 for file in &files {
15633 require_measurements_v18(OUTPUT_SCHEMA_VERSION, &file.evidence.measurements)?;
15634 }
15635 let historical_files = files
15636 .iter()
15637 .map(LintFileReportV19::historical_prediction_view)
15638 .collect::<Result<Vec<_>, _>>()?;
15639 let historical = LintEnvelopeV17::new(tool.clone(), historical_files)?;
15640 Ok(Self {
15641 header: EnvelopeHeaderV2 {
15642 schema_version: OUTPUT_SCHEMA_VERSION,
15643 schema: OUTPUT_SCHEMA_ID,
15644 tool,
15645 command: "lint",
15646 },
15647 summary: historical.summary,
15648 files,
15649 })
15650 }
15651}
15652
15653#[derive(Debug, Clone, Serialize)]
15654struct DiffInputs {
15655 before: String,
15656 after: String,
15657}
15658
15659#[derive(Debug, Clone, Serialize)]
15660struct DiffSummary {
15661 deltas: usize,
15662}
15663
15664#[derive(Debug, Serialize)]
15666pub struct DiffEnvelope {
15667 #[serde(flatten)]
15668 header: EnvelopeHeader,
15669 inputs: DiffInputs,
15670 summary: DiffSummary,
15671 deltas: Vec<MetricDelta>,
15672}
15673
15674impl DiffEnvelope {
15675 pub fn new(
15677 tool: ToolInfo,
15678 before: impl Into<String>,
15679 after: impl Into<String>,
15680 deltas: Vec<MetricDelta>,
15681 ) -> Self {
15682 Self {
15683 header: EnvelopeHeader::new(tool, "diff"),
15684 inputs: DiffInputs {
15685 before: before.into(),
15686 after: after.into(),
15687 },
15688 summary: DiffSummary {
15689 deltas: deltas.len(),
15690 },
15691 deltas,
15692 }
15693 }
15694}